diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/.github/ci-prerequisites-atlas.sh b/.github/ci-prerequisites-atlas.sh new file mode 100755 index 000000000000..b0a16f22b1e8 --- /dev/null +++ b/.github/ci-prerequisites-atlas.sh @@ -0,0 +1,6 @@ +# Reclaims disk space and sanitizes user home on Atlas infrastructure + +# We use the GitHub cache for the relevant parts of these directories. +# Also, we do not want to keep things like ~/.gradle/build-scan-data. +rm -rf ~/.gradle/ +rm -rf ~/.m2/ diff --git a/.github/workflows/atlas.yml b/.github/workflows/atlas.yml deleted file mode 100644 index afd8892e7cf8..000000000000 --- a/.github/workflows/atlas.yml +++ /dev/null @@ -1,118 +0,0 @@ -# The main CI of Hibernate ORM is https://ci.hibernate.org/job/hibernate-orm-pipeline/. -# However, Hibernate ORM builds run on GitHub actions regularly -# to check that it still works and can be used in GitHub forks. -# See https://docs.github.com/en/free-pro-team@latest/actions -# for more information about GitHub actions. - -name: Hibernate ORM build-Atlas - -on: - push: - branches: - - 'main' - # WARNING: Using pull_request_target to access secrets, but we check out the PR head commit. - # See checkout action for details. - pull_request_target: - branches: - - 'main' - -permissions: {} # none - -# See https://github.com/hibernate/hibernate-orm/pull/4615 for a description of the behavior we're getting. -concurrency: - # Consider that two builds are in the same concurrency group (cannot run concurrently) - # if they use the same workflow and are about the same branch ("ref") or pull request. - group: "workflow = ${{ github.workflow }}, ref = ${{ github.event.ref }}, pr = ${{ github.event.pull_request.id }}" - # Cancel previous builds in the same concurrency group even if they are in process - # for pull requests or pushes to forks (not the upstream repository). - cancel-in-progress: ${{ github.event_name == 'pull_request_target' || github.repository != 'hibernate/hibernate-orm' }} - -jobs: - build: - permissions: - contents: read - name: Java 11 - # runs-on: ubuntu-latest - runs-on: [self-hosted, Linux, X64, OCI] - strategy: - fail-fast: false - matrix: - include: - - rdbms: oracle_atps - - rdbms: oracle_db19c - - rdbms: oracle_db21c - - rdbms: oracle_db23c - steps: - - name: Check out commit already pushed to branch - if: "! github.event.pull_request.number" - uses: actions/checkout@v4 - with: - persist-credentials: false - - name: Check out PR head - uses: actions/checkout@v4 - if: github.event.pull_request.number - with: - # WARNING: This is potentially dangerous since we're checking out unreviewed code, - # and since we're using the pull_request_target event we can use secrets. - # Thus, we must be extra careful to never expose secrets to steps that execute this code, - # and to strictly limit our of secrets to those that only pose minor security threats. - # This means in particular we won't expose Develocity credentials to the main gradle executions, - # but instead will execute gradle a second time just to push build scans to Develocity; - # see below. - ref: "refs/pull/${{ github.event.pull_request.number }}/head" - persist-credentials: false - - name: Reclaim Disk Space - run: .github/ci-prerequisites.sh - - name: Start database - env: - RDBMS: ${{ matrix.rdbms }} - RUNID: ${{ github.run_number }} - run: ci/database-start.sh - - name: Set up Java 11 - uses: graalvm/setup-graalvm@v1 - with: - distribution: 'graalvm' - java-version: '21' - - name: Get year/month for cache key - id: get-date - run: echo "yearmonth=$(/bin/date -u "+%Y-%m")" >> $GITHUB_OUTPUT - shell: bash - - name: Cache Maven local repository - uses: actions/cache@v4 - id: cache-maven - with: - path: | - ~/.m2/repository - ~/.gradle/caches/ - ~/.gradle/wrapper/ - # refresh cache every month to avoid unlimited growth - key: maven-localrepo-${{ steps.get-date.outputs.yearmonth }} - - name: Run build script - env: - RDBMS: ${{ matrix.rdbms }} - RUNID: ${{ github.run_number }} - # WARNING: exposes secrets, so must only be passed to a step that doesn't run unapproved code. - # WARNING: As this runs on untrusted nodes, we use the same access key as for PRs: - # it has limited access, essentially it can only push build scans. - DEVELOCITY_ACCESS_KEY: "${{ github.event_name == 'push' && secrets.GRADLE_ENTERPRISE_ACCESS_KEY_PR || '' }}" - run: ./ci/build-github.sh - shell: bash - - name: Publish Develocity build scan for previous build - # Don't fail a build if publishing fails - continue-on-error: true - if: "${{ !cancelled() && github.event_name == 'pull_request_target' && github.repository == 'hibernate/hibernate-orm' }}" - run: | - ./gradlew buildScanPublishPrevious - env: - # WARNING: exposes secrets, so must only be passed to a step that doesn't run unapproved code. - DEVELOCITY_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY_PR }} - - name: Upload test reports (if Gradle failed) - uses: actions/upload-artifact@v4 - if: failure() - with: - name: test-reports-java11-${{ matrix.rdbms }} - path: | - ./**/target/reports/tests/ - ./**/target/reports/checkstyle/ - - name: Omit produced artifacts from build cache - run: ./ci/before-cache.sh \ No newline at end of file diff --git a/.github/workflows/ci-report.yml b/.github/workflows/ci-report.yml new file mode 100644 index 000000000000..daf9bdd17275 --- /dev/null +++ b/.github/workflows/ci-report.yml @@ -0,0 +1,197 @@ +name: GH Actions CI reporting + +on: + workflow_run: + workflows: [ "GH Actions CI" ] + types: [ completed ] + +defaults: + run: + shell: bash + +jobs: + publish-build-scans: + name: Publish Develocity build scans + if: github.repository == 'hibernate/hibernate-orm' && github.event.workflow_run.conclusion != 'cancelled' + runs-on: ubuntu-latest + steps: + # Checkout target branch which has trusted code + - name: Check out target branch + uses: actions/checkout@v4 + with: + persist-credentials: false + ref: ${{ github.ref }} + - name: Set up Java 11 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '11' + + - name: Generate cache key + id: cache-key + run: | + CURRENT_BRANCH="${{ github.repository != 'hibernate/hibernate-orm' && 'fork' || github.base_ref || github.ref_name }}" + CURRENT_MONTH=$(/bin/date -u "+%Y-%m") + CURRENT_DAY=$(/bin/date -u "+%d") + ROOT_CACHE_KEY="buildtool-cache" + echo "buildtool-monthly-cache-key=${ROOT_CACHE_KEY}-${CURRENT_MONTH}" >> $GITHUB_OUTPUT + echo "buildtool-monthly-branch-cache-key=${ROOT_CACHE_KEY}-${CURRENT_MONTH}-${CURRENT_BRANCH}" >> $GITHUB_OUTPUT + echo "buildtool-cache-key=${ROOT_CACHE_KEY}-${CURRENT_MONTH}-${CURRENT_BRANCH}-${CURRENT_DAY}" >> $GITHUB_OUTPUT + - name: Restore Maven/Gradle Dependency/Dist Caches + uses: actions/cache/restore@v4 + with: + path: | + ~/.m2/repository/ + ~/.m2/wrapper/ + ~/.gradle/caches/modules-2 + ~/.gradle/wrapper/ + key: ${{ steps.cache-key.outputs.buildtool-cache-key }} + restore-keys: | + ${{ steps.cache-key.outputs.buildtool-monthly-branch-cache-key }}- + ${{ steps.cache-key.outputs.buildtool-monthly-cache-key }}- + + - name: Download GitHub Actions artifacts for the Develocity build scans + id: downloadBuildScan + uses: actions/download-artifact@v4 + with: + pattern: build-scan-data-* + github-token: ${{ github.token }} + repository: ${{ github.repository }} + run-id: ${{ github.event.workflow_run.id }} + path: /tmp/downloaded-build-scan-data/ + # Don't fail the build if there are no matching artifacts + continue-on-error: true + - name: Publish Develocity build scans for previous builds + if: ${{ steps.downloadBuildScan.outcome != 'failure'}} + run: | + shopt -s nullglob # Don't run the loop below if there are no artifacts + status=0 + mkdir -p ~/.gradle/ + for build_scan_data_directory in /tmp/downloaded-build-scan-data/* + do + rm -rf ~/.gradle/build-scan-data + mv "$build_scan_data_directory" ~/.gradle/build-scan-data \ + && ./gradlew --no-build-cache buildScanPublishPrevious || status=1 + done + exit $status + env: + DEVELOCITY_ACCESS_KEY: ${{ secrets.DEVELOCITY_ACCESS_KEY_PR }} + + 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 "original_branch_ref=$TARGET_BRANCH" >> "$GITHUB_OUTPUT" + echo "pr_id=$PR_ID" >> "$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" + + 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 + TARGET_BRANCH=$(gh pr view "$BRANCH_NAME" --repo ${{ github.event.workflow_run.repository.full_name }} --json baseRefName -q .baseRefName) + PR_ID=$(gh pr view "$BRANCH_NAME" --repo ${{ github.event.workflow_run.repository.full_name }} --json number -q .number) + + EXTRA_ARGS="-Dsonar.pullrequest.branch=$BRANCH_NAME -Dsonar.pullrequest.key=$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 new file mode 100644 index 000000000000..35909c1feb38 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,356 @@ +name: GH Actions CI + +on: + pull_request: + branches: + - '6.6' + +permissions: {} # none + +# See https://github.com/hibernate/hibernate-orm/pull/4615 for a description of the behavior we're getting. +concurrency: + # Consider that two builds are in the same concurrency group (cannot run concurrently) + # if they use the same workflow and are about the same branch ("ref") or pull request. + group: "workflow = ${{ github.workflow }}, ref = ${{ github.event.ref }}, pr = ${{ github.event.pull_request.id }}" + # Cancel previous builds in the same concurrency group even if they are in progress + # for pull requests or pushes to forks (not the upstream repository). + cancel-in-progress: ${{ github.event_name == 'pull_request' || github.repository != 'hibernate/hibernate-orm' }} + +jobs: + + # Main job for h2/docker DBs. + build: + permissions: + contents: read + name: OpenJDK 11 - ${{matrix.rdbms}} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - rdbms: h2 + - rdbms: hsqldb + - rdbms: derby + - rdbms: mysql + - rdbms: mariadb + - rdbms: postgresql + - rdbms: edb + - rdbms: oracle + - rdbms: db2 + - rdbms: mssql + - rdbms: sybase +# Running with CockroachDB requires at least 2-4 vCPUs, which we don't have on GH Actions runners +# - rdbms: cockroachdb +# Running with HANA requires at least 8GB memory just for the database, which we don't have on GH Actions runners +# - rdbms: hana + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + - name: Reclaim Disk Space + run: .github/ci-prerequisites.sh + - name: Start database + env: + RDBMS: ${{ matrix.rdbms }} + run: ci/database-start.sh + - name: Set up Java 11 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '11' + + - name: Generate cache key + id: cache-key + run: | + CURRENT_BRANCH="${{ github.repository != 'hibernate/hibernate-orm' && 'fork' || github.base_ref || github.ref_name }}" + CURRENT_MONTH=$(/bin/date -u "+%Y-%m") + CURRENT_DAY=$(/bin/date -u "+%d") + ROOT_CACHE_KEY="buildtool-cache" + echo "buildtool-monthly-cache-key=${ROOT_CACHE_KEY}-${CURRENT_MONTH}" >> $GITHUB_OUTPUT + echo "buildtool-monthly-branch-cache-key=${ROOT_CACHE_KEY}-${CURRENT_MONTH}-${CURRENT_BRANCH}" >> $GITHUB_OUTPUT + echo "buildtool-cache-key=${ROOT_CACHE_KEY}-${CURRENT_MONTH}-${CURRENT_BRANCH}-${CURRENT_DAY}" >> $GITHUB_OUTPUT + - name: Cache Maven/Gradle Dependency/Dist Caches + id: cache-maven + uses: actions/cache@v4 + # if it's not a pull request, we restore and save the cache + if: github.event_name != 'pull_request' + with: + path: | + ~/.m2/repository/ + ~/.m2/wrapper/ + ~/.gradle/caches/modules-2 + ~/.gradle/wrapper/ + # A new cache will be stored daily. After that first store of the day, cache save actions will fail because the cache is immutable but it's not a problem. + # The whole cache is dropped monthly to prevent unlimited growth. + # The cache is per branch but in case we don't find a branch for a given branch, we will get a cache from another branch. + key: ${{ steps.cache-key.outputs.buildtool-cache-key }} + restore-keys: | + ${{ steps.cache-key.outputs.buildtool-monthly-branch-cache-key }}- + ${{ steps.cache-key.outputs.buildtool-monthly-cache-key }}- + - name: Restore Maven/Gradle Dependency/Dist Caches + uses: actions/cache/restore@v4 + # if it a pull request, we restore the cache but we don't save it + if: github.event_name == 'pull_request' + with: + path: | + ~/.m2/repository/ + ~/.m2/wrapper/ + ~/.gradle/caches/modules-2 + ~/.gradle/wrapper/ + key: ${{ steps.cache-key.outputs.buildtool-cache-key }} + restore-keys: | + ${{ steps.cache-key.outputs.buildtool-monthly-branch-cache-key }}- + ${{ steps.cache-key.outputs.buildtool-monthly-cache-key }}- + + - name: Run build script + run: ./ci/build-github.sh + shell: bash + env: + RDBMS: ${{ matrix.rdbms }} + # For jobs running on 'push', publish build scan and cache immediately. + # This won't work for pull requests, since they don't have access to secrets. + POPULATE_REMOTE_GRADLE_CACHE: ${{ github.event_name == 'push' && github.repository == 'hibernate/hibernate-orm' && 'true' || 'false' }} + DEVELOCITY_ACCESS_KEY: "${{ secrets.DEVELOCITY_ACCESS_KEY }}" + + # For jobs running on 'pull_request', upload build scan data. + # The actual publishing must be done in a separate job (see ci-report.yml). + # We don't write to the remote cache as that would be unsafe. + - name: Upload GitHub Actions artifact for the Develocity build scan + uses: actions/upload-artifact@v4 + if: "${{ github.event_name == 'pull_request' && !cancelled() }}" + with: + name: build-scan-data-${{ matrix.rdbms }} + path: ~/.gradle/build-scan-data + - name: 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@v4 + if: failure() + with: + name: test-reports-java11-${{ matrix.rdbms }} + path: | + ./**/target/reports/tests/ + - name: Omit produced artifacts from build cache + run: ./ci/before-cache.sh + + # Job for builds on Atlas (Oracle) infrastructure. + # This is untrusted, even for pushes, see below. + otp: + permissions: + contents: read + name: GraalVM 21 - ${{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-26ai + #- rdbms: autonomous-transaction-processing-serverless-19c + - rdbms: base-database-service-19c + - rdbms: base-database-service-21c + - rdbms: base-database-service-26ai + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + - name: Set up Java 21 + uses: graalvm/setup-graalvm@aafbedb8d382ed0ca6167d3a051415f20c859274 # v1.2.8 + with: + distribution: 'graalvm' + java-version: '21' + + - name: Generate cache key + id: cache-key + run: | + CURRENT_BRANCH="${{ github.repository != 'hibernate/hibernate-orm' && 'fork' || github.base_ref || github.ref_name }}" + CURRENT_MONTH=$(/bin/date -u "+%Y-%m") + CURRENT_DAY=$(/bin/date -u "+%d") + ROOT_CACHE_KEY="buildtool-cache-atlas" + echo "buildtool-monthly-cache-key=${ROOT_CACHE_KEY}-${CURRENT_MONTH}" >> $GITHUB_OUTPUT + echo "buildtool-monthly-branch-cache-key=${ROOT_CACHE_KEY}-${CURRENT_MONTH}-${CURRENT_BRANCH}" >> $GITHUB_OUTPUT + echo "buildtool-cache-key=${ROOT_CACHE_KEY}-${CURRENT_MONTH}-${CURRENT_BRANCH}-${CURRENT_DAY}" >> $GITHUB_OUTPUT + - name: Cache Maven/Gradle Dependency/Dist Caches + id: cache-maven + uses: actions/cache@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/ + ~/.gradle/caches/modules-2 + !~/.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. + # The whole cache is dropped monthly to prevent unlimited growth. + # The cache is per branch but in case we don't find a branch for a given branch, we will get a cache from another branch. + key: ${{ steps.cache-key.outputs.buildtool-cache-key }} + restore-keys: | + ${{ steps.cache-key.outputs.buildtool-monthly-branch-cache-key }}- + ${{ steps.cache-key.outputs.buildtool-monthly-cache-key }}- + - name: Restore Maven/Gradle Dependency/Dist Caches + uses: actions/cache/restore@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/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 }}- + + - id: create_database + uses: loiclefevre/test@a802f8bb53b42b16c253d75f86b06360d150c6e4 # v1.0.22 + with: + oci-service: ${{ matrix.rdbms }} + action: create + user: hibernate_orm_test + + - name: Run build script + env: + RDBMS: ${{ matrix.rdbms }} + RUNID: ${{ github.run_number }} + TESTPILOT_CONNECTION_STRING_SUFFIX: ${{ steps.create_database.outputs.connection_string_suffix }} + TESTPILOT_PASSWORD: ${{ steps.create_database.outputs.database_password }} + API_HOST: "" + TESTPILOT_CLIENT_ID: "" + TESTPILOT_TOKEN: "" + # Needed for TFO (TCP fast open) + LD_PRELOAD: /home/ubuntu/libtfojdbc1.so + LD_LIBRARY_PATH: /home/ubuntu + run: ./ci/build-github.sh + shell: bash + + - uses: loiclefevre/test@a802f8bb53b42b16c253d75f86b06360d150c6e4 # v1.0.22 + if: always() + with: + oci-service: ${{ matrix.rdbms }} + action: delete + user: hibernate_orm_test + + # Upload build scan data. + # The actual publishing must be done in a separate job (see ci-report.yml). + # We don't write to the remote cache as that would be unsafe. + # That's even on push, because we do not trust Atlas runners to hold secrets: they are shared infrastructure. + - name: Upload GitHub Actions artifact for the Develocity build scan + uses: actions/upload-artifact@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@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + if: failure() + with: + name: test-reports-java11-${{ matrix.rdbms }} + path: | + ./**/target/reports/tests/ + ./**/target/reports/checkstyle/ + - name: Omit produced artifacts from build cache + run: ./ci/before-cache.sh + + 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 11 + uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 + with: + java-version: '11' + 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 a05c7b585b5d..000000000000 --- a/.github/workflows/codeql.yml +++ /dev/null @@ -1,72 +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: Checkout repository - uses: actions/checkout@v4 - - # Initializes the CodeQL tools for scanning. - - name: Initialize CodeQL - uses: github/codeql-action/init@v3 - 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@v3 - - # â„šī¸ 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@v3 - with: - category: "/language:${{matrix.language}}" \ No newline at end of file diff --git a/.github/workflows/contributor-build.yml b/.github/workflows/contributor-build.yml deleted file mode 100644 index 8d393ce5cfdb..000000000000 --- a/.github/workflows/contributor-build.yml +++ /dev/null @@ -1,126 +0,0 @@ -# The main CI of Hibernate ORM is https://ci.hibernate.org/job/hibernate-orm-pipeline/. -# However, Hibernate ORM builds run on GitHub actions regularly -# to check that it still works and can be used in GitHub forks. -# See https://docs.github.com/en/free-pro-team@latest/actions -# for more information about GitHub actions. - -name: Hibernate ORM build - -on: - push: - branches: - - 'main' - # WARNING: Using pull_request_target to access secrets, but we check out the PR head commit. - # See checkout action for details. - pull_request_target: - branches: - - 'main' - -permissions: {} # none - -# See https://github.com/hibernate/hibernate-orm/pull/4615 for a description of the behavior we're getting. -concurrency: - # Consider that two builds are in the same concurrency group (cannot run concurrently) - # if they use the same workflow and are about the same branch ("ref") or pull request. - group: "workflow = ${{ github.workflow }}, ref = ${{ github.event.ref }}, pr = ${{ github.event.pull_request.id }}" - # Cancel previous builds in the same concurrency group even if they are in process - # for pull requests or pushes to forks (not the upstream repository). - cancel-in-progress: ${{ github.event_name == 'pull_request_target' || github.repository != 'hibernate/hibernate-orm' }} - -jobs: - build: - permissions: - contents: read - name: Java 11 - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - include: - - rdbms: h2 - - rdbms: hsqldb - - rdbms: derby - - rdbms: mysql - - rdbms: mariadb - - rdbms: postgresql - - rdbms: edb - - rdbms: oracle - - rdbms: db2 - - rdbms: mssql - - rdbms: sybase -# Running with CockroachDB requires at least 2-4 vCPUs, which we don't have on GH Actions runners -# - rdbms: cockroachdb -# Running with HANA requires at least 8GB memory just for the database, which we don't have on GH Actions runners -# - rdbms: hana - steps: - - name: Check out commit already pushed to branch - if: "! github.event.pull_request.number" - uses: actions/checkout@v4 - with: - persist-credentials: false - - name: Check out PR head - uses: actions/checkout@v4 - if: github.event.pull_request.number - with: - # WARNING: This is potentially dangerous since we're checking out unreviewed code, - # and since we're using the pull_request_target event we can use secrets. - # Thus, we must be extra careful to never expose secrets to steps that execute this code, - # and to strictly limit our of secrets to those that only pose minor security threats. - # This means in particular we won't expose Develocity credentials to the main gradle executions, - # but instead will execute gradle a second time just to push build scans to Develocity; - # see below. - ref: "refs/pull/${{ github.event.pull_request.number }}/head" - persist-credentials: false - - name: Reclaim Disk Space - run: .github/ci-prerequisites.sh - - name: Start database - env: - RDBMS: ${{ matrix.rdbms }} - run: ci/database-start.sh - - name: Set up Java 11 - uses: actions/setup-java@v4 - with: - distribution: 'temurin' - java-version: '11' - - name: Get year/month for cache key - id: get-date - run: echo "yearmonth=$(/bin/date -u "+%Y-%m")" >> $GITHUB_OUTPUT - shell: bash - - name: Cache Maven local repository - uses: actions/cache@v4 - id: cache-maven - with: - path: | - ~/.m2/repository - ~/.gradle/caches/ - ~/.gradle/wrapper/ - # refresh cache every month to avoid unlimited growth - key: maven-localrepo-${{ steps.get-date.outputs.yearmonth }} - - name: Run build script - env: - RDBMS: ${{ matrix.rdbms }} - # Don't populate Develocity cache in pull requests as that's potentially dangerous - POPULATE_REMOTE_GRADLE_CACHE: "${{ github.event_name == 'push' }}" - # WARNING: exposes secrets, so must only be passed to a step that doesn't run unapproved code. - DEVELOCITY_ACCESS_KEY: "${{ github.event_name == 'push' && secrets.GRADLE_ENTERPRISE_ACCESS_KEY || '' }}" - run: ./ci/build-github.sh - shell: bash - - name: Publish Develocity build scan for previous build (pull request) - # Don't fail a build if publishing fails - continue-on-error: true - if: "${{ !cancelled() && github.event_name == 'pull_request_target' && github.repository == 'hibernate/hibernate-orm' }}" - run: | - ./gradlew buildScanPublishPrevious - env: - # WARNING: exposes secrets, so must only be passed to a step that doesn't run unapproved code. - DEVELOCITY_ACCESS_KEY: ${{ secrets.GRADLE_ENTERPRISE_ACCESS_KEY_PR }} - - name: Upload test reports (if Gradle failed) - uses: actions/upload-artifact@v4 - if: failure() - with: - name: test-reports-java11-${{ matrix.rdbms }} - path: | - ./**/target/reports/tests/ - ./**/target/reports/checkstyle/ - - name: Omit produced artifacts from build cache - run: ./ci/before-cache.sh diff --git a/.gitignore b/.gitignore index 1427882b7fdc..9c71eaa8514d 100644 --- a/.gitignore +++ b/.gitignore @@ -52,3 +52,9 @@ databases/postgis/ # Vim *.swp *.swo + +# SDKman, used by some module maintainers +.sdkmanrc + +# Sonar CLI local scan files: +.scannerwork diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b4f6d3a6ab24..57b4caddbd1a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -51,6 +51,7 @@ GitHub there are a few pre-requisite steps to follow: the linked page, this also includes: * [set up your local git install](https://help.github.com/articles/set-up-git) * clone your fork +* Instruct git to ignore certain commits when using `git blame`. From the directory of your local clone, run this: `git config blame.ignoreRevsFile .git-blame-ignore-revs` * See the wiki pages for setting up your IDE, whether you use [IntelliJ IDEA](https://hibernate.org/community/contribute/intellij-idea/) or [Eclipse](https://hibernate.org/community/contribute/eclipse-ide/)(1). diff --git a/Jenkinsfile b/Jenkinsfile index 9b6d025b00c9..df4ad0baf7c6 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -28,49 +28,20 @@ stage('Configure') { requireApprovalForPullRequest 'hibernate' this.environments = [ -// new BuildEnvironment( dbName: 'h2' ), -// new BuildEnvironment( dbName: 'hsqldb' ), -// new BuildEnvironment( dbName: 'derby' ), -// new BuildEnvironment( dbName: 'mysql' ), -// new BuildEnvironment( dbName: 'mariadb' ), -// new BuildEnvironment( dbName: 'postgresql' ), -// new BuildEnvironment( dbName: 'edb' ), -// new BuildEnvironment( dbName: 'oracle' ), -// new BuildEnvironment( dbName: 'db2' ), -// new BuildEnvironment( dbName: 'mssql' ), -// new BuildEnvironment( dbName: 'sybase' ), -// Don't build with HANA by default, but only do it nightly until we receive a 3rd instance -// new BuildEnvironment( dbName: 'hana_cloud', dbLockableResource: 'hana-cloud', dbLockResourceAsHost: true ), new BuildEnvironment( node: 's390x' ), - new BuildEnvironment( dbName: 'tidb', node: 'tidb', - notificationRecipients: 'tidb_hibernate@pingcap.com' ), + new BuildEnvironment( dbName: 'sybase_jconn' ), new BuildEnvironment( testJdkVersion: '17' ), + new BuildEnvironment( testJdkVersion: '21' ), // We want to enable preview features when testing newer builds of OpenJDK: // even if we don't use these features, just enabling them can cause side effects // and it's useful to test that. - new BuildEnvironment( testJdkVersion: '20', testJdkLauncherArgs: '--enable-preview' ), - new BuildEnvironment( testJdkVersion: '21', testJdkLauncherArgs: '--enable-preview' ), - new BuildEnvironment( testJdkVersion: '22', testJdkLauncherArgs: '--enable-preview' ), + new BuildEnvironment( testJdkVersion: '25', testJdkLauncherArgs: '--enable-preview' ) // The following JDKs aren't supported by Hibernate ORM out-of-the box yet: // they require the use of -Dnet.bytebuddy.experimental=true. // Make sure to remove that argument as soon as possible // -- generally that requires upgrading bytebuddy after the JDK goes GA. - new BuildEnvironment( testJdkVersion: '23', testJdkLauncherArgs: '--enable-preview -Dnet.bytebuddy.experimental=true' ), - new BuildEnvironment( testJdkVersion: '24', testJdkLauncherArgs: '--enable-preview -Dnet.bytebuddy.experimental=true' ) ]; - if ( env.CHANGE_ID ) { - if ( pullRequest.labels.contains( 'cockroachdb' ) ) { - this.environments.add( new BuildEnvironment( dbName: 'cockroachdb', node: 'cockroachdb', longRunning: true ) ) - } - if ( pullRequest.labels.contains( 'hana' ) ) { - this.environments.add( new BuildEnvironment( dbName: 'hana_cloud', dbLockableResource: 'hana-cloud', dbLockResourceAsHost: true ) ) - } - if ( pullRequest.labels.contains( 'sybase' ) ) { - this.environments.add( new BuildEnvironment( dbName: 'sybase_jconn' ) ) - } - } - helper.configure { file 'job-configuration.yaml' // We don't require the following, but the build helper plugin apparently does @@ -98,15 +69,17 @@ if (currentBuild.getBuildCauses().toString().contains('BranchIndexingCause')) { currentBuild.result = 'NOT_BUILT' return } +// This is a limited maintenance branch, so don't run this on pushes to the branch, only on PRs +if ( !env.CHANGE_ID ) { + print "INFO: Build skipped because this job should only run for pull request, not for branch pushes" + currentBuild.result = 'NOT_BUILT' + return +} stage('Build') { Map executions = [:] Map> state = [:] environments.each { BuildEnvironment buildEnv -> - // Don't build environments for newer JDKs when this is a PR - if ( helper.scmSource.pullRequest && buildEnv.testJdkVersion ) { - return - } state[buildEnv.tag] = [:] executions.put(buildEnv.tag, { runBuildOnNode(buildEnv.node ?: NODE_PATTERN_BASE) { @@ -144,16 +117,12 @@ stage('Build') { state[buildEnv.tag]['containerName'] = "edb" break; case "sybase_jconn": - docker.withRegistry('https://index.docker.io/v1/', 'hibernateci.hub.docker.com') { - docker.image('nguoianphu/docker-sybase').pull() - } + docker.image('nguoianphu/docker-sybase').pull() sh "./docker_db.sh sybase" state[buildEnv.tag]['containerName'] = "sybase" break; case "cockroachdb": - docker.withRegistry('https://index.docker.io/v1/', 'hibernateci.hub.docker.com') { - docker.image('cockroachdb/cockroach:v23.1.12').pull() - } + docker.image('cockroachdb/cockroach:v23.1.12').pull() sh "./docker_db.sh cockroachdb" state[buildEnv.tag]['containerName'] = "cockroach" break; @@ -199,6 +168,9 @@ stage('Build') { } }) } + executions.put('Hibernate Search Update Dependency', { + build job: '/hibernate-search-dependency-update/7.2', propagate: true, parameters: [string(name: 'UPDATE_JOB', value: 'orm6.6'), string(name: 'ORM_REPOSITORY', value: helper.scmSource.remoteUrl), string(name: 'ORM_PULL_REQUEST_ID', value: helper.scmSource.pullRequest.id)] + }) parallel(executions) } @@ -242,7 +214,7 @@ void ciBuild(buildEnv, String args) { // On untrusted nodes, we use the same access key as for PRs: // it has limited access, essentially it can only push build scans. - def develocityCredentialsId = buildEnv.node ? 'ge.hibernate.org-access-key-pr' : 'ge.hibernate.org-access-key' + def develocityCredentialsId = buildEnv.node ? 'develocity.commonhaus.dev-access-key-pr' : 'develocity.commonhaus.dev-access-key' withCredentials([string(credentialsId: develocityCredentialsId, variable: 'DEVELOCITY_ACCESS_KEY')]) { @@ -251,13 +223,13 @@ void ciBuild(buildEnv, String args) { } } } - else if ( buildEnv.node && buildEnv.node != 's390x' ) { // We couldn't get the code below to work on s390x for some reason. + else if ( buildEnv.node != 's390x' ) { // We couldn't get the code below to work on s390x for some reason. // Pull request: we can't pass credentials to the build, since we'd be exposing secrets to e.g. tests. // We do the build first, then publish the build scan separately. tryFinally({ sh "./ci/build.sh $args" }, { // Finally - withCredentials([string(credentialsId: 'ge.hibernate.org-access-key-pr', + withCredentials([string(credentialsId: 'develocity.commonhaus.dev-access-key-pr', variable: 'DEVELOCITY_ACCESS_KEY')]) { withGradle { // withDevelocity, actually: https://plugins.jenkins.io/gradle/#plugin-content-capturing-build-scans-from-jenkins-pipeline // Don't fail a build if publishing fails diff --git a/MAINTAINERS.md b/MAINTAINERS.md index 87dddb4601ca..4192fecff530 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -89,8 +89,8 @@ In any case, before the release: #### Performing the release Once you trigger the CI job, it automatically pushes artifacts to the -[OSSRH Maven Repository](https://repo1.maven.org/maven2/org/hibernate/orm/), -and the documentation to [docs.jboss.org](https://docs.jboss.org/hibernate/orm/). +[Maven Central Repository](https://repo1.maven.org/maven2/org/hibernate/orm/), +and the documentation to [docs.hibernate.org](https://docs.hibernate.org/orm/). * Do *not* mark the Jira Release as "released" or close issues, the release job does it for you. diff --git a/README.adoc b/README.adoc index 9392eca52045..a1fbc80d7119 100644 --- a/README.adoc +++ b/README.adoc @@ -5,7 +5,7 @@ Hibernate implements JPA, the standard API for object/relational persistence in See https://hibernate.org/orm/[Hibernate.org] for more information. image:https://ci.hibernate.org/job/hibernate-orm-pipeline/job/main/badge/icon[Build Status,link=https://ci.hibernate.org/job/hibernate-orm-pipeline/job/main/] -image:https://img.shields.io/badge/Revved%20up%20by-Develocity-06A0CE?logo=Gradle&labelColor=02303A[link=https://ge.hibernate.org/scans] +image:https://img.shields.io/badge/Revved%20up%20by-Develocity-06A0CE?logo=Gradle&labelColor=02303A[link=https://develocity.commonhaus.dev/scans] == Continuous Integration diff --git a/build.gradle b/build.gradle index ae7a4284a399..2dd27c9270bf 100644 --- a/build.gradle +++ b/build.gradle @@ -28,8 +28,6 @@ plugins { id 'org.checkerframework' version '0.6.40' id 'org.hibernate.orm.build.jdks' - id 'io.github.gradle-nexus.publish-plugin' version '1.1.0' - id 'idea' id 'org.jetbrains.gradle.plugin.idea-ext' version '1.0' id 'eclipse' @@ -42,77 +40,16 @@ apply from: file( 'gradle/module.gradle' ) // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Release Task -task release { - description = "The task performed when we are performing a release build. Relies on " + - "the fact that subprojects will appropriately define a release task " + - "themselves if they have any release-related activities to perform" - - doFirst { - def javaVersionsInUse = jdkVersions.allVersions - if ( javaVersionsInUse != [JavaLanguageVersion.of( 11 )].toSet() ) { - throw new IllegalStateException( "Please use JDK 11 to perform the release. Currently using: ${javaVersionsInUse}" ) - } - } -} - -task publish { +tasks.register('publish') { description = "The task performed when we want to just publish maven artifacts. Relies on " + "the fact that subprojects will appropriately define a release task " + "themselves if they have any publish-related activities to perform" } -ext { - if ( project.hasProperty( 'hibernatePublishUsername' ) ) { - if ( ! project.hasProperty( 'hibernatePublishPassword' ) ) { - throw new GradleException( "Should specify both `hibernatePublishUsername` and `hibernatePublishPassword` as project properties" ); - } - } -} - -nexusPublishing { - repositories { - sonatype { - username = project.hasProperty( 'hibernatePublishUsername' ) ? project.property( 'hibernatePublishUsername' ) : null - password = project.hasProperty( 'hibernatePublishPassword' ) ? project.property( 'hibernatePublishPassword' ) : null - } - } -} - -gradle.taskGraph.addTaskExecutionGraphListener( - new TaskExecutionGraphListener() { - @Override - void graphPopulated(TaskExecutionGraph graph) { - String[] tasksToLookFor = [ - 'publish', - 'publishToSonatype', - 'publishAllPublicationsToSonatype', - 'publishPublishedArtifactsPublicationToSonatypeRepository', - 'publishRelocationArtifactsPublicationToSonatypeRepository', - ] - - for ( String taskToLookFor : tasksToLookFor ) { - if ( graph.hasTask( taskToLookFor ) ) { - // trying to publish - make sure the needed credentials are available - - if ( project.property( 'hibernatePublishUsername' ) == null ) { - throw new RuntimeException( "`-PhibernatePublishUsername=...` not found" ) - } - if ( project.property( 'hibernatePublishPassword' ) == null ) { - throw new RuntimeException( "`-PhibernatePublishPassword=...` not found" ) - } - - break; - } - } - } - } -) - - // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // CI Build Task -task ciBuild { +tasks.register('ciBuild') { description = "The task performed when one of the 'main' jobs are triggered on the " + "CI server. Just as above, relies on the fact that subprojects will " + "appropriately define a release task themselves if they have any tasks " + @@ -140,5 +77,20 @@ idea { } } +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 06f70064586b..a1bc53b4c35a 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,8 +1,713 @@ Hibernate 6 Changelog ======================= +Changes in 6.6.46.Final (March 30, 2026) +------------------------------------------------------------------------------------------------------------------------ + +https://hibernate.atlassian.net/projects/HHH/versions/38385 + + +** Bug + * HHH-20209 Race Condition in JavaTypeRegistry causing SemanticException during parallel UNION queries with projection. + * HHH-19429 ConcurrentModificationException observed while executing JPQL update query with VERSIONED clause + +Changes in 6.6.45.Final (March 22, 2026) +------------------------------------------------------------------------------------------------------------------------ + +https://hibernate.atlassian.net/projects/HHH/versions/38115 + + +** Bug + * HHH-20253 ClassCastException when using hibernate-enhance-maven-plugin plugin + +** Task + * HHH-20232 Update c3p0 to 0.12.0 + +Changes in 6.6.44.Final (March 01, 2026) +------------------------------------------------------------------------------------------------------------------------ + +https://hibernate.atlassian.net/projects/HHH/versions/37977 + + +** Bug + * HHH-20176 Native Query cache causing ArrayIndexOutOfBoundsException with extra columns + +Changes in 6.6.43.Final (February 22, 2026) +------------------------------------------------------------------------------------------------------------------------ + +https://hibernate.atlassian.net/projects/HHH/versions/37572 + + +** Bug + * HHH-20168 Hibernate JsonWithArrayEmbeddableTest.testUpdateMultipleAggregateMembers test fail with Oracle 19c + * HHH-20161 Hibernate ArrayToStringWithArrayAggregateTest test fail with Oracle 19c + +Changes in 6.6.42.Final (February 01, 2026) +------------------------------------------------------------------------------------------------------------------------ + +https://hibernate.atlassian.net/projects/HHH/versions/37272 + + +** Bug + * HHH-20118 Vector operator SQL templates miss parenthesis around + * HHH-20069 `DB2iDialect.rowId` causes an error in merge queries + +Changes in 6.6.41.Final (January 18, 2026) +------------------------------------------------------------------------------------------------------------------------ + +https://hibernate.atlassian.net/projects/HHH/versions/37040 + + +** Bug + * HHH-20041 DB2 for z IN tuple list predicate performs badly + * HHH-20040 DB2iSqlAstTranslator using future versions for feature activation + * HHH-19929 DB2iDialect problem with supportsRowValueConstructorSyntaxInInSubQuery + +Changes in 6.6.40.Final (December 21, 2025) +------------------------------------------------------------------------------------------------------------------------ + +https://hibernate.atlassian.net/projects/HHH/versions/36905 + + +** Bug + * HHH-20008 Bad performance of MEMBER OF translation + +** Task + * HHH-19548 Upgrade to ByteBuddy 1.17.5 + +Changes in 6.6.39.Final (December 14, 2025) +------------------------------------------------------------------------------------------------------------------------ + +https://hibernate.atlassian.net/projects/HHH/versions/36641 + + +** Bug + * HHH-19963 Wrong references in entity fields with circular associations + * HHH-19746 JPA parameters might be incorrectly handled resulting in exceptions or queries returning incorrect results + +** Improvement + * HHH-19943 Comparison of generic nested EmbeddedId's fails for JPQL and Criteria API + +Changes in 6.6.38.Final (November 30, 2025) +------------------------------------------------------------------------------------------------------------------------ + +https://hibernate.atlassian.net/projects/HHH/versions/36410 + + +** Bug + * HHH-19905 Implicit join re-use with nested inner and left joins causes ParsingException + * HHH-19883 JOIN TREAT ignores predicates + * HHH-18871 Nested NativeQuery mappings causing 'Could not locate TableGroup' exception after migration + +Changes in 6.6.37.Final (November 24, 2025) +------------------------------------------------------------------------------------------------------------------------ + +https://hibernate.atlassian.net/projects/HHH/versions/36080 + + +** Bug + * HHH-19936 Parameter casts for by-id lookups don't take column definition into account + * HHH-19926 NullPointerException when executing JPQL IN clause with null parameter on entity association + * HHH-19922 org.hibernate.orm:hibernate-platform:pom:7.1.7.Final is missing + * HHH-19910 EntityInitializer#resolveInstance wrongly initializes existing detached instance + * HHH-19038 Hibernate.get does not work on detached entities + * HHH-16991 EnhancedUserType cannot be used when defining relations + +Changes in 6.6.36.Final (November 16, 2025) +------------------------------------------------------------------------------------------------------------------------ + +https://hibernate.atlassian.net/projects/HHH/versions/36046 + + +** Bug + * HHH-19918 Avoid reflection when instantiating known FormatMapper + * HHH-19758 HQL parse failure with SLL can lead to wrong parse + * HHH-19240 Significant increase in heap allocation for queries after migrating Hibernate ORM 6.5 to 6.6 + +Changes in 6.6.35.Final (November 13, 2025) +------------------------------------------------------------------------------------------------------------------------ + +https://hibernate.atlassian.net/projects/HHH/versions/35782 + + +** Bug + * HHH-19895 hibernate-core 6.6.30.Final breaks compatibility on entities with composite keys for multiple variants of DB2 + * HHH-19888 FetchPlusOffsetParameterBinder fails to apply static offset for root pagination + * HHH-19887 Wrong ClassLoader used for Jackson Module discovery + * HHH-19739 Exceptions during load of entity with different persistent fields with same name + Note: Please refer to JIRA to learn more about each issue. +Changes in 6.6.34.Final (October 27, 2025) +------------------------------------------------------------------------------------------------------------------------ + +https://hibernate.atlassian.net/projects/HHH/versions/35683 + +** Task + * [HHH-19889] - Mention third-party dialects throughout the documentation + * [HHH-19869] - Regroup all dialect information under a single guide + + +Changes in 6.6.33.Final (October 10, 2025) +------------------------------------------------------------------------------------------------------------------------ + +https://hibernate.atlassian.net/projects/HHH/versions/35651 + +** Bug + * [HHH-19853] - Gradle plugin org.hibernate.orm not published for 6.6.31 / 6.2.47 + + +Changes in 6.6.32.Final (October 09, 2025) +------------------------------------------------------------------------------------------------------------------------ + +https://hibernate.atlassian.net/projects/HHH/versions/35485 + +** Bug + * [HHH-19853] - Gradle plugin org.hibernate.orm not published for 6.6.31 / 6.2.47 + + +Changes in 6.6.31.Final (October 03, 2025) +------------------------------------------------------------------------------------------------------------------------ + +https://hibernate.atlassian.net/projects/HHH/versions/35420 + +** Bug + * [HHH-18885] - ClassCastException with queued persist in an extra lazy Map + * [HHH-18936] - remove parent with @OnDelete(CASCADE) leads to TransientObjectException + +** Task + * [HHH-19800] - Migrate to release scripts for documentation publishing + + +Changes in 6.6.30.Final (September 29, 2025) +------------------------------------------------------------------------------------------------------------------------ + +https://hibernate.atlassian.net/projects/HHH/versions/35320 + +** Bug + * [HHH-19784] - Bytecode enhancement generates wrong field access method for classes in different JARs but with same package name deployed in the same EAR + * [HHH-19768] - Wrong supportsRowValueConstructorSyntaxInInSubQuery leads to bad performing queries + * [HHH-19681] - AssertionError on extract results from array containing jsonb values + + +Changes in 6.6.29.Final (September 14, 2025) +------------------------------------------------------------------------------------------------------------------------ + +https://hibernate.atlassian.net/projects/HHH/versions/35156 + +** Bug + * [HHH-19756] - Invalid SQL generated when using treat() with joined = discriminator inheritance and same attribute names + * [HHH-19747] - Hibernate Envers can not handle @EnumeratedValue annotation + * [HHH-19201] - BlobProxy with InputStream reads whole stream into a byte array + + +Changes in 6.6.28.Final (August 31, 2025) +------------------------------------------------------------------------------------------------------------------------ + +https://hibernate.atlassian.net/projects/HHH/versions/35057 + +** Bug + * [HHH-19740] - Collection table deletion for table per class subclass entity fails with UnknownTableReferenceException + * [HHH-19738] - JDBC password logged when specified via jakarta.persistence.jdbc.password + * [HHH-19734] - Cache hit of bytecode enhanced proxy with shallow query cache layout fails + * [HHH-19648] - Recursive @Embeddable mapping leads to stack overflow + + +Changes in 6.6.27.Final (August 24, 2025) +------------------------------------------------------------------------------------------------------------------------ + +https://hibernate.atlassian.net/projects/HHH/versions/34957 + +** Bug + * [HHH-19719] - org.hibernate.query.sqm.function.SelfRenderingSqmWindowFunction#appendHqlString throws IndexOutOfBoundsException when has no arguments + * [HHH-19712] - Column deduplication leads to wrong alias calculation for native query alias expansion + * [HHH-19687] - Criteria query with lazy @OneToOne and @EmbeddedId throws exception + + +Changes in 6.6.26.Final (August 17, 2025) +------------------------------------------------------------------------------------------------------------------------ + +https://hibernate.atlassian.net/projects/HHH/versions/34859 + +** Bug + * [HHH-18968] - MySQLDialect wrongly uses Timestamp as type for localtime function + * [HHH-18621] - Hibernate 6 disregards hibernate.jdbc.batch_versioned_data + + +Changes in 6.6.25.Final (August 10, 2025) +------------------------------------------------------------------------------------------------------------------------ + +https://hibernate.atlassian.net/projects/HHH/versions/34660 + +** Bug + * [HHH-19453] - sequence support not working on db2 As400 7.3 + * [HHH-17522] - Support correlation of CTEs + + +Changes in 6.6.24.Final (August 03, 2025) +------------------------------------------------------------------------------------------------------------------------ + +https://hibernate.atlassian.net/projects/HHH/versions/34495 + +** Bug + * [HHH-19675] - JdbcTypeRegistry#hasRegisteredDescriptor should account for constructed types + * [HHH-19657] - OffsetDateTime in an array is handled incorrectly with setting hibernate.type.java_time_use_direct_jdbc=true + * [HHH-19655] - Connection leak with PhysicalConnectionHandlingMode.IMMEDIATE_ACQUISITION_AND_HOLD + * [HHH-19651] - DB2 for i detection not working reliably + * [HHH-19585] - Object relationship mapping issues | java.lang.NullPointerException: Cannot invoke "java.lang.Comparable.compareTo(Object)" because "one" is null + * [HHH-19579] - Criteria update join - Column 'code' in SET is ambiguous + * [HHH-19031] - Loading an Entity a second time when it contains an embedded object causes IllegalArgumentException + * [HHH-18981] - IndexOutOfBoundsException when using arrayToString with arrayAgg + * [HHH-18956] - Native query brace replacement breaks dollar quoted literals in 6.6+ + * [HHH-18780] - Performance regression on Postgres with polymorphic query due to incorrect casts of null columns + + +Changes in 6.6.23.Final (July 27, 2025) +------------------------------------------------------------------------------------------------------------------------ + +https://hibernate.atlassian.net/projects/HHH/versions/34397 + +** Bug + * [HHH-19575] - empty Struct should be fetched intact or not null + * [HHH-19524] - @OneToOne relationship unnecessary joins in nativeQuery + * [HHH-19261] - OracleDialect getQueryHintString incorrectly joins supplied hints + * [HHH-16253] - [Envers] Schema Validation Failure With Audited (N)Clob Column with Hibernate 6 on H2 + +** Task + * [HHH-19639] - Add getter for EmbeddableFetchImpl#nullIndicatorResult + + +Changes in 6.6.22.Final (July 20, 2025) +------------------------------------------------------------------------------------------------------------------------ + +https://hibernate.atlassian.net/projects/HHH/versions/34297 + +** Bug + * [HHH-19621] - SUBSTRING function for DB2i Series is broken + * [HHH-19579] - Criteria update join - Column 'code' in SET is ambiguous + * [HHH-19550] - Attribute join on correlated from node receives wrong root + * [HHH-19524] - @OneToOne relationship unnecessary joins in nativeQuery + * [HHH-19457] - Inheritance with type JOINED not working in a related entity + * [HHH-19368] - Group by and single-table inheritance sub-select query error + * [HHH-19031] - Loading an Entity a second time when it contains an embedded object causes IllegalArgumentException + +** Task + * [HHH-19624] - Test EDB with the EDB drivers + + +Changes in 6.6.21.Final (July 13, 2025) +------------------------------------------------------------------------------------------------------------------------ + +https://hibernate.atlassian.net/projects/HHH/versions/34199 + +** Bug + * [HHH-19596] - NPE when array/collection of Struct contains null value + * [HHH-19542] - Embeddable in secondary table fails to recognize a nested embeddable is in the same table + + +Changes in 6.6.20.Final (July 06, 2025) +------------------------------------------------------------------------------------------------------------------------ + +https://hibernate.atlassian.net/projects/HHH/versions/34165 + +** Bug + * [HHH-19464] - Storing a binary data into BLOB on Oracle cutting off its content. + * [HHH-19396] - Cannot select the same column twice (with different aliases) while using CTE + * [HHH-19076] - expecting IdClass mapping sessionfactory error with specific @IdClass setup with inheritence + * [HHH-18898] - Specific mistake in HQL gives NullPointerException in AbstractSqlAstTranslator + * [HHH-18837] - Oracle epoch extraction doesn't work with dates + * [HHH-18581] - Performance degradation from Hibernate 5 to 6 on NativeQuery + +** Improvement + * [HHH-19558] - allow JDBC escapes in native SQL queries + + +Changes in 6.6.19.Final (June 29, 2025) +------------------------------------------------------------------------------------------------------------------------ + +https://hibernate.atlassian.net/projects/HHH/versions/33968 + +** Bug + * [HHH-18837] - Oracle epoch extraction doesn't work with dates + * [HHH-19547] - Misleading exception message at DefaultFlushEntityEventListener - mangled ID - misplaced Entity and EntityEntry ID + * [HHH-19560] - TupleTransformer and ResultListTransformer trash the query interpretation cache + * [HHH-19571] - CloningPropertyCall causes non-deterministic bytecode for AccessOptimizer + * [HHH-19573] - Presence of wrapper byte array pollutes BasicTypeRegistry + * [HHH-19577] - BytecodeProviderImpl.SetPropertyValues wrongly emits duplicate stack map frames + + +Changes in 6.6.18.Final (June 13, 2025) +------------------------------------------------------------------------------------------------------------------------ + +https://hibernate.atlassian.net/projects/HHH/versions/33703 + +** Bug + * [HHH-19533] - Implement equals() and hashCode() for NativeQueryConstructorTransformer + * [HHH-19529] - Check bytecode generated classes with stable names class loaders + * [HHH-18891] - java.lang.AssertionError generated in getResolvedInstance even though NotFound IGNORE set + * [HHH-18876] - ArrayInitializer#resolveInstanceSubInitializers should consider @ListIndexBase + * [HHH-18771] - ListInitializer should consistently consider @ListIndexBase + + +Changes in 6.6.17.Final (May 28, 2025) +------------------------------------------------------------------------------------------------------------------------ + +https://hibernate.atlassian.net/projects/HHH/versions/33638 + +** Bug + * [HHH-19490] - NPE when using array_position function + * [HHH-19476] - claimEntityHolderIfPossible Assertion Error + * [HHH-19387] - AssertionError in EntityInitializerImpl data.concreteDescriptor is null + * [HHH-18946] - Startup issues with HANA in failover situations + + +Changes in 6.6.16.Final (May 25, 2025) +------------------------------------------------------------------------------------------------------------------------ + +https://hibernate.atlassian.net/projects/HHH/versions/33606 + +** Bug + * [HHH-19477] - ConnectionReleaseMode.AFTER_STATEMENT ineffective due to missing connection release + * [HHH-19473] - Bytecode enhancement incorrectly generates code for Bidirectional Generic Entities + * [HHH-19472] - Native query "SELECT 1" with result type Object[] return singular object + * [HHH-19387] - AssertionError in EntityInitializerImpl data.concreteDescriptor is null + * [HHH-19372] - AccessOptimizer.setPropertyValues() and getPropertyValues() error with entity hierarchy. + * [HHH-19369] - Entity hierarchy ordering error within metamodel building with enhancement + * [HHH-19207] - JPA OrderBy annotated relation not ordered when using entity graph with criteria api + * [HHH-18813] - DML update of secondary table column fails + + +Changes in 6.6.15.Final (May 13, 2025) +------------------------------------------------------------------------------------------------------------------------ + +https://hibernate.atlassian.net/projects/HHH/versions/33406 + +** Bug + * [HHH-19458] - problem starting MockSessionFactory from Quarkus dev mode with Reactive + + +Changes in 6.6.14.Final (May 11, 2025) +------------------------------------------------------------------------------------------------------------------------ + +https://hibernate.atlassian.net/projects/HHH/versions/33241 + +** Bug + * [HHH-19425] - incorrect class literals in Processor-generated code + * [HHH-19375] - fix check for presence of Quarkus in reactive case + * [HHH-19374] - repositories should always be @Dependent + * [HHH-19320] - Assigned id value is not passed into BeforeExecutionGenerator#generate() method when allowAssignedIdentifiers() is true and id has been assigned + * [HHH-19314] - StackOverflowException when using onConflict with createCriteriaInsertValues and createCriteriaInsertSelect + * [HHH-19306] - Composite generator may not respect the event types of generators it consits of + * [HHH-19036] - ORM Filter are searched for in the wrong order + + +Changes in 6.6.13.Final (April 06, 2025) +------------------------------------------------------------------------------------------------------------------------ + +https://hibernate.atlassian.net/projects/HHH/versions/33142 + +** Bug + * [HHH-19314] - StackOverflowException when using onConflict with createCriteriaInsertValues and createCriteriaInsertSelect + + +Changes in 6.6.12.Final (March 30, 2025) +------------------------------------------------------------------------------------------------------------------------ + +https://hibernate.atlassian.net/projects/HHH/versions/32945 + +** Bug + * [HHH-19109] - Hibernate Data Repositories are @RequestScoped + * [HHH-19059] - Bytecode enhancement fails when inherited fields are mapped using property access in subclass + * [HHH-19017] - Class Cast Exception for PersistentAttributeInterceptable + * [HHH-18920] - Enum parameters in Jakarta Data repository method return type constructor are not properly matched + * [HHH-18745] - Unnecessary joins when use TREAT operator + * [HHH-14694] - Use stable proxy names to avoid managing proxy state and memory leaks + +** Task + * [HHH-19230] - Ensure that thread local for org.hibernate.bytecode.enhance.internal.bytebuddy.SafeCacheProvider + OverridingClassFileLocator are completely cleared + + +Changes in 6.6.11.Final (March 16, 2025) +------------------------------------------------------------------------------------------------------------------------ + +https://hibernate.atlassian.net/projects/HHH/versions/32879 + +** Bug + * [HHH-19246] - Fetch join makes partially covered EntityGraph ineffective + * [HHH-19220] - ClassCastException: class org.hibernate.bytecode.enhance.spi.LazyPropertyInitializer$1 cannot be cast to class java.lang.String + * [HHH-19140] - Enhanced entities with AccessType.PROPERTY does not work well with inheritance + * [HHH-19107] - Entities with @EmbeddedId not supported with CrudRepository + * [HHH-19106] - @Transaction(TxType) not working with Hibernate Data Repositories + * [HHH-19052] - Hibernate 6.6.X regression with join formula + * [HHH-18894] - Hibernate 6.6 enum literal is considered field literal instead + * [HHH-18881] - In MySQL, array of dates are not converted correctly + * [HHH-18858] - array fields and static metamodel + * [HHH-18787] - Custom UserType not recognised for array properties + * [HHH-18570] - Invalid SQL when filter contains identifier named date + + +Changes in 6.6.10.Final (March 09, 2025) +------------------------------------------------------------------------------------------------------------------------ + +https://hibernate.atlassian.net/projects/HHH/versions/32647 + +** Bug + * [HHH-19232] - BeanValidationEventListener not called if only associated collection is updated via getter + * [HHH-19206] - Bytecode-enhanced dirty checking ineffective if entity's embedded ID set manually (to same value) + * [HHH-19195] - Embeddable inheritance: discriminator values are not hierarchically ordered + +** Improvement + * [HHH-19219] - Informix Catalog and schema support + + +Changes in 6.6.9.Final (February 23, 2025) +------------------------------------------------------------------------------------------------------------------------ + +https://hibernate.atlassian.net/projects/HHH/versions/32614 + +** Bug + * [HHH-19173] - PostgreSQLLegacySqlAstTranslator does not implement visitInArrayPredicates + * [HHH-19116] - Error when using fk() function on left joined many-to-one association and is null predicate + * [HHH-19110] - Flush operation fails with "UnsupportedOperationException: compare() not implemented for EntityType" + * [HHH-17151] - NPE when binding null parameter in native query with explicit TemporalType + + +Changes in 6.6.8.Final (February 16, 2025) +------------------------------------------------------------------------------------------------------------------------ + +https://hibernate.atlassian.net/projects/HHH/versions/32516 + +** Bug + * [HHH-19126] - Plural valued paths should be collection-typed instead of element typed + * [HHH-18988] - Embeddable inheritance + default_schema results in NPE at startup + +** Improvement + * [HHH-19098] - Disable implicit loading of the default import script + + +Changes in 6.6.7.Final (February 10, 2025) +------------------------------------------------------------------------------------------------------------------------ + +https://hibernate.atlassian.net/projects/HHH/versions/32416 + +** Bug + * [HHH-19104] - Envers is keeping references to classes and thus classloaders + * [HHH-18901] - AnnotationFormatError: Duplicate annotation for class: interface org.hibernate.bytecode.enhance.spi.EnhancementInfo + * [HHH-18069] - NullPointerException when unioning partition results + +** Task + * [HHH-19050] - Allow configuration of EntityManagerFactoryBuilderImpl to override the BytecodeProvider instance + * [HHH-18928] - Consider the default Access Type as per Spec section 2.3.1 and skip enhancement of properties accessor + + +Changes in 6.6.6.Final (February 02, 2025) +------------------------------------------------------------------------------------------------------------------------ + +https://hibernate.atlassian.net/projects/HHH/versions/32382 + +** Bug + * [HHH-19079] - ComponentType.replace can cause ArrayIndexOutOfBoundsException when used with embeddable inheritance + * [HHH-19069] - Performance regression for wide inheritance models + * [HHH-19034] - Wrong reuse of a Join + * [HHH-18961] - JtaIsolationDelegate, obtaining connection : NPE when SQLExceptionConversionDelegate#convert returns null + * [HHH-18933] - the ordering of the class declaration in persistence.xml seems to affect the metamodel + +** Task + * [HHH-19078] - Improve release process error message when no issues with corresponding version are found + + +Changes in 6.6.5.Final (January 19, 2025) +------------------------------------------------------------------------------------------------------------------------ + +https://hibernate.atlassian.net/projects/HHH/versions/32379 + +** Bug + * [HHH-19011] - @ElementCollection comment overrides class level comment on an Entity + * [HHH-18819] - Error resolving persistent property of @MapperSuperclass if subtype @Embeddable used as @IdClass + * [HHH-17652] - Cannot invoke "org.hibernate.envers.internal.entities.EntityConfiguration.getRelationDescription(String)" because "entCfg" is null + +** Task + * [HHH-18972] - Upgrade to ByteBuddy 1.15.11 + + +Changes in 6.6.4.Final (December 18, 2024) +------------------------------------------------------------------------------------------------------------------------ + +https://hibernate.atlassian.net/projects/HHH/versions/32370 + +** Bug + * [HHH-18949] - Hibernate Processor should not insert underscores within uppercase names + * [HHH-18932] - Wrongly using FK column instead of PK when using joined alias + * [HHH-18912] - ORM release process + * [HHH-18904] - Bytecode Enhancement fails with UnsupportedEnhancementStrategy.FAIL for pre-persist method + * [HHH-18903] - Bytecode enhancement fails for entities that contain a method named get + * [HHH-18872] - ConcreteProxy type not restored from 2LC when loading a ManyToOne + * [HHH-18868] - Wrong behaviour of getAttribute method in impl. of ManagedType when scattered id attributes are used in MappedSuperclass + * [HHH-18863] - jpamodelgen 6.6 performance issue in Eclipse IDE + * [HHH-18850] - createCountQuery with Hibernate 6.6.2 + * [HHH-18709] - CriteriaUpdate involving JSON field containing Map results in SemanticException + * [HHH-18689] - 'FULL' query cache sometimes incomplete + * [HHH-18629] - Inconsistent column alias generated while result class is used for placeholder + * [HHH-18583] - Joined + discriminator inheritance treat in where clause not restricting to subtype + * [HHH-18384] - @JoinColumnsOrFormulas broken + * [HHH-18274] - Problems with generics in queries; proposed partial solution + * [HHH-17838] - @OneToOne relationship + @Embeddable keys + FetchType.LAZY fail in most recent version + * [HHH-17612] - DefaultRevisionEntity: Illegal argument on static metamodel field injection + * [HHH-14725] - Using a InputStream with BlobProxy and Envers results in java.sql.SQLException: could not reset reader + * [HHH-13815] - TransientObjectException after merging a bidirectional one-to-many with orphan deletion + * [HHH-13790] - Temporary session not being closed + * [HHH-13377] - Lazy loaded properties of bytecode enhanced entity are left stale after refresh of entity + +** Sub-task + * [HHH-18369] - Support Informix matches() function + * [HHH-18367] - Informix sum on case expression error + * [HHH-18365] - Informix function bit_length() error + * [HHH-18364] - Informix function locate() error + * [HHH-18363] - Informix component nullness check error + * [HHH-18362] - Informix function substring() error + * [HHH-18361] - Informix current_time error + * [HHH-18360] - Informix function str() error + +** Task + * [HHH-18917] - Follow all of the JavaBeans rules in enhance/internal/bytebuddy/EnhancerImpl when checking if a class can be enhanced + * [HHH-18906] - Allow specifying UnsupportedEnhancementStrategy for Hibernate testing + + +Changes in 6.6.3.Final (November 21, 2024) +------------------------------------------------------------------------------------------------------------------------ + +https://hibernate.atlassian.net/projects/HHH/versions/32365 + +** Bug + * [HHH-18862] - Group by error due to subselect using foreign key reference instead of primary key in HQL query + * [HHH-18851] - ArrayContainsArgumentTypeResolver wrongly infers array type for needle argument + * [HHH-18842] - Regression: CollectionType.replace() breaks if target is PersistentCollection, but not instance of Collection (e.g. PersistentMap) + * [HHH-18832] - Bytecode enhancement skipped for entities with "compute-only" @Transient properties + * [HHH-18816] - Error when rendering the fk-side of an association in an exists subquery + * [HHH-18703] - JoinedSubclassEntityPersister#getTableNameForColumn KO + * [HHH-18647] - SemanticException when using createCriteriaInsertValues to insert into foreign key column + +** Improvement + * [HHH-18841] - Make `_identifierMapper` property added for a IdClass synthetic + * [HHH-18833] - Configuration to fail bytecode enhancement instead of skipping it on unsupported models + +** Task + * [HHH-18846] - Enable release automation for ORM 6.6 + + +Changes in 6.6.2.Final (November 07, 2024) +------------------------------------------------------------------------------------------------------------------------ + +https://hibernate.atlassian.net/projects/HHH/versions/32350 + +** Bug + * [HHH-18773] - Multiple selections of same alias triggers possible non-threadsafe access to the session + * [HHH-18770] - NPE when using the JFR integration with JFR disabled + * [HHH-18764] - Class cast exception when using non basic type as identifier and in an embedded field using a natural ID + * [HHH-18761] - named query method generation for @NamedQuery on entity + * [HHH-18739] - Do not support join queries when using Mysql + * [HHH-18730] - Multi-column association in aggregate component doesn't work + * [HHH-18720] - Type check on select columns in union all gives SemanticException when there is a null column + * [HHH-18719] - Previous row state reuse can provide detached entities to the consumer + * [HHH-18713] - saveOrUpdate changed behaviour with bytecode enhancer + * [HHH-18712] - Warning about attempts to update an immutable entity for normal (not immutable) entity + * [HHH-18702] - Exception using @EmbeddedId with @OneToMany that refers to an alternate key column + * [HHH-18699] - Correctly handle @Id and @Version fields in query validation in Hibernate Processor + * [HHH-18697] - JPA 3.2 spec compliance for uppercasing of names in Hibernate Processor + * [HHH-18696] - @Find method for single @NaturalId field + * [HHH-18692] - Hibernate attempts to close batched statements multiple times + * [HHH-18681] - InterpretationException executing subquery in case-when : o.h.query.sqm.tree.select.SqmSelection.getExpressible() is null + * [HHH-18675] - Self-referencing many-to-many relation on generic entity gives NullPointerException in mapping + * [HHH-18669] - NullPointerException in the AgroalConnectionProvider + * [HHH-18667] - Annotation processor leaks - OOME when used in Eclipse IDE + * [HHH-18658] - Inner join prevents finding an entity instance referencing an empty map + * [HHH-18645] - AssertionError in AbstractBatchEntitySelectFetchInitializer#registerToBatchFetchQueue + * [HHH-18642] - DB2: select from new table with identity column not working when missing read permission + * [HHH-18635] - Avoid using `bigdatetime` column type on Sybase jconn when not necessary + * [HHH-18632] - Concurrency issue with AbstractEntityPersister#nonLazyPropertyLoadPlansByName + * [HHH-18631] - AssertionError when loading an entity after removing another, associated entity + * [HHH-18628] - Regression: Unable to determine TableReference + * [HHH-18617] - Fetching unowned side of bidirectional OneToOne mappings including tenant identifier triggers EntityFilteredException + * [HHH-18614] - TransientObjectException: session.update() does not save new entities in OneToMany relation when using bytecode enhancement + * [HHH-18608] - NPE in EntityInitializerImpl.resolveInstanceSubInitializers + * [HHH-18596] - ValueHandlingMode hack in query pagination + * [HHH-18582] - Mapping array of arrays with @JdbcTypeCode(SqlTypes.ARRAY) causes NPE + * [HHH-18575] - IN predicate with numeric/decimal parameter types leads to Binding is multi-valued; illegal call to #getBindValue + * [HHH-18564] - Literal expressions using AttributeConverters stopped working in hibernate 6 + * [HHH-18551] - Memory leak caused by AbstractArrayJavaType#getRecommendedJdbcType + * [HHH-18515] - Unrecognized discriminator value exception when running native query on entity with discriminator column + * [HHH-18513] - Session Metrics for pre-partial-flushes are wrong + * [HHH-18500] - Gradle plugin crashes on module-info when extended enhancement is set + * [HHH-18494] - UnknownTableReferenceException in native query with placeholder when entity contains a to-one with a join table + * [HHH-18491] - Resuming null transaction in JtaIsolationDelegate + * [HHH-18471] - Since 6.2.2 Dialect SybaseAnywhereDialect does not render table names in the selection query + * [HHH-18450] - Inconsistent "SELECT 1" versus "SELECT ?1" with result type Object[] + * [HHH-18409] - In 6.5.2.Final, byte[] instance variables annotated with @NaturalId cannot be found with a natural ID query (regression from Hibernate 5.6.15.Final) + * [HHH-18389] - `merge()` with `orphanRemoval = true` leads to "HibernateException: A collection with cascade="all-delete-orphan" was no longer referenced by the owning entity instance" + * [HHH-18321] - SqlTreeCreationException when using sum/diff with coalesce on properties of embeddable component + * [HHH-18131] - Composite identifiers with associations stopped working with @IdClass + * [HHH-17739] - unhelpful CCE for field with unsupported collection type + * [HHH-16572] - Skip enhancement for PROPERTY attributes with mismatched field and method names + +** Improvement + * [HHH-18698] - respect @Nonnull annotation applied to parameters of @Find method + * [HHH-18654] - Change setting docs to use an asciidoc section per setting (User Guide) + * [HHH-18640] - Add note to migration guide about @Table and subclasses + + +Changes in 6.6.1.Final (September 17, 2024) +------------------------------------------------------------------------------------------------------------------------ + +https://hibernate.atlassian.net/projects/HHH/versions/32340 + +** Bug + * [HHH-18571] - Entities and collections with batch size 1 are treated as batchable + * [HHH-18565] - Bytecode enhancement, assertion error on reloading *toOne entities + * [HHH-18560] - DB2iDialect executes incompatible query in combination with @AuditJoinTable mapping + * [HHH-18558] - Informix UUID type support + * [HHH-18556] - Expressions.nullExpresion() in querydsl result in NPE in SqmExpressible + * [HHH-18531] - Informix timestamp literal error + * [HHH-18524] - @IdClass and composite key generation not working with meta-annotated @IdGenerationType + * [HHH-18518] - MySQLDialect fails to detect version on azure flexible server + * [HHH-18511] - ArrayIndexOutOfBoundsException in ImmutableFetchList + * [HHH-18506] - Flush performance degradation due to itable stubs + * [HHH-18503] - ID select criteria query fails with joined + discriminator inheritance when entity has subtypes + * [HHH-18502] - Type conversion error due to surprising javac method selection in SqmSubQuery#in + * [HHH-18493] - Resolving already initialized collection elements leads to assertion error + * [HHH-18490] - Static metamodel contains wrong type for Embeddable with generic attribute extending from mapped superclass + * [HHH-18489] - Lazy, unowned one-to-one associations get loaded eagerly in queries - even with bytecode enhancement + * [HHH-18487] - Error: Detached entity wiht id X has an uninitialized version value '1'' + * [HHH-18486] - Unable to add Persist the object because of EntityNameResolver being ignored + * [HHH-18484] - Update fails when a many-to-one property on an entity with a generated value is changed + * [HHH-18480] - ClassCastException when updating a Blob with Oracle + * [HHH-18478] - Cannot invoke "org.hibernate.persister.entity.EntityPersister.getSubclassId()" because "data.concreteDescriptor" is null when reloading entity from query cache + * [HHH-18476] - Fetchable scrolling with KeyToOne leads to assertion error + * [HHH-18472] - Fix memory issues for release job + * [HHH-18470] - Duplicate foreign key names generated for inheritance type TABLE_PER_CLASS + * [HHH-18469] - Assertion Error when fetch joining complex entities structure + * [HHH-18466] - Regression from 5.6.15.Final where a mutable natural IDs query cannot find an entity + * [HHH-18447] - cast as boolean broken in Hibernate 6 + * [HHH-18445] - Embeddable as Java Record has wrong order of columns + * [HHH-18439] - NullPointerException when access data from query cache + * [HHH-18436] - Wrong order-by rendering order for nested collections if loaded with entity graph + * [HHH-18414] - Duplicated filter applied to 'find()' method + * [HHH-18400] - AttributeConverter causing type-check failure when comparing Date field to parameter + * [HHH-18353] - ArrayConstructorFunction comparing argument types by reference causes FunctionArgumentException + * [HHH-18337] - SequenceStyleGenerator not respecting physical naming strategy + * [HHH-18282] - generate error sql in case of @DiscriminatorValue("not null") and the entity is superclass of other entity + * [HHH-18174] - Hibernate polymorphic join in complex subquery not using discriminator + * [HHH-18103] - MappedSuperclass handling for Embeddable is broken in metamodel + +** Improvement + * [HHH-18625] - Add Configurable#create(GeneratorCreationContext) + * [HHH-18507] - allow overwriting the default db image with environment variable in docker_db.sh + * [HHH-18459] - Add SingleStore community dialect + * [HHH-17646] - JOINED @Inheritance leads to bad performance in Hibernate 6 + +** Task + * [HHH-18612] - Avoid writing files to classpath in tests to avoid spurious failures + + Changes in 6.6.0.Final (August 08, 2024) ------------------------------------------------------------------------------------------------------------------------ diff --git a/ci/build.sh b/ci/build.sh index 826347a0a112..fa944dd5b82f 100755 --- a/ci/build.sh +++ b/ci/build.sh @@ -56,6 +56,10 @@ elif [ "$RDBMS" == "oracle_db23c" ]; then export SERVICE=$(echo $INFO | jq -r '.database' | jq -r '.service') # I have no idea why, but these tests don't seem to work on CI... goal="-Pdb=oracle_cloud_db23c -DrunID=$RUNID -DdbHost=$HOST -DdbService=$SERVICE" +# OTP +elif [ "$RDBMS" == "autonomous-transaction-processing-serverless-19c" ] || [ "$RDBMS" == "autonomous-transaction-processing-serverless-26ai" ] || [ "$RDBMS" == "autonomous-transaction-processing-serverless" ] || [ "$RDBMS" == "base-database-service-19c" ] || [ "$RDBMS" == "base-database-service-21c" ] || [ "$RDBMS" == "base-database-service-26ai" ]; then + echo "Managing OTP Database..." + goal="-Pdb=oracle_test_pilot_database -DrunID=$RUNID -DdbPassword=$TESTPILOT_PASSWORD -DdbConnectionStringSuffix=$TESTPILOT_CONNECTION_STRING_SUFFIX" elif [ "$RDBMS" == "db2" ]; then goal="-Pdb=db2_ci" elif [ "$RDBMS" == "db2_10_5" ]; then diff --git a/ci/jpa-3.1-tck.Jenkinsfile b/ci/jpa-3.1-tck.Jenkinsfile index 87b18513bb00..4ad1006e6824 100644 --- a/ci/jpa-3.1-tck.Jenkinsfile +++ b/ci/jpa-3.1-tck.Jenkinsfile @@ -1,4 +1,4 @@ -@Library('hibernate-jenkins-pipeline-helpers@1.13') _ +@Library('hibernate-jenkins-pipeline-helpers') _ // Avoid running the pipeline on branch indexing if (currentBuild.getBuildCauses().toString().contains('BranchIndexingCause')) { @@ -6,106 +6,103 @@ if (currentBuild.getBuildCauses().toString().contains('BranchIndexingCause')) { currentBuild.result = 'NOT_BUILT' return } -def throttleCount -// Don't build the TCK on PRs, unless they use the tck label -if ( env.CHANGE_ID != null ) { - if ( !pullRequest.labels.contains( 'tck' ) ) { - print "INFO: Build skipped because pull request doesn't have 'tck' label" - return - } - throttleCount = 20 -} -else { - throttleCount = 1 +// This is a limited maintenance branch, so don't run this on pushes to the branch, only on PRs +if ( !env.CHANGE_ID ) { + print "INFO: Build skipped because this job should only run for pull request, not for branch pushes" + currentBuild.result = 'NOT_BUILT' + return } pipeline { - agent { - label 'LongDuration' - } + agent none tools { jdk 'OpenJDK 11 Latest' } options { - rateLimitBuilds(throttle: [count: throttleCount, durationName: 'day', userBoost: true]) buildDiscarder(logRotator(numToKeepStr: '3', artifactNumToKeepStr: '3')) disableConcurrentBuilds(abortPrevious: true) } parameters { choice(name: 'IMAGE_JDK', choices: ['jdk11'], description: 'The JDK base image version to use for the TCK image.') - string(name: 'TCK_VERSION', defaultValue: '3.1.5', description: 'The version of the Jakarta JPA TCK i.e. `2.2.0` or `3.0.1`') - string(name: 'TCK_SHA', defaultValue: '01072e6bdf56f0f8818290b8819f492ac95bb83fab14070d36aa7158a4f5eeed', description: 'The SHA256 of the Jakarta JPA TCK that is distributed under https://download.eclipse.org/jakartaee/persistence/3.1/jakarta-persistence-tck-${TCK_VERSION}.zip.sha256') + string(name: 'TCK_VERSION', defaultValue: '3.1.6', description: 'The version of the Jakarta JPA TCK i.e. `2.2.0` or `3.0.1`') + string(name: 'TCK_SHA', defaultValue: '790ca7a2a95ea098cfedafa2689c0d7a379fa62c74fed9505dd23191292f59fe', description: 'The SHA256 of the Jakarta JPA TCK that is distributed under https://download.eclipse.org/jakartaee/persistence/3.1/jakarta-persistence-tck-${TCK_VERSION}.zip.sha256') string(name: 'TCK_URL', defaultValue: '', description: 'The URL from which to download the TCK ZIP file. Only needed for testing staged builds. Ensure the TCK_VERSION variable matches the ZIP file name suffix.') booleanParam(name: 'NO_SLEEP', defaultValue: true, description: 'Whether the NO_SLEEP patch should be applied to speed up the TCK execution') } stages { - stage('Build') { + stage('Checks') { steps { - script { - docker.withRegistry('https://index.docker.io/v1/', 'hibernateci.hub.docker.com') { - docker.image('openjdk:11-jdk').pull() - } - } - dir('hibernate') { - checkout scm - sh './gradlew publishToMavenLocal -PmavenMirror=nexus-load-balancer-c4cf05fd92f43ef8.elb.us-east-1.amazonaws.com -DjakartaJpaVersion=3.1.0' - script { - env.HIBERNATE_VERSION = sh ( - script: "grep hibernateVersion gradle/version.properties|cut -d'=' -f2", - returnStdout: true - ).trim() - } - } - dir('tck') { - checkout changelog: false, poll: false, scm: [$class: 'GitSCM', branches: [[name: '*/main']], extensions: [], userRemoteConfigs: [[url: 'https://github.com/hibernate/jakarta-tck-runner.git']]] - script { - if ( params.TCK_URL == null || params.TCK_URL.isEmpty() ) { - sh "cd jpa-3.1; docker build -f Dockerfile.${params.IMAGE_JDK} -t jakarta-tck-runner --build-arg TCK_VERSION=${params.TCK_VERSION} --build-arg TCK_SHA=${params.TCK_SHA} ." - } - else { - sh "cd jpa-3.1; docker build -f Dockerfile.${params.IMAGE_JDK} -t jakarta-tck-runner --build-arg TCK_VERSION=${params.TCK_VERSION} --build-arg TCK_SHA=${params.TCK_SHA} --build-arg TCK_URL=${params.TCK_URL} ." - } - } - } - } - } - stage('Run TCK') { - steps { - sh """ \ - rm -Rf ./results - docker rm -f tck || true - docker volume rm -f tck-vol || true - docker volume create tck-vol - docker run -v ~/.m2/repository/org/hibernate:/root/.m2/repository/org/hibernate:z -v tck-vol:/tck/persistence-tck/tmp/:z -e NO_SLEEP=${params.NO_SLEEP} -e HIBERNATE_VERSION=$HIBERNATE_VERSION --name tck jakarta-tck-runner - docker cp tck:/tck/persistence-tck/tmp/ ./results - """ - archiveArtifacts artifacts: 'results/**' - script { - failures = sh ( - script: """ \ - set +x - while read line; do - if [[ "\$line" != *"Passed." ]]; then - echo "\$line" - fi - done .*@${env.HIBERNATE_VERSION}@' pom.xml", returnStatus: true) + if ( sedStatus != 0 ) { + throw new IllegalArgumentException( "Unable to replace hibernate version in Quarkus pom. Got exit code $sedStatus" ) + } + } + // Need to override the default maven configuration this way, because there is no other way to do it + sh "sed -i 's/-Xmx5g/-Xmx2048m/' ./.mvn/jvm.config" + sh "echo -e '\\n-XX:MaxMetaspaceSize=1024m'>>./.mvn/jvm.config" + withMaven(mavenLocalRepo: env.WORKSPACE + '/.m2repository', publisherStrategy:'EXPLICIT') { + sh "./mvnw -pl !docs -Dquickly install" + // Need to kill the gradle daemons started during the Maven install run + sh "sudo pkill -f '.*GradleDaemon.*' || true" + // Need to override the default maven configuration this way, because there is no other way to do it + sh "sed -i 's/-Xmx2048m/-Xmx1340m/' ./.mvn/jvm.config" + sh "sed -i 's/MaxMetaspaceSize=1024m/MaxMetaspaceSize=512m/' ./.mvn/jvm.config" + def excludes = "'!integration-tests/kafka-oauth-keycloak,!integration-tests/kafka-sasl-elytron,!integration-tests/hibernate-search-orm-opensearch,!integration-tests/maven,!integration-tests/quartz,!integration-tests/reactive-messaging-kafka,!integration-tests/resteasy-reactive-kotlin/standard,!integration-tests/opentelemetry-reactive-messaging,!integration-tests/virtual-threads/kafka-virtual-threads,!integration-tests/smallrye-jwt-oidc-webapp,!extensions/oidc-db-token-state-manager/deployment,!docs'" + sh "TESTCONTAINERS_RYUK_CONTAINER_PRIVILEGED=true ./mvnw -Dinsecure.repositories=WARN -pl :quarkus-hibernate-orm -amd -pl ${excludes} verify -Dstart-containers -Dtest-containers -Dskip.gradle.build" + } + } + } + } + } + } + post { + always { + notifyBuildResult maintainers: "andrea@hibernate.org steve@hibernate.org christian.beikov@gmail.com mbellade@redhat.com" + } + } +} diff --git a/ci/release/Jenkinsfile b/ci/release/Jenkinsfile index 0304265957d2..54058f4f2a2e 100644 --- a/ci/release/Jenkinsfile +++ b/ci/release/Jenkinsfile @@ -9,7 +9,7 @@ /* * See https://github.com/hibernate/hibernate-jenkins-pipeline-helpers */ -@Library('hibernate-jenkins-pipeline-helpers@1.17') _ +@Library('hibernate-jenkins-pipeline-helpers') _ import org.hibernate.jenkins.pipeline.helpers.version.Version @@ -17,11 +17,10 @@ import org.hibernate.jenkins.pipeline.helpers.version.Version // Global build configuration env.PROJECT = "orm" env.JIRA_KEY = "HHH" -def RELEASE_ON_PUSH = false // Set to `true` *only* on branches where you want a release on each push. +def RELEASE_ON_SCHEDULE = true // Set to `true` *only* on branches where you want a scheduled release. print "INFO: env.PROJECT = ${env.PROJECT}" print "INFO: env.JIRA_KEY = ${env.JIRA_KEY}" -print "INFO: RELEASE_ON_PUSH = ${RELEASE_ON_PUSH}" // -------------------------------------------- // Build conditions @@ -34,10 +33,17 @@ if (currentBuild.getBuildCauses().toString().contains('BranchIndexingCause')) { } def manualRelease = currentBuild.getBuildCauses().toString().contains( 'UserIdCause' ) +def cronRelease = currentBuild.getBuildCauses().toString().contains( 'TimerTriggerCause' ) // Only do automatic release on branches where we opted in -if ( !manualRelease && !RELEASE_ON_PUSH ) { - print "INFO: Build skipped because automated releases are disabled on this branch. See constant RELEASE_ON_PUSH in ci/release/Jenkinsfile" +if ( !manualRelease && !cronRelease ) { + print "INFO: Build skipped because automated releases on push are disabled on this branch." + currentBuild.result = 'NOT_BUILT' + return +} + +if ( !manualRelease && cronRelease && !RELEASE_ON_SCHEDULE ) { + print "INFO: Build skipped because automated releases are disabled on this branch. See constant RELEASE_ON_SCHEDULE in ci/release/Jenkinsfile" currentBuild.result = 'NOT_BUILT' return } @@ -58,14 +64,17 @@ def checkoutReleaseScripts() { pipeline { agent { - label 'Worker&&Containers' + label 'Release' + } + triggers { + // Run every week Sunday midnight + cron('0 0 * * 0') } tools { jdk 'OpenJDK 11 Latest' } options { buildDiscarder logRotator(daysToKeepStr: '30', numToKeepStr: '10') - rateLimitBuilds(throttle: [count: 1, durationName: 'day', userBoost: true]) disableConcurrentBuilds(abortPrevious: false) preserveStashes() } @@ -89,9 +98,13 @@ pipeline { ) } stages { - stage('Release check') { + stage('Check') { steps { script { + print "INFO: params.RELEASE_VERSION = ${params.RELEASE_VERSION}" + print "INFO: params.DEVELOPMENT_VERSION = ${params.DEVELOPMENT_VERSION}" + print "INFO: params.RELEASE_DRY_RUN? = ${params.RELEASE_DRY_RUN}" + checkoutReleaseScripts() def currentVersion = Version.parseDevelopmentVersion( sh( @@ -107,7 +120,9 @@ pipeline { echo "Release was requested manually" if ( !params.RELEASE_VERSION ) { - throw new IllegalArgumentException( 'Missing value for parameter RELEASE_VERSION. This parameter must be set explicitly to prevent mistakes.' ) + throw new IllegalArgumentException( + 'Missing value for parameter RELEASE_VERSION. This parameter must be set explicitly to prevent mistakes.' + ) } releaseVersion = Version.parseReleaseVersion( params.RELEASE_VERSION ) @@ -118,12 +133,15 @@ pipeline { else { echo "Release was triggered automatically" - // Avoid doing an automatic release for commits from a release - def lastCommitter = sh(script: 'git show -s --format=\'%an\'', returnStdout: true) - def secondLastCommitter = sh(script: 'git show -s --format=\'%an\' HEAD~1', returnStdout: true) - if (lastCommitter == 'Hibernate-CI' && secondLastCommitter == 'Hibernate-CI') { - print "INFO: Automatic release skipped because last commits were for the previous release" - currentBuild.result = 'ABORTED' + // Avoid doing an automatic release if there are no "releasable" commits since the last release (see release scripts for determination) + def releasableCommitCount = sh( + script: ".release/scripts/count-releasable-commits.sh ${env.PROJECT}", + returnStdout: true + ).trim().toInteger() + if ( releasableCommitCount <= 0 ) { + print "INFO: Automatic release skipped because no releasable commits were pushed since the previous release" + currentBuild.getRawBuild().getExecutor().interrupt(Result.NOT_BUILT) + sleep(1) // Interrupt is not blocking and does not take effect immediately. return } @@ -148,13 +166,14 @@ pipeline { env.RELEASE_VERSION = releaseVersion.toString() env.DEVELOPMENT_VERSION = developmentVersion.toString() env.SCRIPT_OPTIONS = params.RELEASE_DRY_RUN ? "-d" : "" + env.JRELEASER_DRY_RUN = params.RELEASE_DRY_RUN // Determine version id to check if Jira version exists sh ".release/scripts/determine-jira-version-id.sh ${env.JIRA_KEY} ${releaseVersion.withoutFinalQualifier}" } } } - stage('Release prepare') { + stage('Prepare') { steps { script { checkoutReleaseScripts() @@ -163,31 +182,24 @@ pipeline { configFile(fileId: 'release.config.ssh', targetLocation: "${env.HOME}/.ssh/config"), configFile(fileId: 'release.config.ssh.knownhosts', targetLocation: "${env.HOME}/.ssh/known_hosts") ]) { - withCredentials([ - usernamePassword(credentialsId: 'ossrh.sonatype.org', passwordVariable: 'OSSRH_PASSWORD', usernameVariable: 'OSSRH_USER'), - usernamePassword(credentialsId: 'gradle-plugin-portal-api-key', passwordVariable: 'PLUGIN_PORTAL_PASSWORD', usernameVariable: 'PLUGIN_PORTAL_USERNAME'), - file(credentialsId: 'release.gpg.private-key', variable: 'RELEASE_GPG_PRIVATE_KEY_PATH'), - string(credentialsId: 'release.gpg.passphrase', variable: 'RELEASE_GPG_PASSPHRASE') - ]) { - sshagent(['ed25519.Hibernate-CI.github.com', 'hibernate.filemgmt.jboss.org', 'hibernate-ci.frs.sourceforge.net']) { - // set release version - // update changelog from JIRA - // tags the version - // changes the version to the provided development version - withEnv([ - "BRANCH=${env.GIT_BRANCH}", - // Increase the amount of memory for this part since asciidoctor doc rendering consumes a lot of metaspace - "GRADLE_OPTS=-Dorg.gradle.jvmargs='-Dlog4j2.disableJmx -Xmx4g -XX:MaxMetaspaceSize=768m -XX:+HeapDumpOnOutOfMemoryError -Duser.language=en -Duser.country=US -Duser.timezone=UTC -Dfile.encoding=UTF-8'" - ]) { - sh ".release/scripts/prepare-release.sh ${env.PROJECT} ${env.RELEASE_VERSION} ${env.DEVELOPMENT_VERSION}" - } + sshagent(['ed25519.Hibernate-CI.github.com', 'hibernate-ci.frs.sourceforge.net']) { + // set release version + // update changelog from JIRA + // tags the version + // changes the version to the provided development version + withEnv([ + "DISABLE_REMOTE_GRADLE_CACHE=true", + // Increase the amount of memory for this part since asciidoctor doc rendering consumes a lot of metaspace + "GRADLE_OPTS=-Dorg.gradle.jvmargs='-Dlog4j2.disableJmx -Xmx4g -XX:MaxMetaspaceSize=768m -XX:+HeapDumpOnOutOfMemoryError -Duser.language=en -Duser.country=US -Duser.timezone=UTC -Dfile.encoding=UTF-8'" + ]) { + sh ".release/scripts/prepare-release.sh -j -b ${env.GIT_BRANCH} -v ${env.DEVELOPMENT_VERSION} ${env.PROJECT} ${env.RELEASE_VERSION}" } } } } } } - stage('Publish release') { + stage('Publish') { steps { script { checkoutReleaseScripts() @@ -197,23 +209,42 @@ pipeline { configFile(fileId: 'release.config.ssh.knownhosts', targetLocation: "${env.HOME}/.ssh/known_hosts") ]) { withCredentials([ - usernamePassword(credentialsId: 'ossrh.sonatype.org', passwordVariable: 'OSSRH_PASSWORD', usernameVariable: 'OSSRH_USER'), - usernamePassword(credentialsId: 'gradle-plugin-portal-api-key', passwordVariable: 'PLUGIN_PORTAL_PASSWORD', usernameVariable: 'PLUGIN_PORTAL_USERNAME'), + usernamePassword(credentialsId: 'central.sonatype.com', passwordVariable: 'JRELEASER_MAVENCENTRAL_TOKEN', usernameVariable: 'JRELEASER_MAVENCENTRAL_USERNAME'), + // https://docs.gradle.org/current/userguide/publishing_gradle_plugins.html#account_setup + usernamePassword(credentialsId: 'gradle-plugin-portal-api-key', passwordVariable: 'GRADLE_PUBLISH_SECRET', usernameVariable: 'GRADLE_PUBLISH_KEY'), + gitUsernamePassword(credentialsId: 'username-and-token.Hibernate-CI.github.com', gitToolName: 'Default'), file(credentialsId: 'release.gpg.private-key', variable: 'RELEASE_GPG_PRIVATE_KEY_PATH'), - string(credentialsId: 'release.gpg.passphrase', variable: 'RELEASE_GPG_PASSPHRASE'), - gitUsernamePassword(credentialsId: 'username-and-token.Hibernate-CI.github.com', gitToolName: 'Default') + string(credentialsId: 'release.gpg.passphrase', variable: 'JRELEASER_GPG_PASSPHRASE'), + string(credentialsId: 'Hibernate-CI.github.com', variable: 'JRELEASER_GITHUB_TOKEN') ]) { - sshagent(['ed25519.Hibernate-CI.github.com', 'hibernate.filemgmt.jboss.org', 'hibernate-ci.frs.sourceforge.net']) { + sshagent(['ed25519.Hibernate-CI.github.com', 'jenkins.in.relation.to', 'hibernate-ci.frs.sourceforge.net']) { // performs documentation upload and Sonatype release // push to github - sh ".release/scripts/publish.sh ${env.SCRIPT_OPTIONS} ${env.PROJECT} ${env.RELEASE_VERSION} ${env.DEVELOPMENT_VERSION} ${env.GIT_BRANCH}" + withEnv([ + "DISABLE_REMOTE_GRADLE_CACHE=true" + ]) { + def ghReleaseNote = sh(script: 'realpath -e release_notes.md 2>/dev/null', returnStdout: true).trim() + + sh ".release/scripts/publish.sh -j ${ghReleaseNote != '' ? '--notes=' + ghReleaseNote : ''} ${env.SCRIPT_OPTIONS} ${env.PROJECT} ${env.RELEASE_VERSION} ${env.DEVELOPMENT_VERSION} ${env.GIT_BRANCH} " + } } } } } } } - stage('Website release') { + stage('Release on Jira') { + steps { + script { + checkoutReleaseScripts() + + withCredentials([string(credentialsId: 'release-webhook.hibernate.atlassian.net', variable: 'JIRA_WEBHOOK_SECRET')]) { + sh ".release/scripts/jira-release.sh ${env.SCRIPT_OPTIONS} ${env.PROJECT} ${env.RELEASE_VERSION} ${env.DEVELOPMENT_VERSION}" + } + } + } + } + stage('Update website') { steps { script { checkoutReleaseScripts() @@ -225,14 +256,17 @@ pipeline { withCredentials([ gitUsernamePassword(credentialsId: 'username-and-token.Hibernate-CI.github.com', gitToolName: 'Default') ]) { - sshagent( ['ed25519.Hibernate-CI.github.com', 'hibernate.filemgmt.jboss.org', 'hibernate-ci.frs.sourceforge.net'] ) { + sshagent( ['ed25519.Hibernate-CI.github.com'] ) { dir( '.release/hibernate.org' ) { - checkout scmGit( - branches: [[name: '*/production']], - extensions: [], - userRemoteConfigs: [[credentialsId: 'ed25519.Hibernate-CI.github.com', url: 'https://github.com/hibernate/hibernate.org.git']] - ) - sh "../scripts/website-release.sh ${env.SCRIPT_OPTIONS} ${env.PROJECT} ${env.RELEASE_VERSION}" + // Lock to avoid rejected pushes when multiple releases try to clone-commit-push + lock('hibernate.org-git') { + checkout scmGit( + branches: [[name: '*/production']], + extensions: [], + userRemoteConfigs: [[credentialsId: 'ed25519.Hibernate-CI.github.com', url: 'https://github.com/hibernate/hibernate.org.git']] + ) + sh "../scripts/website-release.sh ${env.SCRIPT_OPTIONS} ${env.PROJECT} ${env.RELEASE_VERSION}" + } } } } @@ -240,16 +274,6 @@ pipeline { } } } - stage('GitHub release') { - steps { - script { - checkoutReleaseScripts() - withCredentials([string(credentialsId: 'Hibernate-CI.github.com', variable: 'GITHUB_API_TOKEN')]) { - sh ".release/scripts/github-release.sh ${env.SCRIPT_OPTIONS} ${env.PROJECT} ${env.RELEASE_VERSION}" - } - } - } - } } post { always { @@ -258,4 +282,4 @@ pipeline { } } } -} \ No newline at end of file +} diff --git a/ci/snapshot-publish.Jenkinsfile b/ci/snapshot-publish.Jenkinsfile index 24cd67d92f1b..9d7a2fb9ff35 100644 --- a/ci/snapshot-publish.Jenkinsfile +++ b/ci/snapshot-publish.Jenkinsfile @@ -10,9 +10,17 @@ if (currentBuild.getBuildCauses().toString().contains('BranchIndexingCause')) { return } +def checkoutReleaseScripts() { + dir('.release/scripts') { + checkout scmGit(branches: [[name: '*/main']], extensions: [], + userRemoteConfigs: [[credentialsId: 'ed25519.Hibernate-CI.github.com', + url: 'https://github.com/hibernate/hibernate-release-scripts.git']]) + } +} + pipeline { agent { - label 'Fedora' + label 'Release' } tools { jdk 'OpenJDK 11 Latest' @@ -30,23 +38,29 @@ pipeline { } stage('Publish') { steps { - withCredentials([ - usernamePassword(credentialsId: 'ossrh.sonatype.org', usernameVariable: 'hibernatePublishUsername', passwordVariable: 'hibernatePublishPassword'), - usernamePassword(credentialsId: 'plugins.gradle.org', usernameVariable: 'hibernatePluginPortalUsername', passwordVariable: 'hibernatePluginPortalPassword'), - string(credentialsId: 'ge.hibernate.org-access-key', variable: 'DEVELOCITY_ACCESS_KEY'), - string(credentialsId: 'release.gpg.passphrase', variable: 'SIGNING_PASS'), - file(credentialsId: 'release.gpg.private-key', variable: 'SIGNING_KEYRING') - ]) { - sh '''./gradlew clean publish \ - -PhibernatePublishUsername=$hibernatePublishUsername \ - -PhibernatePublishPassword=$hibernatePublishPassword \ - -Pgradle.publish.key=$hibernatePluginPortalUsername \ - -Pgradle.publish.secret=$hibernatePluginPortalPassword \ - --no-scan \ - -DsigningPassword=$SIGNING_PASS \ - -DsigningKeyFile=$SIGNING_KEYRING \ - ''' - } + script { + withCredentials([ + // https://github.com/gradle-nexus/publish-plugin#publishing-to-maven-central-via-sonatype-ossrh + // https://docs.gradle.org/current/samples/sample_publishing_credentials.html#:~:text=via%20environment%20variables + usernamePassword(credentialsId: 'central.sonatype.com', passwordVariable: 'ORG_GRADLE_PROJECT_snapshotsPassword', usernameVariable: 'ORG_GRADLE_PROJECT_snapshotsUsername'), + string(credentialsId: 'Hibernate-CI.github.com', variable: 'JRELEASER_GITHUB_TOKEN'), + // https://docs.gradle.org/current/userguide/publishing_gradle_plugins.html#account_setup + usernamePassword(credentialsId: 'gradle-plugin-portal-api-key', passwordVariable: 'GRADLE_PUBLISH_SECRET', usernameVariable: 'GRADLE_PUBLISH_KEY'), + gitUsernamePassword(credentialsId: 'username-and-token.Hibernate-CI.github.com', gitToolName: 'Default') + ]) { + withEnv([ + "DISABLE_REMOTE_GRADLE_CACHE=true" + ]) { + checkoutReleaseScripts() + def version = sh( + script: ".release/scripts/determine-current-version.sh orm", + returnStdout: true + ).trim() + echo "Current version: '${version}'" + sh "bash -xe .release/scripts/snapshot-deploy.sh orm ${version}" + } + } + } } } } @@ -57,4 +71,4 @@ pipeline { } } } -} \ No newline at end of file +} diff --git a/databases/mariadb/matrix.gradle b/databases/mariadb/matrix.gradle index f66f5cacb300..b72fdee03569 100644 --- a/databases/mariadb/matrix.gradle +++ b/databases/mariadb/matrix.gradle @@ -4,4 +4,4 @@ * License: GNU Lesser General Public License (LGPL), version 2.1 or later. * See the lgpl.txt file in the root directory or . */ -jdbcDependency 'org.mariadb.jdbc:mariadb-java-client:3.4.0' +jdbcDependency 'org.mariadb.jdbc:mariadb-java-client:3.5.1' diff --git a/design/working/6.0-posts.adoc b/design/working/6.0-posts.adoc index 690f6780b8a7..d0dec78fade3 100644 --- a/design/working/6.0-posts.adoc +++ b/design/working/6.0-posts.adoc @@ -2,7 +2,7 @@ Steve Ebersole :awestruct-tags: ["Hibernate ORM"] :awestruct-layout: blog-post -:docs-url: https://docs.jboss.org/hibernate/orm/6.0 +:docs-url: https://docs.hibernate.org/orm/6.0 :javadocs-url: {docs-url}/javadocs :migration-guide-url: {docs-url}/migration-guide/migration-guide.html :user-guide-url: {docs-url}/userguide/html_single/Hibernate_User_Guide.html diff --git a/dialects.adoc b/dialects.adoc index ffbf79132468..159b1e2462d2 100644 --- a/dialects.adoc +++ b/dialects.adoc @@ -1,73 +1,6 @@ = Dialects -A dialect is a class that provides information about the specifics of a database and translators for the SQL dialect of the database. +The content of this file has moved to link:./documentation/src/main/asciidoc/dialect/index.adoc. +Going forward, it will be published in rendered form at https://docs.hibernate.org/stable/orm/dialect/. -== Supported dialects - -Hibernate supports a wide range of dialects out of the box. The following is list of officially supported databases: - -* Apache Derby -* Cockroach -* Google Spanner -* H2 -* HSQLDB -* IBM DB2 LUW -* IBM DB2 iSeries -* IBM DB2 z/OS -* MariaDB -* MySQL -* Oracle -* PostgreSQL -* Postgres Plus -* SAP HANA -* SQL Server -* Sybase ASE - -Usually, Hibernate supports at least the database version that is also still supported by the respective vendor. -In many cases though, Hibernate supports even older versions of the databases, -but the support for these versions is not guaranteed. - -Apart from the Hibernate team supported dialects, there are also community dialects. - -== Community dialects - -As of Hibernate 6.0, the Hibernate team decided to provide a clear way forward for community contributed dialects. -The `hibernate-core` artifact had many legacy dialects before 6.0 that were only tested and maintained on a best effort basis. - -More and more database vendors requested to integrate a dialect for their database and even provided a PR with a dialect, -but the Hibernate team didn't want to add new dialects for databases that might not have a wide adoption -or any automated testing into the `hibernate-core` artifact. Even though the dialect was supposedly maintained by the vendor, -the Hibernate team was burdened with reviewing questions, issues and PRs that relate to these dialects. - -To give database vendors and the community a clear way forward, the Hibernate team decided to introduce a new artifact, -called `hibernate-community-dialects` which is the new home for dialects that are maintained by vendors or individuals. -Starting with Hibernate 6.0 the `hibernate-core` artifact will only contain dialects that are supported and tested by the Hibernate team. -All the legacy dialects are moved to the `hibernate-community-dialects` artifact to have a clear separation based on the quality of the dialect. - -Issues with dialects in the `hibernate-community-dialects` are usually not considered by the Hibernate team, -as the community is responsible for providing fixes and improving the dialects for newer database versions or ORM capabilities. - -== Requirements for moving to hibernate-core - -If a database vendor wants their database dialect to be included in the `hibernate-core` artifact, -several requirements have to be fulfilled: - -* The vendor must provide access to a dedicated database server that can be used for testing -* The vendor must provide contact details to at least one employee who is mainly responsible for the maintenance of the dialect -* The responsible employee of the vendor must actively monitor and react to failures of the testsuite against the respective database -* The responsible employee of the vendor must ensure the testsuite is configured correctly in order for it to succeed on the respective database -* If the responsible employee of the vendor leaves the company, the vendor must provide contact details to a new responsible employee - -In case the responsible employee is unreachable for a longer period or issues with the dialect are not attended to in a timely manner, -the Hibernate team will move the dialect back to the `hibernate-community-dialects` artifact. - -The requirements for the database server are: - -* JDK 8 installed through e.g. `sudo yum install -y java-1.8.0-openjdk-devel` -* JDK 11 installed through e.g. `sudo yum install -y java-11-openjdk-devel` -* Git installed through e.g. `sudo yum install -y git` -* Access to the database through non-confidential credentials -* Access via SSH through confidential credentials - -Get in touch with the Hibernate team on https://hibernate.zulipchat.com/#narrow/stream/132096-hibernate-user[Zulip] -if you want to request the move of your dialect to hibernate-core. \ No newline at end of file +If you ended up here following a link, please ask whoever published that link to update it. \ No newline at end of file diff --git a/docker_db.sh b/docker_db.sh index 8cdbce7d9be3..459ccc605b06 100755 --- a/docker_db.sh +++ b/docker_db.sh @@ -1,18 +1,26 @@ #! /bin/bash -if command -v podman > /dev/null; then +if command -v docker > /dev/null; then + CONTAINER_CLI=$(command -v docker) + HEALTCHECK_PATH="{{.State.Health.Status}}" + PRIVILEGED_CLI="" + IS_PODMAN=false + if [[ "$(docker version | grep Podman)" == "" ]]; then + IS_DOCKER_RUNTIME=true + else + IS_DOCKER_RUNTIME=false + fi +else CONTAINER_CLI=$(command -v podman) HEALTCHECK_PATH="{{.State.Healthcheck.Status}}" + IS_PODMAN=true + IS_DOCKER_RUNTIME=false # Only use sudo for podman if command -v sudo > /dev/null; then PRIVILEGED_CLI="sudo" else PRIVILEGED_CLI="" fi -else - CONTAINER_CLI=$(command -v docker) - HEALTCHECK_PATH="{{.State.Health.Status}}" - PRIVILEGED_CLI="" fi mysql() { @@ -21,7 +29,7 @@ mysql() { mysql_8_0() { $CONTAINER_CLI rm -f mysql || true - $CONTAINER_CLI run --name mysql -e MYSQL_USER=hibernate_orm_test -e MYSQL_PASSWORD=hibernate_orm_test -e MYSQL_ROOT_PASSWORD=hibernate_orm_test -e MYSQL_DATABASE=hibernate_orm_test -e MYSQL_ROOT_PASSWORD=hibernate_orm_test -p3306:3306 -d docker.io/mysql:8.0.31 --character-set-server=utf8mb4 --collation-server=utf8mb4_0900_as_cs --skip-character-set-client-handshake --log-bin-trust-function-creators=1 --lower_case_table_names=2 + $CONTAINER_CLI run --name mysql -e MYSQL_USER=hibernate_orm_test -e MYSQL_PASSWORD=hibernate_orm_test -e MYSQL_ROOT_PASSWORD=hibernate_orm_test -e MYSQL_DATABASE=hibernate_orm_test -e MYSQL_ROOT_PASSWORD=hibernate_orm_test -p3306:3306 -d ${DB_IMAGE_MYSQL_8_0:-docker.io/mysql:8.0.31} --character-set-server=utf8mb4 --collation-server=utf8mb4_0900_as_cs --skip-character-set-client-handshake --log-bin-trust-function-creators=1 --lower_case_table_names=2 # Give the container some time to start OUTPUT= n=0 @@ -45,7 +53,7 @@ mysql_8_0() { mysql_8_1() { $CONTAINER_CLI rm -f mysql || true - $CONTAINER_CLI run --name mysql -e MYSQL_USER=hibernate_orm_test -e MYSQL_PASSWORD=hibernate_orm_test -e MYSQL_ROOT_PASSWORD=hibernate_orm_test -e MYSQL_DATABASE=hibernate_orm_test -e MYSQL_ROOT_PASSWORD=hibernate_orm_test -p3306:3306 -d docker.io/mysql:8.1.0 --character-set-server=utf8mb4 --collation-server=utf8mb4_0900_as_cs --skip-character-set-client-handshake --log-bin-trust-function-creators=1 --lower_case_table_names=2 + $CONTAINER_CLI run --name mysql -e MYSQL_USER=hibernate_orm_test -e MYSQL_PASSWORD=hibernate_orm_test -e MYSQL_ROOT_PASSWORD=hibernate_orm_test -e MYSQL_DATABASE=hibernate_orm_test -e MYSQL_ROOT_PASSWORD=hibernate_orm_test -p3306:3306 -d ${DB_IMAGE_MYSQL_8_1:-docker.io/mysql:8.1.0} --character-set-server=utf8mb4 --collation-server=utf8mb4_0900_as_cs --skip-character-set-client-handshake --log-bin-trust-function-creators=1 --lower_case_table_names=2 # Give the container some time to start OUTPUT= n=0 @@ -69,7 +77,7 @@ mysql_8_1() { mysql_8_2() { $CONTAINER_CLI rm -f mysql || true - $CONTAINER_CLI run --name mysql -e MYSQL_USER=hibernate_orm_test -e MYSQL_PASSWORD=hibernate_orm_test -e MYSQL_ROOT_PASSWORD=hibernate_orm_test -e MYSQL_DATABASE=hibernate_orm_test -e MYSQL_ROOT_PASSWORD=hibernate_orm_test -p3306:3306 -d docker.io/mysql:8.2.0 --character-set-server=utf8mb4 --collation-server=utf8mb4_0900_as_cs --skip-character-set-client-handshake --log-bin-trust-function-creators=1 --lower_case_table_names=2 + $CONTAINER_CLI run --name mysql -e MYSQL_USER=hibernate_orm_test -e MYSQL_PASSWORD=hibernate_orm_test -e MYSQL_ROOT_PASSWORD=hibernate_orm_test -e MYSQL_DATABASE=hibernate_orm_test -e MYSQL_ROOT_PASSWORD=hibernate_orm_test -p3306:3306 -d ${DB_IMAGE_MYSQL_8_2:-docker.io/mysql:8.2.0} --character-set-server=utf8mb4 --collation-server=utf8mb4_0900_as_cs --skip-character-set-client-handshake --log-bin-trust-function-creators=1 --lower_case_table_names=2 # Give the container some time to start OUTPUT= n=0 @@ -92,7 +100,7 @@ mysql_8_2() { } mariadb() { - mariadb_11_4 + mariadb_11_7 } mariadb_wait_until_start() @@ -116,31 +124,37 @@ mariadb_wait_until_start() mariadb_10_4() { $CONTAINER_CLI rm -f mariadb || true - $CONTAINER_CLI run --name mariadb -e MARIADB_USER=hibernate_orm_test -e MARIADB_PASSWORD=hibernate_orm_test -e MARIADB_DATABASE=hibernate_orm_test -e MARIADB_ROOT_PASSWORD=hibernate_orm_test -p3306:3306 -d docker.io/mariadb:10.4.33 --character-set-server=utf8mb4 --collation-server=utf8mb4_bin --skip-character-set-client-handshake --lower_case_table_names=2 + $CONTAINER_CLI run --name mariadb -e MARIADB_USER=hibernate_orm_test -e MARIADB_PASSWORD=hibernate_orm_test -e MARIADB_DATABASE=hibernate_orm_test -e MARIADB_ROOT_PASSWORD=hibernate_orm_test -p3306:3306 -d ${DB_IMAGE_MARIADB_10_4:-docker.io/mariadb:10.4.33} --character-set-server=utf8mb4 --collation-server=utf8mb4_bin --skip-character-set-client-handshake --lower_case_table_names=2 mariadb_wait_until_start } mariadb_10_11() { $CONTAINER_CLI rm -f mariadb || true - $CONTAINER_CLI run --name mariadb -e MARIADB_USER=hibernate_orm_test -e MARIADB_PASSWORD=hibernate_orm_test -e MARIADB_DATABASE=hibernate_orm_test -e MARIADB_ROOT_PASSWORD=hibernate_orm_test -p3306:3306 -d docker.io/mariadb:10.11.8 --character-set-server=utf8mb4 --collation-server=utf8mb4_bin --skip-character-set-client-handshake --lower_case_table_names=2 + $CONTAINER_CLI run --name mariadb -e MARIADB_USER=hibernate_orm_test -e MARIADB_PASSWORD=hibernate_orm_test -e MARIADB_DATABASE=hibernate_orm_test -e MARIADB_ROOT_PASSWORD=hibernate_orm_test -p3306:3306 -d ${DB_IMAGE_MARIADB_10_11:-docker.io/mariadb:10.11.8} --character-set-server=utf8mb4 --collation-server=utf8mb4_bin --skip-character-set-client-handshake --lower_case_table_names=2 mariadb_wait_until_start } mariadb_11_1() { $CONTAINER_CLI rm -f mariadb || true - $CONTAINER_CLI run --name mariadb -e MARIADB_USER=hibernate_orm_test -e MARIADB_PASSWORD=hibernate_orm_test -e MARIADB_DATABASE=hibernate_orm_test -e MARIADB_ROOT_PASSWORD=hibernate_orm_test -p3306:3306 -d docker.io/mariadb:11.1.2 --character-set-server=utf8mb4 --collation-server=utf8mb4_bin --skip-character-set-client-handshake --lower_case_table_names=2 + $CONTAINER_CLI run --name mariadb -e MARIADB_USER=hibernate_orm_test -e MARIADB_PASSWORD=hibernate_orm_test -e MARIADB_DATABASE=hibernate_orm_test -e MARIADB_ROOT_PASSWORD=hibernate_orm_test -p3306:3306 -d ${DB_IMAGE_MARIADB_11_1:-docker.io/mariadb:11.1.2} --character-set-server=utf8mb4 --collation-server=utf8mb4_bin --skip-character-set-client-handshake --lower_case_table_names=2 mariadb_wait_until_start } mariadb_11_4() { $CONTAINER_CLI rm -f mariadb || true - $CONTAINER_CLI run --name mariadb -e MARIADB_USER=hibernate_orm_test -e MARIADB_PASSWORD=hibernate_orm_test -e MARIADB_DATABASE=hibernate_orm_test -e MARIADB_ROOT_PASSWORD=hibernate_orm_test -p3306:3306 -d docker.io/mariadb:11.4.2 --character-set-server=utf8mb4 --collation-server=utf8mb4_bin --skip-character-set-client-handshake --lower_case_table_names=2 + $CONTAINER_CLI run --name mariadb -e MARIADB_USER=hibernate_orm_test -e MARIADB_PASSWORD=hibernate_orm_test -e MARIADB_DATABASE=hibernate_orm_test -e MARIADB_ROOT_PASSWORD=hibernate_orm_test -p3306:3306 -d ${DB_IMAGE_MARIADB_11_4:-docker.io/mariadb:11.4.2} --character-set-server=utf8mb4 --collation-server=utf8mb4_bin --skip-character-set-client-handshake --lower_case_table_names=2 + mariadb_wait_until_start +} + +mariadb_11_7() { + $CONTAINER_CLI rm -f mariadb || true + $CONTAINER_CLI run --name mariadb -e MARIADB_USER=hibernate_orm_test -e MARIADB_PASSWORD=hibernate_orm_test -e MARIADB_DATABASE=hibernate_orm_test -e MARIADB_ROOT_PASSWORD=hibernate_orm_test -p3306:3306 -d ${DB_IMAGE_MARIADB_11_7:-docker.io/mariadb:11.7-rc} --character-set-server=utf8mb4 --collation-server=utf8mb4_bin --skip-character-set-client-handshake --lower_case_table_names=2 mariadb_wait_until_start } mariadb_verylatest() { $CONTAINER_CLI rm -f mariadb || true - $CONTAINER_CLI run --name mariadb -e MARIADB_USER=hibernate_orm_test -e MARIADB_PASSWORD=hibernate_orm_test -e MARIADB_DATABASE=hibernate_orm_test -e MARIADB_ROOT_PASSWORD=hibernate_orm_test -p3306:3306 -d quay.io/mariadb-foundation/mariadb-devel:verylatest --character-set-server=utf8mb4 --collation-server=utf8mb4_bin --skip-character-set-client-handshake --lower_case_table_names=2 + $CONTAINER_CLI run --name mariadb -e MARIADB_USER=hibernate_orm_test -e MARIADB_PASSWORD=hibernate_orm_test -e MARIADB_DATABASE=hibernate_orm_test -e MARIADB_ROOT_PASSWORD=hibernate_orm_test -p3306:3306 -d ${DB_IMAGE_MARIADB_VERYLATEST:-quay.io/mariadb-foundation/mariadb-devel:verylatest} --character-set-server=utf8mb4 --collation-server=utf8mb4_bin --skip-character-set-client-handshake --lower_case_table_names=2 mariadb_wait_until_start } @@ -150,32 +164,32 @@ postgresql() { postgresql_12() { $CONTAINER_CLI rm -f postgres || true - $CONTAINER_CLI run --name postgres -e POSTGRES_USER=hibernate_orm_test -e POSTGRES_PASSWORD=hibernate_orm_test -e POSTGRES_DB=hibernate_orm_test -p5432:5432 -d docker.io/postgis/postgis:12-3.4 + $CONTAINER_CLI run --name postgres -e POSTGRES_USER=hibernate_orm_test -e POSTGRES_PASSWORD=hibernate_orm_test -e POSTGRES_DB=hibernate_orm_test -p5432:5432 -d ${DB_IMAGE_POSTGRESQL_12:-docker.io/postgis/postgis:12-3.4} $CONTAINER_CLI exec postgres bash -c '/usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y && apt install -y postgresql-12-pgvector && psql -U hibernate_orm_test -d hibernate_orm_test -c "create extension vector;"' } postgresql_13() { $CONTAINER_CLI rm -f postgres || true - $CONTAINER_CLI run --name postgres -e POSTGRES_USER=hibernate_orm_test -e POSTGRES_PASSWORD=hibernate_orm_test -e POSTGRES_DB=hibernate_orm_test -p5432:5432 -d docker.io/postgis/postgis:13-3.1 + $CONTAINER_CLI run --name postgres -e POSTGRES_USER=hibernate_orm_test -e POSTGRES_PASSWORD=hibernate_orm_test -e POSTGRES_DB=hibernate_orm_test -p5432:5432 -d ${DB_IMAGE_POSTGRESQL_13:-docker.io/postgis/postgis:13-3.1} $CONTAINER_CLI exec postgres bash -c '/usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y && apt install -y postgresql-13-pgvector && psql -U hibernate_orm_test -d hibernate_orm_test -c "create extension vector;"' } postgresql_14() { $CONTAINER_CLI rm -f postgres || true - $CONTAINER_CLI run --name postgres -e POSTGRES_USER=hibernate_orm_test -e POSTGRES_PASSWORD=hibernate_orm_test -e POSTGRES_DB=hibernate_orm_test -p5432:5432 -d docker.io/postgis/postgis:14-3.3 + $CONTAINER_CLI run --name postgres -e POSTGRES_USER=hibernate_orm_test -e POSTGRES_PASSWORD=hibernate_orm_test -e POSTGRES_DB=hibernate_orm_test -p5432:5432 -d ${DB_IMAGE_POSTGRESQL_14:-docker.io/postgis/postgis:14-3.3} $CONTAINER_CLI exec postgres bash -c '/usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y && apt install -y postgresql-14-pgvector && psql -U hibernate_orm_test -d hibernate_orm_test -c "create extension vector;"' } postgresql_15() { $CONTAINER_CLI rm -f postgres || true - $CONTAINER_CLI run --name postgres -e POSTGRES_USER=hibernate_orm_test -e POSTGRES_PASSWORD=hibernate_orm_test -e POSTGRES_DB=hibernate_orm_test -p5432:5432 --tmpfs /pgtmpfs:size=131072k -d docker.io/postgis/postgis:15-3.3 \ + $CONTAINER_CLI run --name postgres -e POSTGRES_USER=hibernate_orm_test -e POSTGRES_PASSWORD=hibernate_orm_test -e POSTGRES_DB=hibernate_orm_test -p5432:5432 --tmpfs /pgtmpfs:size=131072k -d ${DB_IMAGE_POSTGRESQL_15:-docker.io/postgis/postgis:15-3.3} \ -c fsync=off -c synchronous_commit=off -c full_page_writes=off -c shared_buffers=256MB -c maintenance_work_mem=256MB -c max_wal_size=1GB -c checkpoint_timeout=1d $CONTAINER_CLI exec postgres bash -c '/usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y && apt install -y postgresql-15-pgvector && psql -U hibernate_orm_test -d hibernate_orm_test -c "create extension vector;"' } postgresql_16() { $CONTAINER_CLI rm -f postgres || true - $CONTAINER_CLI run --name postgres -e POSTGRES_USER=hibernate_orm_test -e POSTGRES_PASSWORD=hibernate_orm_test -e POSTGRES_DB=hibernate_orm_test -p5432:5432 --tmpfs /pgtmpfs:size=131072k -d docker.io/postgis/postgis:16-3.4 \ + $CONTAINER_CLI run --name postgres -e POSTGRES_USER=hibernate_orm_test -e POSTGRES_PASSWORD=hibernate_orm_test -e POSTGRES_DB=hibernate_orm_test -p5432:5432 --tmpfs /pgtmpfs:size=131072k -d ${DB_IMAGE_POSTGRESQL_16:-docker.io/postgis/postgis:16-3.4} \ -c fsync=off -c synchronous_commit=off -c full_page_writes=off -c shared_buffers=256MB -c maintenance_work_mem=256MB -c max_wal_size=1GB -c checkpoint_timeout=1d $CONTAINER_CLI exec postgres bash -c '/usr/share/postgresql-common/pgdg/apt.postgresql.org.sh -y && apt install -y postgresql-16-pgvector && psql -U hibernate_orm_test -d hibernate_orm_test -c "create extension vector;"' } @@ -186,30 +200,42 @@ edb() { edb_12() { $CONTAINER_CLI rm -f edb || true - # 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:12 -f edb12.Dockerfile .) - $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 edb-test:12 + if [[ -z "${DB_IMAGE_EDB}" ]]; then + DB_IMAGE_EDB="edb-test:12" + # 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:12 -f edb12.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_14() { $CONTAINER_CLI rm -f edb || true - # 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:14 -f edb14.Dockerfile .) - $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 edb-test:14 + if [[ -z "${DB_IMAGE_EDB}" ]]; then + DB_IMAGE_EDB="edb-test:14" + # 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:14 -f edb14.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_15() { $CONTAINER_CLI rm -f edb || true - # 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:15 -f edb15.Dockerfile .) - $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 edb-test:15 + if [[ -z "${DB_IMAGE_EDB}" ]]; then + DB_IMAGE_EDB="edb-test:15" + # 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:15 -f edb15.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_16() { $CONTAINER_CLI rm -f edb || true - # 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:16 -f edb16.Dockerfile .) - $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 edb-test:16 + if [[ -z "${DB_IMAGE_EDB}" ]]; then + DB_IMAGE_EDB="edb-test:16" + # 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:16 -f edb16.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 } db2() { @@ -218,7 +244,7 @@ db2() { db2_11_5() { $PRIVILEGED_CLI $CONTAINER_CLI rm -f db2 || true - $PRIVILEGED_CLI $CONTAINER_CLI run --name db2 --privileged -e DB2INSTANCE=orm_test -e DB2INST1_PASSWORD=orm_test -e DBNAME=orm_test -e LICENSE=accept -e AUTOCONFIG=false -e ARCHIVE_LOGS=false -e TO_CREATE_SAMPLEDB=false -e REPODB=false -p 50000:50000 -d icr.io/db2_community/db2:11.5.9.0 + $PRIVILEGED_CLI $CONTAINER_CLI run --name db2 --privileged -e DB2INSTANCE=orm_test -e DB2INST1_PASSWORD=orm_test -e DBNAME=orm_test -e LICENSE=accept -e AUTOCONFIG=false -e ARCHIVE_LOGS=false -e TO_CREATE_SAMPLEDB=false -e REPODB=false -p 50000:50000 -d ${DB_IMAGE_DB2_11_5:-icr.io/db2_community/db2:11.5.9.0} # Give the container some time to start OUTPUT= while [[ $OUTPUT != *"INSTANCE"* ]]; do @@ -232,7 +258,7 @@ db2_11_5() { db2_10_5() { $PRIVILEGED_CLI $CONTAINER_CLI rm -f db2 || true # The sha represents the tag 10.5.0.5-3.10.0 - $PRIVILEGED_CLI $CONTAINER_CLI run --name db2 --privileged -e DB2INST1_PASSWORD=db2inst1-pwd -e LICENSE=accept -p 50000:50000 -d docker.io/ibmoms/db2express-c@sha256:a499afd9709a1f69fb41703e88def9869955234c3525547e2efc3418d1f4ca2b db2start + $PRIVILEGED_CLI $CONTAINER_CLI run --name db2 --privileged -e DB2INST1_PASSWORD=db2inst1-pwd -e LICENSE=accept -p 50000:50000 -d ${DB_IMAGE_DB2_10_5:-quay.io/hibernate/db2express-c@sha256:a499afd9709a1f69fb41703e88def9869955234c3525547e2efc3418d1f4ca2b} db2start # Give the container some time to start OUTPUT= while [[ $OUTPUT != *"DB2START"* ]]; do @@ -293,7 +319,7 @@ CREATE TRANSFORM FOR db2gse.ST_Geometry DB2_PROGRAM ( EOF $PRIVILEGED_CLI $CONTAINER_CLI run --name db2spatial --privileged -e DB2INSTANCE=orm_test -e DB2INST1_PASSWORD=orm_test -e DBNAME=orm_test -e LICENSE=accept -e AUTOCONFIG=false -e ARCHIVE_LOGS=false -e TO_CREATE_SAMPLEDB=false -e REPODB=false \ -v ${temp_dir}:/conf \ - -p 50000:50000 -d docker.io/ibmcom/db2:11.5.5.0 + -p 50000:50000 -d ${DB_IMAGE_DB2_SPATIAL:-docker.io/ibmcom/db2:11.5.5.0} # Give the container some time to start OUTPUT= @@ -317,7 +343,7 @@ mssql() { mssql_2017() { $CONTAINER_CLI rm -f mssql || true #This sha256 matches a specific tag of mcr.microsoft.com/mssql/server:2017-latest : - $CONTAINER_CLI run --name mssql -d -p 1433:1433 -e "SA_PASSWORD=Hibernate_orm_test" -e ACCEPT_EULA=Y mcr.microsoft.com/mssql/server@sha256:7d194c54e34cb63bca083542369485c8f4141596805611e84d8c8bab2339eede + $CONTAINER_CLI run --name mssql -d -p 1433:1433 -e "SA_PASSWORD=Hibernate_orm_test" -e ACCEPT_EULA=Y ${DB_IMAGE_MSSQL_2017:-mcr.microsoft.com/mssql/server@sha256:7d194c54e34cb63bca083542369485c8f4141596805611e84d8c8bab2339eede} sleep 5 n=0 until [ "$n" -ge 5 ] @@ -339,7 +365,7 @@ mssql_2017() { mssql_2022() { $CONTAINER_CLI rm -f mssql || true #This sha256 matches a specific tag of 2022-CU12-ubuntu-22.04 (https://mcr.microsoft.com/en-us/product/mssql/server/tags): - $CONTAINER_CLI run --name mssql -d -p 1433:1433 -e "SA_PASSWORD=Hibernate_orm_test" -e ACCEPT_EULA=Y mcr.microsoft.com/mssql/server@sha256:b94071acd4612bfe60a73e265097c2b6388d14d9d493db8f37cf4479a4337480 + $CONTAINER_CLI run --name mssql -d -p 1433:1433 -e "SA_PASSWORD=Hibernate_orm_test" -e ACCEPT_EULA=Y ${DB_IMAGE_MSSQL_2022:-mcr.microsoft.com/mssql/server@sha256:b94071acd4612bfe60a73e265097c2b6388d14d9d493db8f37cf4479a4337480} sleep 5 n=0 until [ "$n" -ge 5 ] @@ -361,7 +387,7 @@ mssql_2022() { sybase() { $CONTAINER_CLI rm -f sybase || true # Yup, that sucks, but on ubuntu we need to use -T11889 as per: https://github.com/DataGrip/docker-env/issues/12 - $CONTAINER_CLI run -d -p 9000:5000 -p 9001:5001 --name sybase --entrypoint /bin/bash docker.io/nguoianphu/docker-sybase -c "source /opt/sybase/SYBASE.sh + $CONTAINER_CLI run -d -p 9000:5000 -p 9001:5001 --name sybase --entrypoint /bin/bash ${DB_IMAGE_SYBASE:-docker.io/nguoianphu/docker-sybase} -c "source /opt/sybase/SYBASE.sh /opt/sybase/ASE-16_0/bin/dataserver \ -d/opt/sybase/data/master.dat \ -e/opt/sybase/ASE-16_0/install/MYSYBASE.log \ @@ -640,26 +666,28 @@ EOF\"" } disable_userland_proxy() { - if [[ "$HEALTCHECK_PATH" == "{{.State.Health.Status}}" ]]; then - if [[ ! -f /etc/docker/daemon.json ]]; then - echo "Didn't find /etc/docker/daemon.json but need to disable userland-proxy..." - echo "Stopping docker..." - sudo service docker stop - echo "Creating /etc/docker/daemon.json..." - sudo bash -c "echo '{\"userland-proxy\": false}' > /etc/docker/daemon.json" - echo "Starting docker..." - sudo service docker start - echo "Docker successfully started with userland proxies disabled" - elif ! grep -q userland-proxy /etc/docker/daemon.json; then - echo "Userland proxy is still enabled in /etc/docker/daemon.json, but need to disable it..." - export docker_daemon_json=$( /etc/docker/daemon.json' - echo "Starting docker..." - sudo service docker start - echo "Docker successfully started with userland proxies disabled" + if [[ "$IS_DOCKER_RUNTIME" == "true" ]]; then + if [[ "$HEALTCHECK_PATH" == "{{.State.Health.Status}}" ]]; then + if [[ ! -f /etc/docker/daemon.json ]]; then + echo "Didn't find /etc/docker/daemon.json but need to disable userland-proxy..." + echo "Stopping docker..." + sudo service docker stop + echo "Creating /etc/docker/daemon.json..." + sudo bash -c "echo '{\"userland-proxy\": false}' > /etc/docker/daemon.json" + echo "Starting docker..." + sudo service docker start + echo "Docker successfully started with userland proxies disabled" + elif ! grep -q userland-proxy /etc/docker/daemon.json; then + echo "Userland proxy is still enabled in /etc/docker/daemon.json, but need to disable it..." + export docker_daemon_json=$( /etc/docker/daemon.json' + echo "Starting docker..." + sudo service docker start + echo "Docker successfully started with userland proxies disabled" + fi fi fi } @@ -742,7 +770,7 @@ oracle_21() { --health-interval 5s \ --health-timeout 5s \ --health-retries 10 \ - docker.io/gvenzl/oracle-xe:21.3.0 + ${DB_IMAGE_ORACLE_21:-docker.io/gvenzl/oracle-xe:21.3.0} oracle_setup } @@ -756,7 +784,7 @@ oracle_23() { --health-interval 5s \ --health-timeout 5s \ --health-retries 10 \ - docker.io/gvenzl/oracle-free:23 + ${DB_IMAGE_ORACLE_23:-docker.io/gvenzl/oracle-free:23} oracle_free_setup } @@ -773,7 +801,7 @@ hana() { --sysctl kernel.shmmni=4096 \ --sysctl kernel.shmall=8388608 \ -v $temp_dir:/config:Z \ - docker.io/saplabs/hanaexpress:2.00.072.00.20231123.1 \ + ${DB_IMAGE_HANA:-docker.io/saplabs/hanaexpress:2.00.072.00.20231123.1} \ --passwords-url file:///config/password.json \ --agree-to-sap-license # Give the container some time to start @@ -800,7 +828,7 @@ sinks: redact: false exit-on-error: true " - $CONTAINER_CLI run -d --name=cockroach -m 6g -p 26257:26257 -p 8080:8080 docker.io/cockroachdb/cockroach:v23.1.12 start-single-node \ + $CONTAINER_CLI run -d --name=cockroach -m 6g -p 26257:26257 -p 8080:8080 ${DB_IMAGE_COCKROACHDB_23_1:-docker.io/cockroachdb/cockroach:v23.1.12} start-single-node \ --insecure --store=type=mem,size=0.25 --advertise-addr=localhost --log="$LOG_CONFIG" OUTPUT= while [[ $OUTPUT != *"CockroachDB node starting"* ]]; do @@ -841,7 +869,7 @@ sinks: redact: false exit-on-error: true " - $CONTAINER_CLI run -d --name=cockroach -m 6g -p 26257:26257 -p 8080:8080 docker.io/cockroachdb/cockroach:v22.2.2 start-single-node \ + $CONTAINER_CLI run -d --name=cockroach -m 6g -p 26257:26257 -p 8080:8080 ${DB_IMAGE_COCKROACHDB_22_2:-docker.io/cockroachdb/cockroach:v22.2.2} start-single-node \ --insecure --store=type=mem,size=0.25 --advertise-addr=localhost --log="$LOG_CONFIG" OUTPUT= while [[ $OUTPUT != *"CockroachDB node starting"* ]]; do @@ -881,7 +909,7 @@ tidb_5_4() { $CONTAINER_CLI rm -f tidb || true $CONTAINER_CLI network rm -f tidb_network || true $CONTAINER_CLI network create tidb_network - $CONTAINER_CLI run --name tidb -p4000:4000 -d --network tidb_network docker.io/pingcap/tidb:v5.4.3 + $CONTAINER_CLI run --name tidb -p4000:4000 -d --network tidb_network ${DB_IMAGE_TIDB_5_4:-docker.io/pingcap/tidb:v5.4.3} # Give the container some time to start OUTPUT= n=0 @@ -908,8 +936,11 @@ informix() { } informix_14_10() { + temp_dir=$(mktemp -d) + echo "ALLOW_NEWLINE 1" >$temp_dir/onconfig.mod + chmod 777 -R $temp_dir $PRIVILEGED_CLI $CONTAINER_CLI rm -f informix || true - $PRIVILEGED_CLI $CONTAINER_CLI run --name informix --privileged -p 9088:9088 -e LICENSE=accept -e GL_USEGLU=1 -d icr.io/informix/informix-developer-database:14.10.FC9W1DE + $PRIVILEGED_CLI $CONTAINER_CLI run --name informix --privileged -p 9088:9088 -v $temp_dir:/opt/ibm/config -e LICENSE=accept -e GL_USEGLU=1 -d ${DB_IMAGE_INFORMIX_14_10:-icr.io/informix/informix-developer-database:14.10.FC9W1DE} echo "Starting Informix. This can take a few minutes" # Give the container some time to start OUTPUT= @@ -935,7 +966,7 @@ informix_14_10() { informix_12_10() { $PRIVILEGED_CLI $CONTAINER_CLI rm -f informix || true - $PRIVILEGED_CLI $CONTAINER_CLI run --name informix --privileged -p 9088:9088 -e LICENSE=accept -e GL_USEGLU=1 -d ibmcom/informix-developer-database:12.10.FC12W1DE + $PRIVILEGED_CLI $CONTAINER_CLI run --name informix --privileged -p 9088:9088 -e LICENSE=accept -e GL_USEGLU=1 -d ${DB_IMAGE_INFORMIX_12_10:-ibmcom/informix-developer-database:12.10.FC12W1DE} echo "Starting Informix. This can take a few minutes" # Give the container some time to start OUTPUT= @@ -977,6 +1008,7 @@ if [ -z ${1} ]; then echo -e "\thana" echo -e "\tmariadb" echo -e "\tmariadb_verylatest" + echo -e "\tmariadb_11_7" echo -e "\tmariadb_11_4" echo -e "\tmariadb_11_1" echo -e "\tmariadb_10_11" diff --git a/documentation/documentation.gradle b/documentation/documentation.gradle index 02b5f8829852..fbd22a24e531 100644 --- a/documentation/documentation.gradle +++ b/documentation/documentation.gradle @@ -34,7 +34,6 @@ repositories { apply from: rootProject.file( 'gradle/module.gradle' ) -apply from: rootProject.file( 'gradle/releasable.gradle' ) apply plugin: 'org.hibernate.orm.build.reports' @@ -139,6 +138,7 @@ dependencies { reportAggregation project(':hibernate-ant') reportAggregation project(':hibernate-enhance-maven-plugin') reportAggregation project(':hibernate-jpamodelgen') + reportAggregation project(':hibernate-community-dialects') asciidoctorGems 'rubygems:rouge:4.1.1' @@ -182,6 +182,7 @@ dependencies { javadocClasspath jakartaLibs.jsonbApi javadocClasspath libs.ant javadocClasspath dbLibs.postgresql + javadocClasspath dbLibs.edb javadocClasspath libs.jackson javadocClasspath gradleApi() javadocClasspath libs.jacksonXml @@ -192,10 +193,6 @@ dependencies { if ( project.ormVersion.isSnapshot ) { // only run the ci build tasks for SNAPSHOT versions tasks.register('ciBuild') { dependsOn clean } - tasks.release.enabled false -} -else { - tasks.release.dependsOn clean } @@ -261,7 +258,10 @@ asciidoctorj { fullVersion: rootProject.ormVersion.fullName, javaCompatibleVersions: jdks.versions.compatible.get(), jakartaJpaVersion: rootProject.jakartaJpaVersion, - jdbcVersion: jdks.versions.jdbc.get() + jdbcVersion: jdks.versions.jdbc.get(), + 'root-project-dir': rootProject.layout.projectDirectory.asFile.absolutePath, + 'doc-main-dir': project(':documentation').layout.projectDirectory.dir('src').dir("main").asFile.absolutePath, + 'shared-attributes-dir': project(':documentation').layout.projectDirectory.dir('src').dir("main").dir("asciidoc").dir("shared").asFile.absolutePath options logDocuments: true } @@ -573,7 +573,7 @@ settingsDocumentation { } transaction { explicitPosition = 6 - summary = "Proxool Connection Pool Settings" + summary = "Transaction Environment Settings" description = "Settings which control how Hibernate interacts with and manages transactions" settingsClassName "org.hibernate.cfg.TransactionSettings" } @@ -691,7 +691,7 @@ def renderUserGuideHtmlTask = tasks.register( 'renderUserGuideHtml', Asciidoctor inputs.property "hibernate-version", project.ormVersion inputs.file( generateSettingsDocTask.get().outputFile ) - dependsOn generateSettingsDocTask, generateDialectTableReport + dependsOn generateSettingsDocTask sourceDir = file( 'src/main/asciidoc/userguide' ) sources { @@ -806,6 +806,30 @@ def renderMigrationGuideTask = tasks.register( "renderMigrationGuide", Asciidoct } } +// Dialect Guide ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +def renderDialectGuideTask = tasks.register( 'renderDialectGuide', AsciidoctorTask ) { task -> + group = "Documentation" + description = 'Renders the Dialect guide in HTML format using Asciidoctor.' + inputs.property "hibernate-version", project.ormVersion + dependsOn generateDialectTableReport, generateCommunityDialectTableReport + + sourceDir = file( 'src/main/asciidoc/dialect' ) + sources 'dialect.adoc' + outputDir = layout.buildDirectory.dir( 'asciidoc/dialect' ) + + attributes linkcss: true, stylesheet: "css/hibernate.css", + 'generated-report-dir': layout.buildDirectory.dir( 'orm/generated' ).get() + + task.resources { + from( 'src/main/style/asciidoctor' ) { + include 'images/**' + } + from( 'src/main/style/asciidoctor' ) { + include 'css/**' + } + } +} // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // ORM Reports @@ -835,34 +859,8 @@ def renderLoggingReportTask = tasks.register( 'renderLoggingReport', Asciidoctor } } -def renderDialectReportTask = tasks.register( 'renderDialectReport', AsciidoctorTask ) { task -> - task.group = "hibernate-reports" - task.description = 'Renders the supported Dialect report in HTML format using Asciidoctor.' - task.dependsOn "generateDialectReport" - task.dependsOn "generateDialectTableReport" - - task.inputs.property "version", project.ormVersion - - task.sourceDir = layout.buildDirectory.dir( 'orm/generated/dialect' ) - task.sources 'dialect.adoc' - - task.outputDir = project.layout.buildDirectory.dir( 'asciidoc/dialect' ) - - task.attributes linkcss: true, - stylesheet: "css/hibernate.css" - - task.resources { - from( 'src/main/style/asciidoctor' ) { - include 'images/**' - } - from( 'src/main/style/asciidoctor' ) { - include 'css/**' - } - } -} - def generateReportsTask = tasks.named( "generateReports" ) { - dependsOn renderLoggingReportTask, renderDialectReportTask + dependsOn renderLoggingReportTask } @@ -881,6 +879,7 @@ def buildDocsTask = tasks.register( 'buildDocs' ) { task -> task.dependsOn renderRepositoriesTask task.dependsOn renderIntegrationGuidesTask task.dependsOn renderTopicalGuidesTask + task.dependsOn renderDialectGuideTask task.dependsOn generateReportsTask task.dependsOn renderMigrationGuideTask } @@ -900,4 +899,11 @@ tasks.withType(AsciidoctorTask).configureEach { separateOutputDirs = false backends 'html5' } + // See https://docs.asciidoctor.org/gradle-plugin/latest/common-task-configuration/#choosing-an-execution-mode-for-asciidoctorj + executionMode = org.ysb33r.grolifant.api.core.jvm.ExecutionMode.JAVA_EXEC +} + +tasks.withType(AsciidoctorPdfTask).configureEach { + // See https://docs.asciidoctor.org/gradle-plugin/latest/common-task-configuration/#choosing-an-execution-mode-for-asciidoctorj + executionMode = org.ysb33r.grolifant.api.core.jvm.ExecutionMode.JAVA_EXEC } diff --git a/documentation/src/main/asciidoc/dialect/dialect.adoc b/documentation/src/main/asciidoc/dialect/dialect.adoc new file mode 100644 index 000000000000..51a34fd3d7f1 --- /dev/null +++ b/documentation/src/main/asciidoc/dialect/dialect.adoc @@ -0,0 +1,107 @@ +include::{shared-attributes-dir}/common-attributes.adoc[] +include::{shared-attributes-dir}/url-attributes.adoc[] +include::{shared-attributes-dir}/filesystem-attributes.adoc[] +include::{shared-attributes-dir}/renderer-attributes.adoc[] + += Dialects +:toc2: +:toclevels: 1 +:sectanchors: + +A dialect is a class that provides information about the specifics of a database and translators for the SQL dialect of the database. + +== Supported dialects + +Hibernate ORM supports a wide range of dialects out of the box. + +Usually, Hibernate supports at least the database version that is also still supported by the respective vendor. +In many cases though, Hibernate supports even older versions of the databases, +but the support for these versions is not guaranteed. + +Below is a list of supported dialects and the minimum required version of the database. + +include::{generated-report-dir}/dialect/dialect-table.adoc[] + +[[third-party-dialects]] +== Third-party dialects + +Third-parties publish additional dialects for Hibernate ORM, providing their own support for more databases, or extended support beyond what is built in Hibernate ORM. + +These dialects are not directly supported by the Hibernate team: + +* The Hibernate ORM CI does not run any test against these dialects, the dialect's authors have their own test suite. +* The Hibernate team will not address issues reported against these dialect, but the dialect's authors have their own issue tracker. + +[NOTE] +==== +Third-party dialects may not be compatible with all versions of Hibernate ORM. + +Check the dialect's own documentation to know more about its compatibility constraints. +The https://hibernate.org/orm/releases/#compatibility-matrix[compatibility matrix on the Hibernate website] may also be of help. +==== + +Below is a list of third-party dialects with links to relevant websites. + +[cols="a,a", options="header"] +|=== +|Dialect |Website +|MongoDB| https://github.com/mongodb/mongo-hibernate/[MongoDB extension for Hibernate ORM] +|Google Spanner| https://github.com/GoogleCloudPlatform/google-cloud-spanner-hibernate[Google Cloud Spanner Dialect for Hibernate ORM] +|=== + +[[community-dialects]] +== Community dialects + +Community dialects are not included in `org.hibernate.orm:hibernate-core` and require an additional dependency to `org.hibernate.orm:hibernate-community-dialects`. + +These dialects are not directly supported by the Hibernate team: + +* The Hibernate ORM CI does not run any test against these dialects. +* The Hibernate team will not address issues reported against these dialect. + +Instead, the dialects are maintained on a best-effort basis by vendors or individuals. + +Below is a list of community dialects and the minimum required version of the database. + +include::{generated-report-dir}/dialect/dialect-table-community.adoc[] + +[NOTE] +==== +Community dialects were introduced in Hibernate ORM 6.0. + +The `hibernate-core` artifact had many legacy dialects before 6.0 that were only tested and maintained on a best effort basis. +More and more database vendors requested to integrate a dialect for their database and even provided a PR with a dialect, +but the Hibernate team didn't want to add new dialects for databases that might not have a wide adoption +or any automated testing into the `hibernate-core` artifact. Even though the dialect was supposedly maintained by the vendor, +the Hibernate team was burdened with reviewing questions, issues and PRs that relate to these dialects. + +To give database vendors and the community a clear way forward, the Hibernate team decided to introduce a new artifact, +called `hibernate-community-dialects` which is the new home for dialects that are maintained by vendors or individuals. + +Moving forward, the `hibernate-core` artifact will only contain dialects that are supported and tested by the Hibernate team. +All the legacy dialects were moved to the `hibernate-community-dialects` artifact to have a clear separation based on the quality of the dialect. +==== + +== Requirements for moving from `hibernate-community-dialects` to `hibernate-core` + +If a database vendor wants their database dialect to be included in the `hibernate-core` artifact, +several requirements have to be fulfilled: + +* The vendor must provide access to a dedicated database server that can be used for testing +* The vendor must provide contact details to at least one employee who is mainly responsible for the maintenance of the dialect +* The responsible employee of the vendor must actively monitor and react to failures of the testsuite against the respective database +* The responsible employee of the vendor must ensure the testsuite is configured correctly in order for it to succeed on the respective database +* If the responsible employee of the vendor leaves the company, the vendor must provide contact details to a new responsible employee + +In case the responsible employee is unreachable for a longer period or issues with the dialect are not attended to in a timely manner, +the Hibernate team will move the dialect back to the `hibernate-community-dialects` artifact. + +The requirements for the database server are: + +* JDK 17 installed +* Git installed +* Access to the database through non-confidential credentials +* Access via SSH through confidential credentials + +Get in touch with the Hibernate team on https://hibernate.zulipchat.com/#narrow/stream/132096-hibernate-user[Zulip] +if you want to request the move of your dialect to hibernate-core. diff --git a/documentation/src/main/asciidoc/introduction/Configuration.adoc b/documentation/src/main/asciidoc/introduction/Configuration.adoc index 9293cf6e0ce1..1dbdc1007dfe 100644 --- a/documentation/src/main/asciidoc/introduction/Configuration.adoc +++ b/documentation/src/main/asciidoc/introduction/Configuration.adoc @@ -69,6 +69,8 @@ driver for your database. | Oracle | `com.oracle.database.jdbc:ojdbc11:${version}` | H2 | `com.h2database:h2:{version}` | HSQLDB | `org.hsqldb:hsqldb:{version}` +| MongoDB | The JDBC driver is bundled with the dialect mentioned in <> +| Google Spanner | `com.google.cloud:google-cloud-spanner-jdbc:{version}` |=== Where `{version}` is the latest version of the JDBC driver for your databse. @@ -118,6 +120,14 @@ and `com.github.ben-manes.caffeine:jcache` or `org.eclipse:yasson` | <> | `org.hibernate.orm:hibernate-spatial` | <>, for auditing historical data | `org.hibernate.orm:hibernate-envers` +| link:{doc-dialect-url}#community-dialects[Community dialects] | `org.hibernate.orm:hibernate-community-dialects` +| link:{doc-dialect-url}#third-party-dialects[Third-party dialects] +| +https://github.com/mongodb/mongo-hibernate/[MongoDB]: `org.mongodb:mongodb-hibernate:{version}` + +https://github.com/GoogleCloudPlatform/google-cloud-spanner-hibernate[Google Spanner]: `com.google.cloud:google-cloud-spanner-hibernate-dialect:{version}` + +Where `{version}` is the version of the third-party dialect compatible with the version of Hibernate ORM you are using. See the dialect's own documentation for more information. The https://hibernate.org/orm/releases/#compatibility-matrix[compatibility matrix on the Hibernate website] may also be of help. |=== You might also add the Hibernate {enhancer}[bytecode enhancer] to your @@ -291,7 +301,7 @@ The properties you really do need to get started are these three: ==== In Hibernate 6, you don't need to specify `hibernate.dialect`. The correct Hibernate SQL `Dialect` will be determined for you automatically. -The only reason to specify this property is if you're using a custom user-written `Dialect` class. +The only reason to specify this property is if you're using a custom user-written or link:{doc-dialect-url}#third-party-dialects[third-party] `Dialect` class. Similarly, neither `hibernate.connection.driver_class` nor `jakarta.persistence.jdbc.driver` is needed when working with one of the supported databases. ==== diff --git a/documentation/src/main/asciidoc/introduction/Hibernate_Introduction.adoc b/documentation/src/main/asciidoc/introduction/Hibernate_Introduction.adoc index cf93d8d8b9a5..6526db7ec935 100644 --- a/documentation/src/main/asciidoc/introduction/Hibernate_Introduction.adoc +++ b/documentation/src/main/asciidoc/introduction/Hibernate_Introduction.adoc @@ -1,5 +1,3 @@ -:shared-attributes-dir: ../shared/ - include::{shared-attributes-dir}/common-attributes.adoc[] include::{shared-attributes-dir}/url-attributes.adoc[] include::{shared-attributes-dir}/filesystem-attributes.adoc[] @@ -7,7 +5,7 @@ include::{shared-attributes-dir}/renderer-attributes.adoc[] = An Introduction to Hibernate 6 -:title-logo-image: image:../../style/asciidoctor/images/org/hibernate/logo.png[] +:title-logo-image: image:{doc-main-dir}/style/asciidoctor/images/org/hibernate/logo.png[] :toc: :toclevels: 3 diff --git a/documentation/src/main/asciidoc/introduction/Introduction.adoc b/documentation/src/main/asciidoc/introduction/Introduction.adoc index d7d33e09787c..861aa5023c74 100644 --- a/documentation/src/main/asciidoc/introduction/Introduction.adoc +++ b/documentation/src/main/asciidoc/introduction/Introduction.adoc @@ -533,8 +533,8 @@ Whatever the case, the code which orchestrates a unit of work usually just calls [source,java] ---- @GET -@Path("books/{titlePattern}") -public List findBooks(String titlePattern) { +@Path("books/{titlePattern}/{page:\\d+}") +public List findBooks(String titlePattern, int page) { var books = sessionFactory.fromTransaction(session -> Queries.findBooksByTitleWithPagination(session, titlePattern, Page.page(RESULTS_PER_PAGE, page)); @@ -565,8 +565,8 @@ We can call it just like we called our handwritten version: [source,java] ---- @GET -@Path("books/{titlePattern}") -public List findBooks(String titlePattern) { +@Path("books/{titlePattern}/{page:\\d+}") +public List findBooks(String titlePattern, int page) { var books = sessionFactory.fromTransaction(session -> Queries_.findBooksByTitleWithPagination(session, titlePattern, Page.page(RESULTS_PER_PAGE, page)); diff --git a/documentation/src/main/asciidoc/querylanguage/Expressions.adoc b/documentation/src/main/asciidoc/querylanguage/Expressions.adoc index 3778f5940a18..fdf8bd204eb3 100644 --- a/documentation/src/main/asciidoc/querylanguage/Expressions.adoc +++ b/documentation/src/main/asciidoc/querylanguage/Expressions.adoc @@ -720,7 +720,7 @@ Recognized Field types are listed below. | `offset minute` | `Integer` | 0-59 | Minutes of offset | ✖ |=== -For a full list of field types, see the Javadoc for https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/query/TemporalUnit.html[`TemporalUnit`]. +For a full list of field types, see the Javadoc for https://docs.hibernate.org/orm/{majorMinorVersion}/javadocs/org/hibernate/query/TemporalUnit.html[`TemporalUnit`]. [source, hql] ---- @@ -768,7 +768,7 @@ The pattern must be written in a subset of the pattern language defined by Java' select format(local datetime as 'yyyy-MM-dd HH:mm:ss') ---- -For a full list of `format()` pattern elements, see the Javadoc for https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/dialect/Dialect.html#appendDatetimeFormat[`Dialect.appendDatetimeFormat`]. +For a full list of `format()` pattern elements, see the Javadoc for https://docs.hibernate.org/orm/{majorMinorVersion}/javadocs/org/hibernate/dialect/Dialect.html#appendDatetimeFormat[`Dialect.appendDatetimeFormat`]. [[function-trunc-datetime]] [discrete] diff --git a/documentation/src/main/asciidoc/querylanguage/Hibernate_Query_Language.adoc b/documentation/src/main/asciidoc/querylanguage/Hibernate_Query_Language.adoc index 8d0b63d13fba..c9c425b144e1 100644 --- a/documentation/src/main/asciidoc/querylanguage/Hibernate_Query_Language.adoc +++ b/documentation/src/main/asciidoc/querylanguage/Hibernate_Query_Language.adoc @@ -1,5 +1,3 @@ -:shared-attributes-dir: ../shared/ - include::{shared-attributes-dir}/common-attributes.adoc[] include::{shared-attributes-dir}/url-attributes.adoc[] include::{shared-attributes-dir}/filesystem-attributes.adoc[] @@ -10,7 +8,7 @@ include::{shared-attributes-dir}/renderer-attributes.adoc[] :extrasdir: extras = A Guide to Hibernate Query Language -:title-logo-image: image:../../style/asciidoctor/images/org/hibernate/logo.png[] +:title-logo-image: image:{doc-main-dir}/style/asciidoctor/images/org/hibernate/logo.png[] :toc: :toclevels: 3 diff --git a/documentation/src/main/asciidoc/querylanguage/Preface.adoc b/documentation/src/main/asciidoc/querylanguage/Preface.adoc index 4976e23c64d7..d78667efc973 100644 --- a/documentation/src/main/asciidoc/querylanguage/Preface.adoc +++ b/documentation/src/main/asciidoc/querylanguage/Preface.adoc @@ -1,5 +1,3 @@ -:shared-attributes-dir: ../shared/ - include::{shared-attributes-dir}/url-attributes.adoc[] [[preface]] diff --git a/documentation/src/main/asciidoc/quickstart/guides/index.adoc b/documentation/src/main/asciidoc/quickstart/guides/index.adoc index 753eb12fc9f3..5074ed47aa7c 100644 --- a/documentation/src/main/asciidoc/quickstart/guides/index.adoc +++ b/documentation/src/main/asciidoc/quickstart/guides/index.adoc @@ -1,5 +1,3 @@ -:shared-attributes-dir: ../../shared/ - include::{shared-attributes-dir}/common-attributes.adoc[] include::{shared-attributes-dir}/url-attributes.adoc[] include::{shared-attributes-dir}/filesystem-attributes.adoc[] diff --git a/documentation/src/main/asciidoc/quickstart/guides/obtaining.adoc b/documentation/src/main/asciidoc/quickstart/guides/obtaining.adoc index 81a9a507dcdc..351ec0d7b84c 100644 --- a/documentation/src/main/asciidoc/quickstart/guides/obtaining.adoc +++ b/documentation/src/main/asciidoc/quickstart/guides/obtaining.adoc @@ -58,7 +58,7 @@ transitive dependencies based on the features being used or not. |hibernate-jcache| Integration with https://jcp.org/en/jsr/detail?id=107$$[JCache], allowing any compliant implementation as a second-level cache provider |hibernate-graalvm| Experimental extension to make it easier to compile applications as a https://www.graalvm.org/[GraalVM] native image |hibernate-micrometer| Integration with https://micrometer.io[Micrometer] metrics -|hibernate-community-dialects| Additional community-supported SQL dialects +|hibernate-community-dialects| Additional link:{doc-dialect-url}#community-dialects[community-supported SQL dialects] |=== [cols="40m,~"] @@ -67,6 +67,25 @@ transitive dependencies based on the features being used or not. |hibernate-testing| A series of JUnit extensions for testing Hibernate ORM functionality |=== +[cols="40m,~"] +.Third-party modules +|=== +// Yes, this is a full row containing asciidoc containing an admonition. I don't know have a better idea to add an admonition between a table's title and its content. +2+a| +[NOTE] +==== +Third-party modules, and in particular link:{doc-dialect-url}#third-party-dialects[third-party dialects], are tested by their own authors, +and may not be compatible with all versions of Hibernate ORM. + +1. Check the module's own documentation to know more about its compatibility constraints. +The https://hibernate.org/orm/releases/#compatibility-matrix[compatibility matrix on the Hibernate website] may also be of help. +2. Submit any question or bug reports about these dialects to the dialect's authors: the Hibernate team cannot help. + +==== +|`org.mongodb:mongodb-hibernate:{version}`| https://github.com/mongodb/mongo-hibernate/[MongoDB Extension for Hibernate ORM] +|`com.google.cloud:google-cloud-spanner-hibernate-dialect:{version}`| https://github.com/GoogleCloudPlatform/google-cloud-spanner-hibernate[Google Cloud Spanner Dialect for Hibernate ORM] +|=== + [[platform]] === Platform / BOM diff --git a/documentation/src/main/asciidoc/quickstart/guides/preface.adoc b/documentation/src/main/asciidoc/quickstart/guides/preface.adoc index b44b21b3538c..773af9518e89 100644 --- a/documentation/src/main/asciidoc/quickstart/guides/preface.adoc +++ b/documentation/src/main/asciidoc/quickstart/guides/preface.adoc @@ -1,5 +1,3 @@ -:shared-attributes-dir: ../../shared/ - include::{shared-attributes-dir}/url-attributes.adoc[] include::{shared-attributes-dir}/filesystem-attributes.adoc[] diff --git a/documentation/src/main/asciidoc/repositories/Hibernate_Data_Repositories.adoc b/documentation/src/main/asciidoc/repositories/Hibernate_Data_Repositories.adoc index 9c5bd400580a..1a8f22b32b52 100644 --- a/documentation/src/main/asciidoc/repositories/Hibernate_Data_Repositories.adoc +++ b/documentation/src/main/asciidoc/repositories/Hibernate_Data_Repositories.adoc @@ -1,12 +1,10 @@ -:shared-attributes-dir: ../shared/ - include::{shared-attributes-dir}/common-attributes.adoc[] include::{shared-attributes-dir}/url-attributes.adoc[] include::{shared-attributes-dir}/filesystem-attributes.adoc[] include::{shared-attributes-dir}/renderer-attributes.adoc[] = Introducing Hibernate Data Repositories -:title-logo-image: image:../../style/asciidoctor/images/org/hibernate/logo.png[] +:title-logo-image: image:{doc-main-dir}/style/asciidoctor/images/org/hibernate/logo.png[] :toc: :toclevels: 3 diff --git a/documentation/src/main/asciidoc/repositories/Preface.adoc b/documentation/src/main/asciidoc/repositories/Preface.adoc index 81e76a50a0be..75b5f0a364e9 100644 --- a/documentation/src/main/asciidoc/repositories/Preface.adoc +++ b/documentation/src/main/asciidoc/repositories/Preface.adoc @@ -1,5 +1,3 @@ -:shared-attributes-dir: ../shared/ - include::{shared-attributes-dir}/url-attributes.adoc[] [[preface]] @@ -16,4 +14,4 @@ On the other hand, the programming model for interacting with the database is qu Therefore, this document will show you a different way to use Hibernate. The coverage of Jakarta Data is intentionally inexhaustive. -If exhaustion is sought, this document should be read in conjunction with the specification, which we've worked hard to keep readable. \ No newline at end of file +If exhaustion is sought, this document should be read in conjunction with the specification, which we've worked hard to keep readable. diff --git a/documentation/src/main/asciidoc/shared/filesystem-attributes.adoc b/documentation/src/main/asciidoc/shared/filesystem-attributes.adoc index 4b0c59edba7a..e29f07e36fcd 100644 --- a/documentation/src/main/asciidoc/shared/filesystem-attributes.adoc +++ b/documentation/src/main/asciidoc/shared/filesystem-attributes.adoc @@ -2,14 +2,12 @@ // Centralized definition of Asciidoc attributes for local filesystem paths // **************************************************************************** -:doc-main-dir: ../.. :doc-main-asciidoc-dir: {doc-main-dir}/asciidoc :doc-main-style-dir: {doc-main-dir}/style :pdf-theme: {doc-main-style-dir}/pdf/theme.yml :pdf-fontsdir: {doc-main-style-dir}/pdf/fonts -//:title-logo-image: {doc-main-style-dir}/asciidoctor/images/org/hibernate/logo.png[] +//:title-logo-image: image:{doc-main-dir}/style/asciidoctor/images/org/hibernate/logo.png[] -:root-project-dir: ../../../../.. :core-project-dir: {root-project-dir}/hibernate-core :documentation-project-dir: {root-project-dir}/documentation :testing-project-dir: {root-project-dir}/hibernate-testing diff --git a/documentation/src/main/asciidoc/shared/url-attributes.adoc b/documentation/src/main/asciidoc/shared/url-attributes.adoc index 12c29a944d4f..4497f0a7f76e 100644 --- a/documentation/src/main/asciidoc/shared/url-attributes.adoc +++ b/documentation/src/main/asciidoc/shared/url-attributes.adoc @@ -4,20 +4,20 @@ include::./common-attributes.adoc[] -:doc-base-url: https://docs.jboss.org/hibernate/orm +: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-user-guide-url: {doc-version-base-url}/userguide/html_single/Hibernate_User_Guide.html :doc-javadoc-url: {doc-version-base-url}/javadocs/ :doc-topical-url: {doc-version-base-url}/topical/html_single/ -:doc-registries-url: {doc-topical-url}/registries/ServiceRegistries.html -:doc-logging-url: {doc-topical-url}/logging/Logging.html +:doc-registries-url: {doc-topical-url}/registries/ +:doc-logging-url: {doc-topical-url}/logging/ +:doc-dialect-url: {doc-version-base-url}/dialect/ :report-deprecation-url: {doc-version-base-url}/deprecated/deprecating.txt -:report-dialect-url: {doc-version-base-url}/dialect/dialect.html :report-incubating-url: {doc-version-base-url}/incubating/incubating.txt :report-internals-url: {doc-version-base-url}/internals/internal.txt -:report-logging-url: {doc-version-base-url}/logging/logging.html +:report-logging-url: {doc-version-base-url}/logging/ diff --git a/documentation/src/main/asciidoc/topical/index.adoc b/documentation/src/main/asciidoc/topical/index.adoc index 628562548d79..d4e575987cee 100644 --- a/documentation/src/main/asciidoc/topical/index.adoc +++ b/documentation/src/main/asciidoc/topical/index.adoc @@ -1,5 +1,3 @@ -:shared-attributes-dir: ../shared/ - include::{shared-attributes-dir}/common-attributes.adoc[] include::{shared-attributes-dir}/url-attributes.adoc[] include::{shared-attributes-dir}/filesystem-attributes.adoc[] diff --git a/documentation/src/main/asciidoc/topical/logging/Logging.adoc b/documentation/src/main/asciidoc/topical/logging/Logging.adoc index 4422ff99f054..5db89c05dec1 100644 --- a/documentation/src/main/asciidoc/topical/logging/Logging.adoc +++ b/documentation/src/main/asciidoc/topical/logging/Logging.adoc @@ -1,5 +1,3 @@ -:shared-attributes-dir: ../../shared/ - include::{shared-attributes-dir}/common-attributes.adoc[] include::{shared-attributes-dir}/url-attributes.adoc[] include::{shared-attributes-dir}/filesystem-attributes.adoc[] diff --git a/documentation/src/main/asciidoc/userguide/Hibernate_User_Guide-docinfo.html b/documentation/src/main/asciidoc/userguide/Hibernate_User_Guide-docinfo.html index 0fd842c784cc..8a452d99e232 100644 --- a/documentation/src/main/asciidoc/userguide/Hibernate_User_Guide-docinfo.html +++ b/documentation/src/main/asciidoc/userguide/Hibernate_User_Guide-docinfo.html @@ -1,5 +1,5 @@ - - + + diff --git a/documentation/src/main/asciidoc/userguide/Hibernate_User_Guide.adoc b/documentation/src/main/asciidoc/userguide/Hibernate_User_Guide.adoc index 01e738fc3f8a..9bd5673a0f40 100644 --- a/documentation/src/main/asciidoc/userguide/Hibernate_User_Guide.adoc +++ b/documentation/src/main/asciidoc/userguide/Hibernate_User_Guide.adoc @@ -1,5 +1,3 @@ -:shared-attributes-dir: ../shared/ - include::{shared-attributes-dir}/common-attributes.adoc[] include::{shared-attributes-dir}/url-attributes.adoc[] include::{shared-attributes-dir}/filesystem-attributes.adoc[] diff --git a/documentation/src/main/asciidoc/userguide/Preface.adoc b/documentation/src/main/asciidoc/userguide/Preface.adoc index 5c2de61d009f..613a012b1803 100644 --- a/documentation/src/main/asciidoc/userguide/Preface.adoc +++ b/documentation/src/main/asciidoc/userguide/Preface.adoc @@ -1,4 +1,3 @@ -:shared-attributes-dir: ../shared/ include::{shared-attributes-dir}/url-attributes.adoc[] [[preface]] diff --git a/documentation/src/main/asciidoc/userguide/appendices/LegacyBasicTypeResolution.adoc b/documentation/src/main/asciidoc/userguide/appendices/LegacyBasicTypeResolution.adoc index 273be37a5aa8..bc3b941b57d7 100644 --- a/documentation/src/main/asciidoc/userguide/appendices/LegacyBasicTypeResolution.adoc +++ b/documentation/src/main/asciidoc/userguide/appendices/LegacyBasicTypeResolution.adoc @@ -122,7 +122,7 @@ But first, let's explore how implicit resolution works and how applications can ==== A thorough discussion of `BasicTypeRegistry` and all the different ways to contribute types is beyond the scope of this documentation. -Please see the http://docs.jboss.org/hibernate/orm/{majorMinorVersion}/integrationguide/html_single/Hibernate_Integration_Guide.html[Integration Guide] for complete details. +Please see the http://docs.hibernate.org/orm/{majorMinorVersion}/integrationguide/html_single/Hibernate_Integration_Guide.html[Integration Guide] for complete details. ==== As an example, take a String attribute such as we saw before with Product#sku. @@ -319,4 +319,4 @@ When running the previous test case against the `BitSetUserType` entity mapping, ---- include::{originalextrasdir}/basic/basic-custom-type-BitSetUserType-persistence-sql-example.sql[] ---- -==== \ No newline at end of file +==== diff --git a/documentation/src/main/asciidoc/userguide/chapters/bootstrap/Bootstrap.adoc b/documentation/src/main/asciidoc/userguide/chapters/bootstrap/Bootstrap.adoc index c2db7fed499e..2c36fe596b6f 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/bootstrap/Bootstrap.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/bootstrap/Bootstrap.adoc @@ -73,7 +73,7 @@ include::{example-dir-boot}/BootstrapTest.java[tags=bootstrap-bootstrap-native-r ==== A `StandardServiceRegistry` is also highly configurable via the StandardServiceRegistryBuilder API. -See the `StandardServiceRegistryBuilder` https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/boot/registry/StandardServiceRegistryBuilder.html[Javadocs] for more details. +See the `StandardServiceRegistryBuilder` https://docs.hibernate.org/orm/{majorMinorVersion}/javadocs/org/hibernate/boot/registry/StandardServiceRegistryBuilder.html[Javadocs] for more details. Some specific methods of interest: @@ -107,7 +107,7 @@ The second step in native bootstrapping is the building of an `org.hibernate.boo The first thing we obviously need to build a parsed representation is the source information to be parsed (annotated classes, `hbm.xml` files, `orm.xml` files). This is the purpose of `org.hibernate.boot.MetadataSources`. -`MetadataSources` has many other methods as well. Explore its API and https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/boot/MetadataSources.html[Javadocs] for more information. +`MetadataSources` has many other methods as well. Explore its API and https://docs.hibernate.org/orm/{majorMinorVersion}/javadocs/org/hibernate/boot/MetadataSources.html[Javadocs] for more information. Also, all methods on `MetadataSources` offer fluent-style call chaining:: [[bootstrap-native-metadata-source-example]] @@ -120,7 +120,7 @@ include::{example-dir-boot}/BootstrapTest.java[tags=bootstrap-native-metadata-so ==== Once we have the sources of mapping information defined, we need to build the `Metadata` object. -If you are ok with the default behavior in building the Metadata then you can simply call the https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/boot/MetadataSources.html#buildMetadata--[`buildMetadata`] method of the `MetadataSources`. +If you are ok with the default behavior in building the Metadata then you can simply call the https://docs.hibernate.org/orm/{majorMinorVersion}/javadocs/org/hibernate/boot/MetadataSources.html#buildMetadata--[`buildMetadata`] method of the `MetadataSources`. [NOTE] ==== @@ -132,7 +132,7 @@ From there, `MetadataBuilder`, `Metadata`, `SessionFactoryBuilder`, and `Session However, if you wish to adjust the process of building `Metadata` from `MetadataSources`, you will need to use the `MetadataBuilder` as obtained via `MetadataSources#getMetadataBuilder`. `MetadataBuilder` allows a lot of control over the `Metadata` building process. -See its https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/boot/MetadataBuilder.html[Javadocs] for full details. +See its https://docs.hibernate.org/orm/{majorMinorVersion}/javadocs/org/hibernate/boot/MetadataBuilder.html[Javadocs] for full details. [[bootstrap-native-metadata-builder-example]] .Building Metadata via `MetadataBuilder` @@ -147,9 +147,9 @@ include::{example-dir-boot}/BootstrapTest.java[tags=bootstrap-native-metadata-bu ==== Building the SessionFactory The final step in native bootstrapping is to build the `SessionFactory` itself. -Much like discussed above, if you are ok with the default behavior of building a `SessionFactory` from a `Metadata` reference, you can simply call the https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/boot/Metadata.html#buildSessionFactory--[`buildSessionFactory`] method on the `Metadata` object. +Much like discussed above, if you are ok with the default behavior of building a `SessionFactory` from a `Metadata` reference, you can simply call the https://docs.hibernate.org/orm/{majorMinorVersion}/javadocs/org/hibernate/boot/Metadata.html#buildSessionFactory--[`buildSessionFactory`] method on the `Metadata` object. -However, if you would like to adjust that building process, you will need to use `SessionFactoryBuilder` as obtained via `Metadata#getSessionFactoryBuilder`. Again, see its https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/boot/Metadata.html#getSessionFactoryBuilder--[Javadocs] for more details. +However, if you would like to adjust that building process, you will need to use `SessionFactoryBuilder` as obtained via `Metadata#getSessionFactoryBuilder`. Again, see its https://docs.hibernate.org/orm/{majorMinorVersion}/javadocs/org/hibernate/boot/Metadata.html#getSessionFactoryBuilder--[Javadocs] for more details. [[bootstrap-native-SessionFactory-example]] .Native Bootstrapping - Putting it all together @@ -242,7 +242,7 @@ include::{example-dir-boot}/BootstrapTest.java[tags=bootstrap-jpa-compliant-Enti ==== If you don't want to provide a `persistence.xml` configuration file, Jakarta Persistence allows you to provide all the configuration options in a {jpaJavadocUrlPrefix}spi/PersistenceUnitInfo.html[`PersistenceUnitInfo`] implementation and call -https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/jpa/HibernatePersistenceProvider.html#createContainerEntityManagerFactory-jakarta.persistence.spi.PersistenceUnitInfo-java.util.Map-[`HibernatePersistenceProvider#createContainerEntityManagerFactory()`]. +https://docs.hibernate.org/orm/{majorMinorVersion}/javadocs/org/hibernate/jpa/HibernatePersistenceProvider.html#createContainerEntityManagerFactory-jakarta.persistence.spi.PersistenceUnitInfo-java.util.Map-[`HibernatePersistenceProvider#createContainerEntityManagerFactory()`]. ==== To inject the default Persistence Context, you can use the {jpaJavadocUrlPrefix}PersistenceContext.html[`@PersistenceContext`] annotation. @@ -320,7 +320,7 @@ As previously seen, the Hibernate native bootstrap mechanism allows you to custo When using Hibernate as a Jakarta Persistence provider, the `EntityManagerFactory` is backed by a `SessionFactory`. For this reason, you might still want to use the `Metadata` object to pass various settings which cannot be supplied via the standard Hibernate <>. For this reason, you can use the -https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/boot/spi/MetadataBuilderContributor.html[`MetadataBuilderContributor`] class as you can see in the following examples. +https://docs.hibernate.org/orm/{majorMinorVersion}/javadocs/org/hibernate/boot/spi/MetadataBuilderContributor.html[`MetadataBuilderContributor`] class as you can see in the following examples. [[bootstrap-jpa-compliant-MetadataBuilderContributor-example]] .Implementing a `MetadataBuilderContributor` @@ -335,6 +335,6 @@ org.hibernate.orm.test.bootstrap.spi.metadatabuildercontributor The above `MetadataBuilderContributor` is used to register a `SqlFuction` which is not defined by the currently running Hibernate `Dialect`, but which we need to reference in our JPQL queries. By having access to the -https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/boot/MetadataBuilder.html[`MetadataBuilder`] class that's used by the underlying `SessionFactory`, the Jakarta Persistence bootstrap becomes just as flexible as the Hibernate native bootstrap mechanism. +https://docs.hibernate.org/orm/{majorMinorVersion}/javadocs/org/hibernate/boot/MetadataBuilder.html[`MetadataBuilder`] class that's used by the underlying `SessionFactory`, the Jakarta Persistence bootstrap becomes just as flexible as the Hibernate native bootstrap mechanism. You can then pass the custom `MetadataBuilderContributor` via the `hibernate.metadata_builder_contributor` configuration property as explained in the <>. diff --git a/documentation/src/main/asciidoc/userguide/chapters/caching/Caching.adoc b/documentation/src/main/asciidoc/userguide/chapters/caching/Caching.adoc index b7ca0866857b..23a4b6bb3c38 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/caching/Caching.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/caching/Caching.adoc @@ -39,7 +39,7 @@ Besides provider specific configuration, there are a number of configurations op `hibernate.cache.use_second_level_cache`:: Enable or disable second level caching overall. By default, if the currently configured - https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/cache/spi/RegionFactory.html[`RegionFactory`] is not the `NoCachingRegionFactory`, then the second-level cache is going to be enabled. Otherwise, the second-level cache is disabled. + https://docs.hibernate.org/orm/{majorMinorVersion}/javadocs/org/hibernate/cache/spi/RegionFactory.html[`RegionFactory`] is not the `NoCachingRegionFactory`, then the second-level cache is going to be enabled. Otherwise, the second-level cache is disabled. `hibernate.cache.use_query_cache`:: Enable or disable second level caching of query results. The default is false. `hibernate.cache.query_cache_factory`:: @@ -120,7 +120,7 @@ transactional:: ==== Rather than using a global setting, it is recommended to define the cache concurrency strategy on a per entity basis. -Use the https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/annotations/Cache.html[`@org.hibernate.annotations.Cache`] annotation for this purpose. +Use the https://docs.hibernate.org/orm/{majorMinorVersion}/javadocs/org/hibernate/annotations/Cache.html[`@org.hibernate.annotations.Cache`] annotation for this purpose. ==== The `@Cache` annotation define three attributes: @@ -377,7 +377,7 @@ include::{example-dir-caching}/SecondLevelCacheTest.java[tags=caching-query-regi [NOTE] ==== -When using {jpaJavadocUrlPrefix}CacheStoreMode.html#REFRESH[`CacheStoreMode.REFRESH`] or https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/CacheMode.html#REFRESH[`CacheMode.REFRESH`] in conjunction with the region you have defined for the given query, +When using {jpaJavadocUrlPrefix}CacheStoreMode.html#REFRESH[`CacheStoreMode.REFRESH`] or https://docs.hibernate.org/orm/{majorMinorVersion}/javadocs/org/hibernate/CacheMode.html#REFRESH[`CacheMode.REFRESH`] in conjunction with the region you have defined for the given query, Hibernate will selectively force the results cached in that particular region to be refreshed. This behavior is particularly useful in cases when the underlying data may have been updated via a separate process @@ -419,7 +419,7 @@ by placing the annotation on the entity class or the persistent collection attri [[caching-management]] === Managing the cached data -Traditionally, Hibernate defined the https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/CacheMode.html[`CacheMode`] enumeration to describe +Traditionally, Hibernate defined the https://docs.hibernate.org/orm/{majorMinorVersion}/javadocs/org/hibernate/CacheMode.html[`CacheMode`] enumeration to describe the ways of interactions with the cached data. Jakarta Persistence split cache modes by storage ({jpaJavadocUrlPrefix}CacheStoreMode.html[`CacheStoreMode`]) and retrieval ({jpaJavadocUrlPrefix}CacheRetrieveMode.html[`CacheRetrieveMode`]). @@ -495,7 +495,7 @@ include::{example-dir-caching}/SecondLevelCacheTest.java[tags=caching-management ==== Hibernate is much more flexible in this regard as it offers fine-grained control over what needs to be evicted. -The https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/Cache.html[`org.hibernate.Cache`] interface defines various evicting strategies: +The https://docs.hibernate.org/orm/{majorMinorVersion}/javadocs/org/hibernate/Cache.html[`org.hibernate.Cache`] interface defines various evicting strategies: - entities (by their class or region) - entities stored using the natural-id (by their class or region) @@ -518,7 +518,7 @@ If you enable the `hibernate.generate_statistics` configuration property, Hibernate will expose a number of metrics via `SessionFactory.getStatistics()`. Hibernate can even be configured to expose these statistics via JMX. -This way, you can get access to the https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/stat/Statistics.html[`Statistics`] class which comprises all sort of +This way, you can get access to the https://docs.hibernate.org/orm/{majorMinorVersion}/javadocs/org/hibernate/stat/Statistics.html[`Statistics`] class which comprises all sort of second-level cache metrics. [[caching-statistics-example]] diff --git a/documentation/src/main/asciidoc/userguide/chapters/compatibility/Compatibility.adoc b/documentation/src/main/asciidoc/userguide/chapters/compatibility/Compatibility.adoc index 49bae6674020..f554dbe452a1 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/compatibility/Compatibility.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/compatibility/Compatibility.adoc @@ -70,7 +70,7 @@ Maven:: [[compatibility-database]] === Database -Hibernate {fullVersion} is compatible with the following database versions, -provided you use the corresponding <>: +Hibernate {fullVersion}'s compatibility with a given database and version +depends on the dialect being used. -include::{generated-report-dir}/dialect/dialect-table.adoc[] +Refer to the link:{doc-dialect-url}[Dialects] guide for details about both dialects and supported databases. diff --git a/documentation/src/main/asciidoc/userguide/chapters/domain/associations.adoc b/documentation/src/main/asciidoc/userguide/chapters/domain/associations.adoc index 991646853b23..e762e4d6ec67 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/domain/associations.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/domain/associations.adoc @@ -784,7 +784,7 @@ include::{extrasdir}/associations-many-to-any-query-example.sql[] [[associations-JoinFormula]] ==== `@JoinFormula` mapping -The https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/annotations/JoinFormula.html[`@JoinFormula`] annotation is used to customize the join between a child Foreign Key and a parent row Primary Key. +The https://docs.hibernate.org/orm/{majorMinorVersion}/javadocs/org/hibernate/annotations/JoinFormula.html[`@JoinFormula`] annotation is used to customize the join between a child Foreign Key and a parent row Primary Key. [[associations-JoinFormula-example]] .`@JoinFormula` mapping usage @@ -834,7 +834,7 @@ Therefore, the `@JoinFormula` annotation is used to define a custom join associa [[associations-JoinColumnOrFormula]] ==== `@JoinColumnOrFormula` mapping -The https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/annotations/JoinColumnOrFormula.html[`@JoinColumnOrFormula`] annotation is used to customize the join between a child Foreign Key and a parent row Primary Key when we need to take into consideration a column value as well as a `@JoinFormula`. +The https://docs.hibernate.org/orm/{majorMinorVersion}/javadocs/org/hibernate/annotations/JoinColumnOrFormula.html[`@JoinColumnOrFormula`] annotation is used to customize the join between a child Foreign Key and a parent row Primary Key when we need to take into consideration a column value as well as a `@JoinFormula`. [[associations-JoinColumnOrFormula-example]] .`@JoinColumnOrFormula` mapping usage diff --git a/documentation/src/main/asciidoc/userguide/chapters/domain/basic_types.adoc b/documentation/src/main/asciidoc/userguide/chapters/domain/basic_types.adoc index 63307989a298..6870d77982c0 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/domain/basic_types.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/domain/basic_types.adoc @@ -2023,7 +2023,7 @@ include::{example-dir-resources}/mapping/converter/hbm/MoneyConverterHbmTest.hbm A basic type that's converted by a Jakarta Persistence `AttributeConverter` is immutable if the underlying Java type is immutable and is mutable if the associated attribute type is mutable as well. -Therefore, mutability is given by the https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/type/descriptor/java/JavaType.html#getMutabilityPlan--[`JavaType#getMutabilityPlan`] +Therefore, mutability is given by the https://docs.hibernate.org/orm/{majorMinorVersion}/javadocs/org/hibernate/type/descriptor/java/JavaType.html#getMutabilityPlan--[`JavaType#getMutabilityPlan`] of the associated entity attribute type. This can be adjusted by using `@Immutable` or `@Mutability` on any of: @@ -2603,8 +2603,8 @@ include::{example-dir-generated}/temporals/GeneratedUuidTests.java[tags=mapping- ---- ==== -See https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/annotations/ValueGenerationType.html[`@ValueGenerationType`] -and https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/tuple/AnnotationValueGeneration.html[`AnnotationValueGeneration`] +See https://docs.hibernate.org/orm/{majorMinorVersion}/javadocs/org/hibernate/annotations/ValueGenerationType.html[`@ValueGenerationType`] +and https://docs.hibernate.org/orm/{majorMinorVersion}/javadocs/org/hibernate/generator/tuple/AnnotationValueGeneration.html[`AnnotationBasedGenerator`] for details of each contract diff --git a/documentation/src/main/asciidoc/userguide/chapters/domain/collections.adoc b/documentation/src/main/asciidoc/userguide/chapters/domain/collections.adoc index 94a48f910566..2b8fe67d4497 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/domain/collections.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/domain/collections.adoc @@ -5,7 +5,7 @@ :core-project-dir: {root-project-dir}/hibernate-core :core-test-base: {core-project-dir}/src/test/java :example-dir-collection: {core-test-base}/org/hibernate/orm/test/mapping/collections -:docs-base: https://docs.jboss.org/hibernate/orm/{majorMinorVersion} +:docs-base: https://docs.hibernate.org/orm/{majorMinorVersion} :javadoc-base: {docs-base}/javadoc :java-javadoc-base: https://docs.oracle.com/en/java/javase/11/docs/api/java.base :extrasdir: extras/collections @@ -715,7 +715,7 @@ When fetching the collection, Hibernate will use the fetched ordered columns to [[collections-customizing-ordered-list-ordinal]] ===== Customizing ordered list ordinal -You can customize the ordinal of the underlying ordered list by using the https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/annotations/ListIndexBase.html[`@ListIndexBase`] annotation. +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 @@ -748,7 +748,7 @@ include::{extrasdir}/collections-customizing-ordered-list-ordinal-persist-exampl While the Jakarta Persistence {jpaJavadocUrlPrefix}OrderBy.html[`@OrderBy`] annotation allows you to specify the entity attributes used for sorting when fetching the current annotated collection, the Hibernate specific -https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/annotations/OrderBy.html[`@OrderBy`] annotation is used to specify a *SQL* clause instead. +https://docs.hibernate.org/orm/{majorMinorVersion}/javadocs/org/hibernate/annotations/OrderBy.html[`@OrderBy`] annotation is used to specify a *SQL* clause instead. In the following example, the `@OrderBy` annotation uses the `CHAR_LENGTH` SQL function to order the `Article` entities by the number of characters of the `name` attribute. @@ -929,7 +929,7 @@ include::{extrasdir}/collections-map-value-type-entity-key-add-example.sql[] ===== Maps with a custom key type Hibernate defines the -https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/annotations/MapKeyType.html[`@MapKeyType`] annotation +https://docs.hibernate.org/orm/{majorMinorVersion}/javadocs/org/hibernate/annotations/MapKeyType.html[`@MapKeyType`] annotation which you can use to customize the `Map` key type. Considering you have the following tables in your database: diff --git a/documentation/src/main/asciidoc/userguide/chapters/domain/embeddables.adoc b/documentation/src/main/asciidoc/userguide/chapters/domain/embeddables.adoc index dd8af3f81cd7..568092759e66 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/domain/embeddables.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/domain/embeddables.adoc @@ -161,7 +161,7 @@ Embeddable types that are used as collection entries, map keys or entity type id [[embeddable-Target]] ==== `@Target` mapping -The https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/annotations/Target.html[`@Target`] annotation is used to specify the implementation class of a given association that is mapped via an interface. +The https://docs.hibernate.org/orm/{majorMinorVersion}/javadocs/org/hibernate/annotations/Target.html[`@Target`] annotation is used to specify the implementation class of a given association that is mapped via an interface. The {jpaJavadocUrlPrefix}ManyToOne.html[`@ManyToOne`], {jpaJavadocUrlPrefix}OneToOne.html[`@OneToOne`], @@ -617,4 +617,4 @@ Again, the name and the nullability of the `aggregate` column can be refined thr Mapping <> inside an `@Embeddable` value is supported in most cases. There are a couple exceptions: * If the values of an <> is of embeddable type, that embeddable cannot contain nested collections; -* Explicitly selecting an embeddable that contains collections in a query is currently not supported (we wouldn't be able to correctly initialize the collection since its owning entity instance would be missing from the Persistence Context). \ No newline at end of file +* Explicitly selecting an embeddable that contains collections in a query is currently not supported (we wouldn't be able to correctly initialize the collection since its owning entity instance would be missing from the Persistence Context). diff --git a/documentation/src/main/asciidoc/userguide/chapters/domain/entity.adoc b/documentation/src/main/asciidoc/userguide/chapters/domain/entity.adoc index 0f543a7f6340..3850c55bdfb4 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/domain/entity.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/domain/entity.adoc @@ -405,7 +405,7 @@ For details on mapping the identifier, see the < cls, String entityName, Number revision, boolean includeDeletions)`] +https://docs.hibernate.org/orm/{majorMinorVersion}/javadocs/org/hibernate/envers/query/AuditQueryCreator.html#forEntitiesAtRevision-java.lang.Class-java.lang.String-java.lang.Number-boolean-[`forEntitiesAtRevision(Class cls, String entityName, Number revision, boolean includeDeletions)`] method to get the deleted entity revision so that, instead of a `NoResultException`, all attributes, except for the entity identifier, are going to be `null`. @@ -190,7 +190,7 @@ include::{example-dir-envers}/DefaultAuditTest.java[tags=envers-audited-rev4-exa ---- ==== -See the https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/envers/AuditReader.html[Javadocs] for details on other functionality offered. +See the https://docs.hibernate.org/orm/{majorMinorVersion}/javadocs/org/hibernate/envers/AuditReader.html[Javadocs] for details on other functionality offered. [[envers-configuration]] === Configuration Properties @@ -499,7 +499,7 @@ Simply add the custom revision entity as you do your normal entities and Envers NOTE: It is an error for there to be multiple entities marked as `@org.hibernate.envers.RevisionEntity`. . Second, you need to tell Envers how to create instances of your revision entity which is handled by the -https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/envers/RevisionListener.html#newRevision-java.lang.Object-[`newRevision( Object revisionEntity )`] +https://docs.hibernate.org/orm/{majorMinorVersion}/javadocs/org/hibernate/envers/RevisionListener.html#newRevision-java.lang.Object-[`newRevision( Object revisionEntity )`] method of the `org.hibernate.envers.RevisionListener` interface. + You tell Envers your custom `org.hibernate.envers.RevisionListener` implementation to use by specifying it on the `@org.hibernate.envers.RevisionEntity` annotation, using the value attribute. @@ -578,7 +578,7 @@ As demonstrated by the example above, the username is properly set and propagate **This strategy is deprecated since version 5.2. The alternative is to use dependency injection offered as of version 5.3.** An alternative method to using the `org.hibernate.envers.RevisionListener` is to instead call the -[line-through]#https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/envers/AuditReader.html#getCurrentRevision-java.lang.Class-boolean-[`getCurrentRevision( Class revisionEntityClass, boolean persist )`]# +[line-through]#https://docs.hibernate.org/orm/{majorMinorVersion}/javadocs/org/hibernate/envers/AuditReader.html#getCurrentRevision-java.lang.Class-boolean-[`getCurrentRevision( Class revisionEntityClass, boolean persist )`]# method of the `org.hibernate.envers.AuditReader` interface to obtain the current revision, and fill it with desired information. diff --git a/documentation/src/main/asciidoc/userguide/chapters/fetching/Fetching.adoc b/documentation/src/main/asciidoc/userguide/chapters/fetching/Fetching.adoc index a70d9733cca8..6a0cbf5af2e8 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/fetching/Fetching.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/fetching/Fetching.adoc @@ -411,7 +411,7 @@ However, if the `Employee` data is not resolved in cache, the `Employee` and `Pr [[fetching-batch]] === Batch fetching -Hibernate offers the https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/annotations/BatchSize.html[`@BatchSize`] annotation, +Hibernate offers the https://docs.hibernate.org/orm/{majorMinorVersion}/javadocs/org/hibernate/annotations/BatchSize.html[`@BatchSize`] annotation, which can be used when fetching uninitialized entity proxies. Considering the following entity mapping: @@ -601,8 +601,8 @@ This time, there was no secondary query because the child collection was loaded [[fetching-LazyCollection]] === `@LazyCollection` -The https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/annotations/LazyCollection.html[`@LazyCollection`] annotation is used to specify the lazy fetching behavior of a given collection. -The possible values are given by the `https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/annotations/LazyCollectionOption.html[LazyCollectionOption]` enumeration: +The https://docs.hibernate.org/orm/{majorMinorVersion}/javadocs/org/hibernate/annotations/LazyCollection.html[`@LazyCollection`] annotation is used to specify the lazy fetching behavior of a given collection. +The possible values are given by the `https://docs.hibernate.org/orm/{majorMinorVersion}/javadocs/org/hibernate/annotations/LazyCollectionOption.html[LazyCollectionOption]` enumeration: `TRUE`:: Load it when the state is requested. `FALSE`:: Eagerly load it. diff --git a/documentation/src/main/asciidoc/userguide/chapters/flushing/Flushing.adoc b/documentation/src/main/asciidoc/userguide/chapters/flushing/Flushing.adoc index 63da093aa80e..0a66900e8d14 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/flushing/Flushing.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/flushing/Flushing.adoc @@ -18,7 +18,7 @@ Because DML statements are grouped together, Hibernate can apply batching transp See the <> for more information. ==== -The flushing strategy is given by the https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/Session.html#getFlushMode--[`flushMode`] of the current running Hibernate `Session`. +The flushing strategy is given by the https://docs.hibernate.org/orm/{majorMinorVersion}/javadocs/org/hibernate/Session.html#getFlushMode--[`flushMode`] of the current running Hibernate `Session`. Although Jakarta Persistence defines only two flushing strategies ({jpaJavadocUrlPrefix}FlushModeType.html#AUTO[`AUTO`] and {jpaJavadocUrlPrefix}FlushModeType.html#COMMIT[`COMMIT`]), Hibernate has a much broader spectrum of flush types: diff --git a/documentation/src/main/asciidoc/userguide/chapters/jdbc/Database_Access.adoc b/documentation/src/main/asciidoc/userguide/chapters/jdbc/Database_Access.adoc index b8fafb17eb7e..b0f37b1ce271 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/jdbc/Database_Access.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/jdbc/Database_Access.adoc @@ -8,7 +8,7 @@ As an ORM tool, probably the single most important thing you need to tell Hibern This is ultimately the function of the `org.hibernate.engine.jdbc.connections.spi.ConnectionProvider` interface. Hibernate provides some out of the box implementations of this interface. `ConnectionProvider` is also an extension point so you can also use custom implementations from third parties or written yourself. -The `ConnectionProvider` to use is defined by the `hibernate.connection.provider_class` setting. See the https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/cfg/AvailableSettings.html#CONNECTION_PROVIDER[`org.hibernate.cfg.AvailableSettings#CONNECTION_PROVIDER`] +The `ConnectionProvider` to use is defined by the `hibernate.connection.provider_class` setting. See the https://docs.hibernate.org/orm/{majorMinorVersion}/javadocs/org/hibernate/cfg/AvailableSettings.html#CONNECTION_PROVIDER[`org.hibernate.cfg.AvailableSettings#CONNECTION_PROVIDER`] Generally speaking, applications should not have to configure a `ConnectionProvider` explicitly if using one of the Hibernate-provided implementations. Hibernate will internally determine which `ConnectionProvider` to use based on the following algorithm: @@ -230,7 +230,7 @@ Again, this is only supported for JDBC standard isolation levels, not for isolat === Connection handling The connection handling mode is defined by the -https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/resource/jdbc/spi/PhysicalConnectionHandlingMode.html[`PhysicalConnectionHandlingMode`] enumeration which provides the following strategies: +https://docs.hibernate.org/orm/{majorMinorVersion}/javadocs/org/hibernate/resource/jdbc/spi/PhysicalConnectionHandlingMode.html[`PhysicalConnectionHandlingMode`] enumeration which provides the following strategies: `IMMEDIATE_ACQUISITION_AND_HOLD`:: The `Connection` will be acquired as soon as the `Session` is opened and held until the `Session` is closed. @@ -276,8 +276,8 @@ and no connection leak false positive is being reported, then you should conside ==== User-provided connections If the current `Session` was created using the -https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/SessionBuilder.html[`SessionBuilder`] and a JDBC `Connection` was provided via the -https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/SessionBuilder.html#connection-java.sql.Connection-[`SessionBuilder#connection`] method, then the user-provided `Connection` is going to be used, and +https://docs.hibernate.org/orm/{majorMinorVersion}/javadocs/org/hibernate/SessionBuilder.html[`SessionBuilder`] and a JDBC `Connection` was provided via the +https://docs.hibernate.org/orm/{majorMinorVersion}/javadocs/org/hibernate/SessionBuilder.html#connection-java.sql.Connection-[`SessionBuilder#connection`] method, then the user-provided `Connection` is going to be used, and the connection handling mode will be `IMMEDIATE_ACQUISITION_AND_HOLD`. Therefore for user-provided connection, the connection is acquired right away and held until the current `Session` is closed, without being influenced by the Jakarta Persistence or Hibernate transaction context. @@ -292,5 +292,7 @@ Hibernate abstracts over variations between dialects of SQL via the class `org.h - There's a subclass of `Dialect` for each supported relational database in the package `org.hibernate.dialect`. - Additional community-supported ``Dialect``s are available in the separate module `hibernate-community-dialects`. -In Hibernate 6, it's no longer necessary to explicitly specify a dialect using the configuration property `hibernate.dialect`, and so setting that property is now discouraged. -(An exception is the case of custom user-written ``Dialect``s.) \ No newline at end of file +Starting with Hibernate 6, it's no longer necessary to explicitly specify a dialect using the configuration property `hibernate.dialect`, and so setting that property is now discouraged. +An exception is the case of custom or link:{doc-dialect-url}#third-party-dialects[third-party] ``Dialect``s. + +NOTE: For information about available dialects and compatible database versions, see the link:{doc-dialect-url}[dialect guide]. diff --git a/documentation/src/main/asciidoc/userguide/chapters/locking/Locking.adoc b/documentation/src/main/asciidoc/userguide/chapters/locking/Locking.adoc index e1a0aa2fc7c7..997c04eb9dc1 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/locking/Locking.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/locking/Locking.adoc @@ -146,7 +146,7 @@ include::{extrasdir}/locking-optimistic-version-timestamp-source-persist-example By default, every entity attribute modification is going to trigger a version incrementation. If there is an entity property which should not bump up the entity version, -then you need to annotate it with the Hibernate https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/annotations/OptimisticLock.html[`@OptimisticLock`] annotation, +then you need to annotate it with the Hibernate https://docs.hibernate.org/orm/{majorMinorVersion}/javadocs/org/hibernate/annotations/OptimisticLock.html[`@OptimisticLock`] annotation, as illustrated in the following example. [[locking-optimistic-exclude-attribute-mapping-example]] @@ -197,9 +197,9 @@ This is also useful for use with modeling legacy schemas. The idea is that you can get Hibernate to perform "version checks" using either all of the entity's attributes or just the attributes that have changed. This is achieved through the use of the -https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/annotations/OptimisticLocking.html[`@OptimisticLocking`] +https://docs.hibernate.org/orm/{majorMinorVersion}/javadocs/org/hibernate/annotations/OptimisticLocking.html[`@OptimisticLocking`] annotation which defines a single attribute of type -https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/annotations/OptimisticLockType.html[`org.hibernate.annotations.OptimisticLockType`]. +https://docs.hibernate.org/orm/{majorMinorVersion}/javadocs/org/hibernate/annotations/OptimisticLockType.html[`org.hibernate.annotations.OptimisticLockType`]. There are 4 available OptimisticLockTypes: @@ -291,7 +291,7 @@ is that it allows you to minimize the risk of `OptimisticEntityLockException` ac When using `OptimisticLockType.DIRTY`, you should also use `@DynamicUpdate` because the `UPDATE` statement must take into consideration all the dirty entity property values, and also the `@SelectBeforeUpdate` annotation so that detached entities are properly handled by the -https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/Session.html#update-java.lang.Object-[`Session#update(entity)`] operation. +https://docs.hibernate.org/orm/{majorMinorVersion}/javadocs/org/hibernate/Session.html#update-java.lang.Object-[`Session#update(entity)`] operation. ==== [[locking-pessimistic]] diff --git a/documentation/src/main/asciidoc/userguide/chapters/multitenancy/MultiTenancy.adoc b/documentation/src/main/asciidoc/userguide/chapters/multitenancy/MultiTenancy.adoc index b30f83d1d12e..e9608b5dc643 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/multitenancy/MultiTenancy.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/multitenancy/MultiTenancy.adoc @@ -121,7 +121,7 @@ It could name a `MultiTenantConnectionProvider` instance, a `MultiTenantConnecti * Passed directly to the `org.hibernate.boot.registry.StandardServiceRegistryBuilder`. * If none of the above options match, but the settings do specify a `hibernate.connection.datasource` value, Hibernate will assume it should use the specific `DataSourceBasedMultiTenantConnectionProviderImpl` implementation which works on a number of pretty reasonable assumptions when running inside of an app server and using one `javax.sql.DataSource` per tenant. -See its https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/engine/jdbc/connections/spi/DataSourceBasedMultiTenantConnectionProviderImpl.html[Javadocs] for more details. +See its https://docs.hibernate.org/orm/{majorMinorVersion}/javadocs/org/hibernate/engine/jdbc/connections/spi/DataSourceBasedMultiTenantConnectionProviderImpl.html[Javadocs] for more details. The following example portrays a `MultiTenantConnectionProvider` implementation that handles multiple ``ConnectionProvider``s. diff --git a/documentation/src/main/asciidoc/userguide/chapters/pc/PersistenceContext.adoc b/documentation/src/main/asciidoc/userguide/chapters/pc/PersistenceContext.adoc index d6e3d1c97bcf..a4f02d6f9846 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/pc/PersistenceContext.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/pc/PersistenceContext.adoc @@ -188,10 +188,10 @@ but also inefficient. While the Jakarta Persistence standard does not support retrieving multiple entities at once, other than running a JPQL or Criteria API query, Hibernate offers this functionality via the -https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/Session.html#byMultipleIds-java.lang.Class-[`byMultipleIds` method] of the Hibernate `Session`. +https://docs.hibernate.org/orm/{majorMinorVersion}/javadocs/org/hibernate/Session.html#byMultipleIds-java.lang.Class-[`byMultipleIds` method] of the Hibernate `Session`. The `byMultipleIds` method returns a -https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/MultiIdentifierLoadAccess.html[`MultiIdentifierLoadAccess`] +https://docs.hibernate.org/orm/{majorMinorVersion}/javadocs/org/hibernate/MultiIdentifierLoadAccess.html[`MultiIdentifierLoadAccess`] which you can use to customize the multi-load request. The `MultiIdentifierLoadAccess` interface provides several methods which you can use to @@ -221,10 +221,10 @@ When enabled, the result set will contain deleted entities. When disabled (which is the default behavior), deleted entities are not included in the returning `List`. `with(LockOptions lockOptions)`:: This setting allows you to pass a given -https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/LockOptions.html[`LockOptions`] mode to the multi-load query. +https://docs.hibernate.org/orm/{majorMinorVersion}/javadocs/org/hibernate/LockOptions.html[`LockOptions`] mode to the multi-load query. `with(CacheMode cacheMode)`:: This setting allows you to pass a given -https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/CacheMode.html[`CacheMode`] +https://docs.hibernate.org/orm/{majorMinorVersion}/javadocs/org/hibernate/CacheMode.html[`CacheMode`] strategy so that we can load entities from the second-level cache, therefore skipping the cached entities from being fetched via the multi-load query. `withBatchSize(int batchSize)`:: This setting allows you to specify a batch size for loading the entities (e.g. how many at a time). @@ -256,11 +256,11 @@ include::{extrasdir}/pc-by-multiple-ids-example.sql[] ==== Notice that only one SQL SELECT statement was executed since the second call uses the -https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/MultiIdentifierLoadAccess.html#enableSessionCheck-boolean-[`enableSessionCheck`] method of the `MultiIdentifierLoadAccess` +https://docs.hibernate.org/orm/{majorMinorVersion}/javadocs/org/hibernate/MultiIdentifierLoadAccess.html#enableSessionCheck-boolean-[`enableSessionCheck`] method of the `MultiIdentifierLoadAccess` to instruct Hibernate to skip entities that are already loaded in the current Persistence Context. If the entities are not available in the current Persistence Context but they could be loaded from the second-level cache, you can use the -https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/MultiIdentifierLoadAccess.html#with-org.hibernate.CacheMode-[`with(CacheMode)`] method of the `MultiIdentifierLoadAccess` object. +https://docs.hibernate.org/orm/{majorMinorVersion}/javadocs/org/hibernate/MultiIdentifierLoadAccess.html#with-org.hibernate.CacheMode-[`with(CacheMode)`] method of the `MultiIdentifierLoadAccess` object. [[tag::pc-by-multiple-ids-second-level-cache-example]] .Loading multiple entities from the second-level cache @@ -280,7 +280,7 @@ shared cache. Afterward, when executing the second `byMultipleIds` call for the same entities in a new Hibernate `Session`, we set the -https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/CacheMode.html#NORMAL[`CacheMode.NORMAL`] second-level cache mode so that entities are going to be returned from the second-level cache. +https://docs.hibernate.org/orm/{majorMinorVersion}/javadocs/org/hibernate/CacheMode.html#NORMAL[`CacheMode.NORMAL`] second-level cache mode so that entities are going to be returned from the second-level cache. The `getSecondLevelCacheHitCount` statistics method returns 3 this time, since the 3 entities were loaded from the second-level cache, and, as illustrated by `sqlStatementInterceptor.getSqlQueries()`, no multi-load SELECT statement was executed this time. @@ -648,7 +648,7 @@ For this reason, the second-level collection cache is limited to storing whole c When using the `@Filter` annotation and working with entities that are mapped onto multiple database tables, you will need to use the -https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/annotations/SqlFragmentAlias.html[`@SqlFragmentAlias`] annotation +https://docs.hibernate.org/orm/{majorMinorVersion}/javadocs/org/hibernate/annotations/SqlFragmentAlias.html[`@SqlFragmentAlias`] annotation if the `@Filter` defines a condition that uses predicates across multiple tables. [[pc-filter-sql-fragment-alias-example]] @@ -1425,18 +1425,18 @@ Certain methods of the Jakarta Persistence `EntityManager` or the Hibernate `Ses Rolling back the database transaction does not put your business objects back into the state they were at the start of the transaction. This means that the database state and the business objects will be out of sync. Usually, this is not a problem because exceptions are not recoverable and you will have to start over after rollback anyway. The Jakarta Persistence {jpaJavadocUrlPrefix}PersistenceException.html[`PersistenceException`] or the -https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/HibernateException.html[`HibernateException`] wraps most of the errors that can occur in a Hibernate persistence layer. +https://docs.hibernate.org/orm/{majorMinorVersion}/javadocs/org/hibernate/HibernateException.html[`HibernateException`] wraps most of the errors that can occur in a Hibernate persistence layer. Both the `PersistenceException` and the `HibernateException` are runtime exceptions because, in our opinion, we should not force the application developer to catch an unrecoverable exception at a low layer. In most systems, unchecked and fatal exceptions are handled in one of the first frames of the method call stack (i.e., in higher layers) and either an error message is presented to the application user or some other appropriate action is taken. Note that Hibernate might also throw other unchecked exceptions that are not a `HibernateException`. These are not recoverable either, and appropriate action should be taken. Hibernate wraps the JDBC `SQLException`, thrown while interacting with the database, in a -https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/JDBCException.html[`JDBCException`]. -In fact, Hibernate will attempt to convert the exception into a more meaningful subclass of `JDBCException`. The underlying `SQLException` is always available via https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/JDBCException.html#getSQLException--[`JDBCException.getSQLException()`]. Hibernate converts the `SQLException` into an appropriate JDBCException subclass using the -https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/exception/spi/SQLExceptionConverter.html[`SQLExceptionConverter`] +https://docs.hibernate.org/orm/{majorMinorVersion}/javadocs/org/hibernate/JDBCException.html[`JDBCException`]. +In fact, Hibernate will attempt to convert the exception into a more meaningful subclass of `JDBCException`. The underlying `SQLException` is always available via https://docs.hibernate.org/orm/{majorMinorVersion}/javadocs/org/hibernate/JDBCException.html#getSQLException--[`JDBCException.getSQLException()`]. Hibernate converts the `SQLException` into an appropriate JDBCException subclass using the +https://docs.hibernate.org/orm/{majorMinorVersion}/javadocs/org/hibernate/exception/spi/SQLExceptionConverter.html[`SQLExceptionConverter`] attached to the current `SessionFactory`. By default, the `SQLExceptionConverter` is defined by the configured Hibernate `Dialect` via the -https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/dialect/Dialect.html#buildSQLExceptionConversionDelegate--[`buildSQLExceptionConversionDelegate`] method +https://docs.hibernate.org/orm/{majorMinorVersion}/javadocs/org/hibernate/dialect/Dialect.html#buildSQLExceptionConversionDelegate--[`buildSQLExceptionConversionDelegate`] method which is overridden by several database-specific ``Dialect``s. The standard `JDBCException` subtypes are: @@ -1465,8 +1465,8 @@ SQLGrammarException:: ==== Starting with Hibernate 5.2, the Hibernate `Session` extends the Jakarta Persistence `EntityManager`. For this reason, when a `SessionFactory` is built via Hibernate's native bootstrapping, the `HibernateException` or `SQLException` can be wrapped in a Jakarta Persistence {jpaJavadocUrlPrefix}PersistenceException.html[`PersistenceException`] when thrown -by `Session` methods that implement `EntityManager` methods (e.g., https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/Session.html#merge-java.lang.Object-[Session.merge(Object object)], -https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/Session.html#flush--[Session.flush()]). +by `Session` methods that implement `EntityManager` methods (e.g., https://docs.hibernate.org/orm/{majorMinorVersion}/javadocs/org/hibernate/Session.html#merge-java.lang.Object-[Session.merge(Object object)], +https://docs.hibernate.org/orm/{majorMinorVersion}/javadocs/org/hibernate/Session.html#flush--[Session.flush()]). If your `SessionFactory` is built via Hibernate's native bootstrapping, and you don't want the Hibernate exceptions to be wrapped in the Jakarta Persistence `PersistenceException`, you need to set the `hibernate.native_exception_handling_51_compliance` configuration property to `true`. See the diff --git a/documentation/src/main/asciidoc/userguide/chapters/portability/Portability.adoc b/documentation/src/main/asciidoc/userguide/chapters/portability/Portability.adoc index 21feacb3964d..f83352d0e858 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/portability/Portability.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/portability/Portability.adoc @@ -14,8 +14,11 @@ Regardless of the exact scenario, the basic idea is that you want Hibernate to h The first line of portability for Hibernate is the dialect, which is a specialization of the `org.hibernate.dialect.Dialect` contract. A dialect encapsulates all the differences in how Hibernate must communicate with a particular database to accomplish some task like getting a sequence value or structuring a SELECT query. -Hibernate bundles a wide range of dialects for many of the most popular databases. -If you find that your particular database is not among them, it is not terribly difficult to write your own. + +Hibernate bundles a wide range of dialects for many of the most popular databases: see the link:{doc-dialect-url}[dialect guide] for details. +If you find that your particular database is not among them, +you can check link:{doc-dialect-url}#third-party-dialects[dialects implemented by third parties], +and as a last resort it is not terribly difficult to write your own. [[portability-dialectresolver]] === Dialect resolution diff --git a/documentation/src/main/asciidoc/userguide/chapters/query/hql/Query.adoc b/documentation/src/main/asciidoc/userguide/chapters/query/hql/Query.adoc index d009bab1c41a..948f66b630b9 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/query/hql/Query.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/query/hql/Query.adoc @@ -87,7 +87,7 @@ include::{example-dir-model}/Person.java[tags=jpa-read-only-entities-native-exam ==== Alternatively, Hibernate offers an extended -https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/annotations/NamedQuery.html[`@NamedQuery`] annotation +https://docs.hibernate.org/orm/{majorMinorVersion}/javadocs/org/hibernate/annotations/NamedQuery.html[`@NamedQuery`] annotation which allows the specification of additional properties of the query, including flush mode, cacheability, and timeout interval, in a more typesafe way. [[jpql-api-hibernate-named-query-example]] @@ -348,7 +348,7 @@ On the other hand, `setHint()` refers to the Jakarta Persistence notion of a que This is a completely different concept. ==== -For complete details, see the https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/Query.html[Query] Javadocs. +For complete details, see the https://docs.hibernate.org/orm/{majorMinorVersion}/javadocs/org/hibernate/Query.html[Query] Javadocs. [[hql-api-basic-usage-example]] .Advanced query control @@ -364,7 +364,7 @@ include::{example-dir-query}/HQLTest.java[tags=hql-api-basic-usage-example] A program may hook into the process of building the query results by providing a `org.hibernate.transform.ResultListTransformer` or `org.hibernate.transform.TupleTransformer`. -See the https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/transform/ResultListTransformer.html[Javadocs] along with the built-in implementations for additional details. +See the https://docs.hibernate.org/orm/{majorMinorVersion}/javadocs/org/hibernate/transform/ResultListTransformer.html[Javadocs] along with the built-in implementations for additional details. //[[hql-api-parameters]] //==== Binding arguments to parameters 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 9dddd502024a..416bdabe9027 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc @@ -880,7 +880,7 @@ There are some very important functions for working with dates and times. The special function `extract()` obtains a single field of a date, time, or datetime. Field types include: `day`, `month`, `year`, `second`, `minute`, `hour`, `day of week`, `day of month`, `week of year`, `date`, `time`, `epoch` and more. -For a full list of field types, see the Javadoc for https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/query/TemporalUnit.html[`TemporalUnit`]. +For a full list of field types, see the Javadoc for https://docs.hibernate.org/orm/{majorMinorVersion}/javadocs/org/hibernate/query/TemporalUnit.html[`TemporalUnit`]. ==== [source, JAVA, indent=0] @@ -918,7 +918,7 @@ This function formats a date, time, or datetime according to a pattern. The syntax is `format(datetime as pattern)`, and the pattern must be written in a subset of the pattern language defined by Java's `java.time.format.DateTimeFormatter`. -For a full list of `format()` pattern elements, see the Javadoc for https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/dialect/Dialect.html#appendDatetimeFormat[`Dialect#appendDatetimeFormat`]. +For a full list of `format()` pattern elements, see the Javadoc for https://docs.hibernate.org/orm/{majorMinorVersion}/javadocs/org/hibernate/dialect/Dialect.html#appendDatetimeFormat[`Dialect#appendDatetimeFormat`]. [[hql-function-trunc-datetime]] ===== `trunc()` or `truncate()` diff --git a/documentation/src/main/asciidoc/userguide/chapters/query/native/Native.adoc b/documentation/src/main/asciidoc/userguide/chapters/query/native/Native.adoc index 7b4a99d273ea..452c498a33af 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/query/native/Native.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/query/native/Native.adoc @@ -526,7 +526,7 @@ include::{doc-emeddable-example-dir}/SQLTest.java[tags=sql-hibernate-multiple-sc ---- ==== -You can also use the https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/annotations/NamedNativeQuery.html[`@NamedNativeQuery`] Hibernate annotation +You can also use the https://docs.hibernate.org/orm/{majorMinorVersion}/javadocs/org/hibernate/annotations/NamedNativeQuery.html[`@NamedNativeQuery`] Hibernate annotation to customize the named query using various configurations such as fetch mode, cacheability, time out interval. [[sql-multiple-scalar-values-dto-NamedNativeQuery-hibernate-example]] diff --git a/documentation/src/main/asciidoc/userguide/chapters/schema/Schema.adoc b/documentation/src/main/asciidoc/userguide/chapters/schema/Schema.adoc index 61e030b59325..8c1e6ee1d06e 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/schema/Schema.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/schema/Schema.adoc @@ -122,7 +122,7 @@ include::{extrasdir}/schema-generation-database-checks-persist-example.sql[] [[schema-generation-column-default-value]] === Default value for a database column -With Hibernate, you can specify a default value for a given database column using the https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/annotations/ColumnDefault.html[`@ColumnDefault`] annotation. +With Hibernate, you can specify a default value for a given database column using the https://docs.hibernate.org/orm/{majorMinorVersion}/javadocs/org/hibernate/annotations/ColumnDefault.html[`@ColumnDefault`] annotation. [[schema-generation-column-default-value-mapping-example]] .`@ColumnDefault` mapping example @@ -163,7 +163,7 @@ include::{extrasdir}/schema-generation-column-default-value-persist-example.sql[ [TIP] ==== -If the column value should be generated not only when a row is inserted, but also when it's updated, the https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/annotations/GeneratedColumn.html[`@GeneratedColumn`] annotation should be used. +If the column value should be generated not only when a row is inserted, but also when it's updated, the https://docs.hibernate.org/orm/{majorMinorVersion}/javadocs/org/hibernate/annotations/GeneratedColumn.html[`@GeneratedColumn`] annotation should be used. ==== [[schema-generation-columns-unique-constraint]] diff --git a/documentation/src/main/asciidoc/userguide/chapters/statistics/Statistics.adoc b/documentation/src/main/asciidoc/userguide/chapters/statistics/Statistics.adoc index a520e950dec7..9707a565570a 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/statistics/Statistics.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/statistics/Statistics.adoc @@ -19,7 +19,7 @@ By default, the statistics are not collected because this incurs an additional p === org.hibernate.stat.Statistics methods The Hibernate statistics are made available via the -https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/stat/Statistics.html[`Statistics`] interface which exposes the following methods: +https://docs.hibernate.org/orm/{majorMinorVersion}/javadocs/org/hibernate/stat/Statistics.html[`Statistics`] interface which exposes the following methods: [[statistics-general]] ==== General statistics methods diff --git a/documentation/src/main/asciidoc/userguide/chapters/tooling/modelgen.adoc b/documentation/src/main/asciidoc/userguide/chapters/tooling/modelgen.adoc index 99c0dcd9bc1b..044127c1c005 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/tooling/modelgen.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/tooling/modelgen.adoc @@ -9,7 +9,7 @@ :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.jboss.org/hibernate/orm/6.3/introduction/html_single/Hibernate_Introduction.html#generator +: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 diff --git a/documentation/src/main/asciidoc/userguide/chapters/transactions/Transactions.adoc b/documentation/src/main/asciidoc/userguide/chapters/transactions/Transactions.adoc index 028d770c752e..f34103fe78b9 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/transactions/Transactions.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/transactions/Transactions.adoc @@ -42,7 +42,7 @@ or provide a custom `org.hibernate.resource.transaction.TransactionCoordinatorBu [NOTE] ==== For details on implementing a custom `TransactionCoordinatorBuilder`, or simply better understanding how it works, see the -https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/integrationguide/html_single/Hibernate_Integration_Guide.html[Integration Guide] . +https://docs.hibernate.org/orm/{majorMinorVersion}/integrationguide/html_single/Hibernate_Integration_Guide.html[Integration Guide] . ==== Hibernate uses JDBC connections and JTA resources directly, without adding any additional locking behavior. @@ -164,7 +164,7 @@ However, as of version 3.1, the processing behind `SessionFactory.getCurrentSess To that end, a new extension interface, `org.hibernate.context.spi.CurrentSessionContext`, and a new configuration parameter, `hibernate.current_session_context_class`, have been added to allow pluggability of the scope and context of defining current sessions. -See the https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/context/spi/CurrentSessionContext.html[Javadocs] for the `org.hibernate.context.spi.CurrentSessionContext` interface for a detailed discussion of its contract. +See the https://docs.hibernate.org/orm/{majorMinorVersion}/javadocs/org/hibernate/context/spi/CurrentSessionContext.html[Javadocs] for the `org.hibernate.context.spi.CurrentSessionContext` interface for a detailed discussion of its contract. It defines a single method, `currentSession()`, by which the implementation is responsible for tracking the current contextual session. Out-of-the-box, Hibernate comes with three implementations of this interface: @@ -172,7 +172,7 @@ Out-of-the-box, Hibernate comes with three implementations of this interface: current sessions are tracked and scoped by a `JTA` transaction. The processing here is exactly the same as in the older JTA-only approach. `org.hibernate.context.internal.ThreadLocalSessionContext`:: -current sessions are tracked by thread of execution. See the https://docs.jboss.org/hibernate/orm/{majorMinorVersion}/javadocs/org/hibernate/context/internal/ThreadLocalSessionContext.html[Javadocs] for more details. +current sessions are tracked by thread of execution. See the https://docs.hibernate.org/orm/{majorMinorVersion}/javadocs/org/hibernate/context/internal/ThreadLocalSessionContext.html[Javadocs] for more details. `org.hibernate.context.internal.ManagedSessionContext`:: current sessions are tracked by thread of execution. However, you are responsible to bind and unbind a `Session` instance with static methods on this class; it does not open, flush, or close a `Session`. diff --git a/documentation/src/main/style/asciidoctor/js/toc.js b/documentation/src/main/style/asciidoctor/js/toc.js index ec434553b538..e217934c673c 100644 --- a/documentation/src/main/style/asciidoctor/js/toc.js +++ b/documentation/src/main/style/asciidoctor/js/toc.js @@ -24,7 +24,7 @@ $(document).ready(function() { $('#vchooser').append(''); for(var version in versions) { - var path = 'http://docs.jboss.org/hibernate/orm' + versions[version]; + var path = 'https://docs.hibernate.org/orm' + versions[version]; $('#vchooser').append(''); }; diff --git a/gradle/databases.gradle b/gradle/databases.gradle index d00a6fbf689c..0bac132bbbb0 100644 --- a/gradle/databases.gradle +++ b/gradle/databases.gradle @@ -12,6 +12,8 @@ ext { db = project.hasProperty('db') ? project.getProperty('db') : 'h2' dbHost = System.getProperty( 'dbHost', 'localhost' ) dbService = System.getProperty( 'dbService', '' ) + dbPassword = System.getProperty( 'dbPassword', '' ).replace('"', '') + dbConnectionStringSuffix = System.getProperty( 'dbConnectionStringSuffix', '' ).replace('"', '') runID = System.getProperty( 'runID', '' ) dbBundle = [ h2 : [ @@ -79,13 +81,13 @@ ext { ], edb_ci : [ 'db.dialect' : 'org.hibernate.dialect.PostgresPlusDialect', - 'jdbc.driver': 'org.postgresql.Driver', + 'jdbc.driver': 'com.edb.Driver', 'jdbc.user' : 'hibernate_orm_test', 'jdbc.pass' : 'hibernate_orm_test', // Disable prepared statement caching due to https://www.postgresql.org/message-id/CAEcMXhmmRd4-%2BNQbnjDT26XNdUoXdmntV9zdr8%3DTu8PL9aVCYg%40mail.gmail.com - 'jdbc.url' : 'jdbc:postgresql://' + dbHost + '/hibernate_orm_test?preparedStatementCacheQueries=0&escapeSyntaxCallMode=callIfNoReturn', - 'jdbc.datasource' : 'org.postgresql.Driver', -// 'jdbc.datasource' : 'org.postgresql.ds.PGSimpleDataSource', + 'jdbc.url' : 'jdbc:edb://' + dbHost + '/hibernate_orm_test?preparedStatementCacheQueries=0&escapeSyntaxCallMode=callIfNoReturn', + 'jdbc.datasource' : 'com.edb.Driver', +// 'jdbc.datasource' : 'com.edb.ds.PGSimpleDataSource', 'connection.init_sql' : '' ], sybase_ci : [ @@ -270,6 +272,15 @@ ext { // 'jdbc.datasource' : 'oracle.jdbc.datasource.impl.OracleDataSource', 'connection.init_sql' : '' ], + oracle_test_pilot_database: [ + 'db.dialect' : 'org.hibernate.dialect.OracleDialect', + 'jdbc.driver': 'oracle.jdbc.OracleDriver', + 'jdbc.user' : 'hibernate_orm_test_' + runID, + 'jdbc.pass' : dbPassword, + 'jdbc.url' : 'jdbc:oracle:thin:@' + dbConnectionStringSuffix + '?oracle.jdbc.enableQueryResultCache=false', + 'jdbc.datasource' : 'oracle.jdbc.OracleDriver', + 'connection.init_sql' : '' + ], mssql : [ 'db.dialect' : 'org.hibernate.dialect.SQLServerDialect', 'jdbc.driver': 'com.microsoft.sqlserver.jdbc.SQLServerDriver', @@ -295,7 +306,7 @@ ext { 'jdbc.driver': 'com.informix.jdbc.IfxDriver', 'jdbc.user' : 'informix', 'jdbc.pass' : 'in4mix', - 'jdbc.url' : 'jdbc:informix-sqli://' + dbHost + ':9088/dev:INFORMIXSERVER=informix;user=informix;password=in4mix;DELIMIDENT=Y;DB_LOCALE=en_US.utf8', + 'jdbc.url' : 'jdbc:informix-sqli://' + dbHost + ':9088/dev:INFORMIXSERVER=informix;user=informix;password=in4mix;DBDATE=Y4MD-;DELIMIDENT=Y;DB_LOCALE=en_US.utf8', 'jdbc.datasource' : 'com.informix.jdbc.IfxDriver', // 'jdbc.datasource' : 'com.informix.jdbcx.IfxDataSource', 'connection.init_sql' : '' diff --git a/gradle/gradle-develocity.gradle b/gradle/gradle-develocity.gradle index 26422d767186..0a9e8c98e128 100644 --- a/gradle/gradle-develocity.gradle +++ b/gradle/gradle-develocity.gradle @@ -6,12 +6,13 @@ */ // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -// Applies details for `https://ge.hibernate.org` +// Applies details for `https://develocity.commonhaus.dev` // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ext { isCiEnvironment = isJenkins() || isGitHubActions() || isGenericCi() - populateRemoteBuildCache = isEnabled( "POPULATE_REMOTE_GRADLE_CACHE" ) + populateRemoteBuildCache = getSetting('POPULATE_REMOTE_GRADLE_CACHE').orElse('false').toBoolean() + useRemoteCache = !getSetting('DISABLE_REMOTE_GRADLE_CACHE').orElse('false').toBoolean() } private static boolean isJenkins() { @@ -36,16 +37,8 @@ static java.util.Optional getSetting(String name) { return java.util.Optional.ofNullable(sysProp); } -static boolean isEnabled(String setting) { - if ( System.getenv().hasProperty( setting ) ) { - return true - } - - return System.hasProperty( setting ) -} - develocity { - server = 'https://ge.hibernate.org' + server = 'https://develocity.commonhaus.dev' buildScan { capture { diff --git a/gradle/java-module.gradle b/gradle/java-module.gradle index a840f90db063..733142837616 100644 --- a/gradle/java-module.gradle +++ b/gradle/java-module.gradle @@ -46,6 +46,11 @@ apply plugin: 'project-report' apply plugin: 'org.jetbrains.gradle.plugin.idea-ext' +def skipJacoco = project.hasProperty('skipJacoco') ? project.getProperty('skipJacoco').toBoolean() : false +if (!skipJacoco) { + plugins.apply('jacoco') +} + ext { java9ModuleNameBase = project.name.startsWith( 'hibernate-' ) ? name.drop( 'hibernate-'.length() ): name java9ModuleName = "org.hibernate.orm.$project.java9ModuleNameBase".replace('-','.') @@ -90,6 +95,7 @@ dependencies { testImplementation testLibs.byteman + testRuntimeOnly testLibs.junit5Launcher testRuntimeOnly testLibs.log4j2 testRuntimeOnly libs.byteBuddy @@ -99,6 +105,7 @@ dependencies { testRuntimeOnly dbLibs.derbyTools testRuntimeOnly dbLibs.hsqldb testRuntimeOnly dbLibs.postgresql + testRuntimeOnly dbLibs.edb testRuntimeOnly dbLibs.mssql testRuntimeOnly dbLibs.informix testRuntimeOnly dbLibs.cockroachdb @@ -245,6 +252,24 @@ tasks.withType( Test.class ).each { test -> excludeTestsMatching project.property('excludeTests').toString() } } + if (!skipJacoco) { + def coverageReportSuffix = providers.gradleProperty('db').orElse('default') + .flatMap { db -> + // Oracle DBs tested on OTP are all using the same db name no matter the version... + // this would be a problem for creating/merging Jacoco reports, hence: + if ("oracle_test_pilot_database" == db) { + return providers.environmentVariable("RDBMS").orElse(db) + } else { + return providers.provider { db } + } + } + def javaVersion = JavaVersion.current().majorVersion + test.jacoco { + destinationFile = layout.buildDirectory.file( + coverageReportSuffix.map { db -> "jacoco/report-${db}-jdk${javaVersion}.exec" } + ).get().asFile + } + } } sourceSets { diff --git a/gradle/published-java-module.gradle b/gradle/published-java-module.gradle index b9ea71617b22..45791fabf74c 100644 --- a/gradle/published-java-module.gradle +++ b/gradle/published-java-module.gradle @@ -5,15 +5,9 @@ * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html */ -apply from: rootProject.file( 'gradle/releasable.gradle' ) apply from: rootProject.file( 'gradle/java-module.gradle' ) apply from: rootProject.file( 'gradle/publishing-pom.gradle' ) -apply plugin: 'signing' - -// Make sure that the publishReleaseArtifacts task of the release module runs the release task of this sub module -tasks.getByPath( ':release:publishReleaseArtifacts' ).dependsOn tasks.release - configurations { javadocSources { description 'Used to aggregate javadocs for the whole project' @@ -21,6 +15,7 @@ configurations { } dependencies { + // `javadocSources` is a special Configuration which is used as a basis for the aggregated-javadocs we produce javadocSources sourceSets.main.allJava } @@ -29,6 +24,8 @@ dependencies { // Publishing java { + // Configure the Java "software component" to include javadoc and sources jars in addition to the classes jar. + // Ultimately, this component is what makes up the publication for this project. withJavadocJar() withSourcesJar() } @@ -97,150 +94,26 @@ publishing { } } - -var signingKey = resolveSigningKey() -var signingPassword = findSigningProperty( "signingPassword" ) - -signing { - useInMemoryPgpKeys( signingKey, signingPassword ) - - sign publishing.publications.publishedArtifacts -} - -String resolveSigningKey() { - var key = findSigningProperty( "signingKey" ) - if ( key != null ) { - return key - } - - var keyFile = findSigningProperty( "signingKeyFile" ) - if ( keyFile != null ) { - return new File( keyFile ).text - } - - return null -} - -String findSigningProperty(String propName) { - if ( System.getProperty( propName ) != null ) { - logger.debug "Found `{}` as a system property", propName - return System.getProperty(propName ) - } - else if ( System.getenv().get( propName ) != null ) { - logger.debug "Found `{}` as an env-var property", propName - return System.getenv().get( propName ) - } - else if ( project.hasProperty( propName ) ) { - logger.debug "Found `{}` as a project property", propName - return project.hasProperty( propName ) - } - else { - logger.debug "Did not find `{}`", propName - return null - } -} - - -var signingTask = project.tasks.getByName( "signPublishedArtifactsPublication" ) as Sign -var signingExtension = project.getExtensions().getByType(SigningExtension) as SigningExtension - -task sign { - dependsOn "signPublications" -} - -task signPublications { t -> - tasks.withType( Sign ).all { s -> - t.dependsOn s - } -} - -signingTask.doFirst { - if ( signingKey == null || signingPassword == null ) { - throw new GradleException( - "Cannot perform signing without GPG details. Please set the `signingKey` and `signingKeyFile` properties" - ) - } -} - - -boolean wasSigningExplicitlyRequested() { - // check whether signing task was explicitly requested when running the build - // - // NOTE: due to https://discuss.gradle.org/t/how-to-tell-if-a-task-was-explicitly-asked-for-on-the-command-line/42853/3 - // we cannot definitively know whether the task was requested. Gradle really just does not expose this information. - // so we make a convention - we check the "start parameters" object to see which task-names were requested; - // the problem is that these are the raw names directly from the command line. e.g. it is perfectly legal to - // say `gradlew signPubArtPub` in place of `gradlew signPublishedArtifactsPublication` - Gradle will simply - // "expand" the name it finds. However, it does not make that available. - // - // so the convention is that we will check for the following task names - // - // for each of: - // 1. `sign` - // 2. `signPublications` - // 3. `signPublishedArtifactsPublication` - // - // and we check both forms: - // 1. "${taskName}" - // 2. project.path + ":${taskName}" - // - // we need to check both again because of the "start parameters" discussion - - def signingTaskNames = ["sign", "signPublications", "signPublishedArtifactsPublication"] - - for ( String taskName : signingTaskNames ) { - if ( gradle.startParameter.taskNames.contains( taskName ) - || gradle.startParameter.taskNames.contains( "${project.path}:${taskName}" ) ) { - return true - } - } - - return false -} - -if ( wasSigningExplicitlyRequested() ) { - // signing was explicitly requested - signingExtension.required = true -} -else { - gradle.taskGraph.whenReady { graph -> - if ( graph.hasTask( signingTask ) ) { - // signing is scheduled to happen. - // - // we know, from above if-check, that it was not explicitly requested - - // so it is triggered via task dependency. make sure we want it to happen - var publishingTask = project.tasks.getByName( "publishPublishedArtifactsPublicationToSonatypeRepository" ) as PublishToMavenRepository - if ( graph.hasTask( publishingTask ) ) { - // we are publishing to Sonatype OSSRH - we need the signing to happen - signingExtension.required = true - } - else { - // signing was not explicitly requested and we are not publishing to OSSRH, - // so do not sign. - signingTask.enabled = false - } - } - - } -} - - // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Release / publishing tasks task ciBuild { - dependsOn test, tasks.publishToSonatype + dependsOn test } -tasks.release.dependsOn tasks.test, tasks.publishToSonatype - -tasks.preVerifyRelease.dependsOn build -tasks.preVerifyRelease.dependsOn generateMetadataFileForPublishedArtifactsPublication -tasks.preVerifyRelease.dependsOn generatePomFileForPublishedArtifactsPublication -tasks.preVerifyRelease.dependsOn generatePomFileForRelocationPomPublication - -tasks.publishToSonatype.mustRunAfter test - +task releasePrepare { + group 'Release' + description 'Performs release preparations on local check-out, including updating changelog' + + dependsOn build + dependsOn generateMetadataFileForPublishedArtifactsPublication + dependsOn generatePomFileForPublishedArtifactsPublication + dependsOn generatePomFileForRelocationPomPublication + dependsOn generatePomFileForRelocationPomPublication + // we depend on publishAllPublicationsToStagingRepository to make sure that the artifacts are "published" to a local staging directory + // used by JReleaser during the release process + dependsOn publishAllPublicationsToStagingRepository +} // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // Ancillary tasks @@ -254,4 +127,4 @@ task showPublications { } } } -} \ No newline at end of file +} diff --git a/gradle/publishing-pom.gradle b/gradle/publishing-pom.gradle index 4654f0d873e1..cb8bfa7f72b5 100644 --- a/gradle/publishing-pom.gradle +++ b/gradle/publishing-pom.gradle @@ -59,5 +59,18 @@ publishing { } } + repositories { + maven { + name = "staging" + url = rootProject.layout.buildDirectory.dir("staging-deploy${File.separator}maven") + } + maven { + name = 'snapshots' + url = "https://central.sonatype.com/repository/maven-snapshots/" + // So that Gradle uses the `ORG_GRADLE_PROJECT_snapshotsPassword` / `ORG_GRADLE_PROJECT_snapshotsUsername` + // env variables to read the username/password for the `snapshots` repository publishing: + credentials(PasswordCredentials) + } + } } diff --git a/gradle/releasable.gradle b/gradle/releasable.gradle deleted file mode 100644 index eb5052d02db7..000000000000 --- a/gradle/releasable.gradle +++ /dev/null @@ -1,10 +0,0 @@ -apply from: rootProject.file( 'gradle/base-information.gradle' ) - -task release { - mustRunAfter ':release:releaseChecks' - enabled !project.ormVersion.isSnapshot -} - -task preVerifyRelease { - dependsOn ':release:preVerifyRelease' -} diff --git a/gradle/version.properties b/gradle/version.properties index f61ae8f56a8b..a1568ee1fcc3 100644 --- a/gradle/version.properties +++ b/gradle/version.properties @@ -1 +1 @@ -hibernateVersion=6.6.1-SNAPSHOT \ No newline at end of file +hibernateVersion=6.6.47-SNAPSHOT \ No newline at end of file diff --git a/hibernate-agroal/src/main/java/org/hibernate/agroal/internal/AgroalConnectionProvider.java b/hibernate-agroal/src/main/java/org/hibernate/agroal/internal/AgroalConnectionProvider.java index a96e42277c28..9ef17546b8b6 100644 --- a/hibernate-agroal/src/main/java/org/hibernate/agroal/internal/AgroalConnectionProvider.java +++ b/hibernate-agroal/src/main/java/org/hibernate/agroal/internal/AgroalConnectionProvider.java @@ -9,10 +9,11 @@ import java.sql.Connection; import java.sql.SQLException; +import java.sql.DatabaseMetaData; +import javax.sql.DataSource; import java.util.Map; import java.util.function.Consumer; import java.util.function.Function; -import javax.sql.DataSource; import org.hibernate.HibernateException; import org.hibernate.cfg.AgroalSettings; @@ -36,6 +37,7 @@ import io.agroal.api.security.SimplePassword; import static org.hibernate.cfg.AgroalSettings.AGROAL_CONFIG_PREFIX; +import static org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator.allowJdbcMetadataAccess; /** * ConnectionProvider based on Agroal connection pool @@ -64,6 +66,7 @@ public class AgroalConnectionProvider implements ConnectionProvider, Configurabl public static final String CONFIG_PREFIX = AGROAL_CONFIG_PREFIX + "."; private static final long serialVersionUID = 1L; private AgroalDataSource agroalDataSource = null; + private boolean isMetadataAccessAllowed = true; // --- Configurable @@ -92,6 +95,8 @@ private static void copyProperty(Map properties, String key, @Override public void configure(Map props) throws HibernateException { + isMetadataAccessAllowed = allowJdbcMetadataAccess( props ); + ConnectionInfoLogger.INSTANCE.configureConnectionPool( "Agroal" ); try { AgroalPropertiesReader agroalProperties = new AgroalPropertiesReader( CONFIG_PREFIX ) @@ -139,9 +144,12 @@ public DatabaseConnectionInfo getDatabaseConnectionInfo(Dialect dialect) { final AgroalConnectionPoolConfiguration acpc = agroalDataSource.getConfiguration().connectionPoolConfiguration(); final AgroalConnectionFactoryConfiguration acfc = acpc.connectionFactoryConfiguration(); + return new DatabaseConnectionInfoImpl( acfc.jdbcUrl(), - acfc.connectionProviderClass().toString(), + // Attempt to resolve the driver name from the dialect, in case it wasn't explicitly set and access to + // the database metadata is allowed + acfc.connectionProviderClass() != null ? acfc.connectionProviderClass().toString() : extractDriverNameFromMetadata(), dialect.getVersion(), Boolean.toString( acfc.autoCommit() ), acfc.jdbcTransactionIsolation() != null @@ -152,6 +160,19 @@ public DatabaseConnectionInfo getDatabaseConnectionInfo(Dialect dialect) { ); } + private String extractDriverNameFromMetadata() { + if (isMetadataAccessAllowed) { + try ( Connection conn = getConnection() ) { + DatabaseMetaData dbmd = conn.getMetaData(); + return dbmd.getDriverName(); + } + catch (SQLException e) { + // Do nothing + } + } + return null; + } + @Override public boolean isUnwrappableAs(Class unwrapType) { return ConnectionProvider.class.equals( unwrapType ) diff --git a/hibernate-c3p0/src/test/java/org/hibernate/test/c3p0/C3P0ConnectionProviderTest.java b/hibernate-c3p0/src/test/java/org/hibernate/test/c3p0/C3P0ConnectionProviderTest.java index 7b3a2f519a75..ebe721c90c99 100644 --- a/hibernate-c3p0/src/test/java/org/hibernate/test/c3p0/C3P0ConnectionProviderTest.java +++ b/hibernate-c3p0/src/test/java/org/hibernate/test/c3p0/C3P0ConnectionProviderTest.java @@ -15,11 +15,13 @@ import org.hibernate.c3p0.internal.C3P0ConnectionProvider; import org.hibernate.cfg.Environment; +import org.hibernate.dialect.SybaseASEDialect; import org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator.ConnectionProviderJdbcConnectionAccess; import org.hibernate.engine.jdbc.spi.JdbcServices; import org.hibernate.testing.TestForIssue; import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase; +import org.hibernate.testing.orm.junit.SkipForDialect; import org.junit.Test; import static org.hibernate.testing.junit4.ExtraAssertions.assertTyping; @@ -29,6 +31,8 @@ /** * @author Strong Liu */ +@SkipForDialect(dialectClass = SybaseASEDialect.class, + reason = "JtdsConnection.isValid not implemented") public class C3P0ConnectionProviderTest extends BaseCoreFunctionalTestCase { @Override diff --git a/hibernate-c3p0/src/test/java/org/hibernate/test/c3p0/C3p0TransactionIsolationConfigTest.java b/hibernate-c3p0/src/test/java/org/hibernate/test/c3p0/C3p0TransactionIsolationConfigTest.java index 3c83c192eb46..138ec195567c 100644 --- a/hibernate-c3p0/src/test/java/org/hibernate/test/c3p0/C3p0TransactionIsolationConfigTest.java +++ b/hibernate-c3p0/src/test/java/org/hibernate/test/c3p0/C3p0TransactionIsolationConfigTest.java @@ -10,6 +10,7 @@ import org.hibernate.boot.registry.StandardServiceRegistryBuilder; import org.hibernate.c3p0.internal.C3P0ConnectionProvider; import org.hibernate.community.dialect.AltibaseDialect; +import org.hibernate.dialect.SybaseASEDialect; import org.hibernate.dialect.TiDBDialect; import org.hibernate.engine.jdbc.connections.spi.ConnectionProvider; import org.hibernate.service.spi.ServiceRegistryImplementor; @@ -23,6 +24,7 @@ */ @SkipForDialect(value = TiDBDialect.class, comment = "Doesn't support SERIALIZABLE isolation") @SkipForDialect(value = AltibaseDialect.class, comment = "Altibase cannot change isolation level in autocommit mode") +@SkipForDialect(value = SybaseASEDialect.class, comment = "JtdsConnection.isValid not implemented") public class C3p0TransactionIsolationConfigTest extends BaseTransactionIsolationConfigTest { private StandardServiceRegistry ssr; diff --git a/hibernate-c3p0/src/test/java/org/hibernate/test/c3p0/StatementCacheTest.java b/hibernate-c3p0/src/test/java/org/hibernate/test/c3p0/StatementCacheTest.java index f079f9beb007..7c2445aa5c84 100644 --- a/hibernate-c3p0/src/test/java/org/hibernate/test/c3p0/StatementCacheTest.java +++ b/hibernate-c3p0/src/test/java/org/hibernate/test/c3p0/StatementCacheTest.java @@ -11,10 +11,13 @@ import jakarta.persistence.criteria.CriteriaBuilder; import jakarta.persistence.criteria.CriteriaQuery; +import org.hibernate.dialect.SQLServerDialect; +import org.hibernate.dialect.SybaseASEDialect; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SkipForDialect; import org.junit.Assert; import org.junit.Test; -import org.hibernate.testing.TestForIssue; import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase; /** @@ -22,9 +25,15 @@ * * @author Shawn Clowater */ +@SkipForDialect(dialectClass = SybaseASEDialect.class, + reason = "JtdsConnection.isValid not implemented") +@SkipForDialect(dialectClass = SQLServerDialect.class, + reason = "started failing after upgrade to c3p0 0.10") public class StatementCacheTest extends BaseCoreFunctionalTestCase { @Test - @TestForIssue( jiraKey = "HHH-7193" ) + @JiraKey(value = "HHH-7193") + @SkipForDialect(dialectClass = SQLServerDialect.class, + reason = "started failing after upgrade to c3p0 0.10") public void testStatementCaching() { inSession( session -> { @@ -58,7 +67,8 @@ public void testStatementCaching() { } ); - //only one entity should have been inserted to the database (if the statement in the cache wasn't cleared then it would have inserted both entities) + // only one entity should have been inserted to the database + // (if the statement in the cache wasn't cleared then it would have inserted both entities) inTransaction( session -> { CriteriaBuilder criteriaBuilder = session.getCriteriaBuilder(); diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/AltibaseDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/AltibaseDialect.java index 09d6ccbb2877..b4dc15316072 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/AltibaseDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/AltibaseDialect.java @@ -395,20 +395,26 @@ public String castPattern(CastType from, CastType to) { } break; case INTEGER_BOOLEAN: - result = BooleanDecoder.toIntegerBoolean( from ); + result = from == CastType.STRING + ? buildStringToBooleanCastDecode( "1", "0" ) + : BooleanDecoder.toIntegerBoolean( from ); if ( result != null ) { return result; } break; case YN_BOOLEAN: - result = BooleanDecoder.toYesNoBoolean( from ); + result = from == CastType.STRING + ? buildStringToBooleanCastDecode( "'Y'", "'N'" ) + : BooleanDecoder.toYesNoBoolean( from ); if ( result != null ) { return result; } break; case BOOLEAN: case TF_BOOLEAN: - result = BooleanDecoder.toTrueFalseBoolean( from ); + result = from == CastType.STRING + ? buildStringToBooleanCastDecode( "'T'", "'F'" ) + : BooleanDecoder.toTrueFalseBoolean( from ); if ( result != null ) { return result; } @@ -704,4 +710,14 @@ public SQLExceptionConversionDelegate buildSQLExceptionConversionDelegate() { }; } + @Override + public String getDual() { + return "dual"; + } + + @Override + public String getFromDualForSelectOnly() { + return " from " + getDual(); + } + } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/AltibaseSqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/AltibaseSqlAstTranslator.java index 0ab86ef3ef16..a258f1d977d3 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/AltibaseSqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/AltibaseSqlAstTranslator.java @@ -221,16 +221,6 @@ public void visitQueryPartTableReference(QueryPartTableReference tableReference) emulateQueryPartTableReferenceColumnAliasing( tableReference ); } - @Override - protected String getDual() { - return "dual"; - } - - @Override - protected String getFromDualForSelectOnly() { - return " from " + getDual(); - } - @Override protected boolean needsRecursiveKeywordInWithClause() { return false; diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CUBRIDDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CUBRIDDialect.java index d73897a57aa6..1c4a5fbbca13 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CUBRIDDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CUBRIDDialect.java @@ -517,4 +517,16 @@ private void timediff( sqlAppender.append( diffUnit.conversionFactor( toUnit, this ) ); } + @Override + public String getDual() { + //TODO: is this really needed? + //TODO: would "from table({0})" be better? + return "db_root"; + } + + @Override + public String getFromDualForSelectOnly() { + return " from " + getDual(); + } + } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CUBRIDSqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CUBRIDSqlAstTranslator.java index e187406b3baa..5c59c6a29de3 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CUBRIDSqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CUBRIDSqlAstTranslator.java @@ -80,16 +80,4 @@ protected boolean supportsRowValueConstructorSyntaxInInList() { protected boolean supportsRowValueConstructorSyntaxInQuantifiedPredicates() { return false; } - - @Override - protected String getDual() { - //TODO: is this really needed? - //TODO: would "from table({0})" be better? - return "db_root"; - } - - @Override - protected String getFromDualForSelectOnly() { - return " from " + getDual(); - } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CockroachLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/CockroachLegacyDialect.java index c0b8b5840304..8d27ff9b5232 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 @@ -61,10 +61,11 @@ import org.hibernate.sql.exec.spi.JdbcOperation; import org.hibernate.tool.schema.extract.spi.ColumnTypeInformation; import org.hibernate.type.JavaObjectType; +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.VarbinaryJdbcType; -import org.hibernate.type.descriptor.jdbc.VarcharJdbcType; import org.hibernate.type.descriptor.jdbc.spi.JdbcTypeRegistry; import org.hibernate.type.descriptor.sql.internal.DdlTypeImpl; import org.hibernate.type.descriptor.sql.internal.NamedNativeEnumDdlTypeImpl; @@ -391,9 +392,9 @@ protected void contributeCockroachTypes(TypeContributions typeContributions, Ser } // Force Blob binding to byte[] for CockroachDB - jdbcTypeRegistry.addDescriptor( Types.BLOB, VarbinaryJdbcType.INSTANCE ); - jdbcTypeRegistry.addDescriptor( Types.CLOB, VarcharJdbcType.INSTANCE ); - jdbcTypeRegistry.addDescriptor( Types.NCLOB, VarcharJdbcType.INSTANCE ); + jdbcTypeRegistry.addDescriptor( Types.BLOB, BlobJdbcType.MATERIALIZED ); + jdbcTypeRegistry.addDescriptor( Types.CLOB, ClobJdbcType.MATERIALIZED ); + jdbcTypeRegistry.addDescriptor( Types.NCLOB, NClobJdbcType.MATERIALIZED ); // The next two contributions are the same as for Postgresql typeContributions.contributeJdbcType( ObjectNullAsBinaryTypeJdbcType.INSTANCE ); @@ -1042,6 +1043,8 @@ public boolean supportsOuterJoinForUpdate() { @Override public boolean useInputStreamToInsertBlob() { + // PG-JDBC treats setBinaryStream()/setCharacterStream() calls like bytea/varchar, which are not LOBs, + // so disable stream bindings for this dialect completely return false; } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2LegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2LegacyDialect.java index c0cebad8c745..cf7b73ba713b 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2LegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2LegacyDialect.java @@ -70,6 +70,7 @@ import org.hibernate.metamodel.spi.RuntimeModelCreationContext; import org.hibernate.procedure.internal.DB2CallableStatementSupport; import org.hibernate.procedure.spi.CallableStatementSupport; +import org.hibernate.query.sqm.CastType; import org.hibernate.query.sqm.IntervalType; import org.hibernate.query.sqm.TemporalUnit; import org.hibernate.query.sqm.mutation.internal.cte.CteInsertStrategy; @@ -869,6 +870,20 @@ public boolean supportsLobValueChangePropagation() { return false; } + @Override + public boolean useConnectionToCreateLob() { + return false; + } + + @Override + public boolean supportsNationalizedMethods() { + // See HHH-12753, HHH-18314, HHH-19201 + // Old DB2 JDBC drivers do not support setNClob, setNCharcterStream or setNString. + // In more recent driver versions, some methods just delegate to the non-N variant, but others still fail. + // Ultimately, let's just avoid the N variant methods on DB2 altogether + return false; + } + @Override public boolean doesReadCommittedCauseWritersToBlockReaders() { return true; @@ -1056,6 +1071,19 @@ public IdentityColumnSupport getIdentityColumnSupport() { return DB2IdentityColumnSupport.INSTANCE; } + /** + * @return {@code true} because we can use {@code select ... from new table (insert .... )} + */ + @Override + public boolean supportsInsertReturning() { + return true; + } + + @Override + public boolean supportsInsertReturningRowId() { + return false; + } + @Override public boolean supportsValuesList() { return true; @@ -1141,6 +1169,16 @@ public String extractPattern(TemporalUnit unit) { return super.extractPattern( unit ); } + @Override + public String castPattern(CastType from, CastType to) { + if ( from == CastType.STRING && to == CastType.BOOLEAN ) { + return "cast(?1 as ?2)"; + } + else { + return super.castPattern( from, to ); + } + } + @Override public int getInExpressionCountLimit() { return BIND_PARAMETERS_NUMBER_LIMIT; @@ -1208,4 +1246,14 @@ public DmlTargetColumnQualifierSupport getDmlTargetColumnQualifierSupport() { public boolean supportsFromClauseInUpdate() { return getDB2Version().isSameOrAfter( 11 ); } + + @Override + public String getDual() { + return "sysibm.sysdummy1"; + } + + @Override + public String getFromDualForSelectOnly() { + return " from " + getDual(); + } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2LegacySqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2LegacySqlAstTranslator.java index f8ef6b3fb263..f6e8718b5e26 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2LegacySqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2LegacySqlAstTranslator.java @@ -76,7 +76,7 @@ protected boolean supportsWithClauseInSubquery() { } @Override - protected void renderTableReferenceJoins(TableGroup tableGroup) { + protected void renderTableReferenceJoins(TableGroup tableGroup, int swappedJoinIndex, boolean forceLeftJoin) { // When we are in a recursive CTE, we can't render joins on DB2... // See https://modern-sql.com/feature/with-recursive/db2/error-345-state-42836 if ( isInRecursiveQueryPart() ) { @@ -103,7 +103,7 @@ protected void renderTableReferenceJoins(TableGroup tableGroup) { } } else { - super.renderTableReferenceJoins( tableGroup ); + super.renderTableReferenceJoins( tableGroup, swappedJoinIndex, forceLeftJoin ); } } @@ -596,24 +596,19 @@ protected boolean supportsRowValueConstructorSyntax() { return false; } - @Override - protected boolean supportsRowValueConstructorSyntaxInInList() { - return false; - } - @Override protected boolean supportsRowValueConstructorSyntaxInQuantifiedPredicates() { return false; } @Override - protected String getDual() { - return "sysibm.dual"; + protected boolean supportsRowValueConstructorSyntaxInInList() { + return false; } @Override - protected String getFromDualForSelectOnly() { - return " from " + getDual(); + protected boolean supportsRowValueConstructorSyntaxInInSubQuery() { + return true; } @Override diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2iLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2iLegacyDialect.java index 607efd39fb3e..507127bbbd84 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2iLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2iLegacyDialect.java @@ -9,6 +9,7 @@ import org.hibernate.boot.model.FunctionContributions; import org.hibernate.dialect.DatabaseVersion; import org.hibernate.dialect.function.CommonFunctionFactory; +import org.hibernate.dialect.function.DB2SubstringFunction; import org.hibernate.dialect.identity.DB2390IdentityColumnSupport; import org.hibernate.dialect.identity.DB2IdentityColumnSupport; import org.hibernate.dialect.identity.IdentityColumnSupport; @@ -59,9 +60,14 @@ public DB2iLegacyDialect(DatabaseVersion version) { @Override public void initializeFunctionRegistry(FunctionContributions functionContributions) { - super.initializeFunctionRegistry(functionContributions); + super.initializeFunctionRegistry( functionContributions ); + // DB2 for i doesn't allow code units: https://www.ibm.com/docs/en/i/7.1.0?topic=functions-substring + functionContributions.getFunctionRegistry().register( + "substring", + new DB2SubstringFunction( false, functionContributions.getTypeConfiguration() ) + ); if ( getVersion().isSameOrAfter( 7, 2 ) ) { - CommonFunctionFactory functionFactory = new CommonFunctionFactory(functionContributions); + CommonFunctionFactory functionFactory = new CommonFunctionFactory( functionContributions ); functionFactory.listagg( null ); functionFactory.inverseDistributionOrderedSetAggregates(); functionFactory.hypotheticalOrderedSetAggregates_windowEmulation(); @@ -112,7 +118,7 @@ public SequenceSupport getSequenceSupport() { @Override public String getQuerySequencesString() { if ( getVersion().isSameOrAfter(7,3) ) { - return "select distinct sequence_name from qsys2.syssequences " + + return "select distinct sequence_schema as seqschema, sequence_name as seqname, START, minimum_value as minvalue, maximum_value as maxvalue, increment from qsys2.syssequences " + "where current_schema='*LIBL' and sequence_schema in (select schema_name from qsys2.library_list_info) " + "or sequence_schema=current_schema"; } 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 815f5561bd0f..6cbc9bb86f24 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 @@ -6,12 +6,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; @@ -42,15 +45,15 @@ protected boolean shouldEmulateFetchClause(QueryPart queryPart) { if ( useOffsetFetchClause( queryPart ) && !isRowsOnlyFetchClauseType( queryPart ) ) { return true; } - // According to LegacyDB2LimitHandler, variable limit also isn't supported before 7.10 - return version.isBefore(7, 10) + // According to LegacyDB2LimitHandler, variable limit also isn't supported before 7.1 + return version.isBefore(7, 1) && queryPart.getFetchClauseExpression() != null && !( queryPart.getFetchClauseExpression() instanceof Literal ); } @Override protected boolean supportsOffsetClause() { - return version.isSameOrAfter(7, 10); + return version.isSameOrAfter(7, 1); } @Override @@ -58,6 +61,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/DB2zLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2zLegacyDialect.java index 6e8315dc0899..ad7b07daacf8 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2zLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DB2zLegacyDialect.java @@ -251,4 +251,10 @@ public int rowIdSqlType() { public String getRowIdColumnString(String rowId) { return rowId( rowId ) + " rowid not null generated always"; } + + @Override + public boolean supportsValuesList() { + // DB2 z/OS has a VALUES statement, but that doesn't support multiple values + return false; + } } 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 8d8fb58c9b02..0dccd01de6f5 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 @@ -90,6 +90,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/DerbyLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DerbyLegacyDialect.java index 70e098ece421..df1b6bfa4df1 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DerbyLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DerbyLegacyDialect.java @@ -1055,6 +1055,11 @@ public boolean supportsWindowFunctions() { return getVersion().isSameOrAfter( 10, 4 ); } + @Override + public boolean supportsValuesList() { + return true; + } + @Override public IdentifierHelper buildIdentifierHelper(IdentifierHelperBuilder builder, DatabaseMetaData dbMetaData) throws SQLException { @@ -1066,4 +1071,14 @@ public IdentifierHelper buildIdentifierHelper(IdentifierHelperBuilder builder, D public DmlTargetColumnQualifierSupport getDmlTargetColumnQualifierSupport() { return DmlTargetColumnQualifierSupport.TABLE_ALIAS; } + + @Override + public String getDual() { + return "(values 0)"; + } + + @Override + public String getFromDualForSelectOnly() { + return " from " + getDual() + " dual"; + } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DerbyLegacySqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DerbyLegacySqlAstTranslator.java index 3aea1a5ebc1d..d3fee18516ff 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DerbyLegacySqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/DerbyLegacySqlAstTranslator.java @@ -302,16 +302,6 @@ protected boolean supportsRowValueConstructorSyntaxInQuantifiedPredicates() { return false; } - @Override - protected String getDual() { - return "(values 0)"; - } - - @Override - protected String getFromDualForSelectOnly() { - return " from " + getDual() + " dual"; - } - @Override protected boolean needsRowsToSkip() { return !supportsOffsetFetchClause(); diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/FirebirdDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/FirebirdDialect.java index 6f47f288fa0e..380fea18a3f8 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/FirebirdDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/FirebirdDialect.java @@ -403,25 +403,33 @@ public String castPattern(CastType from, CastType to) { } break; case BOOLEAN: - result = BooleanDecoder.toBoolean( from ); + result = from == CastType.STRING + ? buildStringToBooleanCastDecode( "true", "false" ) + : BooleanDecoder.toBoolean( from ); if ( result != null ) { return result; } break; case INTEGER_BOOLEAN: - result = BooleanDecoder.toIntegerBoolean( from ); + result = from == CastType.STRING + ? buildStringToBooleanCastDecode( "1", "0" ) + : BooleanDecoder.toIntegerBoolean( from ); if ( result != null ) { return result; } break; case YN_BOOLEAN: - result = BooleanDecoder.toYesNoBoolean( from ); + result = from == CastType.STRING + ? buildStringToBooleanCastDecode( "'Y'", "'N'" ) + : BooleanDecoder.toYesNoBoolean( from ); if ( result != null ) { return result; } break; case TF_BOOLEAN: - result = BooleanDecoder.toTrueFalseBoolean( from ); + result = from == CastType.STRING + ? buildStringToBooleanCastDecode( "'T'", "'F'" ) + : BooleanDecoder.toTrueFalseBoolean( from ); if ( result != null ) { return result; } @@ -1096,4 +1104,14 @@ else if ( supportsOffset && temporalAccessor instanceof Instant ) { } } + + @Override + public String getDual() { + return "rdb$database"; + } + + @Override + public String getFromDualForSelectOnly() { + return " from " + getDual(); + } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/FirebirdSqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/FirebirdSqlAstTranslator.java index 6f9c1215fda9..84ca5355bfa3 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/FirebirdSqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/FirebirdSqlAstTranslator.java @@ -262,16 +262,6 @@ protected boolean supportsRowValueConstructorSyntaxInQuantifiedPredicates() { return false; } - @Override - protected String getDual() { - return "rdb$database"; - } - - @Override - protected String getFromDualForSelectOnly() { - return " from " + getDual(); - } - private boolean supportsOffsetFetchClause() { return getDialect().getVersion().isSameOrAfter( 3 ); } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/H2LegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/H2LegacyDialect.java index 133e12225afc..17764e545966 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/H2LegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/H2LegacyDialect.java @@ -48,6 +48,7 @@ import org.hibernate.internal.util.JdbcExceptionHelper; import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.spi.RuntimeModelCreationContext; +import org.hibernate.query.sqm.CastType; import org.hibernate.query.sqm.FetchClauseType; import org.hibernate.query.sqm.IntervalType; import org.hibernate.dialect.NullOrdering; @@ -514,6 +515,16 @@ public String extractPattern(TemporalUnit unit) { : super.extractPattern(unit); } + @Override + public String castPattern(CastType from, CastType to) { + if ( from == CastType.STRING && to == CastType.BOOLEAN ) { + return "cast(?1 as ?2)"; + } + else { + return super.castPattern( from, to ); + } + } + @Override public String timestampaddPattern(TemporalUnit unit, TemporalType temporalType, IntervalType intervalType) { if ( intervalType != null ) { @@ -992,4 +1003,14 @@ public String getCaseInsensitiveLike() { public boolean supportsCaseInsensitiveLike() { return getVersion().isSameOrAfter( 1, 4, 194 ); } + + @Override + public boolean supportsValuesList() { + return true; + } + + @Override + public String getDual() { + return "dual"; + } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/H2LegacySqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/H2LegacySqlAstTranslator.java index 7213cd169079..b081591b6452 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/H2LegacySqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/H2LegacySqlAstTranslator.java @@ -340,13 +340,9 @@ protected boolean renderPrimaryTableReference(TableGroup tableGroup, LockMode lo final TableReference tableRef = tableGroup.getPrimaryTableReference(); // The H2 parser can't handle a sub-query as first element in a nested join // i.e. `join ( (select ...) alias join ... )`, so we have to introduce a dummy table reference - if ( tableRef instanceof QueryPartTableReference || tableRef.getTableId().startsWith( "(select" ) ) { - final boolean realTableGroup = tableGroup.isRealTableGroup() - && ( CollectionHelper.isNotEmpty( tableGroup.getTableReferenceJoins() ) - || hasNestedTableGroupsToRender( tableGroup.getNestedTableGroupJoins() ) ); - if ( realTableGroup ) { - appendSql( "dual cross join " ); - } + if ( getSqlBuffer().charAt( getSqlBuffer().length() - 1 ) == '(' + && ( tableRef instanceof QueryPartTableReference || tableRef.getTableId().startsWith( "(select" ) ) ) { + appendSql( "dual cross join " ); } return super.renderPrimaryTableReference( tableGroup, lockMode ); } @@ -392,11 +388,6 @@ protected boolean supportsNullPrecedence() { return getClauseStack().getCurrent() != Clause.WITHIN_GROUP || getDialect().getVersion().isSameOrAfter( 2 ); } - @Override - protected String getDual() { - return "dual"; - } - private boolean supportsOffsetFetchClause() { return getDialect().getVersion().isSameOrAfter( 1, 4, 195 ); } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/HSQLLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/HSQLLegacyDialect.java index b491f19a8592..5460ff680c8f 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/HSQLLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/HSQLLegacyDialect.java @@ -315,25 +315,33 @@ public String castPattern(CastType from, CastType to) { } break; case BOOLEAN: - result = BooleanDecoder.toBoolean( from ); + result = from == CastType.STRING + ? buildStringToBooleanCastDecode( "true", "false" ) + : BooleanDecoder.toBoolean( from ); if ( result != null ) { return result; } break; case INTEGER_BOOLEAN: - result = BooleanDecoder.toIntegerBoolean( from ); + result = from == CastType.STRING + ? buildStringToBooleanCastDecode( "1", "0" ) + : BooleanDecoder.toIntegerBoolean( from ); if ( result != null ) { return result; } break; case YN_BOOLEAN: - result = BooleanDecoder.toYesNoBoolean( from ); + result = from == CastType.STRING + ? buildStringToBooleanCastDecode( "'Y'", "'N'" ) + : BooleanDecoder.toYesNoBoolean( from ); if ( result != null ) { return result; } break; case TF_BOOLEAN: - result = BooleanDecoder.toTrueFalseBoolean( from ); + result = from == CastType.STRING + ? buildStringToBooleanCastDecode( "'T'", "'F'" ) + : BooleanDecoder.toTrueFalseBoolean( from ); if ( result != null ) { return result; } @@ -825,6 +833,11 @@ public boolean requiresFloatCastingOfIntegerDivision() { return true; } + @Override + public boolean supportsValuesList() { + return true; + } + @Override public IdentityColumnSupport getIdentityColumnSupport() { return identityColumnSupport; @@ -900,4 +913,9 @@ public UniqueDelegate getUniqueDelegate() { public DmlTargetColumnQualifierSupport getDmlTargetColumnQualifierSupport() { return DmlTargetColumnQualifierSupport.TABLE_ALIAS; } + + @Override + public String getFromDualForSelectOnly() { + return " from " + getDual(); + } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/HSQLLegacySqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/HSQLLegacySqlAstTranslator.java index 8f7e958ee22b..e40a779eeda9 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/HSQLLegacySqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/HSQLLegacySqlAstTranslator.java @@ -12,20 +12,14 @@ import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.metamodel.mapping.JdbcMappingContainer; import org.hibernate.query.IllegalQueryOperationException; -import org.hibernate.query.sqm.BinaryArithmeticOperator; import org.hibernate.query.sqm.ComparisonOperator; import org.hibernate.sql.ast.Clause; import org.hibernate.sql.ast.SqlAstNodeRenderingMode; import org.hibernate.sql.ast.spi.AbstractSqlAstTranslator; import org.hibernate.sql.ast.spi.SqlSelection; -import org.hibernate.sql.ast.tree.MutationStatement; import org.hibernate.sql.ast.tree.Statement; -import org.hibernate.sql.ast.tree.cte.CteStatement; -import org.hibernate.sql.ast.tree.delete.DeleteStatement; -import org.hibernate.sql.ast.tree.expression.BinaryArithmeticExpression; import org.hibernate.sql.ast.tree.expression.CaseSearchedExpression; import org.hibernate.sql.ast.tree.expression.CaseSimpleExpression; -import org.hibernate.sql.ast.tree.expression.ColumnReference; import org.hibernate.sql.ast.tree.expression.Expression; import org.hibernate.sql.ast.tree.expression.Literal; import org.hibernate.sql.ast.tree.expression.SqlTuple; @@ -147,8 +141,7 @@ protected void visitRecursivePath(Expression recursivePath, int sizeEstimate) { protected void visitAnsiCaseSearchedExpression( CaseSearchedExpression expression, Consumer resultRenderer) { - if ( getParameterRenderingMode() == SqlAstNodeRenderingMode.DEFAULT && areAllResultsParameters( expression ) - || areAllResultsPlainParametersOrLiterals( expression ) ) { + if ( areAllResultsPlainParametersOrStringLiterals( expression ) ) { final List whenFragments = expression.getWhenFragments(); final Expression firstResult = whenFragments.get( 0 ).getResult(); super.visitAnsiCaseSearchedExpression( @@ -172,8 +165,7 @@ protected void visitAnsiCaseSearchedExpression( protected void visitAnsiCaseSimpleExpression( CaseSimpleExpression expression, Consumer resultRenderer) { - if ( getParameterRenderingMode() == SqlAstNodeRenderingMode.DEFAULT && areAllResultsParameters( expression ) - || areAllResultsPlainParametersOrLiterals( expression ) ) { + if ( areAllResultsPlainParametersOrStringLiterals( expression ) ) { final List whenFragments = expression.getWhenFragments(); final Expression firstResult = whenFragments.get( 0 ).getResult(); super.visitAnsiCaseSimpleExpression( @@ -193,11 +185,11 @@ protected void visitAnsiCaseSimpleExpression( } } - protected boolean areAllResultsPlainParametersOrLiterals(CaseSearchedExpression caseSearchedExpression) { + protected boolean areAllResultsPlainParametersOrStringLiterals(CaseSearchedExpression caseSearchedExpression) { final List whenFragments = caseSearchedExpression.getWhenFragments(); final Expression firstResult = whenFragments.get( 0 ).getResult(); if ( isParameter( firstResult ) && getParameterRenderingMode() == SqlAstNodeRenderingMode.DEFAULT - || isLiteral( firstResult ) ) { + || isStringLiteral( firstResult ) ) { for ( int i = 1; i < whenFragments.size(); i++ ) { final Expression result = whenFragments.get( i ).getResult(); if ( isParameter( result ) ) { @@ -205,7 +197,7 @@ protected boolean areAllResultsPlainParametersOrLiterals(CaseSearchedExpression return false; } } - else if ( !isLiteral( result ) ) { + else if ( !isStringLiteral( result ) ) { return false; } } @@ -214,11 +206,11 @@ else if ( !isLiteral( result ) ) { return false; } - protected boolean areAllResultsPlainParametersOrLiterals(CaseSimpleExpression caseSimpleExpression) { + protected boolean areAllResultsPlainParametersOrStringLiterals(CaseSimpleExpression caseSimpleExpression) { final List whenFragments = caseSimpleExpression.getWhenFragments(); final Expression firstResult = whenFragments.get( 0 ).getResult(); if ( isParameter( firstResult ) && getParameterRenderingMode() == SqlAstNodeRenderingMode.DEFAULT - || isLiteral( firstResult ) ) { + || isStringLiteral( firstResult ) ) { for ( int i = 1; i < whenFragments.size(); i++ ) { final Expression result = whenFragments.get( i ).getResult(); if ( isParameter( result ) ) { @@ -226,7 +218,7 @@ protected boolean areAllResultsPlainParametersOrLiterals(CaseSimpleExpression ca return false; } } - else if ( !isLiteral( result ) ) { + else if ( !isStringLiteral( result ) ) { return false; } } @@ -235,6 +227,13 @@ else if ( !isLiteral( result ) ) { return false; } + private boolean isStringLiteral( Expression expression ) { + if ( expression instanceof Literal ) { + return ( (Literal) expression ).getJdbcMapping().getJdbcType().isStringLike(); + } + return false; + } + @Override public boolean supportsFilterClause() { return true; @@ -329,22 +328,20 @@ else if ( expression instanceof Summarization ) { @Override protected boolean supportsRowValueConstructorSyntax() { - return false; - } - - @Override - protected boolean supportsRowValueConstructorSyntaxInInList() { + // It's supported but not usable due to a bug: https://sourceforge.net/p/hsqldb/bugs/1714/ return false; } @Override protected boolean supportsRowValueConstructorSyntaxInQuantifiedPredicates() { + // It's supported but not usable due to a bug: https://sourceforge.net/p/hsqldb/bugs/1714/ return false; } @Override - protected String getFromDualForSelectOnly() { - return " from " + getDual(); + protected boolean supportsRowValueConstructorSyntaxInInList() { + // It's supported but not usable due to a bug: https://sourceforge.net/p/hsqldb/bugs/1714/ + return false; } private boolean supportsOffsetFetchClause() { diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/InformixDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/InformixDialect.java index b269c9206b36..95e4983dc398 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/InformixDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/InformixDialect.java @@ -7,6 +7,9 @@ package org.hibernate.community.dialect; import java.sql.Types; +import java.time.temporal.TemporalAccessor; +import java.util.Date; +import java.util.TimeZone; import org.hibernate.boot.Metadata; import org.hibernate.boot.model.FunctionContributions; @@ -23,6 +26,7 @@ import org.hibernate.dialect.NullOrdering; import org.hibernate.dialect.Replacer; import org.hibernate.dialect.SelectItemReferenceStrategy; +import org.hibernate.dialect.VarcharUUIDJdbcType; import org.hibernate.dialect.function.CaseLeastGreatestEmulation; import org.hibernate.dialect.function.CommonFunctionFactory; import org.hibernate.dialect.identity.IdentityColumnSupport; @@ -33,6 +37,7 @@ import org.hibernate.dialect.unique.UniqueDelegate; import org.hibernate.engine.jdbc.Size; import org.hibernate.engine.jdbc.dialect.spi.DialectResolutionInfo; +import org.hibernate.engine.jdbc.env.spi.NameQualifierSupport; import org.hibernate.engine.spi.LoadQueryInfluencers; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.exception.spi.TemplatedViolatedConstraintNameExtractor; @@ -51,11 +56,13 @@ import org.hibernate.query.sqm.mutation.internal.temptable.LocalTemporaryTableMutationStrategy; import org.hibernate.query.sqm.mutation.spi.SqmMultiTableInsertStrategy; import org.hibernate.query.sqm.mutation.spi.SqmMultiTableMutationStrategy; +import org.hibernate.query.sqm.produce.function.StandardFunctionArgumentTypeResolvers; import org.hibernate.query.sqm.sql.SqmTranslator; import org.hibernate.query.sqm.sql.SqmTranslatorFactory; import org.hibernate.query.sqm.sql.StandardSqmTranslatorFactory; import org.hibernate.query.sqm.tree.select.SqmSelectStatement; import org.hibernate.service.ServiceRegistry; +import org.hibernate.sql.ast.SqlAstNodeRenderingMode; import org.hibernate.sql.ast.SqlAstTranslator; import org.hibernate.sql.ast.SqlAstTranslatorFactory; import org.hibernate.sql.ast.spi.SqlAppender; @@ -67,14 +74,21 @@ import org.hibernate.tool.schema.extract.spi.SequenceInformationExtractor; import org.hibernate.tool.schema.internal.StandardForeignKeyExporter; import org.hibernate.tool.schema.spi.Exporter; +import org.hibernate.type.JavaObjectType; +import org.hibernate.type.StandardBasicTypes; import org.hibernate.type.descriptor.jdbc.ClobJdbcType; +import org.hibernate.type.descriptor.jdbc.ObjectNullAsBinaryTypeJdbcType; import org.hibernate.type.descriptor.jdbc.spi.JdbcTypeRegistry; import org.hibernate.type.descriptor.sql.DdlType; import org.hibernate.type.descriptor.sql.internal.CapacityDependentDdlType; +import org.hibernate.type.descriptor.sql.internal.DdlTypeImpl; import org.hibernate.type.descriptor.sql.spi.DdlTypeRegistry; import org.hibernate.type.spi.TypeConfiguration; +import jakarta.persistence.TemporalType; + import static org.hibernate.exception.spi.TemplatedViolatedConstraintNameExtractor.extractUsingTemplate; +import static org.hibernate.query.sqm.produce.function.FunctionParameterType.STRING; import static org.hibernate.type.SqlTypes.BIGINT; import static org.hibernate.type.SqlTypes.BINARY; import static org.hibernate.type.SqlTypes.FLOAT; @@ -86,8 +100,17 @@ import static org.hibernate.type.SqlTypes.TIMESTAMP; import static org.hibernate.type.SqlTypes.TIMESTAMP_WITH_TIMEZONE; import static org.hibernate.type.SqlTypes.TINYINT; +import static org.hibernate.type.SqlTypes.UUID; import static org.hibernate.type.SqlTypes.VARBINARY; import static org.hibernate.type.SqlTypes.VARCHAR; +import static org.hibernate.type.descriptor.DateTimeUtils.JDBC_ESCAPE_END; +import static org.hibernate.type.descriptor.DateTimeUtils.JDBC_ESCAPE_START_DATE; +import static org.hibernate.type.descriptor.DateTimeUtils.JDBC_ESCAPE_START_TIME; +import static org.hibernate.type.descriptor.DateTimeUtils.JDBC_ESCAPE_START_TIMESTAMP; +import static org.hibernate.type.descriptor.DateTimeUtils.appendAsDate; +import static org.hibernate.type.descriptor.DateTimeUtils.appendAsLocalTime; +import static org.hibernate.type.descriptor.DateTimeUtils.appendAsTime; +import static org.hibernate.type.descriptor.DateTimeUtils.appendAsTimestampWithMicros; /** * Dialect for Informix 7.31.UD3 with Informix @@ -203,6 +226,7 @@ protected void registerColumnTypes(TypeContributions typeContributions, ServiceR .withTypeCapacity( getMaxNVarcharLength(), columnType( NVARCHAR ) ) .build() ); + ddlTypeRegistry.addDescriptor( new DdlTypeImpl( UUID, "char(36)", this ) ); } @Override @@ -234,6 +258,11 @@ public int getDefaultTimestampPrecision() { return 5; } + @Override + public boolean doesRoundTemporalOnOverflow() { + return false; + } + @Override public int getFloatPrecision() { return 8; @@ -254,10 +283,10 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio super.initializeFunctionRegistry(functionContributions); CommonFunctionFactory functionFactory = new CommonFunctionFactory(functionContributions); + functionFactory.aggregates( this, SqlAstNodeRenderingMode.NO_PLAIN_PARAMETER ); functionFactory.instr(); functionFactory.substr(); - functionFactory.substring_substr(); - //also natively supports ANSI-style substring() + functionFactory.substringFromFor(); functionFactory.trunc(); functionFactory.trim2(); functionFactory.space(); @@ -280,12 +309,30 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.monthsBetween(); functionFactory.stddev(); functionFactory.variance(); - functionFactory.locate_positionSubstring(); + functionFactory.bitLength_pattern( "length(?1)*8" ); + + if ( getVersion().isSameOrAfter( 12 ) ) { + functionFactory.locate_charindex(); + } //coalesce() and nullif() both supported since Informix 12 functionContributions.getFunctionRegistry().register( "least", new CaseLeastGreatestEmulation( true ) ); functionContributions.getFunctionRegistry().register( "greatest", new CaseLeastGreatestEmulation( false ) ); + functionContributions.getFunctionRegistry().namedDescriptorBuilder( "matches" ) + .setInvariantType( functionContributions.getTypeConfiguration() + .getBasicTypeRegistry() + .resolve( StandardBasicTypes.STRING ) + ) + .setExactArgumentCount( 2 ) + .setArgumentTypeResolver( + StandardFunctionArgumentTypeResolvers.impliedOrInvariant( + functionContributions.getTypeConfiguration(), + STRING + ) + ) + .setArgumentListSignature( "(STRING string, STRING pattern)" ) + .register(); if ( supportsWindowFunctions() ) { functionFactory.windowFunctions(); } @@ -620,6 +667,11 @@ public String[] getDropSchemaCommand(String schemaName) { return new String[] { "" }; } + @Override + public NameQualifierSupport getNameQualifierSupport() { + return NameQualifierSupport.BOTH; + } + @Override public boolean useCrossReferenceForeignKeys(){ return true; @@ -650,6 +702,11 @@ public String currentDate() { return "today"; } + @Override + public String currentTime() { + return currentTimestamp(); + } + @Override public String currentTimestamp() { return "current"; @@ -713,6 +770,56 @@ public static Replacer datetimeFormat(String format) { .replace("S", "%F1"); } + @Override + public void appendDateTimeLiteral( + SqlAppender appender, + TemporalAccessor temporalAccessor, + TemporalType precision, + TimeZone jdbcTimeZone) { + switch ( precision ) { + case DATE: + appender.appendSql( JDBC_ESCAPE_START_DATE ); + appendAsDate( appender, temporalAccessor ); + appender.appendSql( JDBC_ESCAPE_END ); + break; + case TIME: + appender.appendSql( JDBC_ESCAPE_START_TIME ); + appendAsTime( appender, temporalAccessor, supportsTemporalLiteralOffset(), jdbcTimeZone ); + appender.appendSql( JDBC_ESCAPE_END ); + break; + case TIMESTAMP: + appender.appendSql( JDBC_ESCAPE_START_TIMESTAMP ); + appendAsTimestampWithMicros( appender, temporalAccessor, supportsTemporalLiteralOffset(), jdbcTimeZone ); + appender.appendSql( JDBC_ESCAPE_END ); + break; + default: + throw new IllegalArgumentException(); + } + } + + @Override + public void appendDateTimeLiteral(SqlAppender appender, Date date, TemporalType precision, TimeZone jdbcTimeZone) { + switch ( precision ) { + case DATE: + appender.appendSql( JDBC_ESCAPE_START_DATE ); + appendAsDate( appender, date ); + appender.appendSql( JDBC_ESCAPE_END ); + break; + case TIME: + appender.appendSql( JDBC_ESCAPE_START_TIME ); + appendAsLocalTime( appender, date ); + appender.appendSql( JDBC_ESCAPE_END ); + break; + case TIMESTAMP: + appender.appendSql( JDBC_ESCAPE_START_TIMESTAMP ); + appendAsTimestampWithMicros( appender, date, jdbcTimeZone ); + appender.appendSql( JDBC_ESCAPE_END ); + break; + default: + throw new IllegalArgumentException(); + } + } + @Override public String getSelectClauseNullString(int sqlType, TypeConfiguration typeConfiguration) { DdlType descriptor = typeConfiguration.getDdlTypeRegistry().getDescriptor( sqlType ); @@ -743,5 +850,27 @@ public void contributeTypes(TypeContributions typeContributions, ServiceRegistry super.contributeTypes( typeContributions, serviceRegistry ); final JdbcTypeRegistry jdbcTypeRegistry = typeContributions.getTypeConfiguration().getJdbcTypeRegistry(); jdbcTypeRegistry.addDescriptor( Types.NCLOB, ClobJdbcType.DEFAULT ); + typeContributions.contributeJdbcType( VarcharUUIDJdbcType.INSTANCE ); + typeContributions.contributeJdbcType( ObjectNullAsBinaryTypeJdbcType.INSTANCE ); + + // Until we remove StandardBasicTypes, we have to keep this + typeContributions.contributeType( + new JavaObjectType( + ObjectNullAsBinaryTypeJdbcType.INSTANCE, + typeContributions.getTypeConfiguration() + .getJavaTypeRegistry() + .getDescriptor( Object.class ) + ) + ); + } + + @Override + public String getDual() { + return "(select 0 from systables where tabid=1)"; + } + + @Override + public String getFromDualForSelectOnly() { + return " from " + getDual() + " dual"; } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/InformixSqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/InformixSqlAstTranslator.java index 22528b21d065..eb451d4369e3 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/InformixSqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/InformixSqlAstTranslator.java @@ -153,16 +153,6 @@ protected boolean supportsRowValueConstructorSyntaxInQuantifiedPredicates() { return false; } - @Override - protected String getDual() { - return "(select 0 from systables where tabid=1)"; - } - - @Override - protected String getFromDualForSelectOnly() { - return " from " + getDual() + " dual"; - } - @Override protected void renderNull(Literal literal) { if ( getParameterRenderingMode() == SqlAstNodeRenderingMode.NO_UNTYPED ) { diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/IngresDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/IngresDialect.java index 5f62bfd28138..c2f5073404d0 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/IngresDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/IngresDialect.java @@ -559,4 +559,15 @@ public String translateExtractField(TemporalUnit unit) { public boolean supportsFetchClause(FetchClauseType type) { return getVersion().isSameOrAfter( 9, 3 ); } + + @Override + public String getDual() { + return "(select 0)"; + } + + @Override + public String getFromDualForSelectOnly() { + //this is only necessary if the query has a where clause + return " from " + getDual() + " dual"; + } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/IngresSqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/IngresSqlAstTranslator.java index 7a4af55cc9a6..be2c420c79e9 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/IngresSqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/IngresSqlAstTranslator.java @@ -138,17 +138,6 @@ protected boolean supportsRowValueConstructorSyntaxInQuantifiedPredicates() { return false; } - @Override - protected String getDual() { - return "(select 0)"; - } - - @Override - protected String getFromDualForSelectOnly() { - //this is only necessary if the query has a where clause - return " from " + getDual() + " dual"; - } - @Override protected boolean needsRowsToSkip() { return !supportsOffsetFetchClause(); diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MariaDBLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MariaDBLegacyDialect.java index d56e791050cb..402b34ee9a03 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MariaDBLegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MariaDBLegacyDialect.java @@ -264,4 +264,14 @@ public IdentifierHelper buildIdentifierHelper(IdentifierHelperBuilder builder, D return super.buildIdentifierHelper( builder, dbMetaData ); } + + @Override + public String getDual() { + return "dual"; + } + + @Override + public String getFromDualForSelectOnly() { + return getVersion().isBefore( 10, 4 ) ? ( " from " + getDual() ) : ""; + } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MariaDBLegacySqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MariaDBLegacySqlAstTranslator.java index 01cf6eb82e42..cfa1de6ec7ac 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MariaDBLegacySqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MariaDBLegacySqlAstTranslator.java @@ -366,16 +366,6 @@ protected boolean supportsDistinctFromPredicate() { return true; } - @Override - protected String getDual() { - return "dual"; - } - - @Override - protected String getFromDualForSelectOnly() { - return getDialect().getVersion().isBefore( 10, 4 ) ? ( " from " + getDual() ) : ""; - } - @Override public MariaDBLegacyDialect getDialect() { return this.dialect; @@ -404,4 +394,13 @@ protected void renderStringContainsExactlyPredicate(Expression haystack, Express needle.accept( this ); appendSql( ",'~','~~'),'?','~?'),'%','~%'),'%') escape '~'" ); } + + @Override + protected void appendAssignmentColumn(ColumnReference column) { + column.appendColumnForWrite( + this, + getAffectedTableNames().size() > 1 && !(getStatement() instanceof InsertSelectStatement) + ? determineColumnReferenceQualifier( column ) + : null ); + } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MaxDBDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MaxDBDialect.java index e527bd5baafa..f179bed01780 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MaxDBDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MaxDBDialect.java @@ -331,5 +331,15 @@ public String getTemporaryTableCreateOptions() { public boolean supportsJdbcConnectionLobCreation(DatabaseMetaData databaseMetaData) { return false; } + + @Override + public String getDual() { + return "dual"; + } + + @Override + public String getFromDualForSelectOnly() { + return " from " + getDual(); + } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MaxDBSqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MaxDBSqlAstTranslator.java index 011098389488..3955edfddcf3 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MaxDBSqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MaxDBSqlAstTranslator.java @@ -92,14 +92,4 @@ protected boolean supportsRowValueConstructorSyntaxInInList() { protected boolean supportsRowValueConstructorSyntaxInQuantifiedPredicates() { return false; } - - @Override - protected String getDual() { - return "dual"; - } - - @Override - protected String getFromDualForSelectOnly() { - return " from " + getDual(); - } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MimerSQLDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MimerSQLDialect.java index 3da7ba4005a5..198ffa412d79 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MimerSQLDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MimerSQLDialect.java @@ -344,4 +344,9 @@ public boolean useConnectionToCreateLob() { public IdentityColumnSupport getIdentityColumnSupport() { return MimerSQLIdentityColumnSupport.INSTANCE; } + + @Override + public String getFromDualForSelectOnly() { + return " from " + getDual(); + } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MimerSQLSqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MimerSQLSqlAstTranslator.java index 7510df6451ba..16d79ec7ffbb 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MimerSQLSqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MimerSQLSqlAstTranslator.java @@ -81,9 +81,4 @@ protected boolean supportsRowValueConstructorSyntaxInInList() { protected boolean supportsRowValueConstructorSyntaxInQuantifiedPredicates() { return false; } - - @Override - protected String getFromDualForSelectOnly() { - return " from " + getDual(); - } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MySQLLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MySQLLegacyDialect.java index 4739b447acd2..377d6f75cb62 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 @@ -604,10 +604,7 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio BasicTypeRegistry basicTypeRegistry = functionContributions.getTypeConfiguration().getBasicTypeRegistry(); SqmFunctionRegistry functionRegistry = functionContributions.getFunctionRegistry(); - functionRegistry.noArgsBuilder( "localtime" ) - .setInvariantType(basicTypeRegistry.resolve( StandardBasicTypes.TIMESTAMP )) - .setUseParenthesesWhenNoArgs( false ) - .register(); + // pi() produces a value with 7 digits unless we're explicit if ( getMySQLVersion().isSameOrAfter( 8 ) ) { functionRegistry.patternDescriptorBuilder( "pi", "cast(pi() as double)" ) @@ -1429,4 +1426,14 @@ public boolean supportsFromClauseInUpdate() { return true; } + @Override + public String getDual() { + return "dual"; + } + + @Override + public String getFromDualForSelectOnly() { + return getVersion().isSameOrAfter( 8 ) ? "" : ( " from " + getDual() ); + } + } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MySQLLegacySqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MySQLLegacySqlAstTranslator.java index ec382f39b65d..3da05b0b441e 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MySQLLegacySqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/MySQLLegacySqlAstTranslator.java @@ -389,16 +389,6 @@ protected boolean supportsWithClause() { return getDialect().getVersion().isSameOrAfter( 8 ); } - @Override - protected String getDual() { - return "dual"; - } - - @Override - protected String getFromDualForSelectOnly() { - return getDialect().getVersion().isSameOrAfter( 8 ) ? "" : ( " from " + getDual() ); - } - @Override public MySQLLegacyDialect getDialect() { return (MySQLLegacyDialect) DialectDelegateWrapper.extractRealDialect( super.getDialect() ); @@ -423,4 +413,13 @@ protected void renderStringContainsExactlyPredicate(Expression haystack, Express needle.accept( this ); appendSql( ",'~','~~'),'?','~?'),'%','~%'),'%') escape '~'" ); } + + @Override + protected void appendAssignmentColumn(ColumnReference column) { + column.appendColumnForWrite( + this, + getAffectedTableNames().size() > 1 && !(getStatement() instanceof InsertSelectStatement) + ? determineColumnReferenceQualifier( column ) + : null ); + } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacyDialect.java index 14d0037cb2ae..ccb9206ea6a8 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 @@ -13,6 +13,7 @@ import java.sql.Types; import java.time.temporal.ChronoField; import java.time.temporal.TemporalAccessor; +import java.util.List; import java.util.Locale; import java.util.TimeZone; import java.util.regex.Matcher; @@ -27,6 +28,7 @@ import org.hibernate.dialect.DmlTargetColumnQualifierSupport; import org.hibernate.dialect.DatabaseVersion; import org.hibernate.dialect.Dialect; +import org.hibernate.dialect.OracleServerConfiguration; import org.hibernate.dialect.OracleBooleanJdbcType; import org.hibernate.dialect.OracleJdbcHelper; import org.hibernate.dialect.OracleJsonJdbcType; @@ -115,8 +117,10 @@ import jakarta.persistence.TemporalType; +import static java.lang.String.join; import static java.util.regex.Pattern.CASE_INSENSITIVE; import static org.hibernate.exception.spi.TemplatedViolatedConstraintNameExtractor.extractUsingTemplate; +import static org.hibernate.internal.util.StringHelper.isEmpty; import static org.hibernate.query.sqm.TemporalUnit.DAY; import static org.hibernate.query.sqm.TemporalUnit.HOUR; import static org.hibernate.query.sqm.TemporalUnit.MINUTE; @@ -185,16 +189,54 @@ public class OracleLegacyDialect extends Dialect { private final UniqueDelegate uniqueDelegate = new CreateTableUniqueDelegate(this); private final SequenceSupport oracleSequenceSupport = OracleSequenceSupport.getInstance(this); + // Is it an Autonomous Database Cloud Service? + protected final boolean autonomous; + + // Is MAX_STRING_SIZE set to EXTENDED? + protected final boolean extended; + + // Is the database accessed using a database service protected by Application Continuity. + protected final boolean applicationContinuity; + + protected final int driverMajorVersion; + protected final int driverMinorVersion; + public OracleLegacyDialect() { this( DatabaseVersion.make( 8, 0 ) ); } public OracleLegacyDialect(DatabaseVersion version) { - super(version); + super( version ); + autonomous = false; + extended = false; + applicationContinuity = false; + driverMajorVersion = 19; + driverMinorVersion = 0; } public OracleLegacyDialect(DialectResolutionInfo info) { - super(info); + this( info, OracleServerConfiguration.fromDialectResolutionInfo( info ) ); + } + + public OracleLegacyDialect(DialectResolutionInfo info, OracleServerConfiguration serverConfiguration) { + super( info ); + autonomous = serverConfiguration.isAutonomous(); + extended = serverConfiguration.isExtended(); + applicationContinuity = serverConfiguration.isApplicationContinuity(); + this.driverMinorVersion = serverConfiguration.getDriverMinorVersion(); + this.driverMajorVersion = serverConfiguration.getDriverMajorVersion(); + } + + public boolean isAutonomous() { + return autonomous; + } + + public boolean isExtended() { + return extended; + } + + public boolean isApplicationContinuity() { + return applicationContinuity; } @Override @@ -398,20 +440,33 @@ public String castPattern(CastType from, CastType to) { } break; case INTEGER_BOOLEAN: - result = BooleanDecoder.toIntegerBoolean( from ); + result = from == CastType.STRING + ? buildStringToBooleanCastDecode( "1", "0" ) + : BooleanDecoder.toIntegerBoolean( from ); if ( result != null ) { return result; } break; case YN_BOOLEAN: - result = BooleanDecoder.toYesNoBoolean( from ); + result = from == CastType.STRING + ? buildStringToBooleanCastDecode( "'Y'", "'N'" ) + : BooleanDecoder.toYesNoBoolean( from ); if ( result != null ) { return result; } break; case BOOLEAN: + result = from == CastType.STRING + ? buildStringToBooleanCastDecode( "true", "false" ) + : BooleanDecoder.toBoolean( from ); + if ( result != null ) { + return result; + } + break; case TF_BOOLEAN: - result = BooleanDecoder.toTrueFalseBoolean( from ); + result = from == CastType.STRING + ? buildStringToBooleanCastDecode( "'T'", "'F'" ) + : BooleanDecoder.toTrueFalseBoolean( from ); if ( result != null ) { return result; } @@ -1203,6 +1258,17 @@ public boolean useFollowOnLocking(String sql, QueryOptions queryOptions) { ); } + @Override + public String getQueryHintString(String query, List hintList) { + if ( hintList.isEmpty() ) { + return query; + } + else { + final String hints = join( " ", hintList ); + return isEmpty( hints ) ? query : getQueryHintString( query, hints ); + } + } + @Override public String getQueryHintString(String sql, String hints) { final String statementType = statementType( sql ); @@ -1560,8 +1626,18 @@ public boolean supportsFromClauseInUpdate() { @Override public boolean useInputStreamToInsertBlob() { - // see HHH-18206 - return false; + // If application continuity is enabled, don't use stream bindings, since a replay could otherwise fail + // if the underlying stream doesn't support mark and reset + return !isApplicationContinuity(); + } + @Override + public String getDual() { + return "dual"; + } + + @Override + public String getFromDualForSelectOnly() { + return " from " + getDual(); } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacySqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacySqlAstTranslator.java index 1b8af5cc5d6c..15f5f489c427 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacySqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/OracleLegacySqlAstTranslator.java @@ -674,16 +674,6 @@ protected boolean supportsRowValueConstructorSyntaxInInSubQuery() { return getDialect().getVersion().isSameOrAfter( 9 ); } - @Override - protected String getDual() { - return "dual"; - } - - @Override - protected String getFromDualForSelectOnly() { - return " from " + getDual(); - } - private boolean supportsOffsetFetchClause() { return getDialect().supportsFetchClause( FetchClauseType.ROWS_ONLY ); } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgreSQLLegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgreSQLLegacyDialect.java index 219079710bcf..27f526d1c1e9 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 @@ -55,11 +55,14 @@ import org.hibernate.exception.spi.ViolatedConstraintNameExtractor; import org.hibernate.internal.util.JdbcExceptionHelper; import org.hibernate.metamodel.mapping.EntityMappingType; +import org.hibernate.metamodel.mapping.SqlExpressible; +import org.hibernate.metamodel.mapping.SqlTypedMapping; import org.hibernate.metamodel.spi.RuntimeModelCreationContext; import org.hibernate.procedure.internal.PostgreSQLCallableStatementSupport; import org.hibernate.procedure.spi.CallableStatementSupport; import org.hibernate.query.SemanticException; import org.hibernate.query.spi.QueryOptions; +import org.hibernate.query.sqm.CastType; import org.hibernate.query.sqm.FetchClauseType; import org.hibernate.query.sqm.IntervalType; import org.hibernate.query.sqm.TemporalUnit; @@ -216,6 +219,7 @@ protected String castType(int sqlTypeCode) { case NCHAR: case VARCHAR: case NVARCHAR: + return "varchar"; case LONG32VARCHAR: case LONG32NVARCHAR: return "text"; @@ -418,6 +422,16 @@ public String extractPattern(TemporalUnit unit) { } } + @Override + public String castPattern(CastType from, CastType to) { + if ( from == CastType.STRING && to == CastType.BOOLEAN ) { + return "cast(?1 as ?2)"; + } + else { + return super.castPattern( from, to ); + } + } + /** * {@code microsecond} is the smallest unit for an {@code interval}, * and the highest precision for a {@code timestamp}, so we could @@ -847,6 +861,8 @@ public boolean supportsOuterJoinForUpdate() { @Override public boolean useInputStreamToInsertBlob() { + // PG-JDBC treats setBinaryStream()/setCharacterStream() calls like bytea/varchar, which are not LOBs, + // so disable stream bindings for this dialect completely return false; } @@ -861,6 +877,14 @@ public String getSelectClauseNullString(int sqlType, TypeConfiguration typeConfi return "cast(null as " + typeConfiguration.getDdlTypeRegistry().getDescriptor( sqlType ).getRawTypeName() + ")"; } + @Override + public String getSelectClauseNullString(SqlTypedMapping sqlType, TypeConfiguration typeConfiguration) { + final String castTypeName = typeConfiguration.getDdlTypeRegistry() + .getDescriptor( sqlType.getJdbcMapping().getJdbcType().getDdlTypeCode() ) + .getCastTypeName( sqlType.toSize(), (SqlExpressible) sqlType.getJdbcMapping(), typeConfiguration.getDdlTypeRegistry() ); + return "cast(null as " + castTypeName + ")"; + } + @Override public boolean supportsCommentOn() { return true; diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgreSQLLegacySqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgreSQLLegacySqlAstTranslator.java index 4dae9260dac3..2a96aa8d8c55 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgreSQLLegacySqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/PostgreSQLLegacySqlAstTranslator.java @@ -24,6 +24,7 @@ import org.hibernate.sql.ast.tree.insert.ConflictClause; import org.hibernate.sql.ast.tree.insert.InsertSelectStatement; import org.hibernate.sql.ast.tree.predicate.BooleanExpressionPredicate; +import org.hibernate.sql.ast.tree.predicate.InArrayPredicate; import org.hibernate.sql.ast.tree.predicate.LikePredicate; import org.hibernate.sql.ast.tree.predicate.NullnessPredicate; import org.hibernate.sql.ast.tree.select.QueryGroup; @@ -46,6 +47,14 @@ public class PostgreSQLLegacySqlAstTranslator extends A public PostgreSQLLegacySqlAstTranslator(SessionFactoryImplementor sessionFactory, Statement statement) { super( sessionFactory, statement ); } + + @Override + public void visitInArrayPredicate(InArrayPredicate inArrayPredicate) { + inArrayPredicate.getTestExpression().accept( this ); + appendSql( " = any (" ); + inArrayPredicate.getArrayParameter().accept( this ); + appendSql( ")" ); + } @Override protected void renderInsertIntoNoColumns(TableInsertStandard tableInsert) { diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/RDMSOS2200Dialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/RDMSOS2200Dialect.java index b4b111aa52a1..1e5f5db6cfcf 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/RDMSOS2200Dialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/RDMSOS2200Dialect.java @@ -434,4 +434,14 @@ public void appendDatetimeFormat(SqlAppender appender, String format) { public String trimPattern(TrimSpec specification, boolean isWhitespace) { return AbstractTransactSQLDialect.replaceLtrimRtrim( specification, isWhitespace ); } + + @Override + public String getDual() { + return "rdms.rdms_dummy"; + } + + @Override + public String getFromDualForSelectOnly() { + return " from " + getDual() + " where key_col=1"; + } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/RDMSOS2200SqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/RDMSOS2200SqlAstTranslator.java index 9a1f97109d78..24f726c877a1 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/RDMSOS2200SqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/RDMSOS2200SqlAstTranslator.java @@ -124,14 +124,4 @@ protected boolean supportsRowValueConstructorSyntaxInInList() { protected boolean supportsRowValueConstructorSyntaxInQuantifiedPredicates() { return false; } - - @Override - protected String getDual() { - return "rdms.rdms_dummy"; - } - - @Override - protected String getFromDualForSelectOnly() { - return " from " + getDual() + " where key_col=1"; - } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SingleStoreDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SingleStoreDialect.java index 8af3ae96b905..7c0347fa07ce 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SingleStoreDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SingleStoreDialect.java @@ -1411,4 +1411,9 @@ public boolean isForUpdateLockingEnabled() { * @settingDefault {@code false} */ public static final String SINGLE_STORE_FOR_UPDATE_LOCK_ENABLED = "hibernate.dialect.singlestore.for_update_lock_enabled"; + + @Override + public String getDual() { + return "dual"; + } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SingleStoreSqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SingleStoreSqlAstTranslator.java index c646a5500542..dac437dd4015 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SingleStoreSqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SingleStoreSqlAstTranslator.java @@ -437,11 +437,6 @@ protected boolean supportsDistinctFromPredicate() { return false; } - @Override - protected String getDual() { - return "dual"; - } - @Override public SingleStoreDialect getDialect() { return this.dialect; diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseASELegacyDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseASELegacyDialect.java index 230b6d4f1e6a..db66214344cd 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseASELegacyDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseASELegacyDialect.java @@ -125,13 +125,8 @@ protected void registerColumnTypes(TypeContributions typeContributions, ServiceR // But with jTDS we can't use them as the driver can't handle the types if ( getVersion().isSameOrAfter( 15, 5 ) && getDriverKind() != SybaseDriverKind.JTDS ) { ddlTypeRegistry.addDescriptor( - CapacityDependentDdlType.builder( DATE, "bigdatetime", "bigdatetime", this ) - .withTypeCapacity( 3, "datetime" ) - .build() - ); - ddlTypeRegistry.addDescriptor( - CapacityDependentDdlType.builder( TIME, "bigdatetime", "bigdatetime", this ) - .withTypeCapacity( 3, "datetime" ) + CapacityDependentDdlType.builder( TIME, "bigtime", "bigtime", this ) + .withTypeCapacity( 3, "time" ) .build() ); ddlTypeRegistry.addDescriptor( @@ -696,4 +691,9 @@ public LimitHandler getLimitHandler() { } return new TopLimitHandler(false); } + + @Override + public String getDual() { + return "(select 1 c1)"; + } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseASELegacySqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseASELegacySqlAstTranslator.java index b5ace7590994..18197f136559 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseASELegacySqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseASELegacySqlAstTranslator.java @@ -530,11 +530,6 @@ protected boolean supportsRowValueConstructorSyntaxInQuantifiedPredicates() { return false; } - @Override - protected String getDual() { - return "(select 1 c1)"; - } - private boolean supportsTopClause() { return getDialect().getVersion().isSameOrAfter( 12, 5 ); } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseAnywhereDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseAnywhereDialect.java index e77527c4c9a6..0a5d8d137367 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseAnywhereDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseAnywhereDialect.java @@ -215,4 +215,14 @@ public LimitHandler getLimitHandler() { return TopLimitHandler.INSTANCE; } + @Override + public String getDual() { + return "sys.dummy"; + } + + @Override + public String getFromDualForSelectOnly() { + return " from " + getDual(); + } + } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseAnywhereSqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseAnywhereSqlAstTranslator.java index 841594f12e66..eb086e24fb5e 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseAnywhereSqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/SybaseAnywhereSqlAstTranslator.java @@ -125,6 +125,7 @@ protected boolean renderNamedTableReference(NamedTableReference tableReference, // Just always return true because SQL Server doesn't support the FOR UPDATE clause return true; } + super.renderNamedTableReference( tableReference, lockMode ); return false; } @@ -239,14 +240,4 @@ protected boolean supportsRowValueConstructorSyntaxInInList() { protected boolean supportsRowValueConstructorSyntaxInQuantifiedPredicates() { return false; } - - @Override - protected String getDual() { - return "sys.dummy"; - } - - @Override - protected String getFromDualForSelectOnly() { - return " from " + getDual(); - } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/TimesTenDialect.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/TimesTenDialect.java index aa30500db527..0a7299d5c06e 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/TimesTenDialect.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/TimesTenDialect.java @@ -15,8 +15,11 @@ import org.hibernate.community.dialect.sequence.SequenceInformationExtractorTimesTenDatabaseImpl; import org.hibernate.community.dialect.sequence.TimesTenSequenceSupport; import org.hibernate.dialect.Dialect; +import org.hibernate.dialect.BooleanDecoder; import org.hibernate.dialect.RowLockStrategy; import org.hibernate.dialect.function.CommonFunctionFactory; +import org.hibernate.dialect.function.OracleTruncFunction; +import org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers; import org.hibernate.dialect.lock.LockingStrategy; import org.hibernate.dialect.lock.OptimisticForceIncrementLockingStrategy; import org.hibernate.dialect.lock.OptimisticLockingStrategy; @@ -34,6 +37,7 @@ import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.spi.RuntimeModelCreationContext; import org.hibernate.persister.entity.Lockable; +import org.hibernate.query.sqm.CastType; import org.hibernate.query.sqm.IntervalType; import org.hibernate.query.sqm.TemporalUnit; import org.hibernate.query.sqm.mutation.internal.temptable.GlobalTemporaryTableInsertStrategy; @@ -42,6 +46,7 @@ import org.hibernate.query.sqm.mutation.spi.SqmMultiTableMutationStrategy; import org.hibernate.sql.ast.SqlAstTranslator; import org.hibernate.sql.ast.SqlAstTranslatorFactory; +import org.hibernate.sql.ast.SqlAstNodeRenderingMode; import org.hibernate.sql.ast.spi.StandardSqlAstTranslatorFactory; import org.hibernate.sql.ast.tree.Statement; import org.hibernate.sql.exec.spi.JdbcOperation; @@ -52,6 +57,15 @@ import org.hibernate.type.descriptor.jdbc.spi.JdbcTypeRegistry; import org.hibernate.type.spi.TypeConfiguration; +import org.hibernate.type.BasicType; +import org.hibernate.type.BasicTypeRegistry; +import org.hibernate.type.StandardBasicTypes; +import org.hibernate.dialect.function.StandardSQLFunction; +import org.hibernate.dialect.function.CurrentFunction; +import org.hibernate.query.sqm.produce.function.StandardFunctionArgumentTypeResolvers; +import jakarta.persistence.GenerationType; +import java.util.Date; + import jakarta.persistence.TemporalType; import static org.hibernate.dialect.SimpleDatabaseVersion.ZERO_VERSION; @@ -59,14 +73,13 @@ import static org.hibernate.query.sqm.produce.function.FunctionParameterType.STRING; /** - * A SQL dialect for TimesTen 5.1. + * A SQL dialect for Oracle TimesTen *

* Known limitations: * joined-subclass support because of no CASE support in TimesTen * No support for subqueries that includes aggregation * - size() in HQL not supported * - user queries that does subqueries with aggregation - * No CLOB/BLOB support * No cascade delete support. * No Calendar support * No support for updating primary keys. @@ -90,6 +103,7 @@ protected String columnType(int sqlTypeCode) { // for the default Oracle type mode // TypeMode=0 case SqlTypes.BOOLEAN: + case SqlTypes.BIT: case SqlTypes.TINYINT: return "tt_tinyint"; case SqlTypes.SMALLINT: @@ -101,15 +115,26 @@ protected String columnType(int sqlTypeCode) { //note that 'binary_float'/'binary_double' might //be better mappings for Java Float/Double + case SqlTypes.VARCHAR: + case SqlTypes.LONGVARCHAR: + return "varchar2($l)"; + + case SqlTypes.LONGVARBINARY: + return "varbinary($l)"; + //'numeric'/'decimal' are synonyms for 'number' case SqlTypes.NUMERIC: case SqlTypes.DECIMAL: return "number($p,$s)"; + case SqlTypes.FLOAT: + return "binary_float"; + case SqlTypes.DOUBLE: + return "binary_double"; + case SqlTypes.DATE: return "tt_date"; case SqlTypes.TIME: return "tt_time"; - //`timestamp` has more precision than `tt_timestamp` case SqlTypes.TIMESTAMP_WITH_TIMEZONE: return "timestamp($p)"; @@ -157,22 +182,97 @@ public int getDefaultDecimalPrecision() { public void initializeFunctionRegistry(FunctionContributions functionContributions) { super.initializeFunctionRegistry(functionContributions); - CommonFunctionFactory functionFactory = new CommonFunctionFactory(functionContributions); + final TypeConfiguration typeConfiguration = functionContributions.getTypeConfiguration(); + CommonFunctionFactory functionFactory = new CommonFunctionFactory(functionContributions); + final BasicTypeRegistry basicTypeRegistry = typeConfiguration.getBasicTypeRegistry(); + final BasicType timestampType = basicTypeRegistry.resolve( StandardBasicTypes.TIMESTAMP ); + final BasicType stringType = basicTypeRegistry.resolve( StandardBasicTypes.STRING ); + final BasicType longType = basicTypeRegistry.resolve( StandardBasicTypes.LONG ); + final BasicTypeintType = basicTypeRegistry.resolve( StandardBasicTypes.INTEGER ); + + // String Functions functionFactory.trim2(); - functionFactory.soundex(); - functionFactory.trunc(); + functionFactory.characterLength_length( SqlAstNodeRenderingMode.DEFAULT ); + functionFactory.concat_pipeOperator(); functionFactory.toCharNumberDateTimestamp(); - functionFactory.ceiling_ceil(); + functionFactory.char_chr(); functionFactory.instr(); functionFactory.substr(); functionFactory.substring_substr(); - functionFactory.leftRight_substr(); - functionFactory.char_chr(); - functionFactory.rownumRowid(); - functionFactory.sysdate(); + functionFactory.soundex(); + + // Date/Time Functions + functionContributions.getFunctionRegistry().register( + "sysdate", new CurrentFunction("sysdate", "sysdate", timestampType) + ); + functionContributions.getFunctionRegistry().register( + "getdate", new CurrentFunction("getdate", "getdate()", timestampType ) + ); + + // Multi-param date dialect functions functionFactory.addMonths(); functionFactory.monthsBetween(); + // Math functions + functionFactory.ceiling_ceil(); + functionFactory.radians_acos(); + functionFactory.degrees_acos(); + functionFactory.sinh(); + functionFactory.tanh(); + functionContributions.getFunctionRegistry().register( + "trunc", + new OracleTruncFunction( functionContributions.getTypeConfiguration() ) + ); + functionContributions.getFunctionRegistry().registerAlternateKey( "truncate", "trunc" ); + functionFactory.round(); + + // Bitwise functions + functionContributions.getFunctionRegistry() + .patternDescriptorBuilder( "bitor", "(?1+?2-bitand(?1,?2))") + .setExactArgumentCount( 2 ) + .setArgumentTypeResolver( StandardFunctionArgumentTypeResolvers + .ARGUMENT_OR_IMPLIED_RESULT_TYPE ) + .register(); + + functionContributions.getFunctionRegistry() + .patternDescriptorBuilder( "bitxor", "(?1+?2-2*bitand(?1,?2))") + .setExactArgumentCount( 2 ) + .setArgumentTypeResolver( StandardFunctionArgumentTypeResolvers + .ARGUMENT_OR_IMPLIED_RESULT_TYPE ) + .register(); + + // Misc. functions + functionContributions.getFunctionRegistry().namedDescriptorBuilder( "nvl" ) + .setMinArgumentCount( 2 ) + .setArgumentTypeResolver( StandardFunctionArgumentTypeResolvers.ARGUMENT_OR_IMPLIED_RESULT_TYPE ) + .setReturnTypeResolver( StandardFunctionReturnTypeResolvers.useFirstNonNull() ) + .register(); + + functionContributions.getFunctionRegistry().register( + "user", new CurrentFunction("user", "user", stringType) + ); + functionContributions.getFunctionRegistry().register( + "rowid", new CurrentFunction("rowid", "rowid", stringType) + ); + functionContributions.getFunctionRegistry().register( + "uid", new CurrentFunction("uid", "uid", intType) + ); + functionContributions.getFunctionRegistry().register( + "rownum", new CurrentFunction("rownum", "rownum", longType) + ); + functionContributions.getFunctionRegistry().register( + "vsize", new StandardSQLFunction("vsize", StandardBasicTypes.DOUBLE) + ); + functionContributions.getFunctionRegistry().register( + "SESSION_USER", new CurrentFunction("SESSION_USER","SESSION_USER", stringType) + ); + functionContributions.getFunctionRegistry().register( + "SYSTEM_USER", new CurrentFunction("SYSTEM_USER", "SYSTEM_USER", stringType) + ); + functionContributions.getFunctionRegistry().register( + "CURRENT_USER", new CurrentFunction("CURRENT_USER","CURRENT_USER", stringType) + ); + functionContributions.getFunctionRegistry().registerBinaryTernaryPattern( "locate", functionContributions.getTypeConfiguration().getBasicTypeRegistry().resolve( StandardBasicTypes.INTEGER ), @@ -426,4 +526,104 @@ public String getSelectClauseNullString(int sqlType, TypeConfiguration typeConfi } } + @Override + public String getNativeIdentifierGeneratorStrategy() { + return "sequence"; + } + + @Override + public String currentDate() { + return "sysdate"; + } + + @Override + public String currentTime() { + return "sysdate"; + } + + @Override + public String currentTimestamp() { + return "sysdate"; + } + + @Override + public int getMaxVarcharLength() { + // 1 to 4,194,304 bytes according to TimesTen Doc + return 4194304; + } + + @Override + public int getMaxVarbinaryLength() { + // 1 to 4,194,304 bytes according to TimesTen Doc + return 4194304; + } + + @Override + public boolean isEmptyStringTreatedAsNull() { + return true; + } + + @Override + public boolean supportsTupleDistinctCounts() { + return false; + } + + @Override + public String getDual() { + return "dual"; + } + + @Override + public String getFromDualForSelectOnly() { + return " from dual"; + } + + @Override + public String castPattern(CastType from, CastType to) { + String result; + switch ( to ) { + case INTEGER: + case LONG: + result = BooleanDecoder.toInteger( from ); + if ( result != null ) { + return result; + } + break; + case STRING: + switch ( from ) { + case BOOLEAN: + case INTEGER_BOOLEAN: + case TF_BOOLEAN: + case YN_BOOLEAN: + return BooleanDecoder.toString( from ); + case DATE: + return "to_char(?1,'YYYY-MM-DD')"; + case TIME: + return "to_char(?1,'HH24:MI:SS')"; + case TIMESTAMP: + return "to_char(?1,'YYYY-MM-DD HH24:MI:SS.FF9')"; + } + break; + case CLOB: + return "to_clob(?1)"; + case DATE: + if ( from == CastType.STRING ) { + return "to_date(?1,'YYYY-MM-DD')"; + } + break; + case TIME: + if ( from == CastType.STRING ) { + return "to_date(?1,'HH24:MI:SS')"; + } + break; + case TIMESTAMP: + if ( from == CastType.STRING ) { + return "to_timestamp(?1,'YYYY-MM-DD HH24:MI:SS.FF9')"; + } + break; + } + return super.castPattern(from, to); + } + + } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/TimesTenSqlAstTranslator.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/TimesTenSqlAstTranslator.java index b0eadebbfa06..b4ff40f70993 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/TimesTenSqlAstTranslator.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/TimesTenSqlAstTranslator.java @@ -28,6 +28,8 @@ import org.hibernate.sql.ast.tree.select.QuerySpec; import org.hibernate.sql.ast.tree.select.SelectClause; import org.hibernate.sql.exec.spi.JdbcOperation; +import org.hibernate.internal.util.collections.Stack; +import org.hibernate.sql.ast.Clause; /** * A SQL AST translator for TimesTen. @@ -143,4 +145,66 @@ protected boolean supportsRowValueConstructorSyntaxInInList() { protected boolean supportsRowValueConstructorSyntaxInQuantifiedPredicates() { return false; } + + protected void renderRowsToClause(QuerySpec querySpec) { + if ( querySpec.isRoot() && hasLimit() ) { + prepareLimitOffsetParameters(); + renderRowsToClause( getOffsetParameter(), getLimitParameter() ); + } + else { + assertRowsOnlyFetchClauseType( querySpec ); + renderRowsToClause( querySpec.getOffsetClauseExpression(), querySpec.getFetchClauseExpression() ); + } + } + + protected void renderRowsToClause(Expression offsetClauseExpression, Expression fetchClauseExpression) { + // offsetClauseExpression -> firstRow + // fetchClauseExpression -> maxRows + final Stack clauseStack = getClauseStack(); + + if ( offsetClauseExpression == null && fetchClauseExpression != null ) { + // We only have a maxRows/limit. We use 'SELECT FIRST n' syntax + appendSql("first "); + clauseStack.push( Clause.FETCH ); + try { + renderFetchExpression( fetchClauseExpression ); + } + finally { + clauseStack.pop(); + } + } + else if ( offsetClauseExpression != null ) { + // We have an offset. We use 'SELECT ROWS m TO n' syntax + appendSql( "rows " ); + + // Render offset parameter + clauseStack.push( Clause.OFFSET ); + try { + renderOffsetExpression( offsetClauseExpression ); + } + finally { + clauseStack.pop(); + } + + appendSql( " to " ); + + // Render maxRows/limit parameter + clauseStack.push( Clause.FETCH ); + try { + if ( fetchClauseExpression != null ) { + // We need to substract 1 row to fit maxRows + renderFetchPlusOffsetExpressionAsSingleParameter( fetchClauseExpression, offsetClauseExpression, -1 ); + } + else{ + // We dont have a maxRows param, we will just use a MAX_VALUE + appendSql( Integer.MAX_VALUE ); + } + } + finally { + clauseStack.pop(); + } + } + + appendSql( WHITESPACE ); + } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/pagination/TimesTenLimitHandler.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/pagination/TimesTenLimitHandler.java index 4d95ef2af0df..3dafe56b977c 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/pagination/TimesTenLimitHandler.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/pagination/TimesTenLimitHandler.java @@ -6,23 +6,48 @@ */ package org.hibernate.community.dialect.pagination; +import org.hibernate.dialect.pagination.AbstractSimpleLimitHandler; import org.hibernate.dialect.pagination.LimitHandler; /** * A {@link LimitHandler} for TimesTen, which uses {@code ROWS n}, * but at the start of the query instead of at the end. */ -public class TimesTenLimitHandler extends RowsLimitHandler { +public class TimesTenLimitHandler extends AbstractSimpleLimitHandler { public static final TimesTenLimitHandler INSTANCE = new TimesTenLimitHandler(); + public TimesTenLimitHandler(){ + } + + @Override + public boolean supportsOffset() { + return false; + } + + @Override + public boolean supportsLimitOffset() { + return true; + } + @Override - protected String insert(String rows, String sql) { - return insertAfterSelect( rows, sql ); + // TimesTen is 1 based + public int convertToFirstRowValue(int zeroBasedFirstResult) { + return zeroBasedFirstResult + 1; + } + + @Override + public boolean useMaxForLimit() { + return true; } @Override public boolean bindLimitParametersFirst() { return true; } + + @Override + protected String limitClause(boolean hasFirstRow) { + return hasFirstRow ? " rows ? to ?" : " first ?"; + } } diff --git a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/sequence/TimesTenSequenceSupport.java b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/sequence/TimesTenSequenceSupport.java index 802aa1b5801d..c0aa8308c519 100644 --- a/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/sequence/TimesTenSequenceSupport.java +++ b/hibernate-community-dialects/src/main/java/org/hibernate/community/dialect/sequence/TimesTenSequenceSupport.java @@ -6,7 +6,6 @@ */ package org.hibernate.community.dialect.sequence; -import org.hibernate.dialect.sequence.NextvalSequenceSupport; import org.hibernate.dialect.sequence.SequenceSupport; /** @@ -14,13 +13,37 @@ * * @author Gavin King */ -public final class TimesTenSequenceSupport extends NextvalSequenceSupport { +public final class TimesTenSequenceSupport implements SequenceSupport { public static final SequenceSupport INSTANCE = new TimesTenSequenceSupport(); + @Override + public boolean supportsPooledSequences() { + return true; + } + + @Override + public String getSelectSequenceNextValString(String sequenceName) { + return sequenceName + ".nextval"; + } + + @Override + public String getSequenceNextValString(String sequenceName) { + return "select " + sequenceName + ".nextval from sys.dual"; + } + @Override public String getFromDual() { return " from sys.dual"; } + @Override + public String getCreateSequenceString(String sequenceName) { + return "create sequence " + sequenceName; + } + + @Override + public String getDropSequenceString(String sequenceName) { + return "drop sequence " + sequenceName; + } } diff --git a/hibernate-community-dialects/src/test/java/org/hibernate/community/dialect/FetchPlusOffsetParameterTest.java b/hibernate-community-dialects/src/test/java/org/hibernate/community/dialect/FetchPlusOffsetParameterTest.java new file mode 100644 index 000000000000..786a84c20d99 --- /dev/null +++ b/hibernate-community-dialects/src/test/java/org/hibernate/community/dialect/FetchPlusOffsetParameterTest.java @@ -0,0 +1,155 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.community.dialect; + +import java.util.List; + +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.dialect.H2Dialect; +import org.hibernate.dialect.H2SqlAstTranslator; +import org.hibernate.engine.jdbc.dialect.spi.DialectResolutionInfo; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.query.sqm.FetchClauseType; +import org.hibernate.sql.ast.Clause; +import org.hibernate.sql.ast.SqlAstTranslator; +import org.hibernate.sql.ast.SqlAstTranslatorFactory; +import org.hibernate.sql.ast.spi.StandardSqlAstTranslatorFactory; +import org.hibernate.sql.ast.tree.Statement; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.select.QueryPart; +import org.hibernate.sql.exec.spi.JdbcOperation; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.RequiresDialect; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.SettingProvider; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@RequiresDialect(H2Dialect.class) +@DomainModel(annotatedClasses = FetchPlusOffsetParameterTest.Book.class) +@SessionFactory +@ServiceRegistry( + settingProviders = @SettingProvider(settingName = AvailableSettings.DIALECT, provider = FetchPlusOffsetParameterTest.TestSettingProvider.class) +) +@Jira("https://hibernate.atlassian.net/browse/HHH-19888") +public class FetchPlusOffsetParameterTest { + + @BeforeEach + protected void prepareTest(SessionFactoryScope scope) { + scope.inTransaction( + (session) -> { + for ( int i = 1; i <= 3; i++ ) { + session.persist( new Book( i, "Book " + i ) ); + } + } + ); + } + + @Test + public void testStaticOffset(SessionFactoryScope scope) { + scope.inTransaction( + (session) -> { + final List books = session.createSelectionQuery( + "from Book b order by b.id", + Book.class + ) + .setFirstResult( 2 ) + .setMaxResults( 1 ).getResultList(); + // The custom dialect will fetch offset + limit + staticOffset rows + // Since staticOffset is -1, it must yield 2 rows + assertEquals( 2, books.size() ); + } + ); + } + + @Entity(name = "Book") + public static class Book { + @Id + private Integer id; + private String title; + + public Book() { + } + + public Book(Integer id, String title) { + this.id = id; + this.title = title; + } + } + + + public static class TestSettingProvider implements SettingProvider.Provider { + + @Override + public String getSetting() { + return TestDialect.class.getName(); + } + } + + public static class TestDialect extends H2Dialect { + + public TestDialect(DialectResolutionInfo info) { + super( info ); + } + + public TestDialect() { + } + + @Override + public SqlAstTranslatorFactory getSqlAstTranslatorFactory() { + return new StandardSqlAstTranslatorFactory() { + @Override + protected SqlAstTranslator buildTranslator( + SessionFactoryImplementor sessionFactory, Statement statement) { + return new H2SqlAstTranslator<>( sessionFactory, statement ) { + @Override + public void visitOffsetFetchClause(QueryPart queryPart) { + final Expression offsetClauseExpression; + final Expression fetchClauseExpression; + if ( queryPart.isRoot() && hasLimit() ) { + prepareLimitOffsetParameters(); + offsetClauseExpression = getOffsetParameter(); + fetchClauseExpression = getLimitParameter(); + } + else { + assert queryPart.getFetchClauseType() == FetchClauseType.ROWS_ONLY; + offsetClauseExpression = queryPart.getOffsetClauseExpression(); + fetchClauseExpression = queryPart.getFetchClauseExpression(); + } + if ( offsetClauseExpression != null && fetchClauseExpression != null ) { + appendSql( " fetch first " ); + getClauseStack().push( Clause.FETCH ); + try { + renderFetchPlusOffsetExpressionAsSingleParameter( + fetchClauseExpression, + offsetClauseExpression, + -1 + ); + } + finally { + getClauseStack().pop(); + } + appendSql( " rows only" ); + } + } + }; + } + }; + } + } +} + + diff --git a/hibernate-community-dialects/src/test/java/org/hibernate/community/dialect/InformixFunctionTest.java b/hibernate-community-dialects/src/test/java/org/hibernate/community/dialect/InformixFunctionTest.java index 91070aaf6587..762bf92940bf 100644 --- a/hibernate-community-dialects/src/test/java/org/hibernate/community/dialect/InformixFunctionTest.java +++ b/hibernate-community-dialects/src/test/java/org/hibernate/community/dialect/InformixFunctionTest.java @@ -181,6 +181,23 @@ public void testCurrentTimestamp(SessionFactoryScope scope) { ); } + @Test + @TestForIssue(jiraKey = "HHH-18369") + public void testMatches(SessionFactoryScope scope) { + scope.inTransaction( + (session) -> { + String country = (String) session.createQuery( + "select e.country " + + "from Event e " + + "where e.id = :id and matches(e.country, :country) = 'T'" ) + .setParameter( "id", event.id ) + .setParameter( "country", "R*" ) + .getSingleResult(); + assertEquals( "Romania", country ); + } + ); + } + private Calendar todayCalendar() { Calendar calendar = Calendar.getInstance(); calendar.set(Calendar.HOUR_OF_DAY, 0); diff --git a/hibernate-core/hibernate-core.gradle b/hibernate-core/hibernate-core.gradle index 689fddb9b73b..8995db8eba19 100644 --- a/hibernate-core/hibernate-core.gradle +++ b/hibernate-core/hibernate-core.gradle @@ -45,6 +45,7 @@ dependencies { compileOnly libs.jackson compileOnly libs.jacksonXml compileOnly dbLibs.postgresql + compileOnly dbLibs.edb testImplementation project(':hibernate-testing') testImplementation project(':hibernate-ant') 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 b9a113092a4d..9b65eff78379 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 @@ -159,8 +159,7 @@ cycleClause * A toplevel query of subquery, which may be a union or intersection of subqueries */ queryExpression - : withClause? orderedQuery # SimpleQueryGroup - | withClause? orderedQuery (setOperator orderedQuery)+ # SetQueryGroup + : withClause? orderedQuery (setOperator orderedQuery)* ; /** @@ -430,8 +429,6 @@ pathContinuation * * VALUE( path ) * * KEY( path ) * * path[ selector ] - * * ARRAY_GET( embeddableArrayPath, index ).path - * * COALESCE( array1, array2 )[ selector ].path */ syntacticDomainPath : treatedNavigablePath @@ -439,10 +436,6 @@ syntacticDomainPath | mapKeyNavigablePath | simplePath indexedPathAccessFragment | simplePath slicedPathAccessFragment - | toOneFkReference - | function pathContinuation - | function indexedPathAccessFragment pathContinuation? - | function slicedPathAccessFragment ; /** @@ -664,19 +657,21 @@ whereClause predicate //highest to lowest precedence : LEFT_PAREN predicate RIGHT_PAREN # GroupedPredicate - | expression IS NOT? NULL # IsNullPredicate - | expression IS NOT? EMPTY # IsEmptyPredicate - | expression IS NOT? TRUE # IsTruePredicate - | expression IS NOT? FALSE # IsFalsePredicate - | expression IS NOT? DISTINCT FROM expression # IsDistinctFromPredicate + | expression IS NOT? (NULL|EMPTY|TRUE|FALSE) # UnaryIsPredicate | expression NOT? MEMBER OF? path # MemberOfPredicate | expression NOT? IN inList # InPredicate | expression NOT? BETWEEN expression AND expression # BetweenPredicate | expression NOT? (LIKE | ILIKE) expression likeEscape? # LikePredicate - | expression NOT? CONTAINS expression # ContainsPredicate - | expression NOT? INCLUDES expression # IncludesPredicate - | expression NOT? INTERSECTS expression # IntersectsPredicate - | expression comparisonOperator expression # ComparisonPredicate + | expression + ( NOT? (CONTAINS | INCLUDES | INTERSECTS) + | IS NOT? DISTINCT FROM + | EQUAL + | NOT_EQUAL + | GREATER + | GREATER_EQUAL + | LESS + | LESS_EQUAL + ) expression # BinaryExpressionPredicate | EXISTS collectionQuantifier LEFT_PAREN simplePath RIGHT_PAREN # ExistsCollectionPartPredicate | EXISTS expression # ExistsPredicate | NOT predicate # NegatedPredicate @@ -685,18 +680,6 @@ predicate | expression # BooleanExpressionPredicate ; -/** - * An operator which compares values for equality or order - */ -comparisonOperator - : EQUAL - | NOT_EQUAL - | GREATER - | GREATER_EQUAL - | LESS - | LESS_EQUAL - ; - /** * Any right operand of the 'in' operator * @@ -751,7 +734,14 @@ primaryExpression | entityVersionReference # EntityVersionExpression | entityNaturalIdReference # EntityNaturalIdExpression | syntacticDomainPath pathContinuation? # SyntacticPathExpression - | function # FunctionExpression + // ARRAY_GET( embeddableArrayPath, index ).path + // COALESCE( array1, array2 )[ selector ].path + // COALESCE( array1, array2 )[ start : end ] + | function ( + pathContinuation + | slicedPathAccessFragment + | indexedPathAccessFragment pathContinuation? + )? # FunctionExpression | generalPathFragment # GeneralPathExpression ; @@ -1109,6 +1099,7 @@ function | collectionFunctionMisuse | jpaNonstandardFunction | columnFunction + | toOneFkReference | genericFunction ; diff --git a/hibernate-core/src/main/java/org/hibernate/annotations/OnDelete.java b/hibernate-core/src/main/java/org/hibernate/annotations/OnDelete.java index 8a748bdc5216..791200e790a9 100644 --- a/hibernate-core/src/main/java/org/hibernate/annotations/OnDelete.java +++ b/hibernate-core/src/main/java/org/hibernate/annotations/OnDelete.java @@ -17,10 +17,33 @@ /** * Specifies an {@code on delete} action for a foreign key constraint. * The most common usage is {@code @OnDelete(action = CASCADE)}. + *

+ * @ManyToOne
+ * @OnDelete(action = CASCADE)
+ * Parent parent;
+ * 
* Note that this results in an {@code on delete cascade} clause in * the DDL definition of the foreign key. It's completely different * to {@link jakarta.persistence.CascadeType#REMOVE}. *

+ * In fact, {@code @OnDelete} may be combined with {@code cascade=REMOVE}. + *

+ * @ManyToOne(cascade = REMOVE)
+ * @OnDelete(action = CASCADE)
+ * Parent parent;
+ * 
+ *
    + *
  • If {@code @OnDelete(action = CASCADE)} is used in conjunction + * with {@code cascade=REMOVE}, then associated entities are fetched + * from the database, marked deleted in the persistence context, + * and evicted from the second-level cache. + *
  • If {@code @OnDelete(action = CASCADE)} is used on its own, + * without {@code cascade=REMOVE}, then associated + * entities are not fetched from the database, are not marked + * deleted in the persistence context, and are not automatically + * evicted from the second-level cache. + *
+ *

* Like database triggers, {@code on delete} actions can cause state * held in memory to lose synchronization with the database. * diff --git a/hibernate-core/src/main/java/org/hibernate/binder/internal/CommentBinder.java b/hibernate-core/src/main/java/org/hibernate/binder/internal/CommentBinder.java index bafe20609a14..148962b76215 100644 --- a/hibernate-core/src/main/java/org/hibernate/binder/internal/CommentBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/binder/internal/CommentBinder.java @@ -38,26 +38,30 @@ public void bind(Comment comment, MetadataBuildingContext context, PersistentCla } else if ( value instanceof Collection ) { Collection collection = (Collection) value; - Table table = collection.getTable(); + Table table = collection.getCollectionTable(); // by default, the comment goes on the table if ( on.isEmpty() || table.getName().equalsIgnoreCase( on ) ) { table.setComment( text ); } - // but if 'on' is explicit, it can go on a column - Value element = collection.getElement(); - for ( Column column : element.getColumns() ) { - if ( column.getName().equalsIgnoreCase( on ) ) { - column.setComment( text ); + else { + // but if 'on' is explicit, it can go on a column + for ( Column column : table.getColumns() ) { + if ( column.getName().equalsIgnoreCase( on ) ) { + column.setComment( text ); + return; + } } + throw new AnnotationException( "No matching column for '@Comment(on=\"" + on + "\")'" ); } - //TODO: list index / map key columns } else { for ( Column column : value.getColumns() ) { if ( on.isEmpty() || column.getName().equalsIgnoreCase( on ) ) { column.setComment( text ); + return; } } + throw new AnnotationException( "No matching column for '@Comment(on=\"" + on + "\")'" ); } } @@ -70,12 +74,16 @@ public void bind(Comment comment, MetadataBuildingContext context, PersistentCla if ( on.isEmpty() || primary.getName().equalsIgnoreCase( on ) ) { primary.setComment( text ); } - // but if 'on' is explicit, it can go on a secondary table - for ( Join join : entity.getJoins() ) { - Table secondary = join.getTable(); - if ( secondary.getName().equalsIgnoreCase( on ) ) { - secondary.setComment( text ); + else { + // but if 'on' is explicit, it can go on a secondary table + for ( Join join : entity.getJoins() ) { + Table secondary = join.getTable(); + if ( secondary.getName().equalsIgnoreCase( on ) ) { + secondary.setComment( text ); + return; + } } + throw new AnnotationException( "No matching column for '@Comment(on=\"" + on + "\")'" ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/boot/beanvalidation/BeanValidationEventListener.java b/hibernate-core/src/main/java/org/hibernate/boot/beanvalidation/BeanValidationEventListener.java index b579c91b16c5..574e560b0f1d 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/beanvalidation/BeanValidationEventListener.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/beanvalidation/BeanValidationEventListener.java @@ -14,6 +14,8 @@ import org.hibernate.boot.internal.ClassLoaderAccessImpl; import org.hibernate.boot.registry.classloading.spi.ClassLoaderService; import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.event.spi.PreCollectionUpdateEvent; +import org.hibernate.event.spi.PreCollectionUpdateEventListener; import org.hibernate.event.spi.PreDeleteEvent; import org.hibernate.event.spi.PreDeleteEventListener; import org.hibernate.event.spi.PreInsertEvent; @@ -36,6 +38,8 @@ import jakarta.validation.Validator; import jakarta.validation.ValidatorFactory; +import static org.hibernate.internal.util.NullnessUtil.castNonNull; + /** * Event listener used to enable Bean Validation for insert/update/delete events. * @@ -44,7 +48,7 @@ */ //FIXME review exception model public class BeanValidationEventListener - implements PreInsertEventListener, PreUpdateEventListener, PreDeleteEventListener, PreUpsertEventListener { + implements PreInsertEventListener, PreUpdateEventListener, PreDeleteEventListener, PreUpsertEventListener, PreCollectionUpdateEventListener { private static final CoreMessageLogger LOG = Logger.getMessageLogger( CoreMessageLogger.class, @@ -121,6 +125,17 @@ public boolean onPreUpsert(PreUpsertEvent event) { return false; } + @Override + public void onPreUpdateCollection(PreCollectionUpdateEvent event) { + final Object entity = castNonNull( event.getCollection().getOwner() ); + validate( + entity, + event.getSession().getEntityPersister( event.getAffectedOwnerEntityName(), entity ), + event.getFactory(), + GroupsPerOperation.Operation.UPDATE + ); + } + private void validate( T object, EntityPersister persister, diff --git a/hibernate-core/src/main/java/org/hibernate/boot/beanvalidation/HibernateTraversableResolver.java b/hibernate-core/src/main/java/org/hibernate/boot/beanvalidation/HibernateTraversableResolver.java index 1058a19823e5..c0afa9d0dc1d 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/beanvalidation/HibernateTraversableResolver.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/beanvalidation/HibernateTraversableResolver.java @@ -15,8 +15,11 @@ import org.hibernate.Hibernate; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.type.AnyType; import org.hibernate.type.CollectionType; +import org.hibernate.type.ComponentType; import org.hibernate.type.CompositeType; +import org.hibernate.type.EntityType; import org.hibernate.type.Type; import jakarta.validation.Path; @@ -54,17 +57,17 @@ private void addAssociationsToTheSetForAllProperties(String[] names, Type[] type private void addAssociationsToTheSetForOneProperty(String name, Type type, String prefix, SessionFactoryImplementor factory) { - if ( type.isCollectionType() ) { + if ( type instanceof CollectionType ) { CollectionType collType = (CollectionType) type; Type assocType = collType.getElementType( factory ); addAssociationsToTheSetForOneProperty(name, assocType, prefix, factory); } //ToOne association - else if ( type.isEntityType() || type.isAnyType() ) { + else if ( type instanceof EntityType || type instanceof AnyType ) { associations.add( prefix + name ); } - else if ( type.isComponentType() ) { - CompositeType componentType = (CompositeType) type; + else if ( type instanceof ComponentType ) { + ComponentType componentType = (ComponentType) type; addAssociationsToTheSetForAllProperties( componentType.getPropertyNames(), componentType.getSubtypes(), diff --git a/hibernate-core/src/main/java/org/hibernate/boot/beanvalidation/TypeSafeActivator.java b/hibernate-core/src/main/java/org/hibernate/boot/beanvalidation/TypeSafeActivator.java index b66b4d623ada..c8086f409e87 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/beanvalidation/TypeSafeActivator.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/beanvalidation/TypeSafeActivator.java @@ -126,6 +126,7 @@ public static void applyCallbackListeners(ValidatorFactory validatorFactory, Act listenerRegistry.appendListeners( EventType.PRE_UPDATE, listener ); listenerRegistry.appendListeners( EventType.PRE_DELETE, listener ); listenerRegistry.appendListeners( EventType.PRE_UPSERT, listener ); + listenerRegistry.appendListeners( EventType.PRE_COLLECTION_UPDATE, listener ); listener.initialize( cfgService.getSettings(), classLoaderService ); } diff --git a/hibernate-core/src/main/java/org/hibernate/boot/internal/NamedHqlQueryDefinitionImpl.java b/hibernate-core/src/main/java/org/hibernate/boot/internal/NamedHqlQueryDefinitionImpl.java index 284a97cf9699..5bdb30e81522 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/internal/NamedHqlQueryDefinitionImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/internal/NamedHqlQueryDefinitionImpl.java @@ -70,6 +70,7 @@ public String getHqlString() { public NamedSqmQueryMemento resolve(SessionFactoryImplementor factory) { return new NamedHqlQueryMementoImpl( getRegistrationName(), + null, hqlString, firstResult, maxResults, diff --git a/hibernate-core/src/main/java/org/hibernate/boot/internal/NamedNativeQueryDefinitionImpl.java b/hibernate-core/src/main/java/org/hibernate/boot/internal/NamedNativeQueryDefinitionImpl.java index 688d8dc5e16e..987b873730b4 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/internal/NamedNativeQueryDefinitionImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/internal/NamedNativeQueryDefinitionImpl.java @@ -18,6 +18,8 @@ import org.hibernate.query.sql.internal.NamedNativeQueryMementoImpl; import org.hibernate.query.sql.spi.NamedNativeQueryMemento; +import org.checkerframework.checker.nullness.qual.Nullable; + import static org.hibernate.internal.util.StringHelper.isNotEmpty; /** @@ -86,15 +88,16 @@ public String getResultSetMappingClassName() { @Override public NamedNativeQueryMemento resolve(SessionFactoryImplementor factory) { + Class resultClass = isNotEmpty( resultSetMappingClassName ) + ? factory.getServiceRegistry().requireService( ClassLoaderService.class ).classForName( resultSetMappingClassName ) + : null; return new NamedNativeQueryMementoImpl( getRegistrationName(), + resultClass, sqlString, sqlString, resultSetMappingName, - isNotEmpty( resultSetMappingClassName ) - ? factory.getServiceRegistry().requireService( ClassLoaderService.class ) - .classForName( resultSetMappingClassName ) - : null, + resultClass, querySpaces, getCacheable(), getCacheRegion(), diff --git a/hibernate-core/src/main/java/org/hibernate/boot/internal/SessionFactoryOptionsBuilder.java b/hibernate-core/src/main/java/org/hibernate/boot/internal/SessionFactoryOptionsBuilder.java index 85008f7ccd40..b65f8a5282ae 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/internal/SessionFactoryOptionsBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/internal/SessionFactoryOptionsBuilder.java @@ -7,6 +7,7 @@ package org.hibernate.boot.internal; import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; import java.time.ZoneId; import java.util.ArrayList; import java.util.Collections; @@ -70,6 +71,7 @@ import org.hibernate.resource.transaction.spi.TransactionCoordinatorBuilder; import org.hibernate.stat.Statistics; import org.hibernate.type.format.FormatMapper; +import org.hibernate.type.format.FormatMapperCreationContext; import org.hibernate.type.format.jackson.JacksonIntegration; import org.hibernate.type.format.jakartajson.JakartaJsonIntegration; import org.hibernate.type.format.jaxb.JaxbXmlFormatMapper; @@ -313,13 +315,23 @@ public SessionFactoryOptionsBuilder(StandardServiceRegistry serviceRegistry, Boo AvailableSettings.JPA_VALIDATION_FACTORY, configurationSettings.get( AvailableSettings.JAKARTA_VALIDATION_FACTORY ) ); - this.jsonFormatMapper = determineJsonFormatMapper( + + final var formatMapperCreationContext = new FormatMapperCreationContext() { + @Override + public BootstrapContext getBootstrapContext() { + return context; + } + }; + jsonFormatMapper = jsonFormatMapper( configurationSettings.get( AvailableSettings.JSON_FORMAT_MAPPER ), - strategySelector + strategySelector, + formatMapperCreationContext ); - this.xmlFormatMapper = determineXmlFormatMapper( + + xmlFormatMapper = xmlFormatMapper( configurationSettings.get( AvailableSettings.XML_FORMAT_MAPPER ), - strategySelector + strategySelector, + formatMapperCreationContext ); this.sessionFactoryName = (String) configurationSettings.get( SESSION_FACTORY_NAME ); @@ -827,7 +839,7 @@ private static Supplier interceptorSupplier(Class configurationSettings, + Map configurationSettings, StandardServiceRegistry serviceRegistry) { final PhysicalConnectionHandlingMode specifiedHandlingMode = PhysicalConnectionHandlingMode.interpret( configurationSettings.get( CONNECTION_HANDLING ) @@ -840,36 +852,62 @@ private PhysicalConnectionHandlingMode interpretConnectionHandlingMode( return serviceRegistry.requireService( TransactionCoordinatorBuilder.class ).getDefaultConnectionHandlingMode(); } - private static FormatMapper determineJsonFormatMapper(Object setting, StrategySelector strategySelector) { - return strategySelector.resolveDefaultableStrategy( - FormatMapper.class, + private static FormatMapper jsonFormatMapper(Object setting, StrategySelector selector, FormatMapperCreationContext creationContext) { + return formatMapper( setting, - (Callable) () -> { - final FormatMapper jsonJacksonFormatMapper = JacksonIntegration.getJsonJacksonFormatMapperOrNull(); - if (jsonJacksonFormatMapper != null) { - return jsonJacksonFormatMapper; - } - else { - return JakartaJsonIntegration.getJakartaJsonBFormatMapperOrNull(); - } - } + selector, + () -> { + final FormatMapper jsonJacksonFormatMapper = JacksonIntegration.getJsonJacksonFormatMapperOrNull( creationContext ); + return jsonJacksonFormatMapper != null + ? jsonJacksonFormatMapper + : JakartaJsonIntegration.getJakartaJsonBFormatMapperOrNull(); + }, + creationContext ); } - private static FormatMapper determineXmlFormatMapper(Object setting, StrategySelector strategySelector) { - return strategySelector.resolveDefaultableStrategy( - FormatMapper.class, + private static FormatMapper xmlFormatMapper(Object setting, StrategySelector selector, FormatMapperCreationContext creationContext) { + return formatMapper( setting, - (Callable) () -> { - final FormatMapper jacksonFormatMapper = JacksonIntegration.getXMLJacksonFormatMapperOrNull(); - if (jacksonFormatMapper != null) { - return jacksonFormatMapper; - } - return new JaxbXmlFormatMapper(); - } + selector, + () -> { + final FormatMapper jacksonFormatMapper = JacksonIntegration.getXMLJacksonFormatMapperOrNull( creationContext ); + return jacksonFormatMapper != null + ? jacksonFormatMapper + : new JaxbXmlFormatMapper(); + }, + creationContext ); } + private static FormatMapper formatMapper(Object setting, StrategySelector selector, Callable defaultResolver, FormatMapperCreationContext creationContext) { + return selector.resolveStrategy( FormatMapper.class, setting, defaultResolver, strategyClass -> { + try { + final Constructor creationContextConstructor = + strategyClass.getDeclaredConstructor( FormatMapperCreationContext.class ); + return creationContextConstructor.newInstance( creationContext ); + } + catch (NoSuchMethodException e) { + // Ignore + } + catch (InvocationTargetException | InstantiationException | IllegalAccessException e) { + throw new StrategySelectionException( + String.format( "Could not instantiate named strategy class [%s]", strategyClass.getName() ), + e + ); + } + try { + return strategyClass.getDeclaredConstructor().newInstance(); + } + catch (Exception e) { + throw new StrategySelectionException( + String.format( "Could not instantiate named strategy class [%s]", strategyClass.getName() ), + e + ); + } + } ); + } + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // SessionFactoryOptionsState diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/TypeContributions.java b/hibernate-core/src/main/java/org/hibernate/boot/model/TypeContributions.java index 4e12ec503c0d..21752b741bf8 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/TypeContributions.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/TypeContributions.java @@ -58,7 +58,7 @@ default void contributeJdbcTypeConstructor(JdbcTypeConstructor typeConstructor) * type for values of type {@link UserType#returnedClass()}. */ default void contributeType(UserType type) { - contributeType( type, type.returnedClass().getName() ); + contributeType( type, type.returnedClass().getTypeName() ); } /** diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/AggregateComponentSecondPass.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/AggregateComponentSecondPass.java index 8872cfb80a51..d57fa9b82273 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/AggregateComponentSecondPass.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/AggregateComponentSecondPass.java @@ -15,6 +15,7 @@ import org.hibernate.MappingException; import org.hibernate.annotations.Comment; import org.hibernate.annotations.common.reflection.XClass; +import org.hibernate.boot.model.naming.Identifier; import org.hibernate.boot.model.relational.Database; import org.hibernate.boot.model.relational.Namespace; import org.hibernate.boot.model.relational.QualifiedName; @@ -109,7 +110,8 @@ public void doSecondPass(Map persistentClasses) throws orderColumns( registeredUdt, originalOrder ); } else { - addAuxiliaryObjects = false; + addAuxiliaryObjects = + isAggregateArray() && namespace.locateUserDefinedArrayType( Identifier.toIdentifier( aggregateColumn.getSqlType() ) ) == null; validateEqual( registeredUdt, udt ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/AnnotatedColumns.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/AnnotatedColumns.java index 96d0e30b2eeb..954a83cc4a8b 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/AnnotatedColumns.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/AnnotatedColumns.java @@ -106,7 +106,7 @@ public boolean isSecondary() { final String explicitTableName = firstColumn.getExplicitTableName(); //note: checkPropertyConsistency() is responsible for ensuring they all have the same table name return isNotEmpty( explicitTableName ) - && !getPropertyHolder().getTable().getName().equals( explicitTableName ); + && !getOwnerTable().getName().equals( explicitTableName ); } /** @@ -125,10 +125,18 @@ public Table getTable() { // all the columns have to be mapped to the same table // even though at the annotation level it looks like // they could each specify a different table - return isSecondary() ? getJoin().getTable() : getPropertyHolder().getTable(); + return isSecondary() ? getJoin().getTable() : getOwnerTable(); } } + private Table getOwnerTable() { + PropertyHolder holder = getPropertyHolder(); + while ( holder instanceof ComponentPropertyHolder ) { + holder = ( (ComponentPropertyHolder) holder ).parent; + } + return holder.getTable(); + } + public void setTable(Table table) { this.table = table; } diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/AnnotationBinder.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/AnnotationBinder.java index c0d55e21caf4..bb246dfc32a5 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/AnnotationBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/AnnotationBinder.java @@ -42,7 +42,6 @@ import org.hibernate.boot.registry.classloading.spi.ClassLoaderService; import org.hibernate.boot.spi.MetadataBuildingContext; import org.hibernate.internal.CoreMessageLogger; -import org.hibernate.internal.util.StringHelper; import org.hibernate.resource.beans.internal.FallbackBeanInstanceProducer; import org.hibernate.resource.beans.spi.ManagedBeanRegistry; import org.hibernate.type.descriptor.java.BasicJavaType; @@ -69,7 +68,6 @@ import static org.hibernate.boot.model.internal.AnnotatedClassType.EMBEDDABLE; import static org.hibernate.boot.model.internal.AnnotatedClassType.ENTITY; -import static org.hibernate.boot.model.internal.FilterDefBinder.bindFilterDefs; import static org.hibernate.boot.model.internal.GeneratorBinder.buildGenerators; import static org.hibernate.boot.model.internal.GeneratorBinder.buildIdGenerator; import static org.hibernate.boot.model.internal.InheritanceState.getInheritanceStateOfSuperEntity; @@ -226,7 +224,7 @@ public static void bindPackage(ClassLoaderService cls, String packageName, Metad bindGenericGenerators( annotatedPackage, context ); bindQueries( annotatedPackage, context ); - bindFilterDefs( annotatedPackage, context ); + FilterDefBinder.bindFilterDefs( annotatedPackage, context ); } private static void handleIdGenerators(XPackage annotatedPackage, MetadataBuildingContext context) { @@ -371,6 +369,12 @@ private static void bindNamedStoredProcedureQuery( } } + public static void bindFilterDefs( + XClass annotatedClass, + MetadataBuildingContext context) throws MappingException { + FilterDefBinder.bindFilterDefs( annotatedClass, context ); + } + /** * Bind an annotated class. A subclass must be bound after its superclass. * @@ -388,7 +392,6 @@ public static void bindClass( bindQueries( annotatedClass, context ); handleImport( annotatedClass, context ); - bindFilterDefs( annotatedClass, context ); bindTypeDescriptorRegistrations( annotatedClass, context ); bindEmbeddableInstantiatorRegistrations( annotatedClass, context ); bindUserTypeRegistrations( annotatedClass, context ); diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/ClassPropertyHolder.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/ClassPropertyHolder.java index 995d43a6e763..720e9e76614b 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/ClassPropertyHolder.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/ClassPropertyHolder.java @@ -224,6 +224,19 @@ public void addProperty(Property prop, XClass declaringClass) { } } + @Override + public void movePropertyToJoin(Property property, Join join, XClass declaringClass) { + if ( property.getValue() instanceof Component ) { + //TODO handle quote and non quote table comparison + final String tableName = property.getValue().getTable().getName(); + if ( getJoinsPerRealTableName().get( tableName ) == join ) { + // Skip moving the property, since it was already added to the join + return; + } + } + persistentClass.movePropertyToJoin( property, join ); + } + @Override public Join addJoin(JoinTable joinTableAnn, boolean noDelayInPkColumnCreation) { final Join join = entityBinder.addJoin( joinTableAnn, this, noDelayInPkColumnCreation ); diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/CollectionBinder.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/CollectionBinder.java index 786e6dc7f875..5432f18e16d5 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/CollectionBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/CollectionBinder.java @@ -2340,8 +2340,7 @@ private AnnotatedClassType annotatedElementType( } else { //force in case of attribute override - final boolean attributeOverride = property.isAnnotationPresent( AttributeOverride.class ) - || property.isAnnotationPresent( AttributeOverrides.class ); + final boolean attributeOverride = mappingDefinedAttributeOverrideOnElement(property); // todo : force in the case of Convert annotation(s) with embedded paths (beyond key/value prefixes)? return isEmbedded || attributeOverride ? EMBEDDABLE @@ -2349,6 +2348,11 @@ private AnnotatedClassType annotatedElementType( } } + protected boolean mappingDefinedAttributeOverrideOnElement(XProperty property) { + return property.isAnnotationPresent( AttributeOverride.class ) + || property.isAnnotationPresent( AttributeOverrides.class ); + } + static AnnotatedColumns createElementColumnsIfNecessary( Collection collection, AnnotatedColumns elementColumns, diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/CollectionPropertyHolder.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/CollectionPropertyHolder.java index ca42acc34155..fba3931e83ae 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/CollectionPropertyHolder.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/CollectionPropertyHolder.java @@ -286,6 +286,11 @@ public void addProperty(Property prop, XClass declaringClass) { throw new AssertionFailure( "Cannot add property to a collection" ); } + @Override + public void movePropertyToJoin(Property prop, Join join, XClass declaringClass) { + throw new AssertionFailure( "Cannot add property to a collection" ); + } + @Override public KeyValue getIdentifier() { throw new AssertionFailure( "Identifier collection not yet managed" ); diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/ComponentPropertyHolder.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/ComponentPropertyHolder.java index a9ae72117918..b94ea059a71b 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/ComponentPropertyHolder.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/ComponentPropertyHolder.java @@ -266,26 +266,31 @@ public String getEntityName() { @Override public void addProperty(Property property, AnnotatedColumns columns, XClass declaringClass) { - //Ejb3Column.checkPropertyConsistency( ); //already called earlier + //AnnotatedColumns.checkPropertyConsistency( ); //already called earlier // Check table matches between the component and the columns // if not, change the component table if no properties are set // if a property is set already the core cannot support that - if ( columns != null ) { - final Table table = columns.getTable(); - if ( !table.equals( getTable() ) ) { - if ( component.getPropertySpan() == 0 ) { - component.setTable( table ); - } - else { - throw new AnnotationException( - "Embeddable class '" + component.getComponentClassName() - + "' has properties mapped to two different tables" - + " (all properties of the embeddable class must map to the same table)" - ); - } + assert columns == null || property.getValue().getTable() == columns.getTable(); + setTable( property.getValue().getTable() ); + addProperty( property, declaringClass ); + } + + private void setTable(Table table) { + if ( !table.equals( getTable() ) ) { + if ( component.getPropertySpan() == 0 ) { + component.setTable( table ); + } + else { + throw new AnnotationException( + "Embeddable class '" + component.getComponentClassName() + + "' has properties mapped to two different tables" + + " (all properties of the embeddable class must map to the same table)" + ); + } + if ( parent instanceof ComponentPropertyHolder ) { + ( (ComponentPropertyHolder) parent ).setTable( table ); } } - addProperty( property, declaringClass ); } @Override @@ -330,6 +335,16 @@ public void addProperty(Property prop, XClass declaringClass) { component.addProperty( prop, declaringClass ); } + @Override + public void movePropertyToJoin(Property prop, Join join, XClass declaringClass) { + // or maybe only throw if component.getTable() != join.getTable() + throw new AnnotationException( + "Embeddable class '" + component.getComponentClassName() + + "' has an unowned @OneToOne property " + prop.getName() + + "mapped to a join table which is unsupported" + ); + } + @Override public KeyValue getIdentifier() { return component.getOwner().getIdentifier(); diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/EmbeddableBinder.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/EmbeddableBinder.java index 92243de3333b..f06189c55fd0 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/EmbeddableBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/EmbeddableBinder.java @@ -11,11 +11,13 @@ import java.lang.reflect.Type; import java.util.ArrayList; import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Locale; import java.util.Map; -import java.util.TreeMap; import org.hibernate.AnnotationException; +import org.hibernate.MappingException; import org.hibernate.annotations.DiscriminatorFormula; import org.hibernate.annotations.Instantiator; import org.hibernate.annotations.TypeBinderType; @@ -28,7 +30,6 @@ import org.hibernate.boot.spi.MetadataBuildingContext; import org.hibernate.boot.spi.PropertyData; import org.hibernate.internal.CoreMessageLogger; -import org.hibernate.mapping.AggregateColumn; import org.hibernate.mapping.BasicValue; import org.hibernate.mapping.Component; import org.hibernate.mapping.Property; @@ -66,19 +67,15 @@ import static org.hibernate.boot.model.internal.BinderHelper.getPropertyOverriddenByMapperOrMapsId; import static org.hibernate.boot.model.internal.BinderHelper.getRelativePath; import static org.hibernate.boot.model.internal.BinderHelper.hasToOneAnnotation; -import static org.hibernate.boot.model.internal.BinderHelper.isGlobalGeneratorNameGlobal; -import static org.hibernate.boot.model.internal.GeneratorBinder.buildGenerators; -import static org.hibernate.boot.model.internal.GeneratorBinder.generatorType; -import static org.hibernate.boot.model.internal.GeneratorBinder.makeIdGenerator; import static org.hibernate.boot.model.internal.HCANNHelper.findContainingAnnotations; import static org.hibernate.boot.model.internal.PropertyBinder.addElementsOfClass; import static org.hibernate.boot.model.internal.PropertyBinder.processElementAnnotations; +import static org.hibernate.boot.model.internal.PropertyBinder.processId; import static org.hibernate.boot.model.internal.PropertyHolderBuilder.buildPropertyHolder; import static org.hibernate.internal.CoreLogging.messageLogger; import static org.hibernate.internal.util.StringHelper.isEmpty; import static org.hibernate.internal.util.StringHelper.qualify; import static org.hibernate.internal.util.StringHelper.unqualify; -import static org.hibernate.mapping.SimpleValue.DEFAULT_ID_GEN_STRATEGY; /** * A binder responsible for interpreting {@link Embeddable} classes and producing @@ -249,15 +246,15 @@ private static PropertyBinder createEmbeddedProperty( final PropertyBinder binder = new PropertyBinder(); binder.setDeclaringClass( inferredData.getDeclaringClass() ); binder.setName( inferredData.getPropertyName() ); - binder.setValue(component); + binder.setValue( component ); binder.setProperty( inferredData.getProperty() ); binder.setAccessType( inferredData.getDefaultAccess() ); - binder.setEmbedded(isComponentEmbedded); - binder.setHolder(propertyHolder); - binder.setId(isId); - binder.setEntityBinder(entityBinder); - binder.setInheritanceStatePerClass(inheritanceStatePerClass); - binder.setBuildingContext(context); + binder.setEmbedded( isComponentEmbedded ); + binder.setHolder( propertyHolder ); + binder.setId( isId ); + binder.setEntityBinder( entityBinder ); + binder.setInheritanceStatePerClass( inheritanceStatePerClass ); + binder.setBuildingContext( context ); binder.makePropertyAndBind(); return binder; } @@ -413,7 +410,7 @@ static Component fillEmbeddable( final BasicType discriminatorType = (BasicType) component.getDiscriminator().getType(); // Discriminator values are used to construct the embeddable domain // type hierarchy so order of processing is important - final Map discriminatorValues = new TreeMap<>(); + final Map discriminatorValues = new LinkedHashMap<>(); collectDiscriminatorValue( returnedClassOrElement, discriminatorType, discriminatorValues ); collectSubclassElements( propertyAccessor, @@ -452,18 +449,27 @@ static Component fillEmbeddable( inheritanceStatePerClass ); - final XProperty property = propertyAnnotatedElement.getProperty(); - if ( property.isAnnotationPresent( GeneratedValue.class ) ) { - if ( isIdClass || subholder.isOrWithinEmbeddedId() ) { - processGeneratedId( context, component, property ); - } - else { - throw new AnnotationException( - "Property '" + property.getName() + "' of '" - + getPath( propertyHolder, inferredData ) - + "' is annotated '@GeneratedValue' but is not part of an identifier" ); + final XProperty member = propertyAnnotatedElement.getProperty(); + if ( isIdClass || subholder.isOrWithinEmbeddedId() ) { + final Property property = findProperty( component, member.getName() ); + if ( property != null ) { + // Identifier properties are always simple values + final SimpleValue value = (SimpleValue) property.getValue(); + processId( + subholder, + propertyAnnotatedElement, + value, + Map.of(), + context + ); } } + else if ( member.isAnnotationPresent( GeneratedValue.class ) ) { + throw new AnnotationException( + "Property '" + member.getName() + "' of '" + + getPath( propertyHolder, inferredData ) + + "' is annotated '@GeneratedValue' but is not part of an identifier" ); + } } if ( compositeUserType != null ) { @@ -473,6 +479,15 @@ static Component fillEmbeddable( return component; } + private static Property findProperty(Component component, String name) { + for ( Property property : component.getProperties() ) { + if ( property.getName().equals( name ) ) { + return property; + } + } + return null; + } + private static CompositeUserType compositeUserType( Class> compositeUserTypeClass, MetadataBuildingContext context) { @@ -616,7 +631,7 @@ private static List collectClassElements( //embeddable elements can have type defs final PropertyContainer container = new PropertyContainer( returnedClassOrElement, annotatedClass, propertyAccessor ); - addElementsOfClass( classElements, container, context); + addElementsOfClass( classElements, container, context, 0 ); //add elements of the embeddable's mapped superclasses XClass subclass = returnedClassOrElement; XClass superClass; @@ -624,7 +639,7 @@ private static List collectClassElements( //FIXME: proper support of type variables incl var resolved at upper levels final PropertyContainer superContainer = new PropertyContainer( superClass, annotatedClass, propertyAccessor ); - addElementsOfClass( classElements, superContainer, context ); + addElementsOfClass( classElements, superContainer, context, 0 ); if ( subclassToSuperclass != null ) { subclassToSuperclass.put( subclass.getName(), superClass.getName() ); } @@ -655,7 +670,7 @@ private static void collectSubclassElements( assert put == null; // collect property of subclass final PropertyContainer superContainer = new PropertyContainer( subclass, superclass, propertyAccessor ); - addElementsOfClass( classElements, superContainer, context ); + addElementsOfClass( classElements, superContainer, context, 0 ); // recursively do that same for all subclasses collectSubclassElements( propertyAccessor, @@ -726,7 +741,7 @@ private static List collectBaseClassElements( while ( !Object.class.getName().equals( baseReturnedClassOrElement.getName() ) ) { final PropertyContainer container = new PropertyContainer( baseReturnedClassOrElement, annotatedClass, propertyAccessor ); - addElementsOfClass( baseClassElements, container, context ); + addElementsOfClass( baseClassElements, container, context, 0 ); baseReturnedClassOrElement = baseReturnedClassOrElement.getSuperclass(); } return baseClassElements; @@ -782,40 +797,6 @@ private static boolean hasTriggeringAnnotation(XAnnotatedElement property) { || property.isAnnotationPresent(ManyToMany.class); } - private static void processGeneratedId(MetadataBuildingContext context, Component component, XProperty property) { - final GeneratedValue generatedValue = property.getAnnotation( GeneratedValue.class ); - final String generatorType = generatedValue != null - ? generatorType( generatedValue, property.getType(), context ) - : DEFAULT_ID_GEN_STRATEGY; - final String generator = generatedValue != null ? generatedValue.generator() : ""; - - if ( isGlobalGeneratorNameGlobal( context ) ) { - buildGenerators( property, context ); - context.getMetadataCollector().addSecondPass( new IdGeneratorResolverSecondPass( - (SimpleValue) component.getProperty( property.getName() ).getValue(), - property, - generatorType, - generator, - context - ) ); - -// handleTypeDescriptorRegistrations( property, context ); -// bindEmbeddableInstantiatorRegistrations( property, context ); -// bindCompositeUserTypeRegistrations( property, context ); -// handleConverterRegistrations( property, context ); - } - else { - makeIdGenerator( - (SimpleValue) component.getProperty( property.getName() ).getValue(), - property, - generatorType, - generator, - context, - new HashMap<>( buildGenerators( property, context ) ) - ); - } - } - private static void processIdClassElements( PropertyHolder propertyHolder, PropertyData baseInferredData, @@ -871,6 +852,7 @@ static Component createEmbeddable( else { embeddableClass = inferredData.getClassOrPluralElement(); component.setComponentClassName( embeddableClass.getName() ); + checkEmbeddableRecursiveHierarchy( embeddableClass, inferredData, propertyHolder ); } component.setCustomInstantiator( customInstantiatorImpl ); final Constructor constructor = resolveInstantiator( embeddableClass, context ); @@ -884,6 +866,29 @@ static Component createEmbeddable( return component; } + private static void checkEmbeddableRecursiveHierarchy( + XClass embeddableClass, + PropertyData propertyData, + PropertyHolder propertyHolder) { + while ( propertyHolder.isComponent() ) { + final ComponentPropertyHolder componentHolder = (ComponentPropertyHolder) propertyHolder; + // we need to check that the embeddable is not used in a recursive hierarchy + XClass classDetails = embeddableClass; + while ( classDetails != null ) { + if ( propertyHolder.getClassName().equals( classDetails.getName() ) ) { + throw new MappingException( String.format( + Locale.ROOT, + "Recursive embeddable mapping detected for property '%s' for type [%s]", + getPath( propertyHolder, propertyData ), + propertyHolder.getClassName() + ) ); + } + classDetails = classDetails.getSuperclass(); + } + propertyHolder = componentHolder.parent; + } + } + private static Constructor resolveInstantiator(XClass embeddableClass, MetadataBuildingContext buildingContext) { if ( embeddableClass != null ) { final Constructor[] declaredConstructors = buildingContext.getBootstrapContext().getReflectionManager() diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/EntityBinder.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/EntityBinder.java index edc1d878b08d..751c8b90f9d7 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/EntityBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/EntityBinder.java @@ -105,6 +105,7 @@ import org.hibernate.mapping.SimpleValue; import org.hibernate.mapping.SingleTableSubclass; import org.hibernate.mapping.Subclass; +import org.hibernate.mapping.SyntheticProperty; import org.hibernate.mapping.Table; import org.hibernate.mapping.TableOwner; import org.hibernate.mapping.UnionSubclass; @@ -550,7 +551,7 @@ private Component createMapperProperty( propertyAccessor, isIdClass ); - final Property mapperProperty = new Property(); + final Property mapperProperty = new SyntheticProperty(); mapperProperty.setName( NavigablePath.IDENTIFIER_MAPPER_PROPERTY ); mapperProperty.setUpdateable( false ); mapperProperty.setInsertable( false ); @@ -616,7 +617,8 @@ private static PropertyData getUniqueIdPropertyFromBaseClass( inferredData.getPropertyClass(), propertyAccessor ); - addElementsOfClass( baseClassElements, propContainer, context ); + final int idPropertyCount = addElementsOfClass( baseClassElements, propContainer, context, 0 ); + assert idPropertyCount == 1; //Id properties are on top and there is only one return baseClassElements.get( 0 ); } diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/GeneratorBinder.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/GeneratorBinder.java index 8f6707d8312c..d60ba173d99f 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/GeneratorBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/GeneratorBinder.java @@ -59,6 +59,7 @@ import static org.hibernate.boot.model.internal.BinderHelper.isCompositeId; import static org.hibernate.boot.model.internal.BinderHelper.isGlobalGeneratorNameGlobal; import static org.hibernate.id.factory.internal.IdentifierGeneratorUtil.collectParameters; +import static org.hibernate.internal.util.NullnessUtil.castNonNull; import static org.hibernate.mapping.SimpleValue.DEFAULT_ID_GEN_STRATEGY; public class GeneratorBinder { @@ -113,16 +114,10 @@ public static void makeIdGenerator( || identifierGeneratorStrategy.equals( "seqhilo" ); if ( generatorType == null || !avoidOverriding ) { id.setIdentifierGeneratorStrategy( identifierGeneratorStrategy ); - if ( DEFAULT_ID_GEN_STRATEGY.equals( identifierGeneratorStrategy ) ) { - id.setNullValue( "undefined" ); - } } //checkIfMatchingGenerator(definition, generatorType, generatorName); parameters.putAll( definition.getParameters() ); } - if ( DEFAULT_ID_GEN_STRATEGY.equals( generatorType ) ) { - id.setNullValue( "undefined" ); - } id.setIdentifierGeneratorParameters( parameters ); } @@ -534,14 +529,20 @@ private static void checkVersionGenerationAlways(XProperty property, Generator g private static void callConfigure(GeneratorCreationContext creationContext, Generator generator) { if ( generator instanceof Configurable ) { + final Configurable configurable = (Configurable) generator; final Value value = creationContext.getProperty().getValue(); - ( (Configurable) generator ).configure( value.getType(), collectParameters( - (SimpleValue) value, - creationContext.getDatabase().getDialect(), - creationContext.getDefaultCatalog(), - creationContext.getDefaultSchema(), - creationContext.getPersistentClass().getRootClass() - ), creationContext.getServiceRegistry() ); + configurable.create( creationContext ); + configurable.configure( + value.getType(), + collectParameters( + (SimpleValue) value, + creationContext.getDatabase().getDialect(), + creationContext.getDefaultCatalog(), + creationContext.getDefaultSchema(), + creationContext.getPersistentClass().getRootClass() + ), + creationContext.getServiceRegistry() + ); } } @@ -564,9 +565,9 @@ static void createIdGenerator( XProperty idProperty) { //manage composite related metadata //guess if its a component and find id data access (property, field etc) - final GeneratedValue generatedValue = idProperty.getAnnotation( GeneratedValue.class ); + final GeneratedValue generatedValue = castNonNull( idProperty.getAnnotation( GeneratedValue.class ) ); final String generatorType = generatorType( context, entityClass, isCompositeId( entityClass, idProperty ), generatedValue ); - final String generatorName = generatedValue == null ? "" : generatedValue.generator(); + final String generatorName = generatedValue.generator(); if ( isGlobalGeneratorNameGlobal( context ) ) { buildGenerators( idProperty, context ); context.getMetadataCollector().addSecondPass( new IdGeneratorResolverSecondPass( diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/InheritanceState.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/InheritanceState.java index 16156c606db8..7d7a9ba9d561 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/InheritanceState.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/InheritanceState.java @@ -234,12 +234,11 @@ private ElementsToProcess getElementsToProcess() { clazz, accessType ); - int currentIdPropertyCount = addElementsOfClass( + idPropertyCount = addElementsOfClass( elements, propertyContainer, - buildingContext - ); - idPropertyCount += currentIdPropertyCount; + buildingContext, + idPropertyCount ); } if ( idPropertyCount == 0 && !inheritanceState.hasParents() ) { diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/MapBinder.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/MapBinder.java index a5b5665fdf2f..745a444b7950 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/MapBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/MapBinder.java @@ -107,8 +107,27 @@ public void secondPass(java.util.Map persistentClasses) }; } - private void makeOneToManyMapKeyColumnNullableIfNotInProperty( - final XProperty property) { + @Override + protected boolean mappingDefinedAttributeOverrideOnElement(XProperty property) { + if ( property.isAnnotationPresent( AttributeOverride.class ) ) { + return namedMapValue( property.getAnnotation( AttributeOverride.class ) ); + } + if ( property.isAnnotationPresent( AttributeOverrides.class ) ) { + final AttributeOverrides annotations = property.getAnnotation( AttributeOverrides.class ); + for ( AttributeOverride attributeOverride : annotations.value() ) { + if ( namedMapValue( attributeOverride ) ) { + return true; + } + } + } + return false; + } + + private boolean namedMapValue(AttributeOverride annotation) { + return annotation.name().startsWith( "value." ); + } + + private void makeOneToManyMapKeyColumnNullableIfNotInProperty(XProperty property) { final Map map = (Map) this.collection; if ( map.isOneToMany() && property.isAnnotationPresent( MapKeyColumn.class ) ) { diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/OneToOneSecondPass.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/OneToOneSecondPass.java index fc04221faaa1..4baa1dc1f25f 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/OneToOneSecondPass.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/OneToOneSecondPass.java @@ -27,6 +27,7 @@ import org.hibernate.mapping.PersistentClass; import org.hibernate.mapping.Property; import org.hibernate.mapping.SortableValue; +import org.hibernate.mapping.Value; import org.hibernate.type.ForeignKeyDirection; import jakarta.persistence.ForeignKey; @@ -34,11 +35,7 @@ import static org.hibernate.boot.model.internal.BinderHelper.checkMappedByType; import static org.hibernate.boot.model.internal.BinderHelper.findPropertyByName; import static org.hibernate.boot.model.internal.BinderHelper.getPath; -import static org.hibernate.boot.model.internal.ToOneBinder.bindForeignKeyNameAndDefinition; -import static org.hibernate.boot.model.internal.ToOneBinder.defineFetchingStrategy; import static org.hibernate.internal.util.StringHelper.qualify; -import static org.hibernate.type.ForeignKeyDirection.FROM_PARENT; -import static org.hibernate.type.ForeignKeyDirection.TO_PARENT; /** * We have to handle {@link jakarta.persistence.OneToOne} associations @@ -46,96 +43,55 @@ */ public class OneToOneSecondPass implements SecondPass { private final MetadataBuildingContext buildingContext; + private final OneToOne oneToOne; + private final PropertyBinder binder; + private final Property property; private final String mappedBy; private final String ownerEntity; private final PropertyHolder propertyHolder; - private final NotFoundAction notFoundAction; private final PropertyData inferredData; - private final OnDeleteAction onDeleteAction; - private final boolean optional; - private final String cascadeStrategy; + private final NotFoundAction notFoundAction; private final AnnotatedJoinColumns joinColumns; - private final String referencedEntityName; private final boolean annotatedEntity; public OneToOneSecondPass( + PropertyBinder binder, + Property property, + OneToOne oneToOne, String mappedBy, String ownerEntity, PropertyHolder propertyHolder, PropertyData inferredData, - String referencedEntityName, boolean annotatedEntity, NotFoundAction notFoundAction, - OnDeleteAction onDeleteAction, - boolean optional, - String cascadeStrategy, AnnotatedJoinColumns columns, MetadataBuildingContext buildingContext) { + this.binder = binder; + this.property = property; + this.oneToOne = oneToOne; this.ownerEntity = ownerEntity; this.mappedBy = mappedBy; this.propertyHolder = propertyHolder; - this.referencedEntityName = referencedEntityName; this.buildingContext = buildingContext; this.notFoundAction = notFoundAction; this.inferredData = inferredData; this.annotatedEntity = annotatedEntity; - this.onDeleteAction = onDeleteAction; - this.optional = optional; - this.cascadeStrategy = cascadeStrategy; this.joinColumns = columns; } @Override public void doSecondPass(Map persistentClasses) throws MappingException { - final OneToOne value = new OneToOne( - buildingContext, - propertyHolder.getTable(), - propertyHolder.getPersistentClass() - ); - final String propertyName = inferredData.getPropertyName(); - value.setPropertyName( propertyName ); - value.setReferencedEntityName( referencedEntityName ); - XProperty property = inferredData.getProperty(); - defineFetchingStrategy( value, property, inferredData, propertyHolder ); - //value.setFetchMode( fetchMode ); - value.setOnDeleteAction( onDeleteAction ); - //value.setLazy( fetchMode != FetchMode.JOIN ); - - value.setConstrained( !optional ); - value.setForeignKeyType( getForeignKeyDirection() ); - bindForeignKeyNameAndDefinition( value, property, property.getAnnotation( ForeignKey.class ), buildingContext ); - - final PropertyBinder binder = new PropertyBinder(); - binder.setName( propertyName ); - binder.setProperty( property ); - binder.setValue( value ); - binder.setCascade( cascadeStrategy ); - binder.setAccessType( inferredData.getDefaultAccess() ); - binder.setBuildingContext( buildingContext ); - binder.setHolder( propertyHolder ); - - final LazyGroup lazyGroupAnnotation = property.getAnnotation( LazyGroup.class ); - if ( lazyGroupAnnotation != null ) { - binder.setLazyGroup( lazyGroupAnnotation.value() ); - } - - final Property result = binder.makeProperty(); - result.setOptional( optional ); if ( mappedBy == null ) { - bindOwned( persistentClasses, value, propertyName, result ); + bindOwned( persistentClasses, oneToOne, inferredData.getPropertyName() ); } else { - bindUnowned( persistentClasses, value, result ); + bindUnowned( persistentClasses, oneToOne ); } - binder.callAttributeBindersInSecondPass( result ); - value.sortProperties(); - } - - private ForeignKeyDirection getForeignKeyDirection() { - return mappedBy == null ? FROM_PARENT : TO_PARENT; + binder.callAttributeBindersInSecondPass( property ); + oneToOne.sortProperties(); } - private void bindUnowned(Map persistentClasses, OneToOne oneToOne, Property property) { + private void bindUnowned(Map persistentClasses, OneToOne oneToOne) { oneToOne.setMappedByProperty( mappedBy ); final String targetEntityName = oneToOne.getReferencedEntityName(); final PersistentClass targetEntity = persistentClasses.get( targetEntityName ); @@ -147,13 +103,11 @@ private void bindUnowned(Map persistentClasses, OneToOn + "' targets the type '" + targetEntityName + "'" + problem ); } final Property targetProperty = targetProperty( oneToOne, targetEntity ); - if ( targetProperty.getValue() instanceof OneToOne ) { - propertyHolder.addProperty( property, inferredData.getDeclaringClass() ); - } - else if ( targetProperty.getValue() instanceof ManyToOne ) { - bindTargetManyToOne( persistentClasses, oneToOne, property, targetEntity, targetProperty ); + final Value targetPropertyValue = targetProperty.getValue(); + if ( targetPropertyValue instanceof ManyToOne ) { + bindTargetManyToOne( persistentClasses, oneToOne, targetEntity, targetProperty ); } - else { + else if ( !(targetPropertyValue instanceof OneToOne) ) { throw new AnnotationException( "Association '" + getPath( propertyHolder, inferredData ) + "' is 'mappedBy' a property named '" + mappedBy + "' of the target entity type '" + targetEntityName @@ -171,7 +125,6 @@ else if ( targetProperty.getValue() instanceof ManyToOne ) { private void bindTargetManyToOne( Map persistentClasses, OneToOne oneToOne, - Property property, PersistentClass targetEntity, Property targetProperty) { Join otherSideJoin = null; @@ -201,10 +154,9 @@ private void bindTargetManyToOne( copy.setValue( manyToOne ); manyToOne.addColumn( copy ); } - mappedByJoin.addProperty( property ); - } - else { - propertyHolder.addProperty( property, inferredData.getDeclaringClass() ); + // The property was added to the propertyHolder eagerly to have knowledge about this property, + // in order for de-duplication to kick in, but we move it to a join if necessary + propertyHolder.movePropertyToJoin( property, mappedByJoin, inferredData.getDeclaringClass() ); } oneToOne.setReferencedPropertyName( mappedBy ); @@ -243,8 +195,7 @@ private Property targetProperty(OneToOne oneToOne, PersistentClass targetEntity) private void bindOwned( Map persistentClasses, OneToOne oneToOne, - String propertyName, - Property property) { + String propertyName) { final ToOneFkSecondPass secondPass = new ToOneFkSecondPass( oneToOne, joinColumns, @@ -256,7 +207,6 @@ private void bindOwned( ); secondPass.doSecondPass(persistentClasses); //no column associated since it's a one to one - propertyHolder.addProperty( property, inferredData.getDeclaringClass() ); } /** diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/PropertyBinder.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/PropertyBinder.java index 5c98e401d0ba..611543cabd54 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/PropertyBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/PropertyBinder.java @@ -15,6 +15,7 @@ import jakarta.persistence.Column; import jakarta.persistence.ElementCollection; import jakarta.persistence.EmbeddedId; +import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.JoinColumns; @@ -53,6 +54,8 @@ import org.hibernate.boot.spi.MetadataBuildingContext; import org.hibernate.boot.spi.PropertyData; import org.hibernate.engine.OptimisticLockStyle; +import org.hibernate.id.IdentifierGenerator; +import org.hibernate.id.PersistentIdentifierGenerator; import org.hibernate.internal.CoreMessageLogger; import org.hibernate.mapping.Collection; import org.hibernate.mapping.Component; @@ -552,17 +555,17 @@ private void validateOptimisticLock(OptimisticLock optimisticLock) { /** * @param elements List of {@link PropertyData} instances * @param propertyContainer Metadata about a class and its properties + * @param idPropertyCounter number of id properties already present in list of {@link PropertyData} instances * - * @return the number of id properties found while iterating the elements of - * {@code annotatedClass} using the determined access strategy + * @return total number of id properties found after iterating the elements of {@code annotatedClass} + * using the determined access strategy (starting from the provided {@code idPropertyCounter}) */ static int addElementsOfClass( List elements, PropertyContainer propertyContainer, - MetadataBuildingContext context) { - int idPropertyCounter = 0; + MetadataBuildingContext context, int idPropertyCounter) { for ( XProperty property : propertyContainer.propertyIterator() ) { - idPropertyCounter += addProperty( propertyContainer, property, elements, context ); + idPropertyCounter = addProperty( propertyContainer, property, elements, context, idPropertyCounter ); } return idPropertyCounter; } @@ -571,20 +574,20 @@ private static int addProperty( PropertyContainer propertyContainer, XProperty property, List inFlightPropertyDataList, - MetadataBuildingContext context) { + MetadataBuildingContext context, + int idPropertyCounter) { // see if inFlightPropertyDataList already contains a PropertyData for this name, // and if so, skip it.. for ( PropertyData propertyData : inFlightPropertyDataList ) { if ( propertyData.getPropertyName().equals( property.getName() ) ) { checkIdProperty( property, propertyData ); // EARLY EXIT!!! - return 0; + return idPropertyCounter; } } final XClass declaringClass = propertyContainer.getDeclaringClass(); final XClass entity = propertyContainer.getEntityAtStake(); - int idPropertyCounter = 0; final PropertyData propertyAnnotatedElement = new PropertyInferredData( declaringClass, property, @@ -1156,13 +1159,16 @@ && isEmbedded( property, property.getElementClass() ) ) { ); } else if ( propertyBinder.isId() ) { + if ( isIdentifierMapper ) { + throw new AnnotationException( "Property '"+ getPath( propertyHolder, inferredData ) + + "' belongs to an '@IdClass' and may not be annotated '@Id' or '@EmbeddedId'" ); + } //components and regular basic types create SimpleValue objects processId( propertyHolder, inferredData, (SimpleValue) propertyBinder.getValue(), classGenerators, - isIdentifierMapper, context ); } @@ -1415,17 +1421,12 @@ private static Class> resolveCompositeUserType( return null; } - private static void processId( + static void processId( PropertyHolder propertyHolder, PropertyData inferredData, SimpleValue idValue, Map classGenerators, - boolean isIdentifierMapper, MetadataBuildingContext context) { - if ( isIdentifierMapper ) { - throw new AnnotationException( "Property '"+ getPath( propertyHolder, inferredData ) - + "' belongs to an '@IdClass' and may not be annotated '@Id' or '@EmbeddedId'" ); - } final XProperty idProperty = inferredData.getProperty(); final List idGeneratorAnnotations = findContainingAnnotations( idProperty, IdGeneratorType.class ); final List generatorAnnotations = findContainingAnnotations( idProperty, ValueGenerationType.class ); @@ -1449,7 +1450,7 @@ else if ( !generatorAnnotations.isEmpty() ) { + "' is annotated '" + generatorAnnotations.get(0).annotationType() + "' which is not an '@IdGeneratorType'" ); } - else { + else if ( idProperty.isAnnotationPresent( GeneratedValue.class ) ) { final XClass entityClass = inferredData.getClassOrElement(); createIdGenerator( idValue, classGenerators, context, entityClass, idProperty ); if ( LOG.isTraceEnabled() ) { diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/PropertyContainer.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/PropertyContainer.java index 3248eee16f21..5b20e1c5e47e 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/PropertyContainer.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/PropertyContainer.java @@ -97,8 +97,8 @@ public PropertyContainer(XClass clazz, XClass entityAtStake, AccessType defaultC final Map localAttributeMap; // If the record class has only record components which match up with fields and no additional getters, - // we can retain the property order, to match up with the record component order - if ( !recordComponents.isEmpty() && recordComponents.size() == fields.size() && getters.isEmpty() ) { + // we must retain the property order, to match up with the record component order + if ( !recordComponents.isEmpty() && recordComponents.size() == fields.size() ) { localAttributeMap = new LinkedHashMap<>(); } //otherwise we sort them in alphabetical order, since this is at least deterministic diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/PropertyHolder.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/PropertyHolder.java index ec486bef2087..6275e0cb36b6 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/PropertyHolder.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/PropertyHolder.java @@ -37,6 +37,8 @@ public interface PropertyHolder { void addProperty(Property prop, AnnotatedColumns columns, XClass declaringClass); + void movePropertyToJoin(Property prop, Join join, XClass declaringClass); + KeyValue getIdentifier(); /** diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/ToOneBinder.java b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/ToOneBinder.java index 5af830477815..19a3d20a7743 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/internal/ToOneBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/internal/ToOneBinder.java @@ -20,6 +20,7 @@ import org.hibernate.annotations.FetchProfileOverrides; import org.hibernate.annotations.LazyToOne; import org.hibernate.annotations.LazyToOneOption; +import org.hibernate.annotations.LazyGroup; import org.hibernate.annotations.NotFound; import org.hibernate.annotations.NotFoundAction; import org.hibernate.annotations.OnDelete; @@ -33,6 +34,7 @@ import org.hibernate.internal.CoreMessageLogger; import org.hibernate.mapping.Join; import org.hibernate.mapping.KeyValue; +import org.hibernate.mapping.Property; import org.hibernate.mapping.SimpleValue; import org.hibernate.mapping.ToOne; @@ -61,6 +63,8 @@ import static org.hibernate.internal.util.StringHelper.isNotEmpty; import static org.hibernate.internal.util.StringHelper.nullIfEmpty; import static org.hibernate.internal.util.StringHelper.qualify; +import static org.hibernate.type.ForeignKeyDirection.FROM_PARENT; +import static org.hibernate.type.ForeignKeyDirection.TO_PARENT; /** * Responsible for interpreting {@link ManyToOne} and {@link OneToOne} associations @@ -527,30 +531,21 @@ private static void bindOneToOne( //column.getTable() => persistentClass.getTable() final String propertyName = inferredData.getPropertyName(); LOG.tracev( "Fetching {0} with {1}", propertyName, fetchMode ); - if ( isMapToPK( joinColumns, propertyHolder, trueOneToOne ) - || mappedBy != null ) { - //is a true one-to-one - //FIXME referencedColumnName ignored => ordering may fail. - final OneToOneSecondPass secondPass = new OneToOneSecondPass( - mappedBy, - propertyHolder.getEntityName(), - propertyHolder, - inferredData, - getReferenceEntityName( inferredData, targetEntity, context ), - isTargetAnnotatedEntity( targetEntity, annotatedProperty, context ), - notFoundAction, - cascadeOnDelete, - optional, + if ( mappedBy != null || isMapToPK( joinColumns, propertyHolder, trueOneToOne ) ) { + bindTrueOneToOne( cascadeStrategy, joinColumns, + optional, + notFoundAction, + cascadeOnDelete, + targetEntity, + annotatedProperty, + propertyHolder, + inferredData, + mappedBy, + inSecondPass, context ); - if ( inSecondPass ) { - secondPass.doSecondPass( context.getMetadataCollector().getEntityBindingMap() ); - } - else { - context.getMetadataCollector().addSecondPass( secondPass, mappedBy == null ); - } } else { //has a FK on the table @@ -572,6 +567,77 @@ private static void bindOneToOne( } } + private static void bindTrueOneToOne( + String cascadeStrategy, + AnnotatedJoinColumns joinColumns, + boolean optional, + NotFoundAction notFoundAction, + OnDeleteAction cascadeOnDelete, + XClass targetEntity, + XProperty annotatedProperty, + PropertyHolder propertyHolder, + PropertyData inferredData, + String mappedBy, + boolean inSecondPass, + MetadataBuildingContext context) { + //is a true one-to-one + //FIXME referencedColumnName ignored => ordering may fail. + final org.hibernate.mapping.OneToOne oneToOne = + new org.hibernate.mapping.OneToOne( context, propertyHolder.getTable(), + propertyHolder.getPersistentClass() ); + final String propertyName = inferredData.getPropertyName(); + oneToOne.setPropertyName( propertyName ); + oneToOne.setReferencedEntityName( getReferenceEntityName( inferredData, targetEntity, context ) ); + defineFetchingStrategy( oneToOne, annotatedProperty, inferredData, propertyHolder ); + //value.setFetchMode( fetchMode ); + oneToOne.setOnDeleteAction( cascadeOnDelete ); + //value.setLazy( fetchMode != FetchMode.JOIN ); + + oneToOne.setConstrained( !optional ); + oneToOne.setForeignKeyType( mappedBy == null ? FROM_PARENT : TO_PARENT ); + bindForeignKeyNameAndDefinition( oneToOne, annotatedProperty, + annotatedProperty.getAnnotation( ForeignKey.class ), + context ); + + final PropertyBinder binder = new PropertyBinder(); + binder.setName( inferredData.getPropertyName() ); + binder.setProperty( annotatedProperty ); + binder.setValue( oneToOne ); + binder.setCascade( cascadeStrategy ); + binder.setAccessType( inferredData.getDefaultAccess() ); + binder.setBuildingContext( context ); + binder.setHolder( propertyHolder ); + + final LazyGroup lazyGroupAnnotation = annotatedProperty.getAnnotation( LazyGroup.class ); + if ( lazyGroupAnnotation != null ) { + binder.setLazyGroup( lazyGroupAnnotation.value() ); + } + + final Property property = binder.makeProperty(); + property.setOptional( optional ); + propertyHolder.addProperty( property, joinColumns, inferredData.getDeclaringClass() ); + + final OneToOneSecondPass secondPass = new OneToOneSecondPass( + binder, + property, + oneToOne, + mappedBy, + propertyHolder.getEntityName(), + propertyHolder, + inferredData, + isTargetAnnotatedEntity( targetEntity, annotatedProperty, context ), + notFoundAction, + joinColumns, + context + ); + if ( inSecondPass ) { + secondPass.doSecondPass( context.getMetadataCollector().getEntityBindingMap() ); + } + else { + context.getMetadataCollector().addSecondPass( secondPass, mappedBy == null ); + } + } + private static boolean isMapToPK(AnnotatedJoinColumns joinColumns, PropertyHolder propertyHolder, boolean trueOneToOne) { if ( trueOneToOne ) { return true; diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/naming/ObjectNameNormalizer.java b/hibernate-core/src/main/java/org/hibernate/boot/model/naming/ObjectNameNormalizer.java index 08155aee23dd..c12454cee041 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/naming/ObjectNameNormalizer.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/naming/ObjectNameNormalizer.java @@ -34,7 +34,7 @@ public Identifier normalizeIdentifierQuoting(String identifierText) { return database().toIdentifier( identifierText ); } - protected Database database() { + public Database database() { if ( database == null ) { database = getBuildingContext().getMetadataCollector().getDatabase(); } @@ -42,10 +42,7 @@ protected Database database() { } public Identifier normalizeIdentifierQuoting(Identifier identifier) { - return getBuildingContext().getMetadataCollector() - .getDatabase() - .getJdbcEnvironment() - .getIdentifierHelper() + return database().getJdbcEnvironment().getIdentifierHelper() .normalizeQuoting( identifier ); } diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/source/internal/annotations/AnnotationMetadataSourceProcessorImpl.java b/hibernate-core/src/main/java/org/hibernate/boot/model/source/internal/annotations/AnnotationMetadataSourceProcessorImpl.java index fde786ffcbf3..a799c0365cd0 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/source/internal/annotations/AnnotationMetadataSourceProcessorImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/source/internal/annotations/AnnotationMetadataSourceProcessorImpl.java @@ -37,6 +37,8 @@ import org.jboss.jandex.IndexView; import org.jboss.logging.Logger; +import static org.hibernate.boot.model.internal.AnnotationBinder.bindFilterDefs; + /** * @author Steve Ebersole */ @@ -248,6 +250,13 @@ public void processEntityHierarchies(Set processedEntityNames) { orderedClasses, rootMetadataBuildingContext ); + // we want to go through all classes and collect the filter definitions first, + // so that when we bind the classes we have the complete list of filters to search from: + for ( XClass clazz : orderedClasses ) { + if ( !processedEntityNames.contains( clazz.getName() ) ) { + bindFilterDefs( clazz, rootMetadataBuildingContext ); + } + } for ( XClass clazz : orderedClasses ) { if ( processedEntityNames.contains( clazz.getName() ) ) { @@ -304,7 +313,28 @@ private List orderAndFillHierarchy(List classes) { } } } - return new ArrayList<>( orderedClasses ); + + // order the hierarchy + ArrayList workingCopy = new ArrayList<>( orderedClasses ); + List newList = new ArrayList<>( orderedClasses.size() ); + while ( !workingCopy.isEmpty() ) { + XClass clazz = workingCopy.get( 0 ); + orderHierarchy( workingCopy, newList, orderedClasses, clazz ); + } + return newList; + } + + private void orderHierarchy(List copy, List newList, LinkedHashSet original, XClass clazz) { + if ( clazz != null && !Object.class.getName().equals( clazz.getName() ) ) { + //process superclass first + orderHierarchy( copy, newList, original, clazz.getSuperclass() ); + if ( original.contains( clazz ) ) { + if ( !newList.contains( clazz ) ) { + newList.add( clazz ); + } + copy.remove( clazz ); + } + } } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/boot/model/source/internal/hbm/ModelBinder.java b/hibernate-core/src/main/java/org/hibernate/boot/model/source/internal/hbm/ModelBinder.java index 544c38379a06..148e0d4c7d4f 100644 --- a/hibernate-core/src/main/java/org/hibernate/boot/model/source/internal/hbm/ModelBinder.java +++ b/hibernate-core/src/main/java/org/hibernate/boot/model/source/internal/hbm/ModelBinder.java @@ -155,6 +155,7 @@ import org.hibernate.type.CustomType; import org.hibernate.type.ForeignKeyDirection; import org.hibernate.type.StandardBasicTypes; +import org.hibernate.type.internal.BasicTypeImpl; import org.hibernate.type.spi.TypeConfiguration; import org.hibernate.usertype.CompositeUserType; import org.hibernate.usertype.ParameterizedType; @@ -1877,11 +1878,14 @@ private void resolveLob(final SingularAttributeSourceBasic attributeSource, Simp // Resolves whether the property is LOB based on the type attribute on the attribute property source. // Essentially this expects the type to map to a CLOB/NCLOB/BLOB sql type internally and compares. if ( !value.isLob() && value.getTypeName() != null ) { - final BasicType basicType = attributeSource.getBuildingContext() - .getMetadataCollector() - .getTypeConfiguration() - .getBasicTypeRegistry() - .getRegisteredType( value.getTypeName() ); + final String typeName = value.getTypeName(); + final MetadataBuildingContext context = attributeSource.getBuildingContext(); + final BasicType basicType = + typeName.startsWith( BasicTypeImpl.EXTERNALIZED_PREFIX ) + ? context.getBootstrapContext().resolveAdHocBasicType( typeName ) + : context.getMetadataCollector().getTypeConfiguration() + .getBasicTypeRegistry().getRegisteredType( typeName ); + if ( basicType instanceof AbstractSingleColumnStandardBasicType ) { if ( isLob( basicType.getJdbcType().getDdlTypeCode(), null ) ) { value.makeLob(); diff --git a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/BiDirectionalAssociationHandler.java b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/BiDirectionalAssociationHandler.java index 7fcb1503a704..66483541f934 100644 --- a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/BiDirectionalAssociationHandler.java +++ b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/BiDirectionalAssociationHandler.java @@ -80,6 +80,7 @@ static Implementation wrap( TypeDescription targetType = FieldLocator.ForClassHierarchy.Factory.INSTANCE.make( targetEntity ) .locate( bidirectionalAttributeName ) .getField() + .asDefined() .getType() .asErasure(); diff --git a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/ByteBuddyEnhancementContext.java b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/ByteBuddyEnhancementContext.java index 5e495b30f1ad..dd2097ae0bb4 100644 --- a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/ByteBuddyEnhancementContext.java +++ b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/ByteBuddyEnhancementContext.java @@ -24,6 +24,7 @@ import net.bytebuddy.dynamic.scaffold.MethodGraph; import net.bytebuddy.matcher.ElementMatcher; import net.bytebuddy.pool.TypePool; +import org.hibernate.bytecode.enhance.spi.UnsupportedEnhancementStrategy; import static net.bytebuddy.matcher.ElementMatchers.isGetter; @@ -106,6 +107,10 @@ public void registerDiscoveredType(TypeDescription typeDescription, Type.Persist enhancementContext.registerDiscoveredType( new UnloadedTypeDescription( typeDescription ), type ); } + public UnsupportedEnhancementStrategy getUnsupportedEnhancementStrategy() { + return enhancementContext.getUnsupportedEnhancementStrategy(); + } + public void discoverCompositeTypes(TypeDescription managedCtClass, TypePool typePool) { if ( isDiscoveredType( managedCtClass ) ) { return; diff --git a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/CoreCacheProvider.java b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/CoreCacheProvider.java new file mode 100644 index 000000000000..f7aa7f3cdfe8 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/CoreCacheProvider.java @@ -0,0 +1,71 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.bytecode.enhance.internal.bytebuddy; + +import java.util.Objects; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.pool.TypePool; + +/** + * A CacheProvider for ByteBuddy which is specifically designed for + * our CoreTypePool: it ensures the cache content doesn't get tainted + * by model types or anything else which isn't responsibility of this + * particular type pool. + * @implNote This cache instance shares the same @{@link CorePrefixFilter} + * instance of the @{@link CoreTypePool} which is using it, and uses it + * to guard writes into its internal caches. + */ +class CoreCacheProvider implements TypePool.CacheProvider { + + private final ConcurrentMap storage = new ConcurrentHashMap<>(); + private final CorePrefixFilter acceptedPrefixes; + + CoreCacheProvider(final CorePrefixFilter acceptedPrefixes) { + this.acceptedPrefixes = Objects.requireNonNull( acceptedPrefixes ); + register( + Object.class.getName(), + new TypePool.Resolution.Simple( TypeDescription.ForLoadedType.of( Object.class ) ) + ); + } + + /** + * {@inheritDoc} + */ + @Override + public TypePool.Resolution find(final String name) { + return storage.get( name ); + } + + /** + * {@inheritDoc} + */ + @Override + public TypePool.Resolution register(String name, TypePool.Resolution resolution) { + //Ensure we DO NOT cache anything from a non-core namespace, to not leak application specific code: + if ( acceptedPrefixes.isCoreClassName( name ) ) { + TypePool.Resolution cached = storage.putIfAbsent( name, resolution ); + return cached == null + ? resolution + : cached; + } + else { + return resolution; + } + } + + /** + * {@inheritDoc} + */ + @Override + public void clear() { + storage.clear(); + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/CorePrefixFilter.java b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/CorePrefixFilter.java new file mode 100644 index 000000000000..291e90d25b27 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/CorePrefixFilter.java @@ -0,0 +1,57 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.bytecode.enhance.internal.bytebuddy; + +import java.util.Objects; + +/** + * We differentiate between core classes and application classes during symbol + * resolution for the purposes of entity enhancement. + * The discriminator is the prefix of the fully qualified classname, for + * example it could be package names. + * The "core classes" don't have to be comprehensively defined: we want a small + * set of prefixes for which we know with certainty that a)They won't be used + * in application code (assuming people honour reasonable package name rules) + * or any code that needs being enhanced. and b) frequently need to be looked up + * during the enhancement process. + * A great example is the "jakarta.persistence.Entity" annotation: we'll most likely + * need to load it when doing any form of introspection on user's code, but we expect + * the bytecode which represents the annotation to not be enhanced. + * We then benefit from caching such representations of object types which are frequently + * loaded; since caching end user code would lead to enhancement problems, it's best + * to keep the list conservative when in doubt. + * For example, don't consider all of {@code "org.hibernate."} prefixes as safe, as + * that would include entities used during our own testsuite and entities defined by Envers. + * + */ +public final class CorePrefixFilter { + + private final String[] acceptedPrefixes; + public static final CorePrefixFilter DEFAULT_INSTANCE = new CorePrefixFilter(); + + /** + * Do not invoke: use DEFAULT_INSTANCE + */ + CorePrefixFilter() { + //By default optimise for jakarta annotations, java util collections, and Hibernate marker interfaces + this("jakarta.", "java.", "org.hibernate.annotations.", "org.hibernate.bytecode.enhance.spi.", "org.hibernate.engine.spi."); + } + + public CorePrefixFilter(final String... acceptedPrefixes) { + this.acceptedPrefixes = Objects.requireNonNull( acceptedPrefixes ); + } + + public boolean isCoreClassName(final String name) { + for ( String acceptedPrefix : this.acceptedPrefixes ) { + if ( name.startsWith( acceptedPrefix ) ) { + return true; + } + } + return false; + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/CoreTypePool.java b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/CoreTypePool.java index 87dbc33729d9..48d45eddc2aa 100644 --- a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/CoreTypePool.java +++ b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/CoreTypePool.java @@ -6,6 +6,7 @@ */ package org.hibernate.bytecode.enhance.internal.bytebuddy; +import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; import net.bytebuddy.description.type.TypeDescription; @@ -27,42 +28,39 @@ public class CoreTypePool extends TypePool.AbstractBase implements TypePool { private final ClassLoader hibernateClassLoader = CoreTypePool.class.getClassLoader(); private final ConcurrentHashMap resolutions = new ConcurrentHashMap<>(); - private final String[] acceptedPrefixes; + private final CorePrefixFilter acceptedPrefixes; /** - * Construct a new {@link CoreTypePool} with its default configuration: - * to only load classes whose package names start with either "jakarta." - * or "java." + * Construct a new {@link CoreTypePool} with its default configuration. + * + * @see CorePrefixFilter */ public CoreTypePool() { - //By default optimise for jakarta annotations, and java util collections - this("jakarta.", "java.", "org.hibernate.annotations."); + this( CorePrefixFilter.DEFAULT_INSTANCE ); } /** * Construct a new {@link CoreTypePool} with a choice of which prefixes * for fully qualified classnames will be loaded by this {@link TypePool}. + * + * @deprecated used by Quarkus */ + @Deprecated public CoreTypePool(final String... acceptedPrefixes) { + this( new CorePrefixFilter( acceptedPrefixes ) ); + } + + public CoreTypePool(CorePrefixFilter acceptedPrefixes) { //While we implement a cache in this class we also want to enable //ByteBuddy's default caching mechanism as it will cache the more //useful output of the parsing and introspection of such types. - super( new TypePool.CacheProvider.Simple() ); - this.acceptedPrefixes = acceptedPrefixes; - } - - private boolean isCoreClassName(final String name) { - for ( String acceptedPrefix : this.acceptedPrefixes ) { - if ( name.startsWith( acceptedPrefix ) ) { - return true; - } - } - return false; + super( new CoreCacheProvider( acceptedPrefixes ) ); + this.acceptedPrefixes = Objects.requireNonNull( acceptedPrefixes ); } @Override protected Resolution doDescribe(final String name) { - if ( isCoreClassName( name ) ) { + if ( acceptedPrefixes.isCoreClassName( name ) ) { final Resolution resolution = resolutions.get( name ); if ( resolution != null ) { return resolution; diff --git a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/EnhancerCacheProvider.java b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/EnhancerCacheProvider.java new file mode 100644 index 000000000000..dea753830635 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/EnhancerCacheProvider.java @@ -0,0 +1,66 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.bytecode.enhance.internal.bytebuddy; + +import net.bytebuddy.dynamic.ClassFileLocator; +import net.bytebuddy.pool.TypePool; + +/** + * A simple cache provider that allows overriding the resolution for the class that is currently being enhanced. + */ +final class EnhancerCacheProvider extends TypePool.CacheProvider.Simple { + + private final ThreadLocal enhancementState = new ThreadLocal<>(); + + @Override + public TypePool.Resolution find(final String name) { + final EnhancementState enhancementState = getEnhancementState(); + if ( enhancementState != null && enhancementState.getClassName().equals( name ) ) { + return enhancementState.getTypePoolResolution(); + } + return super.find( name ); + } + + EnhancementState getEnhancementState() { + return enhancementState.get(); + } + + void setEnhancementState(EnhancementState state) { + enhancementState.set( state ); + } + + void removeEnhancementState() { + enhancementState.remove(); + } + + static final class EnhancementState { + private final String className; + private final ClassFileLocator.Resolution classFileResolution; + private TypePool.Resolution typePoolResolution; + + public EnhancementState(String className, ClassFileLocator.Resolution classFileResolution) { + this.className = className; + this.classFileResolution = classFileResolution; + } + + public String getClassName() { + return className; + } + + public ClassFileLocator.Resolution getClassFileResolution() { + return classFileResolution; + } + + public TypePool.Resolution getTypePoolResolution() { + return typePoolResolution; + } + + public void setTypePoolResolution(TypePool.Resolution typePoolResolution) { + this.typePoolResolution = typePoolResolution; + } + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/EnhancerClassFileLocator.java b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/EnhancerClassFileLocator.java new file mode 100644 index 000000000000..1fa193479133 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/EnhancerClassFileLocator.java @@ -0,0 +1,40 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.bytecode.enhance.internal.bytebuddy; + +import net.bytebuddy.dynamic.ClassFileLocator; + +import java.io.IOException; + +/** + * A delegating ClassFileLocator that allows overriding the resolution for the class that is currently being enhanced. + */ +final class EnhancerClassFileLocator implements ClassFileLocator { + + private final EnhancerCacheProvider cacheProvider; + private final ClassFileLocator delegate; + + public EnhancerClassFileLocator(EnhancerCacheProvider cacheProvider, ClassFileLocator delegate) { + this.cacheProvider = cacheProvider; + this.delegate = delegate; + } + + @Override + public Resolution locate(final String name) throws IOException { + final EnhancerCacheProvider.EnhancementState enhancementState = cacheProvider.getEnhancementState(); + if ( enhancementState != null && enhancementState.getClassName().equals( name ) ) { + return enhancementState.getClassFileResolution(); + } + return delegate.locate( name ); + } + + @Override + public void close() throws IOException { + delegate.close(); + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/EnhancerImpl.java b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/EnhancerImpl.java index e696d1beb802..85212a079a3b 100644 --- a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/EnhancerImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/EnhancerImpl.java @@ -6,17 +6,35 @@ */ package org.hibernate.bytecode.enhance.internal.bytebuddy; -import java.lang.annotation.Annotation; -import java.lang.reflect.Modifier; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Optional; -import java.util.function.Supplier; +import jakarta.persistence.Access; +import jakarta.persistence.AccessType; +import jakarta.persistence.Embeddable; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.MappedSuperclass; +import jakarta.persistence.Transient; +import jakarta.persistence.metamodel.Type; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.annotation.AnnotationDescription; +import net.bytebuddy.description.annotation.AnnotationList; +import net.bytebuddy.description.annotation.AnnotationSource; +import net.bytebuddy.description.field.FieldDescription; +import net.bytebuddy.description.field.FieldDescription.InDefinedShape; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.method.MethodList; +import net.bytebuddy.description.type.TypeDefinition; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.description.type.TypeDescription.Generic; +import net.bytebuddy.dynamic.DynamicType; +import net.bytebuddy.dynamic.scaffold.MethodGraph; +import net.bytebuddy.implementation.FieldAccessor; +import net.bytebuddy.implementation.FixedValue; +import net.bytebuddy.implementation.Implementation; +import net.bytebuddy.implementation.StubMethod; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.hibernate.AssertionFailure; import org.hibernate.Version; import org.hibernate.bytecode.enhance.VersionMismatchException; import org.hibernate.bytecode.enhance.internal.tracker.CompositeOwnerTracker; @@ -27,6 +45,7 @@ import org.hibernate.bytecode.enhance.spi.Enhancer; import org.hibernate.bytecode.enhance.spi.EnhancerConstants; import org.hibernate.bytecode.enhance.spi.UnloadedField; +import org.hibernate.bytecode.enhance.spi.UnsupportedEnhancementStrategy; import org.hibernate.bytecode.enhance.spi.interceptor.LazyAttributeLoadingInterceptor; import org.hibernate.bytecode.internal.bytebuddy.ByteBuddyState; import org.hibernate.engine.spi.CompositeOwner; @@ -41,25 +60,24 @@ import org.hibernate.internal.CoreLogging; import org.hibernate.internal.CoreMessageLogger; -import jakarta.persistence.Access; -import jakarta.persistence.AccessType; -import jakarta.persistence.metamodel.Type; -import net.bytebuddy.asm.Advice; -import net.bytebuddy.description.annotation.AnnotationDescription; -import net.bytebuddy.description.annotation.AnnotationList; -import net.bytebuddy.description.field.FieldDescription; -import net.bytebuddy.description.field.FieldDescription.InDefinedShape; -import net.bytebuddy.description.method.MethodDescription; -import net.bytebuddy.description.type.TypeDefinition; -import net.bytebuddy.description.type.TypeDescription; -import net.bytebuddy.description.type.TypeDescription.Generic; -import net.bytebuddy.dynamic.DynamicType; -import net.bytebuddy.implementation.FieldAccessor; -import net.bytebuddy.implementation.FixedValue; -import net.bytebuddy.implementation.Implementation; -import net.bytebuddy.implementation.StubMethod; +import java.lang.annotation.Annotation; +import java.lang.reflect.Modifier; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.function.Supplier; import static net.bytebuddy.matcher.ElementMatchers.isDefaultFinalizer; +import static net.bytebuddy.matcher.ElementMatchers.isGetter; +import static net.bytebuddy.matcher.ElementMatchers.isSetter; +import static net.bytebuddy.matcher.ElementMatchers.isStatic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.not; public class EnhancerImpl implements Enhancer { @@ -151,6 +169,18 @@ public void discoverTypes(String className, byte[] originalBytes) { } private DynamicType.Builder doEnhance(Supplier> builderSupplier, TypeDescription managedCtClass) { + // skip if the class was already enhanced. This is very common in WildFly as classloading is highly concurrent. + // We need to ensure that no class is instrumented multiple times as that might result in incorrect bytecode. + // N.B. there is a second check below using a different approach: checking for the marker interfaces, + // which does not address the case of extended bytecode enhancement + // (because it enhances classes that do not end up with these marker interfaces). + // I'm currently inclined to keep both checks, as one is safer and the other has better backwards compatibility. + if ( managedCtClass.getDeclaredAnnotations().isAnnotationPresent( EnhancementInfo.class ) ) { + verifyVersions( managedCtClass, enhancementContext ); + log.debugf( "Skipping enhancement of [%s]: it's already annotated with @EnhancementInfo", managedCtClass.getName() ); + return null; + } + // can't effectively enhance interfaces if ( managedCtClass.isInterface() ) { log.debugf( "Skipping enhancement of [%s]: it's an interface", managedCtClass.getName() ); @@ -167,11 +197,16 @@ private DynamicType.Builder doEnhance(Supplier> builde if ( alreadyEnhanced( managedCtClass ) ) { verifyVersions( managedCtClass, enhancementContext ); - log.debugf( "Skipping enhancement of [%s]: already enhanced", managedCtClass.getName() ); + log.debugf( "Skipping enhancement of [%s]: it's already implementing 'Managed'", managedCtClass.getName() ); return null; } if ( enhancementContext.isEntityClass( managedCtClass ) ) { + if ( checkUnsupportedAttributeNaming( managedCtClass, enhancementContext ) ) { + // do not enhance classes with mismatched names for PROPERTY-access persistent attributes + return null; + } + log.debugf( "Enhancing [%s] as Entity", managedCtClass.getName() ); DynamicType.Builder builder = builderSupplier.get(); builder = builder.implement( ManagedEntity.class ) @@ -331,6 +366,11 @@ private DynamicType.Builder doEnhance(Supplier> builde return createTransformer( managedCtClass ).applyTo( builder ); } else if ( enhancementContext.isCompositeClass( managedCtClass ) ) { + if ( checkUnsupportedAttributeNaming( managedCtClass, enhancementContext ) ) { + // do not enhance classes with mismatched names for PROPERTY-access persistent attributes + return null; + } + log.debugf( "Enhancing [%s] as Composite", managedCtClass.getName() ); DynamicType.Builder builder = builderSupplier.get(); @@ -364,6 +404,11 @@ else if ( enhancementContext.isCompositeClass( managedCtClass ) ) { return createTransformer( managedCtClass ).applyTo( builder ); } else if ( enhancementContext.isMappedSuperclassClass( managedCtClass ) ) { + // Check for HHH-16572 (PROPERTY attributes with mismatched field and method names) + if ( checkUnsupportedAttributeNaming( managedCtClass, enhancementContext ) ) { + return null; + } + log.debugf( "Enhancing [%s] as MappedSuperclass", managedCtClass.getName() ); DynamicType.Builder builder = builderSupplier.get(); @@ -380,6 +425,198 @@ else if ( enhancementContext.doExtendedEnhancement( managedCtClass ) ) { } } + /** + * Utility that determines the access-type of a mapped class based on an explicit annotation + * or guessing it from the placement of its identifier property. Implementation should be + * aligned with {@code InheritanceState#determineDefaultAccessType()}. + * + * @return the {@link AccessType} used by the mapped class + * + * @implNote this does not fully account for embeddables, as they should inherit the access-type + * from the entities they're used in - defaulting to PROPERTY to always run the accessor check + */ + private static AccessType determineDefaultAccessType(TypeDefinition typeDefinition) { + for ( TypeDefinition candidate = typeDefinition; candidate != null && !candidate.represents( Object.class ); candidate = candidate.getSuperClass() ) { + final AnnotationList annotations = candidate.asErasure().getDeclaredAnnotations(); + if ( hasMappingAnnotation( annotations ) ) { + final AnnotationDescription.Loadable access = annotations.ofType( Access.class ); + if ( access != null ) { + return access.load().value(); + } + } + } + // Guess from identifier. + // FIX: Shouldn't this be determined by the first attribute (i.e., field or property) with annotations, + // but without an explicit Access annotation, according to JPA 2.0 spec 2.3.1: Default Access Type? + for ( TypeDefinition candidate = typeDefinition; candidate != null && !candidate.represents( Object.class ); candidate = candidate.getSuperClass() ) { + final AnnotationList annotations = candidate.asErasure().getDeclaredAnnotations(); + if ( hasMappingAnnotation( annotations ) ) { + for ( FieldDescription ctField : candidate.getDeclaredFields() ) { + if ( !Modifier.isStatic( ctField.getModifiers() ) ) { + final AnnotationList annotationList = ctField.getDeclaredAnnotations(); + if ( annotationList.isAnnotationPresent( Id.class ) || annotationList.isAnnotationPresent( EmbeddedId.class ) ) { + return AccessType.FIELD; + } + } + } + } + } + // We can assume AccessType.PROPERTY here + return AccessType.PROPERTY; + } + + /** + * Determines the access-type of the given annotation source if an explicit {@link Access} annotation + * is present, otherwise defaults to the provided {@code defaultAccessType} + */ + private static AccessType determineAccessType(AnnotationSource annotationSource, AccessType defaultAccessType) { + final AnnotationDescription.Loadable access = annotationSource.getDeclaredAnnotations().ofType( Access.class ); + return access != null ? access.load().value() : defaultAccessType; + } + + private static boolean hasMappingAnnotation(AnnotationList annotations) { + return annotations.isAnnotationPresent( Entity.class ) + || annotations.isAnnotationPresent( MappedSuperclass.class ) + || annotations.isAnnotationPresent( Embeddable.class ); + } + + private static boolean isPersistentMethod(MethodDescription method) { + final AnnotationList annotations = method.getDeclaredAnnotations(); + if ( annotations.isAnnotationPresent( Transient.class ) ) { + return false; + } + + return annotations.stream().noneMatch( a -> IGNORED_PERSISTENCE_ANNOTATIONS.contains( a.getAnnotationType().getName() ) ); + } + + private static final Set IGNORED_PERSISTENCE_ANNOTATIONS = Set.of( + "jakarta.persistence.PostLoad", + "jakarta.persistence.PostPersist", + "jakarta.persistence.PostRemove", + "jakarta.persistence.PostUpdate", + "jakarta.persistence.PrePersist", + "jakarta.persistence.PreRemove", + "jakarta.persistence.PreUpdate" + ); + + private static boolean containsField(Generic type, String fieldName) { + do { + if ( !type.getDeclaredFields().filter( not( isStatic() ).and( named( fieldName ) ) ).isEmpty() ) { + return true; + } + type = type.getSuperClass(); + } + while ( type != null && !type.represents( Object.class ) ); + return false; + } + + /** + * Check whether an entity class ({@code managedCtClass}) has mismatched names between a persistent field and its + * getter/setter when using {@link AccessType#PROPERTY}, which Hibernate does not currently support for enhancement. + * See HHH-16572 + * + * @return {@code true} if enhancement of the class must be {@link UnsupportedEnhancementStrategy#SKIP skipped} + * because it has mismatched names. + * {@code false} if enhancement of the class must proceed, either because it doesn't have any mismatched names, + * or because {@link UnsupportedEnhancementStrategy#LEGACY legacy mode} was opted into. + * @throws EnhancementException if enhancement of the class must {@link UnsupportedEnhancementStrategy#FAIL abort} because it has mismatched names. + */ + @SuppressWarnings("deprecation") + private static boolean checkUnsupportedAttributeNaming(TypeDescription managedCtClass, ByteBuddyEnhancementContext enhancementContext) { + var strategy = enhancementContext.getUnsupportedEnhancementStrategy(); + if ( UnsupportedEnhancementStrategy.LEGACY.equals( strategy ) ) { + // Don't check anything and act as if there was nothing unsupported in the class. + // This is unsafe but that's what LEGACY is about. + return false; + } + + // For process access rules, See https://jakarta.ee/specifications/persistence/3.2/jakarta-persistence-spec-3.2#default-access-type + // and https://jakarta.ee/specifications/persistence/3.2/jakarta-persistence-spec-3.2#a122 + // + // This check will determine if entity field names do not match Property accessor method name + // For example: + // @Entity + // class Book { + // Integer id; + // String smtg; + // + // @Id Integer getId() { return id; } + // String getSomething() { return smtg; } + // } + // + // Check name of the getter/setter method with persistence annotation and getter/setter method name that doesn't refer to an entity field + // and will return false. If the property accessor method(s) are named to match the field name(s), return true. + final AccessType defaultAccessType = determineDefaultAccessType( managedCtClass ); + final MethodList methods = MethodGraph.Compiler.DEFAULT.compile( (TypeDefinition) managedCtClass ) + .listNodes() + .asMethodList() + .filter( isGetter().or( isSetter() ) ); + for ( final MethodDescription methodDescription : methods ) { + if ( methodDescription.getDeclaringType().represents( Object.class ) + || determineAccessType( methodDescription, defaultAccessType ) != AccessType.PROPERTY ) { + // We only need to check this for AccessType.PROPERTY + continue; + } + + final String methodName = methodDescription.getActualName(); + String fieldName; + if ( methodName.startsWith( "get" ) || methodName.startsWith( "set" ) ) { + fieldName = methodName.substring( 3 ); + } + else { + assert methodName.startsWith( "is" ); + fieldName = methodName.substring( 2 ); + } + // convert first field letter to lower case + fieldName = getJavaBeansFieldName( fieldName ); + if ( fieldName != null && isPersistentMethod( methodDescription ) + && !containsField( managedCtClass.asGenericType(), fieldName ) ) { + // We shouldn't even be in this method if using LEGACY, see top of this method. + switch ( strategy ) { + case SKIP: + log.debugf( + "Skipping enhancement of [%s] because no field named [%s] could be found for property accessor method [%s]." + + " To fix this, make sure all property accessor methods have a matching field.", + managedCtClass.getName(), + fieldName, + methodDescription.getName() + ); + return true; + case FAIL: + throw new EnhancementException( String.format( + "Enhancement of [%s] failed because no field named [%s] could be found for property accessor method [%s]." + + " To fix this, make sure all property accessor methods have a matching field.", + managedCtClass.getName(), + fieldName, + methodDescription.getName() + ) ); + default: + throw new AssertionFailure( "Unexpected strategy at this point: " + strategy ); + } + } + } + return false; + } + + /** + * If the first two characters are upper case, assume all characters are upper case to be returned as is. + * Otherwise, return the name with the first character converted to lower case and the remaining part returned as is. + * + * @param name is the property accessor name to be updated following Persistence property name rules. + * @return name that follows JavaBeans rules, or {@code null} if the provided string is empty + */ + private static @Nullable String getJavaBeansFieldName(String name) { + if ( name.isEmpty() ) { + return null; + } + if ( name.length() > 1 && Character.isUpperCase( name.charAt( 1 ) ) && Character.isUpperCase( name.charAt( 0 ) ) ) { + return name; + } + final char[] chars = name.toCharArray(); + chars[0] = Character.toLowerCase( chars[0] ); + return new String( chars ); + } + private static void verifyVersions(TypeDescription managedCtClass, ByteBuddyEnhancementContext enhancementContext) { final AnnotationDescription.Loadable existingInfo = managedCtClass .getDeclaredAnnotations() @@ -557,8 +794,23 @@ String getDescriptor() { return fieldDescription.getDescriptor(); } - boolean isVisibleTo(TypeDescription typeDescription) { - return fieldDescription.isVisibleTo( typeDescription ); + boolean isVisibleTo(TypeDescription type) { + final var declaringType = fieldDescription.getDeclaringType().asErasure(); + if ( declaringType.isVisibleTo( type ) ) { + if ( fieldDescription.isPublic() || type.equals( declaringType ) ) { + return true; + } + else if ( fieldDescription.isProtected() ) { + return declaringType.isAssignableFrom( type ); + } + else if ( fieldDescription.isPrivate() ) { + return type.isNestMateOf( declaringType ); + } + // We explicitly consider package-private fields as not visible, as the classes + // might have the same package name but be loaded by different class loaders. + // (see https://hibernate.atlassian.net/browse/HHH-19784) + } + return false; } FieldDescription getFieldDescription() { diff --git a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/InlineDirtyCheckingHandler.java b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/InlineDirtyCheckingHandler.java index 4e687362419d..f263f9a31b88 100644 --- a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/InlineDirtyCheckingHandler.java +++ b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/InlineDirtyCheckingHandler.java @@ -78,8 +78,9 @@ else if ( !persistentField.hasAnnotation( Id.class ) } if ( enhancementContext.isCompositeField( persistentField ) - // Don't do composite owner tracking for records - && !persistentField.getType().isRecord() ) { + && !persistentField.hasAnnotation( EmbeddedId.class ) + // Don't do composite owner tracking for records + && !persistentField.getType().isRecord() ) { // HHH-13759 - Call getter on superclass if field is not visible // An embedded field won't be visible if declared private in a superclass diff --git a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/ModelTypePool.java b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/ModelTypePool.java index ecbc1ede0fea..7c40165a7a5c 100644 --- a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/ModelTypePool.java +++ b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/ModelTypePool.java @@ -6,23 +6,23 @@ */ package org.hibernate.bytecode.enhance.internal.bytebuddy; -import java.util.Objects; -import java.util.concurrent.ConcurrentHashMap; - import net.bytebuddy.dynamic.ClassFileLocator; import net.bytebuddy.pool.TypePool; +import java.util.Objects; + /** * A TypePool suitable for loading user's classes, * potentially in parallel operations. */ public class ModelTypePool extends TypePool.Default implements EnhancerClassLocator { - private final ConcurrentHashMap resolutions = new ConcurrentHashMap<>(); - private final OverridingClassFileLocator locator; + private final EnhancerClassFileLocator locator; + private final EnhancerCacheProvider poolCache; - private ModelTypePool(CacheProvider cacheProvider, OverridingClassFileLocator classFileLocator, CoreTypePool parent) { + private ModelTypePool(EnhancerCacheProvider cacheProvider, EnhancerClassFileLocator classFileLocator, CoreTypePool parent) { super( cacheProvider, classFileLocator, ReaderMode.FAST, parent ); + this.poolCache = cacheProvider; this.locator = classFileLocator; } @@ -62,7 +62,7 @@ public static EnhancerClassLocator buildModelTypePool(ClassFileLocator classFile * @return */ public static EnhancerClassLocator buildModelTypePool(ClassFileLocator classFileLocator, CoreTypePool coreTypePool) { - return buildModelTypePool( classFileLocator, coreTypePool, new TypePool.CacheProvider.Simple() ); + return buildModelTypePool( classFileLocator, coreTypePool, new EnhancerCacheProvider() ); } /** @@ -72,37 +72,35 @@ public static EnhancerClassLocator buildModelTypePool(ClassFileLocator classFile * @param cacheProvider * @return */ - public static EnhancerClassLocator buildModelTypePool(ClassFileLocator classFileLocator, CoreTypePool coreTypePool, CacheProvider cacheProvider) { + static EnhancerClassLocator buildModelTypePool(ClassFileLocator classFileLocator, CoreTypePool coreTypePool, EnhancerCacheProvider cacheProvider) { Objects.requireNonNull( classFileLocator ); Objects.requireNonNull( coreTypePool ); Objects.requireNonNull( cacheProvider ); - return new ModelTypePool( cacheProvider, new OverridingClassFileLocator( classFileLocator ), coreTypePool ); - } - - @Override - protected Resolution doDescribe(final String name) { - final Resolution resolution = resolutions.get( name ); - if ( resolution != null ) { - return resolution; - } - else { - return resolutions.computeIfAbsent( name, super::doDescribe ); - } + return new ModelTypePool( cacheProvider, new EnhancerClassFileLocator( cacheProvider, classFileLocator ), coreTypePool ); } @Override public void registerClassNameAndBytes(final String className, final byte[] bytes) { - locator.put( className, new ClassFileLocator.Resolution.Explicit( Objects.requireNonNull( bytes ) ) ); + final EnhancerCacheProvider.EnhancementState currentEnhancementState = poolCache.getEnhancementState(); + if ( currentEnhancementState != null ) { + throw new IllegalStateException( "Re-entrant enhancement is not supported: " + className ); + } + final EnhancerCacheProvider.EnhancementState state = new EnhancerCacheProvider.EnhancementState( + className, + new ClassFileLocator.Resolution.Explicit( Objects.requireNonNull( bytes ) ) + ); + // Set the state first because the ClassFileLocator needs this in the doDescribe() call below + poolCache.setEnhancementState( state ); + state.setTypePoolResolution( doDescribe( className ) ); } @Override - public void deregisterClassNameAndBytes(final String className) { - locator.remove( className ); + public void deregisterClassNameAndBytes(String className) { + poolCache.removeEnhancementState(); } @Override public ClassFileLocator asClassFileLocator() { return locator; } - } diff --git a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/OverridingClassFileLocator.java b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/OverridingClassFileLocator.java deleted file mode 100644 index 2f64dabbdd70..000000000000 --- a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/OverridingClassFileLocator.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Hibernate, Relational Persistence for Idiomatic Java - * - * License: GNU Lesser General Public License (LGPL), version 2.1 or later. - * See the lgpl.txt file in the root directory or . - */ -package org.hibernate.bytecode.enhance.internal.bytebuddy; - -import java.io.IOException; -import java.util.Objects; -import java.util.concurrent.ConcurrentHashMap; - -import net.bytebuddy.dynamic.ClassFileLocator; - -/** - * Allows wrapping another ClassFileLocator to add the ability to define - * resolution overrides for specific resources. - */ -public final class OverridingClassFileLocator implements ClassFileLocator { - - private final ConcurrentHashMap registeredResolutions = new ConcurrentHashMap<>(); - private final ClassFileLocator parent; - - public OverridingClassFileLocator(final ClassFileLocator parent) { - this.parent = Objects.requireNonNull( parent ); - } - - @Override - public Resolution locate(final String name) throws IOException { - final Resolution resolution = registeredResolutions.get( name ); - if ( resolution != null ) { - return resolution; - } - else { - return parent.locate( name ); - } - } - - @Override - public void close() throws IOException { - //Nothing to do: we're not responsible for parent - } - - void put(String className, Resolution.Explicit explicit) { - registeredResolutions.put( className, explicit ); - } - - void remove(String className) { - registeredResolutions.remove( className ); - } - -} diff --git a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/PersistentAttributeTransformer.java b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/PersistentAttributeTransformer.java index 5457474a38c9..61b8976c7a93 100644 --- a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/PersistentAttributeTransformer.java +++ b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/internal/bytebuddy/PersistentAttributeTransformer.java @@ -148,7 +148,10 @@ private static Collection collectInheritPersistentFie } TypeDefinition managedCtSuperclass = managedCtClass.getSuperClass(); - if ( enhancementContext.isEntityClass( managedCtSuperclass.asErasure() ) ) { + // If managedCtSuperclass is null, managedCtClass can be either interface or module-info. + // Interfaces are already filtered-out, and module-info does not have any fields to enhance + // so we can safely return empty list. + if ( managedCtSuperclass == null || enhancementContext.isEntityClass( managedCtSuperclass.asErasure() ) ) { return Collections.emptyList(); } else if ( !enhancementContext.isMappedSuperclassClass( managedCtSuperclass.asErasure() ) ) { diff --git a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/spi/EnhancementContext.java b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/spi/EnhancementContext.java index 06086c65d2e8..e35bac133693 100644 --- a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/spi/EnhancementContext.java +++ b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/spi/EnhancementContext.java @@ -6,7 +6,10 @@ */ package org.hibernate.bytecode.enhance.spi; +import org.hibernate.bytecode.spi.BytecodeProvider; + import jakarta.persistence.metamodel.Type; +import org.hibernate.Incubating; /** * The context for performing an enhancement. Enhancement can happen in any number of ways:

    @@ -146,4 +149,26 @@ public interface EnhancementContext { boolean isDiscoveredType(UnloadedClass classDescriptor); void registerDiscoveredType(UnloadedClass classDescriptor, Type.PersistenceType type); + + /** + * @return The expected behavior when encountering a class that cannot be enhanced, + * in particular when attribute names don't match field names. + * @see HHH-16572 + * @see HHH-18833 + */ + @Incubating + default UnsupportedEnhancementStrategy getUnsupportedEnhancementStrategy() { + return UnsupportedEnhancementStrategy.SKIP; + } + + /** + * Allows to force the use of a specific instance of BytecodeProvider to perform the enhancement. + * @return When returning {code null} the default implementation will be used. Only return a different instance if + * you need to override the default implementation. + */ + @Incubating + default BytecodeProvider getBytecodeProvider() { + return null; + } + } diff --git a/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/spi/UnsupportedEnhancementStrategy.java b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/spi/UnsupportedEnhancementStrategy.java new file mode 100644 index 000000000000..ce5c97883fa1 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/bytecode/enhance/spi/UnsupportedEnhancementStrategy.java @@ -0,0 +1,40 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.bytecode.enhance.spi; + +import org.hibernate.Incubating; + +/** + * The expected behavior when encountering a class that cannot be enhanced, + * in particular when attribute names don't match field names. + * + * @see org.hibernate.bytecode.enhance.spi.EnhancementContext#getUnsupportedEnhancementStrategy + */ +@Incubating +public enum UnsupportedEnhancementStrategy { + + /** + * When a class cannot be enhanced, skip enhancement for that class only. + */ + SKIP, + /** + * When a class cannot be enhanced, throw an exception with an actionable message. + */ + FAIL, + /** + * Legacy behavior: when a class cannot be enhanced, ignore that fact and try to enhance it anyway. + *

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

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

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

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

- *

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

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

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

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

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

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

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

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

    * The default behavior is to disallow update operations outside a transaction. * + * @settingDefault {@code false} (disabled) + * + * @apiNote Generally speaking, all access to transactional data should be done in a transaction. + * Combining this with second-level caching, e.g., will cause problems. + * * @see org.hibernate.boot.SessionFactoryBuilder#allowOutOfTransactionUpdateOperations(boolean) * * @since 5.2 */ + @Unsafe String ALLOW_UPDATE_OUTSIDE_TRANSACTION = "hibernate.allow_update_outside_transaction"; - - /** - * When enabled, specifies that the {@link org.hibernate.Session} should be - * closed automatically at the end of each transaction. - * - * @settingDefault {@code false} - * - * @see org.hibernate.boot.SessionFactoryBuilder#applyAutoClosing(boolean) - */ - String AUTO_CLOSE_SESSION = "hibernate.transaction.auto_close_session"; - - /** - * When enabled, specifies that automatic flushing should occur during the JTA - * {@link jakarta.transaction.Synchronization#beforeCompletion()} callback. - * - * @settingDefault {@code true} unless using JPA bootstrap - * - * @see org.hibernate.boot.SessionFactoryBuilder#applyAutoFlushing(boolean) - */ - String FLUSH_BEFORE_COMPLETION = "hibernate.transaction.flush_before_completion"; } diff --git a/hibernate-core/src/main/java/org/hibernate/cfg/Unsafe.java b/hibernate-core/src/main/java/org/hibernate/cfg/Unsafe.java new file mode 100644 index 000000000000..ca9056957d72 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/cfg/Unsafe.java @@ -0,0 +1,24 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.cfg; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; +import java.lang.annotation.Retention; + +/** + * Denotes that a setting is considered unsafe. Generally these are settings + * added for temporary use during porting of applications. Unsafe settings + * are largely considered unsupported. + */ +@Target(ElementType.FIELD) +@Retention(RetentionPolicy.CLASS) +@Documented +public @interface Unsafe { +} diff --git a/hibernate-core/src/main/java/org/hibernate/collection/spi/AbstractPersistentCollection.java b/hibernate-core/src/main/java/org/hibernate/collection/spi/AbstractPersistentCollection.java index 1df1aed42822..dc2b4fb4be1b 100644 --- a/hibernate-core/src/main/java/org/hibernate/collection/spi/AbstractPersistentCollection.java +++ b/hibernate-core/src/main/java/org/hibernate/collection/spi/AbstractPersistentCollection.java @@ -179,9 +179,9 @@ public int getSize() { if ( cachedSize>=0 ) { return cachedSize; } + throwLazyInitializationExceptionIfNotConnected(); final CollectionEntry entry = session.getPersistenceContextInternal().getCollectionEntry( this ); if ( entry == null ) { - throwLazyInitializationExceptionIfNotConnected(); throwLazyInitializationException("collection not associated with session"); throw new AssertionFailure("impossible"); } @@ -238,31 +238,30 @@ else if ( !session.isConnected() ) { SharedSessionContractImplementor originalSession = null; boolean isJTA = false; + try { + if ( tempSession != null ) { + isTempSession = true; + originalSession = session; + session = tempSession; + + isJTA = session.getTransactionCoordinator().getTransactionCoordinatorBuilder().isJta(); + + if ( !isJTA ) { + // Explicitly handle the transactions only if we're not in + // a JTA environment. A lazy loading temporary session can + // be created even if a current session and transaction are + // open (ex: session.clear() was used). We must prevent + // multiple transactions. + session.beginTransaction(); + } - if ( tempSession != null ) { - isTempSession = true; - originalSession = session; - session = tempSession; - - isJTA = session.getTransactionCoordinator().getTransactionCoordinatorBuilder().isJta(); - - if ( !isJTA ) { - // Explicitly handle the transactions only if we're not in - // a JTA environment. A lazy loading temporary session can - // be created even if a current session and transaction are - // open (ex: session.clear() was used). We must prevent - // multiple transactions. - session.beginTransaction(); + final CollectionPersister collectionDescriptor = + session.getSessionFactory() + .getMappingMetamodel() + .getCollectionDescriptor( getRole() ); + session.getPersistenceContextInternal() + .addUninitializedDetachedCollection( collectionDescriptor, this ); } - - final CollectionPersister collectionDescriptor = - session.getSessionFactory() - .getMappingMetamodel() - .getCollectionDescriptor( getRole() ); - session.getPersistenceContextInternal().addUninitializedDetachedCollection( collectionDescriptor, this ); - } - - try { return lazyInitializationWork.doWork(); } finally { @@ -356,9 +355,9 @@ protected Boolean readElementExistence(final Object element) { @Override public boolean elementExists(Object element) { + throwLazyInitializationExceptionIfNotConnected(); final CollectionEntry entry = session.getPersistenceContextInternal().getCollectionEntry( this ); if ( entry == null ) { - throwLazyInitializationExceptionIfNotConnected(); throwLazyInitializationException("collection not associated with session"); throw new AssertionFailure("impossible"); } @@ -410,9 +409,9 @@ public Object doWork() { @Override public Object elementByIndex(Object index) { + throwLazyInitializationExceptionIfNotConnected(); final CollectionEntry entry = session.getPersistenceContextInternal().getCollectionEntry( this ); if ( entry == null ) { - throwLazyInitializationExceptionIfNotConnected(); throwLazyInitializationException("collection not associated with session"); throw new AssertionFailure("impossible"); } @@ -850,7 +849,8 @@ public final Iterator queuedAdditionIterator() { @Override public E next() { - return operationQueue.get( index++ ).getAddedInstance(); + //noinspection unchecked + return (E) operationQueue.get( index++ ).getAddedEntry(); } @Override @@ -1205,6 +1205,10 @@ protected interface DelayedOperation { void operate(); E getAddedInstance(); + + default Object getAddedEntry() { + return getAddedInstance(); + } E getOrphan(); } @@ -1298,8 +1302,8 @@ protected static Collection getOrphans( // iterate over the *old* list for ( E old : oldElements ) { if ( !currentSaving.contains( old ) ) { - final Object oldId = ForeignKeys.getEntityIdentifierIfNotUnsaved( entityName, old, session ); - if ( !currentIds.contains( useIdDirect ? oldId : new TypedValue( idType, oldId ) ) ) { + final Object oldId = ForeignKeys.getEntityIdentifier( entityName, old, session ); + if ( oldId != null && !currentIds.contains( useIdDirect ? oldId : new TypedValue( idType, oldId ) ) ) { res.add( old ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/collection/spi/PersistentArrayHolder.java b/hibernate-core/src/main/java/org/hibernate/collection/spi/PersistentArrayHolder.java index 778dce73413e..4e718070bb5a 100644 --- a/hibernate-core/src/main/java/org/hibernate/collection/spi/PersistentArrayHolder.java +++ b/hibernate-core/src/main/java/org/hibernate/collection/spi/PersistentArrayHolder.java @@ -9,6 +9,7 @@ import java.io.Serializable; import java.lang.reflect.Array; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.Iterator; @@ -94,6 +95,9 @@ public boolean isSnapshotEmpty(Serializable snapshot) { public Collection getOrphans(Serializable snapshot, String entityName) throws HibernateException { final Object[] sn = (Object[]) snapshot; final Object[] arr = (Object[]) array; + if ( arr.length == 0 ) { + return Arrays.asList( sn ); + } final ArrayList result = new ArrayList(); Collections.addAll( result, sn ); for ( int i=0; i) pluralType.getElementType(), - returnEmbeddable, options ); assert string.charAt( subEnd - 1 ) == '}'; @@ -624,7 +623,6 @@ else if ( jdbcMapping.getJavaTypeDescriptor().getJavaTypeClass().isEnum() quotes + 1, arrayList, (BasicType) pluralType.getElementType(), - returnEmbeddable, options ); assert string.charAt( i - 1 ) == '}'; @@ -667,7 +665,6 @@ private int deserializeArray( int quotes, ArrayList values, BasicType elementType, - boolean returnEmbeddable, WrapperOptions options) throws SQLException { boolean inQuote = false; StringBuilder escapingSb = null; @@ -860,30 +857,17 @@ private int deserializeArray( i + 1, quotes + 1, subValues, - returnEmbeddable, + true, options ); - if ( returnEmbeddable ) { - final StructAttributeValues attributeValues = structJdbcType.getAttributeValues( - structJdbcType.embeddableMappingType, - structJdbcType.orderMapping, - subValues, - options - ); - final Object subValue = instantiate( structJdbcType.embeddableMappingType, attributeValues, options.getSessionFactory() ); + final StructAttributeValues attributeValues = structJdbcType.getAttributeValues( + structJdbcType.embeddableMappingType, + structJdbcType.orderMapping, + subValues, + options + ); + final Object subValue = instantiate( structJdbcType.embeddableMappingType, attributeValues, options.getSessionFactory() ); values.add( subValue ); - } - else { - if ( structJdbcType.inverseOrderMapping != null ) { - StructHelper.orderJdbcValues( - structJdbcType.embeddableMappingType, - structJdbcType.inverseOrderMapping, - subValues.clone(), - subValues - ); - } - values.add( subValues ); - } // The subEnd points to the first character after the '}', // so move forward the index to point to the next char after quotes assert isDoubleQuote( string, subEnd, expectedQuotes ); @@ -1001,40 +985,8 @@ else if ( elementType.getJavaTypeDescriptor().getJavaTypeClass().isEnum() } private SelectableMapping getJdbcValueSelectable(int jdbcValueSelectableIndex) { - if ( orderMapping != null ) { - final int numberOfAttributeMappings = embeddableMappingType.getNumberOfAttributeMappings(); - final int size = numberOfAttributeMappings + ( embeddableMappingType.isPolymorphic() ? 1 : 0 ); - int count = 0; - for ( int i = 0; i < size; i++ ) { - final ValuedModelPart modelPart = getEmbeddedPart( embeddableMappingType, orderMapping[i] ); - final MappingType mappedType = modelPart.getMappedType(); - if ( mappedType instanceof EmbeddableMappingType ) { - final EmbeddableMappingType embeddableMappingType = (EmbeddableMappingType) mappedType; - final SelectableMapping aggregateMapping = embeddableMappingType.getAggregateMapping(); - if ( aggregateMapping == null ) { - final SelectableMapping subSelectable = embeddableMappingType.getJdbcValueSelectable( jdbcValueSelectableIndex - count ); - if ( subSelectable != null ) { - return subSelectable; - } - count += embeddableMappingType.getJdbcValueCount(); - } - else { - if ( count == jdbcValueSelectableIndex ) { - return aggregateMapping; - } - count++; - } - } - else { - if ( count == jdbcValueSelectableIndex ) { - return (SelectableMapping) modelPart; - } - count += modelPart.getJdbcTypeCount(); - } - } - return null; - } - return embeddableMappingType.getJdbcValueSelectable( jdbcValueSelectableIndex ); + return embeddableMappingType.getJdbcValueSelectable( + orderMapping != null ? orderMapping[jdbcValueSelectableIndex] : jdbcValueSelectableIndex ); } private static boolean repeatsChar(String string, int start, int times, char expectedChar) { @@ -1156,6 +1108,9 @@ private static String unescape(CharSequence string, int start, int end) { @Override public Object createJdbcValue(Object domainValue, WrapperOptions options) throws SQLException { assert embeddableMappingType != null; + if (domainValue == null) { + return null; + } final StringBuilder sb = new StringBuilder(); serializeStructTo( new PostgreSQLAppender( sb ), domainValue, options ); return sb.toString(); @@ -1165,7 +1120,11 @@ public Object createJdbcValue(Object domainValue, WrapperOptions options) throws public Object[] extractJdbcValues(Object rawJdbcValue, WrapperOptions options) throws SQLException { assert embeddableMappingType != null; final Object[] array = new Object[embeddableMappingType.getJdbcValueCount()]; - deserializeStruct( getRawStructFromJdbcValue( rawJdbcValue ), 0, 0, array, true, options ); + final String struct = getRawStructFromJdbcValue( rawJdbcValue ); + if ( struct == null ) { + return null; + } + deserializeStruct( struct, 0, 0, array, true, options ); if ( inverseOrderMapping != null ) { StructHelper.orderJdbcValues( embeddableMappingType, inverseOrderMapping, array.clone(), array ); } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java index c9d3c00c5550..09f712464641 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/CockroachDialect.java @@ -61,11 +61,12 @@ import org.hibernate.sql.exec.spi.JdbcOperation; import org.hibernate.tool.schema.extract.spi.ColumnTypeInformation; import org.hibernate.type.JavaObjectType; +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.UUIDJdbcType; -import org.hibernate.type.descriptor.jdbc.VarbinaryJdbcType; -import org.hibernate.type.descriptor.jdbc.VarcharJdbcType; import org.hibernate.type.descriptor.jdbc.spi.JdbcTypeRegistry; import org.hibernate.type.descriptor.sql.internal.DdlTypeImpl; import org.hibernate.type.descriptor.sql.internal.NamedNativeEnumDdlTypeImpl; @@ -394,9 +395,9 @@ protected void contributeCockroachTypes(TypeContributions typeContributions, Ser } // Force Blob binding to byte[] for CockroachDB - jdbcTypeRegistry.addDescriptor( Types.BLOB, VarbinaryJdbcType.INSTANCE ); - jdbcTypeRegistry.addDescriptor( Types.CLOB, VarcharJdbcType.INSTANCE ); - jdbcTypeRegistry.addDescriptor( Types.NCLOB, VarcharJdbcType.INSTANCE ); + jdbcTypeRegistry.addDescriptor( Types.BLOB, BlobJdbcType.MATERIALIZED ); + jdbcTypeRegistry.addDescriptor( Types.CLOB, ClobJdbcType.MATERIALIZED ); + jdbcTypeRegistry.addDescriptor( Types.NCLOB, NClobJdbcType.MATERIALIZED ); // The next two contributions are the same as for Postgresql typeContributions.contributeJdbcType( ObjectNullAsBinaryTypeJdbcType.INSTANCE ); @@ -1011,6 +1012,8 @@ public boolean supportsOuterJoinForUpdate() { @Override public boolean useInputStreamToInsertBlob() { + // PG-JDBC treats setBinaryStream()/setCharacterStream() calls like bytea/varchar, which are not LOBs, + // so disable stream bindings for this dialect completely return false; } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java index 070091a2661b..af12b1360f19 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/DB2Dialect.java @@ -61,6 +61,7 @@ import org.hibernate.metamodel.spi.RuntimeModelCreationContext; import org.hibernate.procedure.internal.DB2CallableStatementSupport; import org.hibernate.procedure.spi.CallableStatementSupport; +import org.hibernate.query.sqm.CastType; import org.hibernate.query.sqm.IntervalType; import org.hibernate.query.sqm.TemporalUnit; import org.hibernate.query.sqm.mutation.internal.cte.CteInsertStrategy; @@ -964,6 +965,20 @@ public boolean supportsLobValueChangePropagation() { return false; } + @Override + public boolean useConnectionToCreateLob() { + return false; + } + + @Override + public boolean supportsNationalizedMethods() { + // See HHH-12753, HHH-18314, HHH-19201 + // Old DB2 JDBC drivers do not support setNClob, setNCharcterStream or setNString. + // In more recent driver versions, some methods just delegate to the non-N variant, but others still fail. + // Ultimately, let's just avoid the N variant methods on DB2 altogether + return false; + } + @Override public boolean doesReadCommittedCauseWritersToBlockReaders() { return true; @@ -1249,6 +1264,16 @@ public String extractPattern(TemporalUnit unit) { return super.extractPattern( unit ); } + @Override + public String castPattern(CastType from, CastType to) { + if ( from == CastType.STRING && to == CastType.BOOLEAN ) { + return "cast(?1 as ?2)"; + } + else { + return super.castPattern( from, to ); + } + } + @Override public int getInExpressionCountLimit() { return BIND_PARAMETERS_NUMBER_LIMIT; @@ -1316,4 +1341,14 @@ public DmlTargetColumnQualifierSupport getDmlTargetColumnQualifierSupport() { public boolean supportsFromClauseInUpdate() { return getDB2Version().isSameOrAfter( 11 ); } + + @Override + public String getDual() { + return "sysibm.sysdummy1"; + } + + @Override + public String getFromDualForSelectOnly() { + return " from " + getDual(); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/DB2SqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/DB2SqlAstTranslator.java index 4e057b1d743e..2350522eccd7 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/DB2SqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/DB2SqlAstTranslator.java @@ -75,7 +75,7 @@ protected boolean supportsWithClauseInSubquery() { } @Override - protected void renderTableReferenceJoins(TableGroup tableGroup) { + protected void renderTableReferenceJoins(TableGroup tableGroup, int swappedJoinIndex, boolean forceLeftJoin) { // When we are in a recursive CTE, we can't render joins on DB2... // See https://modern-sql.com/feature/with-recursive/db2/error-345-state-42836 if ( isInRecursiveQueryPart() ) { @@ -102,7 +102,7 @@ protected void renderTableReferenceJoins(TableGroup tableGroup) { } } else { - super.renderTableReferenceJoins( tableGroup ); + super.renderTableReferenceJoins( tableGroup, swappedJoinIndex, forceLeftJoin ); } } @@ -577,24 +577,19 @@ protected boolean supportsRowValueConstructorSyntax() { return false; } - @Override - protected boolean supportsRowValueConstructorSyntaxInInList() { - return false; - } - @Override protected boolean supportsRowValueConstructorSyntaxInQuantifiedPredicates() { return false; } @Override - protected String getDual() { - return "sysibm.dual"; + protected boolean supportsRowValueConstructorSyntaxInInList() { + return false; } @Override - protected String getFromDualForSelectOnly() { - return " from " + getDual(); + protected boolean supportsRowValueConstructorSyntaxInInSubQuery() { + return true; } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/DB2iDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/DB2iDialect.java index 3b0a9fcc1212..82dd64d56578 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/DB2iDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/DB2iDialect.java @@ -6,8 +6,10 @@ */ package org.hibernate.dialect; +import org.hibernate.LockOptions; import org.hibernate.boot.model.FunctionContributions; import org.hibernate.dialect.function.CommonFunctionFactory; +import org.hibernate.dialect.function.DB2SubstringFunction; import org.hibernate.dialect.identity.DB2IdentityColumnSupport; import org.hibernate.dialect.identity.DB2zIdentityColumnSupport; import org.hibernate.dialect.identity.IdentityColumnSupport; @@ -41,6 +43,9 @@ public class DB2iDialect extends DB2Dialect { private final static DatabaseVersion MINIMUM_VERSION = DatabaseVersion.make( 7, 1 ); final static DatabaseVersion DB2_LUW_VERSION = DB2Dialect.MINIMUM_VERSION; + private static final String FOR_UPDATE_SQL = " for update with rs"; + private static final String FOR_UPDATE_SKIP_LOCKED_SQL = FOR_UPDATE_SQL + " skip locked data"; + public DB2iDialect(DialectResolutionInfo info) { this( info.makeCopyOrDefault( MINIMUM_VERSION ) ); registerKeywords( info ); @@ -61,9 +66,14 @@ protected DatabaseVersion getMinimumSupportedVersion() { @Override public void initializeFunctionRegistry(FunctionContributions functionContributions) { - super.initializeFunctionRegistry(functionContributions); + super.initializeFunctionRegistry( functionContributions ); + // DB2 for i doesn't allow code units: https://www.ibm.com/docs/en/i/7.1.0?topic=functions-substring + functionContributions.getFunctionRegistry().register( + "substring", + new DB2SubstringFunction( false, functionContributions.getTypeConfiguration() ) + ); if ( getVersion().isSameOrAfter( 7, 2 ) ) { - CommonFunctionFactory functionFactory = new CommonFunctionFactory(functionContributions); + CommonFunctionFactory functionFactory = new CommonFunctionFactory( functionContributions ); functionFactory.listagg( null ); functionFactory.inverseDistributionOrderedSetAggregates(); functionFactory.hypotheticalOrderedSetAggregates_windowEmulation(); @@ -115,7 +125,7 @@ public SequenceSupport getSequenceSupport() { @Override public String getQuerySequencesString() { if ( getVersion().isSameOrAfter(7,3) ) { - return "select distinct sequence_name from qsys2.syssequences " + + return "select distinct sequence_schema as seqschema, sequence_name as seqname, START, minimum_value as minvalue, maximum_value as maxvalue, increment from qsys2.syssequences " + "where current_schema='*LIBL' and sequence_schema in (select schema_name from qsys2.library_list_info) " + "or sequence_schema=current_schema"; } @@ -124,6 +134,7 @@ public String getQuerySequencesString() { } } + @Override public LimitHandler getLimitHandler() { return getVersion().isSameOrAfter(7, 3) @@ -153,13 +164,9 @@ protected SqlAstTranslator buildTranslator( }; } - // I speculate that this is a correct implementation of rowids for DB2 for i, - // just on the basis of the DB2 docs, but I currently have no way to test it - // Note that the implementation inherited from DB2Dialect for LUW will not work! - @Override public String rowId(String rowId) { - return rowId == null || rowId.isEmpty() ? "rowid_" : rowId; + return rowId; } @Override @@ -171,4 +178,35 @@ public int rowIdSqlType() { public String getRowIdColumnString(String rowId) { return rowId( rowId ) + " rowid not null generated always"; } + + @Override + public String getForUpdateString() { + return FOR_UPDATE_SQL; + } + + @Override + public String getForUpdateSkipLockedString() { + return supportsSkipLocked() + ? FOR_UPDATE_SKIP_LOCKED_SQL + : FOR_UPDATE_SQL; + } + + @Override + public String getForUpdateSkipLockedString(String aliases) { + return getForUpdateSkipLockedString(); + } + + @Override + public String getWriteLockString(int timeout) { + return timeout == LockOptions.SKIP_LOCKED && supportsSkipLocked() + ? FOR_UPDATE_SKIP_LOCKED_SQL + : FOR_UPDATE_SQL; + } + + @Override + public String getReadLockString(int timeout) { + return timeout == LockOptions.SKIP_LOCKED && supportsSkipLocked() + ? FOR_UPDATE_SKIP_LOCKED_SQL + : FOR_UPDATE_SQL; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/DB2iSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/DB2iSqlAstTranslator.java index 0ea3a26bfabf..5e46dbdd91a0 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/DB2iSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/DB2iSqlAstTranslator.java @@ -6,11 +6,14 @@ */ package org.hibernate.dialect; +import java.util.List; + 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; @@ -40,15 +43,15 @@ protected boolean shouldEmulateFetchClause(QueryPart queryPart) { if ( useOffsetFetchClause( queryPart ) && !isRowsOnlyFetchClauseType( queryPart ) ) { return true; } - // According to LegacyDB2LimitHandler, variable limit also isn't supported before 7.10 - return version.isBefore(7, 10) + // According to LegacyDB2LimitHandler, variable limit also isn't supported before 7.1 + return version.isBefore(7, 1) && queryPart.getFetchClauseExpression() != null && !( queryPart.getFetchClauseExpression() instanceof Literal ); } @Override protected boolean supportsOffsetClause() { - return version.isSameOrAfter(7, 10); + return version.isSameOrAfter(7, 1); } @Override @@ -56,8 +59,27 @@ 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; } + + @Override + protected String getForUpdate() { + return " for update with rs"; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/DB2zDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/DB2zDialect.java index 3726cc5a9492..08ecb068e2ea 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/DB2zDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/DB2zDialect.java @@ -215,13 +215,9 @@ protected SqlAstTranslator buildTranslator( }; } - // I speculate that this is a correct implementation of rowids for DB2 for z/OS, - // just on the basis of the DB2 docs, but I currently have no way to test it - // Note that the implementation inherited from DB2Dialect for LUW will not work! - @Override public String rowId(String rowId) { - return rowId == null || rowId.isEmpty() ? "rowid_" : rowId; + return rowId; } @Override @@ -233,4 +229,10 @@ public int rowIdSqlType() { public String getRowIdColumnString(String rowId) { return rowId( rowId ) + " rowid not null generated always"; } + + @Override + public boolean supportsValuesList() { + // DB2 z/OS has a VALUES statement, but that doesn't support multiple values + return false; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/DB2zSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/DB2zSqlAstTranslator.java index 86e7bfa76109..6ed3197284ba 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/DB2zSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/DB2zSqlAstTranslator.java @@ -83,6 +83,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/Database.java b/hibernate-core/src/main/java/org/hibernate/dialect/Database.java index 0e323ae5647e..18720bd65145 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/Database.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/Database.java @@ -44,11 +44,15 @@ public Dialect createDialect(DialectResolutionInfo info) { return new DB2zDialect( info ); } case "QSQ": { - // i + // i, this only works if "use drda metadata version" property is set to true in the drivers properties return new DB2iDialect( info ); } } } + if ("DB2 UDB for AS/400".equals(info.getDatabaseName())) { + // i + return new DB2iDialect( info ); + } return new DB2Dialect( info ); } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/DerbyDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/DerbyDialect.java index d4a5e9ff15ce..1b0ddc2fee2b 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/DerbyDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/DerbyDialect.java @@ -1040,6 +1040,11 @@ public boolean supportsWindowFunctions() { return true; } + @Override + public boolean supportsValuesList() { + return true; + } + @Override public IdentifierHelper buildIdentifierHelper(IdentifierHelperBuilder builder, DatabaseMetaData dbMetaData) throws SQLException { @@ -1057,4 +1062,14 @@ public DmlTargetColumnQualifierSupport getDmlTargetColumnQualifierSupport() { return DmlTargetColumnQualifierSupport.TABLE_ALIAS; } + @Override + public String getDual() { + return "(values 0)"; + } + + @Override + public String getFromDualForSelectOnly() { + return " from " + getDual() + " dual"; + } + } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/DerbySqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/DerbySqlAstTranslator.java index 2788fca3a688..a830f79ada6c 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/DerbySqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/DerbySqlAstTranslator.java @@ -302,16 +302,6 @@ protected boolean supportsRowValueConstructorSyntaxInQuantifiedPredicates() { return false; } - @Override - protected String getDual() { - return "(values 0)"; - } - - @Override - protected String getFromDualForSelectOnly() { - return " from " + getDual() + " dual"; - } - @Override protected boolean needsRowsToSkip() { return !supportsOffsetFetchClause(); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java index 597c2d248677..de2f6529f356 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/Dialect.java @@ -116,6 +116,7 @@ import org.hibernate.mapping.Table; import org.hibernate.mapping.UserDefinedType; import org.hibernate.metamodel.mapping.EntityMappingType; +import org.hibernate.metamodel.mapping.SqlTypedMapping; import org.hibernate.metamodel.spi.RuntimeModelCreationContext; import org.hibernate.persister.entity.Lockable; import org.hibernate.persister.entity.mutation.EntityMutationTarget; @@ -1441,7 +1442,7 @@ public String castPattern(CastType from, CastType to) { case INTEGER_BOOLEAN: switch ( from ) { case STRING: - return "case ?1 when 'T' then 1 when 'Y' then 1 when 'F' then 0 when 'N' then 0 else null end"; + return buildStringToBooleanCast( "1", "0" ); case INTEGER: case LONG: return "abs(sign(?1))"; @@ -1456,7 +1457,7 @@ public String castPattern(CastType from, CastType to) { case YN_BOOLEAN: switch ( from ) { case STRING: - return "case ?1 when 'T' then 'Y' when 'Y' then 'Y' when 'F' then 'N' when 'N' then 'N' else null end"; + return buildStringToBooleanCast( "'Y'", "'N'" ); case INTEGER_BOOLEAN: return "case ?1 when 1 then 'Y' when 0 then 'N' else null end"; case INTEGER: @@ -1471,7 +1472,7 @@ public String castPattern(CastType from, CastType to) { case TF_BOOLEAN: switch ( from ) { case STRING: - return "case ?1 when 'T' then 'T' when 'Y' then 'T' when 'F' then 'F' when 'N' then 'F' else null end"; + return buildStringToBooleanCast( "'T'", "'F'" ); case INTEGER_BOOLEAN: return "case ?1 when 1 then 'T' when 0 then 'F' else null end"; case INTEGER: @@ -1486,7 +1487,7 @@ public String castPattern(CastType from, CastType to) { case BOOLEAN: switch ( from ) { case STRING: - return "case ?1 when 'T' then true when 'Y' then true when 'F' then false when 'N' then false else null end"; + return buildStringToBooleanCast( "true", "false" ); case INTEGER_BOOLEAN: case INTEGER: case LONG: @@ -1501,6 +1502,153 @@ public String castPattern(CastType from, CastType to) { return "cast(?1 as ?2)"; } + protected static final String[] TRUE_STRING_VALUES = new String[] { "t", "true", "y", "1" }; + protected static final String[] FALSE_STRING_VALUES = new String[] { "f", "false", "n", "0" }; + + protected String buildStringToBooleanCast(String trueValue, String falseValue) { + final boolean supportsValuesList = supportsValuesList(); + final StringBuilder sb = new StringBuilder(); + sb.append( "(select v.x from (" ); + if ( supportsValuesList ) { + sb.append( "values (" ); + sb.append( trueValue ); + sb.append( "),(" ); + sb.append( falseValue ); + sb.append( ")) v(x)" ); + } + else { + sb.append( "select " ); + sb.append( trueValue ); + sb.append( " x"); + sb.append( getFromDualForSelectOnly() ); + sb.append(" union all select " ); + sb.append( falseValue ); + sb.append( getFromDualForSelectOnly() ); + sb.append( ") v" ); + } + sb.append( " left join (" ); + if ( supportsValuesList ) { + sb.append( "values" ); + char separator = ' '; + for ( String trueStringValue : Dialect.TRUE_STRING_VALUES ) { + sb.append( separator ); + sb.append( "('" ); + sb.append( trueStringValue ); + sb.append( "'," ); + sb.append( trueValue ); + sb.append( ')' ); + separator = ','; + } + for ( String falseStringValue : Dialect.FALSE_STRING_VALUES ) { + sb.append( ",('" ); + sb.append( falseStringValue ); + sb.append( "'," ); + sb.append( falseValue ); + sb.append( ')' ); + } + sb.append( ") t(k,v)" ); + } + else { + sb.append( "select '" ); + sb.append( Dialect.TRUE_STRING_VALUES[0] ); + sb.append( "' k," ); + sb.append( trueValue ); + sb.append( " v" ); + sb.append( getFromDualForSelectOnly() ); + for ( int i = 1; i < Dialect.TRUE_STRING_VALUES.length; i++ ) { + sb.append( " union all select '" ); + sb.append( Dialect.TRUE_STRING_VALUES[i] ); + sb.append( "'," ); + sb.append( trueValue ); + sb.append( getFromDualForSelectOnly() ); + } + for ( String falseStringValue : Dialect.FALSE_STRING_VALUES ) { + sb.append( " union all select '" ); + sb.append( falseStringValue ); + sb.append( "'," ); + sb.append( falseValue ); + sb.append( getFromDualForSelectOnly() ); + } + sb.append( ") t" ); + } + sb.append( " on " ); + sb.append( getLowercaseFunction() ); + sb.append( "(?1)=t.k where t.v is null or v.x=t.v)" ); + return sb.toString(); + } + + protected String buildStringToBooleanCastDecode(String trueValue, String falseValue) { + final boolean supportsValuesList = supportsValuesList(); + final StringBuilder sb = new StringBuilder(); + sb.append( "(select v.x from (" ); + if ( supportsValuesList ) { + sb.append( "values (" ); + sb.append( trueValue ); + sb.append( "),(" ); + sb.append( falseValue ); + sb.append( ")) v(x)" ); + } + else { + sb.append( "select " ); + sb.append( trueValue ); + sb.append( " x"); + sb.append( getFromDualForSelectOnly() ); + sb.append(" union all select " ); + sb.append( falseValue ); + sb.append( getFromDualForSelectOnly() ); + sb.append( ") v" ); + } + sb.append( ", (" ); + if ( supportsValuesList ) { + sb.append( "values (" ); + sb.append( buildStringToBooleanDecode( trueValue, falseValue ) ); + sb.append( ")) t(v)" ); + } + else { + sb.append( "select " ); + sb.append( buildStringToBooleanDecode( trueValue, falseValue ) ); + sb.append( " v"); + sb.append( getFromDualForSelectOnly() ); + sb.append(") t" ); + } + sb.append( " where t.v is null or v.x=t.v)" ); + return sb.toString(); + } + + protected String buildStringToBooleanDecode(String trueValue, String falseValue) { + final StringBuilder sb = new StringBuilder(); + sb.append( "decode(" ); + sb.append( getLowercaseFunction() ); + sb.append( "(?1)" ); + for ( String trueStringValue : TRUE_STRING_VALUES ) { + sb.append( ",'" ); + sb.append( trueStringValue ); + sb.append( "'," ); + sb.append( trueValue ); + } + for ( String falseStringValue : FALSE_STRING_VALUES ) { + sb.append( ",'" ); + sb.append( falseStringValue ); + sb.append( "'," ); + sb.append( falseValue ); + } + sb.append( ",null)" ); + return sb.toString(); + } + + /** + * Returns a table expression that has one row. + * + * @return the SQL equivalent to Oracle's {@code dual}. + */ + public String getDual() { + return "(values(0))"; + } + + public String getFromDualForSelectOnly() { + return ""; + } + /** * Obtain a pattern for the SQL equivalent to a * {@code trim()} function call. The resulting @@ -2968,11 +3116,31 @@ public ViolatedConstraintNameExtractor getViolatedConstraintNameExtractor() { * @param sqlType The {@link Types} type code. * @param typeConfiguration The type configuration * @return The appropriate select clause value fragment. + * @deprecated Use {@link #getSelectClauseNullString(SqlTypedMapping, TypeConfiguration)} instead */ + @Deprecated(forRemoval = true) public String getSelectClauseNullString(int sqlType, TypeConfiguration typeConfiguration) { return "null"; } + /** + * Given a type mapping, return the expression + * for a literal null value of that type, to use in a {@code select} + * clause. + *

    + * The {@code select} query will be an element of a {@code UNION} + * or {@code UNION ALL}. + * + * @implNote Some databases require an explicit type cast. + * + * @param sqlTypeMapping The type mapping. + * @param typeConfiguration The type configuration + * @return The appropriate select clause value fragment. + */ + public String getSelectClauseNullString(SqlTypedMapping sqlTypeMapping, TypeConfiguration typeConfiguration) { + return getSelectClauseNullString( sqlTypeMapping.getJdbcMapping().getJdbcType().getDdlTypeCode(), typeConfiguration ); + } + /** * Does this dialect support {@code UNION ALL}? * @@ -5627,4 +5795,26 @@ public FunctionalDependencyAnalysisSupport getFunctionalDependencyAnalysisSuppor return FunctionalDependencyAnalysisSupportImpl.NONE; } + /** + * Does this dialect support binding {@link Types#NULL} for {@link PreparedStatement#setNull(int, int)}? + * if it does, then call of {@link PreparedStatement#getParameterMetaData()} could be eliminated for better performance. + * + * @return {@code true} indicates it does; {@code false} indicates it does not; + * @see org.hibernate.type.descriptor.jdbc.ObjectNullResolvingJdbcType + */ + public boolean supportsBindingNullSqlTypeForSetNull() { + return false; + } + + /** + * Does this dialect support binding {@code null} for {@link PreparedStatement#setObject(int, Object)}? + * if it does, then call of {@link PreparedStatement#getParameterMetaData()} could be eliminated for better performance. + * + * @return {@code true} indicates it does; {@code false} indicates it does not; + * @see org.hibernate.type.descriptor.jdbc.ObjectNullResolvingJdbcType + */ + public boolean supportsBindingNullForSetObject() { + return false; + } + } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/DialectDelegateWrapper.java b/hibernate-core/src/main/java/org/hibernate/dialect/DialectDelegateWrapper.java index e048ad1d6ad0..18a36b649ce9 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/DialectDelegateWrapper.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/DialectDelegateWrapper.java @@ -55,6 +55,7 @@ import org.hibernate.mapping.Table; import org.hibernate.mapping.UserDefinedType; import org.hibernate.metamodel.mapping.EntityMappingType; +import org.hibernate.metamodel.mapping.SqlTypedMapping; import org.hibernate.metamodel.spi.RuntimeModelCreationContext; import org.hibernate.persister.entity.Lockable; import org.hibernate.persister.entity.mutation.EntityMutationTarget; @@ -731,6 +732,11 @@ public String getSelectClauseNullString(int sqlType, TypeConfiguration typeConfi return wrapped.getSelectClauseNullString( sqlType, typeConfiguration ); } + @Override + public String getSelectClauseNullString(SqlTypedMapping sqlTypeMapping, TypeConfiguration typeConfiguration) { + return wrapped.getSelectClauseNullString( sqlTypeMapping, typeConfiguration ); + } + @Override public boolean supportsUnionAll() { return wrapped.supportsUnionAll(); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java index 622479b703e7..e9c299548105 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/H2Dialect.java @@ -44,6 +44,7 @@ import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.spi.RuntimeModelCreationContext; import org.hibernate.persister.entity.mutation.EntityMutationTarget; +import org.hibernate.query.sqm.CastType; import org.hibernate.query.sqm.FetchClauseType; import org.hibernate.query.sqm.IntervalType; import org.hibernate.query.sqm.TemporalUnit; @@ -445,6 +446,16 @@ public String extractPattern(TemporalUnit unit) { : super.extractPattern(unit); } + @Override + public String castPattern(CastType from, CastType to) { + if ( from == CastType.STRING && to == CastType.BOOLEAN ) { + return "cast(?1 as ?2)"; + } + else { + return super.castPattern( from, to ); + } + } + @Override public String timestampaddPattern(TemporalUnit unit, TemporalType temporalType, IntervalType intervalType) { if ( intervalType != null ) { @@ -1007,4 +1018,19 @@ public boolean supportsCaseInsensitiveLike(){ return true; } + @Override + public boolean supportsValuesList() { + return true; + } + + @Override + public String getDual() { + return "dual"; + } + + @Override + public boolean supportsBindingNullSqlTypeForSetNull() { + return true; + } + } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/H2SqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/H2SqlAstTranslator.java index 4554d948dac5..fe824b0dd052 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/H2SqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/H2SqlAstTranslator.java @@ -314,13 +314,9 @@ protected boolean renderPrimaryTableReference(TableGroup tableGroup, LockMode lo final TableReference tableRef = tableGroup.getPrimaryTableReference(); // The H2 parser can't handle a sub-query as first element in a nested join // i.e. `join ( (select ...) alias join ... )`, so we have to introduce a dummy table reference - if ( tableRef instanceof QueryPartTableReference || tableRef.getTableId().startsWith( "(select" ) ) { - final boolean realTableGroup = tableGroup.isRealTableGroup() - && ( isNotEmpty( tableGroup.getTableReferenceJoins() ) - || hasNestedTableGroupsToRender( tableGroup.getNestedTableGroupJoins() ) ); - if ( realTableGroup ) { - appendSql( "dual cross join " ); - } + if ( getSqlBuffer().charAt( getSqlBuffer().length() - 1 ) == '(' + && ( tableRef instanceof QueryPartTableReference || tableRef.getTableId().startsWith( "(select" ) ) ) { + appendSql( "dual cross join " ); } return super.renderPrimaryTableReference( tableGroup, lockMode ); } @@ -365,11 +361,6 @@ protected boolean supportsNullPrecedence() { return true; } - @Override - protected String getDual() { - return "dual"; - } - private boolean supportsOffsetFetchClause() { return true; } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/HANAServerConfiguration.java b/hibernate-core/src/main/java/org/hibernate/dialect/HANAServerConfiguration.java index d582326cf294..8442ace83d9a 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/HANAServerConfiguration.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/HANAServerConfiguration.java @@ -50,21 +50,34 @@ public static HANAServerConfiguration fromDialectResolutionInfo(DialectResolutio Integer maxLobPrefetchSize = null; final DatabaseMetaData databaseMetaData = info.getDatabaseMetadata(); if ( databaseMetaData != null ) { - try (final Statement statement = databaseMetaData.getConnection().createStatement()) { - try ( ResultSet rs = statement.executeQuery( - "SELECT TOP 1 VALUE,MAP(LAYER_NAME,'DEFAULT',1,'SYSTEM',2,'DATABASE',3,4) AS LAYER FROM SYS.M_INIFILE_CONTENTS WHERE FILE_NAME='indexserver.ini' AND SECTION='session' AND KEY='max_lob_prefetch_size' ORDER BY LAYER DESC" ) ) { - // This only works if the current user has the privilege INIFILE ADMIN - if ( rs.next() ) { - maxLobPrefetchSize = rs.getInt( 1 ); - } - } + int databaseMajorVersion = -1; + try { + databaseMajorVersion = databaseMetaData.getDatabaseMajorVersion(); } catch (SQLException e) { // Ignore LOG.debug( - "An error occurred while trying to determine the value of the HANA parameter indexserver.ini / session / max_lob_prefetch_size.", + "An error occurred while trying to determine the database version.", e ); } + + if (databaseMajorVersion > 0 && databaseMajorVersion < 4) { + try (final Statement statement = databaseMetaData.getConnection().createStatement()) { + try ( ResultSet rs = statement.executeQuery( + "SELECT TOP 1 VALUE,MAP(LAYER_NAME,'DEFAULT',1,'SYSTEM',2,'DATABASE',3,4) AS LAYER FROM SYS.M_CONFIGURATION_PARAMETER_VALUES WHERE FILE_NAME='indexserver.ini' AND SECTION='session' AND KEY='max_lob_prefetch_size' ORDER BY LAYER DESC" ) ) { + // This only works if the current user has the privilege INIFILE ADMIN + if ( rs.next() ) { + maxLobPrefetchSize = rs.getInt( 1 ); + } + } + } + catch (SQLException e) { + // Ignore + LOG.debug( + "An error occurred while trying to determine the value of the HANA parameter indexserver.ini / session / max_lob_prefetch_size.", + e ); + } + } } // default to the dialect-specific configuration settings if ( maxLobPrefetchSize == null ) { diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/HANASqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/HANASqlAstTranslator.java index c77eac47d6ce..859e1fa38f26 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/HANASqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/HANASqlAstTranslator.java @@ -252,16 +252,6 @@ protected boolean supportsRowValueConstructorGtLtSyntax() { return false; } - @Override - protected String getDual() { - return "sys.dummy"; - } - - @Override - protected String getFromDualForSelectOnly() { - return " from " + getDual(); - } - @Override protected void renderInsertIntoNoColumns(TableInsertStandard tableInsert) { throw new MappingException( diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/HSQLDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/HSQLDialect.java index fa0ca8189f4a..91f831288e19 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/HSQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/HSQLDialect.java @@ -255,25 +255,33 @@ public String castPattern(CastType from, CastType to) { } break; case BOOLEAN: - result = BooleanDecoder.toBoolean( from ); + result = from == CastType.STRING + ? buildStringToBooleanCastDecode( "true", "false" ) + : BooleanDecoder.toBoolean( from ); if ( result != null ) { return result; } break; case INTEGER_BOOLEAN: - result = BooleanDecoder.toIntegerBoolean( from ); + result = from == CastType.STRING + ? buildStringToBooleanCastDecode( "1", "0" ) + : BooleanDecoder.toIntegerBoolean( from ); if ( result != null ) { return result; } break; case YN_BOOLEAN: - result = BooleanDecoder.toYesNoBoolean( from ); + result = from == CastType.STRING + ? buildStringToBooleanCastDecode( "'Y'", "'N'" ) + : BooleanDecoder.toYesNoBoolean( from ); if ( result != null ) { return result; } break; case TF_BOOLEAN: - result = BooleanDecoder.toTrueFalseBoolean( from ); + result = from == CastType.STRING + ? buildStringToBooleanCastDecode( "'T'", "'F'" ) + : BooleanDecoder.toTrueFalseBoolean( from ); if ( result != null ) { return result; } @@ -647,6 +655,11 @@ public boolean requiresFloatCastingOfIntegerDivision() { return true; } + @Override + public boolean supportsValuesList() { + return true; + } + @Override public IdentityColumnSupport getIdentityColumnSupport() { return identityColumnSupport; @@ -727,4 +740,9 @@ public String quoteCollation(String collation) { public DmlTargetColumnQualifierSupport getDmlTargetColumnQualifierSupport() { return DmlTargetColumnQualifierSupport.TABLE_ALIAS; } + + @Override + public String getFromDualForSelectOnly() { + return " from " + getDual(); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/HSQLSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/HSQLSqlAstTranslator.java index f458720b9b7a..9ba0d4a5eb9f 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/HSQLSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/HSQLSqlAstTranslator.java @@ -12,14 +12,12 @@ import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.metamodel.mapping.JdbcMappingContainer; import org.hibernate.query.IllegalQueryOperationException; -import org.hibernate.query.sqm.BinaryArithmeticOperator; import org.hibernate.query.sqm.ComparisonOperator; import org.hibernate.sql.ast.Clause; import org.hibernate.sql.ast.SqlAstNodeRenderingMode; import org.hibernate.sql.ast.spi.AbstractSqlAstTranslator; import org.hibernate.sql.ast.spi.SqlSelection; import org.hibernate.sql.ast.tree.Statement; -import org.hibernate.sql.ast.tree.expression.BinaryArithmeticExpression; import org.hibernate.sql.ast.tree.expression.CaseSearchedExpression; import org.hibernate.sql.ast.tree.expression.CaseSimpleExpression; import org.hibernate.sql.ast.tree.expression.Expression; @@ -152,8 +150,7 @@ protected void visitRecursivePath(Expression recursivePath, int sizeEstimate) { protected void visitAnsiCaseSearchedExpression( CaseSearchedExpression expression, Consumer resultRenderer) { - if ( getParameterRenderingMode() == SqlAstNodeRenderingMode.DEFAULT && areAllResultsParameters( expression ) - || areAllResultsPlainParametersOrLiterals( expression ) ) { + if ( areAllResultsPlainParametersOrStringLiterals( expression ) ) { final List whenFragments = expression.getWhenFragments(); final Expression firstResult = whenFragments.get( 0 ).getResult(); super.visitAnsiCaseSearchedExpression( @@ -177,8 +174,7 @@ protected void visitAnsiCaseSearchedExpression( protected void visitAnsiCaseSimpleExpression( CaseSimpleExpression expression, Consumer resultRenderer) { - if ( getParameterRenderingMode() == SqlAstNodeRenderingMode.DEFAULT && areAllResultsParameters( expression ) - || areAllResultsPlainParametersOrLiterals( expression ) ) { + if ( areAllResultsPlainParametersOrStringLiterals( expression ) ) { final List whenFragments = expression.getWhenFragments(); final Expression firstResult = whenFragments.get( 0 ).getResult(); super.visitAnsiCaseSimpleExpression( @@ -198,11 +194,11 @@ protected void visitAnsiCaseSimpleExpression( } } - protected boolean areAllResultsPlainParametersOrLiterals(CaseSearchedExpression caseSearchedExpression) { + protected boolean areAllResultsPlainParametersOrStringLiterals(CaseSearchedExpression caseSearchedExpression) { final List whenFragments = caseSearchedExpression.getWhenFragments(); final Expression firstResult = whenFragments.get( 0 ).getResult(); if ( isParameter( firstResult ) && getParameterRenderingMode() == SqlAstNodeRenderingMode.DEFAULT - || isLiteral( firstResult ) ) { + || isStringLiteral( firstResult ) ) { for ( int i = 1; i < whenFragments.size(); i++ ) { final Expression result = whenFragments.get( i ).getResult(); if ( isParameter( result ) ) { @@ -210,7 +206,7 @@ protected boolean areAllResultsPlainParametersOrLiterals(CaseSearchedExpression return false; } } - else if ( !isLiteral( result ) ) { + else if ( !isStringLiteral( result ) ) { return false; } } @@ -219,11 +215,11 @@ else if ( !isLiteral( result ) ) { return false; } - protected boolean areAllResultsPlainParametersOrLiterals(CaseSimpleExpression caseSimpleExpression) { + protected boolean areAllResultsPlainParametersOrStringLiterals(CaseSimpleExpression caseSimpleExpression) { final List whenFragments = caseSimpleExpression.getWhenFragments(); final Expression firstResult = whenFragments.get( 0 ).getResult(); if ( isParameter( firstResult ) && getParameterRenderingMode() == SqlAstNodeRenderingMode.DEFAULT - || isLiteral( firstResult ) ) { + || isStringLiteral( firstResult ) ) { for ( int i = 1; i < whenFragments.size(); i++ ) { final Expression result = whenFragments.get( i ).getResult(); if ( isParameter( result ) ) { @@ -231,7 +227,7 @@ protected boolean areAllResultsPlainParametersOrLiterals(CaseSimpleExpression ca return false; } } - else if ( !isLiteral( result ) ) { + else if ( !isStringLiteral( result ) ) { return false; } } @@ -240,6 +236,13 @@ else if ( !isLiteral( result ) ) { return false; } + private boolean isStringLiteral( Expression expression ) { + if ( expression instanceof Literal ) { + return ( (Literal) expression ).getJdbcMapping().getJdbcType().isStringLike(); + } + return false; + } + @Override public boolean supportsFilterClause() { return true; @@ -319,22 +322,20 @@ else if ( expression instanceof Summarization ) { @Override protected boolean supportsRowValueConstructorSyntax() { - return false; - } - - @Override - protected boolean supportsRowValueConstructorSyntaxInInList() { + // It's supported but not usable due to a bug: https://sourceforge.net/p/hsqldb/bugs/1714/ return false; } @Override protected boolean supportsRowValueConstructorSyntaxInQuantifiedPredicates() { + // It's supported but not usable due to a bug: https://sourceforge.net/p/hsqldb/bugs/1714/ return false; } @Override - protected String getFromDualForSelectOnly() { - return " from " + getDual(); + protected boolean supportsRowValueConstructorSyntaxInInList() { + // It's supported but not usable due to a bug: https://sourceforge.net/p/hsqldb/bugs/1714/ + return false; } private boolean supportsOffsetFetchClause() { diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/MariaDBDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/MariaDBDialect.java index c77ee258a435..6dc4ce2bda91 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/MariaDBDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/MariaDBDialect.java @@ -9,6 +9,7 @@ import java.sql.DatabaseMetaData; import java.sql.SQLException; +import org.hibernate.PessimisticLockException; import org.hibernate.boot.model.FunctionContributions; import org.hibernate.boot.model.TypeContributions; import org.hibernate.dialect.function.CommonFunctionFactory; @@ -21,6 +22,10 @@ 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.LockAcquisitionException; +import org.hibernate.exception.LockTimeoutException; +import org.hibernate.exception.spi.SQLExceptionConversionDelegate; import org.hibernate.service.ServiceRegistry; import org.hibernate.sql.ast.SqlAstTranslator; import org.hibernate.sql.ast.SqlAstTranslatorFactory; @@ -37,6 +42,7 @@ import org.hibernate.type.descriptor.sql.internal.DdlTypeImpl; import org.hibernate.type.descriptor.sql.spi.DdlTypeRegistry; +import static org.hibernate.internal.util.JdbcExceptionHelper.extractSqlState; import static org.hibernate.query.sqm.produce.function.FunctionParameterType.NUMERIC; import static org.hibernate.type.SqlTypes.GEOMETRY; import static org.hibernate.type.SqlTypes.OTHER; @@ -276,4 +282,49 @@ public IdentifierHelper buildIdentifierHelper(IdentifierHelperBuilder builder, D return super.buildIdentifierHelper( builder, dbMetaData ); } + + @Override + public String getDual() { + return "dual"; + } + + @Override + public SQLExceptionConversionDelegate buildSQLExceptionConversionDelegate() { + return (sqlException, message, sql) -> { + switch ( sqlException.getErrorCode() ) { + // If @@innodb_snapshot_isolation is set (default since 11.6.2), + // if an attempt to acquire a lock on a record that does not exist in the current read view is made, + // an error DB_RECORD_CHANGED will be raised. + case 1020: + return new LockAcquisitionException( message, sqlException, sql ); + case 1205: + case 3572: + return new PessimisticLockException( message, sqlException, sql ); + case 1207: + case 1206: + return new LockAcquisitionException( message, sqlException, sql ); + case 1062: + // Unique constraint violation + return new ConstraintViolationException( + message, + sqlException, + sql, + ConstraintViolationException.ConstraintKind.UNIQUE, + getViolatedConstraintNameExtractor().extractConstraintName( sqlException ) + ); + } + + final String sqlState = extractSqlState( sqlException ); + if ( sqlState != null ) { + switch ( sqlState ) { + case "41000": + return new LockTimeoutException( message, sqlException, sql ); + case "40001": + return new LockAcquisitionException( message, sqlException, sql ); + } + } + + return null; + }; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/MariaDBSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/MariaDBSqlAstTranslator.java index 78272afe716e..e6bbee577cfd 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/MariaDBSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/MariaDBSqlAstTranslator.java @@ -369,11 +369,6 @@ protected boolean supportsDistinctFromPredicate() { return true; } - @Override - protected String getDual() { - return "dual"; - } - @Override public MariaDBDialect getDialect() { return this.dialect; @@ -403,4 +398,13 @@ protected void renderStringContainsExactlyPredicate(Expression haystack, Express appendSql( ",'~','~~'),'?','~?'),'%','~%'),'%') escape '~'" ); } + @Override + protected void appendAssignmentColumn(ColumnReference column) { + column.appendColumnForWrite( + this, + getAffectedTableNames().size() > 1 && !(getStatement() instanceof InsertSelectStatement) + ? determineColumnReferenceQualifier( column ) + : null ); + } + } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java index 08a425ddda31..2473fc233045 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java @@ -47,6 +47,7 @@ import org.hibernate.exception.spi.TemplatedViolatedConstraintNameExtractor; import org.hibernate.exception.spi.ViolatedConstraintNameExtractor; import org.hibernate.internal.util.JdbcExceptionHelper; +import org.hibernate.internal.util.StringHelper; import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.spi.RuntimeModelCreationContext; import org.hibernate.query.sqm.CastType; @@ -210,7 +211,7 @@ protected static DatabaseVersion createVersion(DialectResolutionInfo info) { protected static DatabaseVersion createVersion(DialectResolutionInfo info, DatabaseVersion defaultVersion) { final String versionString = info.getDatabaseVersion(); if ( versionString != null ) { - final String[] components = versionString.split( "\\." ); + final String[] components = StringHelper.split(".-", versionString); if ( components.length >= 3 ) { try { final int majorVersion = Integer.parseInt( components[0] ); @@ -653,11 +654,6 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio SqmFunctionRegistry functionRegistry = functionContributions.getFunctionRegistry(); - functionRegistry.noArgsBuilder( "localtime" ) - .setInvariantType(basicTypeRegistry.resolve( StandardBasicTypes.TIMESTAMP )) - .setUseParenthesesWhenNoArgs( false ) - .register(); - // pi() produces a value with 7 digits unless we're explicit functionRegistry.patternDescriptorBuilder( "pi", "cast(pi() as double)" ) .setInvariantType( basicTypeRegistry.resolve( StandardBasicTypes.DOUBLE ) ) @@ -1571,4 +1567,14 @@ public DmlTargetColumnQualifierSupport getDmlTargetColumnQualifierSupport() { public boolean supportsFromClauseInUpdate() { return true; } + + @Override + public String getDual() { + return "dual"; + } + + @Override + public boolean supportsBindingNullSqlTypeForSetNull() { + return true; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/MySQLSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/MySQLSqlAstTranslator.java index 6be62a55c40e..23bb81496e90 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/MySQLSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/MySQLSqlAstTranslator.java @@ -443,11 +443,6 @@ protected boolean supportsWithClause() { return true; } - @Override - protected String getDual() { - return "dual"; - } - @Override public MySQLDialect getDialect() { return (MySQLDialect) DialectDelegateWrapper.extractRealDialect( super.getDialect() ); @@ -472,4 +467,13 @@ protected void renderStringContainsExactlyPredicate(Expression haystack, Express needle.accept( this ); appendSql( ",'~','~~'),'?','~?'),'%','~%'),'%') escape '~'" ); } + + @Override + protected void appendAssignmentColumn(ColumnReference column) { + column.appendColumnForWrite( + this, + getAffectedTableNames().size() > 1 && !(getStatement() instanceof InsertSelectStatement) + ? determineColumnReferenceQualifier( column ) + : null ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java index 24e33db59d31..dbab33346ec8 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/OracleDialect.java @@ -13,6 +13,7 @@ import java.sql.Types; import java.time.temporal.ChronoField; import java.time.temporal.TemporalAccessor; +import java.util.List; import java.util.TimeZone; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -25,6 +26,7 @@ import org.hibernate.dialect.aggregate.OracleAggregateSupport; import org.hibernate.dialect.function.CommonFunctionFactory; import org.hibernate.dialect.function.ModeStatsModeEmulation; +import org.hibernate.dialect.function.OracleExtractFunction; import org.hibernate.dialect.function.OracleTruncFunction; import org.hibernate.dialect.identity.IdentityColumnSupport; import org.hibernate.dialect.identity.Oracle12cIdentityColumnSupport; @@ -103,6 +105,7 @@ import jakarta.persistence.TemporalType; +import static java.lang.String.join; import static java.util.regex.Pattern.CASE_INSENSITIVE; import static org.hibernate.LockOptions.NO_WAIT; import static org.hibernate.LockOptions.SKIP_LOCKED; @@ -386,6 +389,11 @@ public void initializeFunctionRegistry(FunctionContributions functionContributio functionFactory.arrayTrim_oracle(); functionFactory.arrayFill_oracle(); functionFactory.arrayToString_oracle(); + + functionContributions.getFunctionRegistry().register( + "extract", + new OracleExtractFunction( this, typeConfiguration ) + ); } @Override @@ -462,25 +470,33 @@ public String castPattern(CastType from, CastType to) { } break; case INTEGER_BOOLEAN: - result = BooleanDecoder.toIntegerBoolean( from ); + result = from == CastType.STRING + ? buildStringToBooleanCastDecode( "1", "0" ) + : BooleanDecoder.toIntegerBoolean( from ); if ( result != null ) { return result; } break; case YN_BOOLEAN: - result = BooleanDecoder.toYesNoBoolean( from ); + result = from == CastType.STRING + ? buildStringToBooleanCastDecode( "'Y'", "'N'" ) + : BooleanDecoder.toYesNoBoolean( from ); if ( result != null ) { return result; } break; case BOOLEAN: - result = BooleanDecoder.toBoolean( from ); + result = from == CastType.STRING + ? buildStringToBooleanCastDecode( "true", "false" ) + : BooleanDecoder.toBoolean( from ); if ( result != null ) { return result; } break; case TF_BOOLEAN: - result = BooleanDecoder.toTrueFalseBoolean( from ); + result = from == CastType.STRING + ? buildStringToBooleanCastDecode( "'T'", "'F'" ) + : BooleanDecoder.toTrueFalseBoolean( from ); if ( result != null ) { return result; } @@ -1285,6 +1301,17 @@ public boolean useFollowOnLocking(String sql, QueryOptions queryOptions) { || queryOptions.hasLimit() && queryOptions.getLimit().getFirstRow() != null; } + @Override + public String getQueryHintString(String query, List hintList) { + if ( hintList.isEmpty() ) { + return query; + } + else { + final String hints = join( " ", hintList ); + return isEmpty( hints ) ? query : getQueryHintString( query, hints ); + } + } + @Override public String getQueryHintString(String sql, String hints) { final String statementType = statementType( sql ); @@ -1683,8 +1710,18 @@ public String[] getDropEnumTypeCommand(String name) { @Override public boolean useInputStreamToInsertBlob() { - // see HHH-18206 - return false; + // If application continuity is enabled, don't use stream bindings, since a replay could otherwise fail + // if the underlying stream doesn't support mark and reset + return !isApplicationContinuity(); + } + @Override + public String getDual() { + return "dual"; + } + + @Override + public String getFromDualForSelectOnly() { + return getVersion().isSameOrAfter( 23 ) ? "" : ( " from " + getDual() ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/OracleSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/OracleSqlAstTranslator.java index 00558358553b..6f87c4f0cf65 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/OracleSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/OracleSqlAstTranslator.java @@ -617,16 +617,6 @@ protected boolean supportsRowValueConstructorSyntaxInQuantifiedPredicates() { return false; } - @Override - protected String getDual() { - return "dual"; - } - - @Override - protected String getFromDualForSelectOnly() { - return getDialect().getVersion().isSameOrAfter( 23 ) ? "" : ( " from " + getDual() ); - } - private boolean supportsOffsetFetchClause() { return getDialect().supportsFetchClause( FetchClauseType.ROWS_ONLY ); } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLCastingIntervalSecondJdbcType.java b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLCastingIntervalSecondJdbcType.java index 5be4dc1bf508..a5508348a008 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLCastingIntervalSecondJdbcType.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLCastingIntervalSecondJdbcType.java @@ -79,7 +79,7 @@ public void appendWriteExpression( Dialect dialect) { appender.append( '(' ); appender.append( writeExpression ); - appender.append( "*interval'1 second)" ); + appender.append( "*interval'1 second')" ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java index 67fa1ce56a10..e39bb3c9f4db 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/PostgreSQLDialect.java @@ -52,12 +52,15 @@ import org.hibernate.exception.spi.ViolatedConstraintNameExtractor; import org.hibernate.internal.util.JdbcExceptionHelper; import org.hibernate.metamodel.mapping.EntityMappingType; +import org.hibernate.metamodel.mapping.SqlExpressible; +import org.hibernate.metamodel.mapping.SqlTypedMapping; import org.hibernate.metamodel.spi.RuntimeModelCreationContext; import org.hibernate.persister.entity.mutation.EntityMutationTarget; import org.hibernate.procedure.internal.PostgreSQLCallableStatementSupport; import org.hibernate.procedure.spi.CallableStatementSupport; import org.hibernate.query.SemanticException; import org.hibernate.query.spi.QueryOptions; +import org.hibernate.query.sqm.CastType; import org.hibernate.query.sqm.FetchClauseType; import org.hibernate.query.sqm.IntervalType; import org.hibernate.query.sqm.TemporalUnit; @@ -178,6 +181,10 @@ private static OptionalTableUpdateStrategy determineOptionalTableUpdateStrategy( : PostgreSQLDialect::withoutMerge; } + public PostgreSQLDriverKind getDriverKind() { + return driverKind; + } + @Override protected DatabaseVersion getMinimumSupportedVersion() { return MINIMUM_VERSION; @@ -242,6 +249,7 @@ protected String castType(int sqlTypeCode) { case NCHAR: case VARCHAR: case NVARCHAR: + return "varchar"; case LONG32VARCHAR: case LONG32NVARCHAR: return "text"; @@ -467,6 +475,16 @@ public String extractPattern(TemporalUnit unit) { } } + @Override + public String castPattern(CastType from, CastType to) { + if ( from == CastType.STRING && to == CastType.BOOLEAN ) { + return "cast(?1 as ?2)"; + } + else { + return super.castPattern( from, to ); + } + } + /** * {@code microsecond} is the smallest unit for an {@code interval}, * and the highest precision for a {@code timestamp}, so we could @@ -909,6 +927,8 @@ public boolean supportsOuterJoinForUpdate() { @Override public boolean useInputStreamToInsertBlob() { + // PG-JDBC treats setBinaryStream()/setCharacterStream() calls like bytea/varchar, which are not LOBs, + // so disable stream bindings for this dialect completely return false; } @@ -924,6 +944,17 @@ public String getSelectClauseNullString(int sqlType, TypeConfiguration typeConfi return "cast(null as " + typeConfiguration.getDdlTypeRegistry().getDescriptor( sqlType ).getRawTypeName() + ")"; } + @Override + public String getSelectClauseNullString(SqlTypedMapping sqlType, TypeConfiguration typeConfiguration) { + final DdlTypeRegistry ddlTypeRegistry = typeConfiguration.getDdlTypeRegistry(); + final String castTypeName = ddlTypeRegistry + .getDescriptor( sqlType.getJdbcMapping().getJdbcType().getDdlTypeCode() ) + .getCastTypeName( sqlType.toSize(), (SqlExpressible) sqlType.getJdbcMapping(), ddlTypeRegistry ); + // PostgreSQL assumes a plain null literal in the select statement to be of type text, + // which can lead to issues in e.g. the union subclass strategy, so do a cast + return "cast(null as " + castTypeName + ")"; + } + @Override public String quoteCollation(String collation) { return '\"' + collation + '\"'; @@ -1598,4 +1629,9 @@ public DmlTargetColumnQualifierSupport getDmlTargetColumnQualifierSupport() { public boolean supportsFromClauseInUpdate() { return true; } + + @Override + public boolean supportsBindingNullSqlTypeForSetNull() { + return true; + } } 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 1ecf96529d22..ec129e43a5ff 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SQLServerDialect.java @@ -1202,4 +1202,9 @@ public CallableStatementSupport getCallableStatementSupport() { return SQLServerCallableStatementSupport.INSTANCE; } + + @Override + public boolean supportsBindingNullForSetObject() { + return true; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/StructHelper.java b/hibernate-core/src/main/java/org/hibernate/dialect/StructHelper.java index 1b9ab7c6369e..9af79531a4f8 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/StructHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/StructHelper.java @@ -12,6 +12,7 @@ import java.sql.NClob; import java.sql.SQLException; +import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.Internal; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.metamodel.mapping.BasicValuedMapping; @@ -104,6 +105,9 @@ public static Object[] getJdbcValues( WrapperOptions options) throws SQLException { final int jdbcValueCount = embeddableMappingType.getJdbcValueCount(); final int valueCount = jdbcValueCount + ( embeddableMappingType.isPolymorphic() ? 1 : 0 ); + if (domainValue == null) { + return null; + } final Object[] values = embeddableMappingType.getValues( domainValue ); final Object[] jdbcValues; if ( valueCount != values.length || orderMapping != null ) { @@ -130,13 +134,13 @@ public static Object[] getJdbcValues( private static int injectJdbcValues( EmbeddableMappingType embeddableMappingType, - Object domainValue, + @Nullable Object domainValue, Object[] jdbcValues, int jdbcIndex, WrapperOptions options) throws SQLException { return injectJdbcValues( embeddableMappingType, - embeddableMappingType.getValues( domainValue ), + domainValue == null ? null : embeddableMappingType.getValues( domainValue ), jdbcValues, jdbcIndex, options @@ -145,12 +149,15 @@ private static int injectJdbcValues( private static int injectJdbcValues( EmbeddableMappingType embeddableMappingType, - Object[] values, + @Nullable Object[] values, Object[] jdbcValues, int jdbcIndex, WrapperOptions options) throws SQLException { final int jdbcValueCount = embeddableMappingType.getJdbcValueCount(); final int valueCount = jdbcValueCount + ( embeddableMappingType.isPolymorphic() ? 1 : 0 ); + if ( values == null ) { + return valueCount; + } int offset = 0; for ( int i = 0; i < values.length; i++ ) { offset += injectJdbcValue( @@ -255,22 +262,13 @@ else if ( attributeMapping instanceof EmbeddableValuedModelPart ) { ); } else { - jdbcValueCount = embeddableMappingType.getJdbcValueCount() + ( embeddableMappingType.isPolymorphic() ? 1 : 0 ); - final int numberOfAttributeMappings = embeddableMappingType.getNumberOfAttributeMappings(); - final int numberOfValues = numberOfAttributeMappings + ( embeddableMappingType.isPolymorphic() ? 1 : 0 ); - final Object[] subValues = embeddableMappingType.getValues( attributeValues[attributeIndex] ); - int offset = 0; - for ( int i = 0; i < numberOfValues; i++ ) { - offset += injectJdbcValue( - getEmbeddedPart( embeddableMappingType, i ), - subValues, - i, - jdbcValues, - jdbcIndex + offset, - options - ); - } - assert offset == jdbcValueCount; + jdbcValueCount = injectJdbcValues( + embeddableMappingType, + attributeValues[attributeIndex], + jdbcValues, + jdbcIndex, + options + ); } } else { diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/StructJdbcType.java b/hibernate-core/src/main/java/org/hibernate/dialect/StructJdbcType.java index 2ae5ed01f137..6f5fac124402 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/StructJdbcType.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/StructJdbcType.java @@ -141,11 +141,13 @@ public Object createJdbcValue(Object domainValue, WrapperOptions options) throws domainValue, options ); - return options.getSession() + return jdbcValues == null + ? null + : options.getSession() .getJdbcCoordinator() .getLogicalConnection() .getPhysicalConnection() - .createStruct( typeName, jdbcValues ); + .createStruct(typeName, jdbcValues); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/SybaseASEDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/SybaseASEDialect.java index 52d320a52b4f..0eaf758b2dd9 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SybaseASEDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SybaseASEDialect.java @@ -142,13 +142,8 @@ protected void registerColumnTypes(TypeContributions typeContributions, ServiceR // But with jTDS we can't use them as the driver can't handle the types if ( getDriverKind() != SybaseDriverKind.JTDS ) { ddlTypeRegistry.addDescriptor( - CapacityDependentDdlType.builder( DATE, "bigdatetime", "bigdatetime", this ) - .withTypeCapacity( 3, "datetime" ) - .build() - ); - ddlTypeRegistry.addDescriptor( - CapacityDependentDdlType.builder( TIME, "bigdatetime", "bigdatetime", this ) - .withTypeCapacity( 3, "datetime" ) + CapacityDependentDdlType.builder( TIME, "bigtime", "bigtime", this ) + .withTypeCapacity( 3, "time" ) .build() ); ddlTypeRegistry.addDescriptor( @@ -712,4 +707,9 @@ public SQLExceptionConversionDelegate buildSQLExceptionConversionDelegate() { public LimitHandler getLimitHandler() { return new TopLimitHandler(false); } + + @Override + public String getDual() { + return "(select 1 c1)"; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/SybaseASESqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/SybaseASESqlAstTranslator.java index eee92fa20b04..6fd185de98bc 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/SybaseASESqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/SybaseASESqlAstTranslator.java @@ -508,11 +508,6 @@ protected boolean supportsRowValueConstructorSyntaxInQuantifiedPredicates() { return false; } - @Override - protected String getDual() { - return "(select 1 c1)"; - } - private boolean supportsParameterOffsetFetchExpression() { return false; } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/TiDBDialect.java b/hibernate-core/src/main/java/org/hibernate/dialect/TiDBDialect.java index 524128640903..4557bbfb53c8 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/TiDBDialect.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/TiDBDialect.java @@ -190,4 +190,9 @@ public String timestampaddPattern(TemporalUnit unit, TemporalType temporalType, } return super.timestampaddPattern( unit, temporalType, intervalType ); } + + @Override + public String getDual() { + return "dual"; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/TiDBSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/dialect/TiDBSqlAstTranslator.java index 1a4320239020..751e3ba6c810 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/TiDBSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/TiDBSqlAstTranslator.java @@ -332,11 +332,6 @@ protected boolean supportsRowValueConstructorSyntaxInQuantifiedPredicates() { return false; } - @Override - protected String getDual() { - return "dual"; - } - @Override protected String getForShare(int timeoutMillis) { if ( timeoutMillis == LockOptions.NO_WAIT ) { diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/XmlHelper.java b/hibernate-core/src/main/java/org/hibernate/dialect/XmlHelper.java index f363feeb2fe8..5a913a6c1391 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/XmlHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/XmlHelper.java @@ -521,6 +521,10 @@ else if ( attributeMapping instanceof EmbeddedAttributeMapping ) { final String tagName = aggregateMapping.getSelectableName(); sb.append( '<' ); sb.append( tagName ); + if ( array[i] == null ) { + sb.append( "/>" ); + continue; + } sb.append( '>' ); toString( mappingType, 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 81cb418de6bf..3e463852c6fa 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 @@ -333,9 +333,9 @@ public List aggregateAuxiliaryDatabaseObjects( var serializerSb = new StringBuilder(); var deserializerSb = new StringBuilder(); serializerSb.append( "create function " ).append( columnType ).append( "_serializer(v " ).append( columnType ).append( ") returns xml language sql " ) - .append( "return xmlelement(name \"").append( XmlHelper.ROOT_TAG ).append( "\"" ); + .append( "return case when v is null then null else xmlelement(name \"").append( XmlHelper.ROOT_TAG ).append( "\"" ); appendSerializer( aggregatedColumns, serializerSb, "v.." ); - serializerSb.append( ')' ); + serializerSb.append( ") end" ); deserializerSb.append( "create function " ).append( columnType ).append( "_deserializer(v xml) returns " ).append( columnType ).append( " language sql " ) .append( "return select " ).append( columnType ).append( "()" ); @@ -385,6 +385,10 @@ private static void appendSerializer(List aggregatedColumns, StringBuild } for ( Column udtColumn : aggregatedColumns ) { serializerSb.append( sep ); + if ( udtColumn.getSqlTypeCode() == STRUCT ) { + serializerSb.append( "case when ").append( prefix ).append( udtColumn.getName() ) + .append( " is null then null else " ); + } serializerSb.append( "xmlelement(name \"" ).append( udtColumn.getName() ).append( "\"" ); if ( udtColumn.getSqlTypeCode() == STRUCT ) { final AggregateColumn aggregateColumn = (AggregateColumn) udtColumn; @@ -402,6 +406,9 @@ else if ( needsVarcharForBitDataCast( udtColumn.getSqlType() ) ) { serializerSb.append( ',' ).append( prefix ).append( udtColumn.getName() ); } serializerSb.append( ')' ); + if ( udtColumn.getSqlTypeCode() == STRUCT ) { + serializerSb.append( " end" ); + } sep = ','; } if ( aggregatedColumns.size() > 1 ) { diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/DB2SubstringFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/DB2SubstringFunction.java index 977a55aea1aa..9b177c0794b3 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/DB2SubstringFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/DB2SubstringFunction.java @@ -30,7 +30,13 @@ */ public class DB2SubstringFunction extends AbstractSqmSelfRenderingFunctionDescriptor { + private final boolean needsCodeUnit; + public DB2SubstringFunction(TypeConfiguration typeConfiguration) { + this( true, typeConfiguration ); + } + + public DB2SubstringFunction(boolean needsCodeUnit, TypeConfiguration typeConfiguration) { super( "substring", new ArgumentTypesValidator( StandardArgumentsValidators.between( 2, 4 ), STRING, INTEGER, INTEGER, FunctionParameterType.ANY ), @@ -38,6 +44,7 @@ public DB2SubstringFunction(TypeConfiguration typeConfiguration) { StandardBasicTypes.STRING ) ), StandardFunctionArgumentTypeResolvers.invariant( typeConfiguration, STRING, INTEGER, INTEGER ) ); + this.needsCodeUnit = needsCodeUnit; } @Override @@ -53,7 +60,7 @@ public void render( sqlAppender.appendSql( ',' ); arguments.get( i ).accept( walker ); } - if ( argumentCount != 4 ) { + if ( argumentCount != 4 && needsCodeUnit ) { sqlAppender.appendSql( ",codeunits32" ); } sqlAppender.appendSql( ')' ); 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 dd4cae71dfaf..827e0d52a076 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 @@ -50,7 +50,7 @@ */ public class ExtractFunction extends AbstractSqmFunctionDescriptor implements FunctionRenderer { - private final Dialect dialect; + final Dialect dialect; public ExtractFunction(Dialect dialect, TypeConfiguration typeConfiguration) { super( diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/OracleExtractFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/OracleExtractFunction.java new file mode 100644 index 000000000000..d89e0ca50974 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/OracleExtractFunction.java @@ -0,0 +1,55 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.dialect.function; + +import jakarta.persistence.TemporalType; +import org.hibernate.dialect.Dialect; +import org.hibernate.metamodel.mapping.JdbcMappingContainer; +import org.hibernate.query.ReturnableType; +import org.hibernate.query.sqm.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.query.sqm.TemporalUnit.EPOCH; +import static org.hibernate.type.spi.TypeConfiguration.getSqlTemporalType; + +public class OracleExtractFunction extends ExtractFunction { + public OracleExtractFunction(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) { + final ExtractUnit field = (ExtractUnit) sqlAstArguments.get( 0 ); + final TemporalUnit unit = field.getUnit(); + if ( unit == EPOCH ) { + final Expression expression = (Expression) sqlAstArguments.get( 1 ); + final JdbcMappingContainer type = expression.getExpressionType(); + final TemporalType temporalType = type != null ? getSqlTemporalType( type ) : null; + if ( temporalType == TemporalType.DATE ) { + return "trunc((cast(from_tz(cast(?2 as timestamp),'UTC') as date) - date '1970-1-1')*86400)"; + } + } + return dialect.extractPattern( unit ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/SqlFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/SqlFunction.java index f2454a15c84d..20460d3340d2 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/SqlFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/SqlFunction.java @@ -7,16 +7,22 @@ package org.hibernate.dialect.function; import java.util.List; +import java.util.function.Supplier; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.hibernate.metamodel.mapping.BasicValuedMapping; import org.hibernate.query.ReturnableType; import org.hibernate.query.sqm.function.AbstractSqmSelfRenderingFunctionDescriptor; +import org.hibernate.query.sqm.produce.function.FunctionReturnTypeResolver; import org.hibernate.query.sqm.produce.function.StandardArgumentsValidators; -import org.hibernate.query.sqm.produce.function.StandardFunctionReturnTypeResolvers; +import org.hibernate.query.sqm.sql.SqmToSqlAstConverter; +import org.hibernate.query.sqm.tree.SqmTypedNode; 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.QueryLiteral; import org.hibernate.type.JavaObjectType; +import org.hibernate.type.spi.TypeConfiguration; /** * A function to pass through a SQL fragment. @@ -30,7 +36,24 @@ public SqlFunction() { super( "sql", StandardArgumentsValidators.min( 1 ), - StandardFunctionReturnTypeResolvers.invariant( JavaObjectType.INSTANCE ), + new FunctionReturnTypeResolver() { + @Override + public ReturnableType resolveFunctionReturnType( + ReturnableType impliedType, + @Nullable SqmToSqlAstConverter converter, + List> arguments, + TypeConfiguration typeConfiguration) { + return impliedType != null + ? impliedType + : typeConfiguration.getBasicTypeForJavaType( Object.class ); + } + + @Override + public BasicValuedMapping resolveFunctionReturnType(Supplier impliedTypeAccess, List arguments) { + final BasicValuedMapping impliedType = impliedTypeAccess.get(); + return impliedType != null ? impliedType : JavaObjectType.INSTANCE; + } + }, null ); } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/ArrayAndElementArgumentTypeResolver.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/ArrayAndElementArgumentTypeResolver.java index 3dd759167a60..8cb4aae51c1b 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/ArrayAndElementArgumentTypeResolver.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/ArrayAndElementArgumentTypeResolver.java @@ -6,6 +6,8 @@ */ package org.hibernate.dialect.function.array; +import java.util.List; + import org.hibernate.internal.util.collections.ArrayHelper; import org.hibernate.metamodel.mapping.MappingModelExpressible; import org.hibernate.metamodel.model.domain.DomainType; @@ -37,9 +39,13 @@ public MappingModelExpressible resolveFunctionArgumentType( SqmFunction function, int argumentIndex, SqmToSqlAstConverter converter) { + final List> arguments = function.getArguments(); if ( argumentIndex == arrayIndex ) { for ( int elementIndex : elementIndexes ) { - final SqmTypedNode node = function.getArguments().get( elementIndex ); + if ( elementIndex >= arguments.size() ) { + continue; + } + final SqmTypedNode node = arguments.get( elementIndex ); if ( node instanceof SqmExpression ) { final MappingModelExpressible expressible = converter.determineValueMapping( (SqmExpression) node ); if ( expressible != null ) { @@ -52,7 +58,7 @@ public MappingModelExpressible resolveFunctionArgumentType( } } else if ( ArrayHelper.contains( elementIndexes, argumentIndex ) ) { - final SqmTypedNode node = function.getArguments().get( arrayIndex ); + final SqmTypedNode node = arguments.get( arrayIndex ); if ( node instanceof SqmExpression ) { final MappingModelExpressible expressible = converter.determineValueMapping( (SqmExpression) node ); if ( expressible != null ) { diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/ArrayAndElementArgumentValidator.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/ArrayAndElementArgumentValidator.java index 398bdf5feb19..223e57e14442 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/ArrayAndElementArgumentValidator.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/ArrayAndElementArgumentValidator.java @@ -38,7 +38,8 @@ public void validate( for ( int elementIndex : elementIndexes ) { if ( elementIndex < arguments.size() ) { final SqmTypedNode elementArgument = arguments.get( elementIndex ); - final SqmExpressible elementType = elementArgument.getExpressible().getSqmType(); + final SqmExpressible expressible = elementArgument.getExpressible(); + final SqmExpressible elementType = expressible != null ? expressible.getSqmType() : null; if ( expectedElementType != null && elementType != null && expectedElementType != elementType ) { throw new FunctionArgumentException( String.format( diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/ArrayConstructorFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/ArrayConstructorFunction.java index 233232cfd7d7..b7a658008f5f 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/ArrayConstructorFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/ArrayConstructorFunction.java @@ -109,13 +109,13 @@ public void validateSqlTypes(List arguments, String functi if ( firstType == null ) { firstType = argumentType; } - else if ( firstType != argumentType ) { + else if ( firstType.getSingleJdbcMapping() != argumentType.getSingleJdbcMapping() ) { throw new FunctionArgumentException( String.format( "All array arguments must have a type compatible to the first argument type [%s], but argument %d has type '%s'", - firstType, + firstType.getSingleJdbcMapping(), i + 1, - argumentType + argumentType.getSingleJdbcMapping() ) ); } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/ArrayContainsArgumentTypeResolver.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/ArrayContainsArgumentTypeResolver.java index af076cb3aa58..f52ce438e3c5 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/ArrayContainsArgumentTypeResolver.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/ArrayContainsArgumentTypeResolver.java @@ -47,10 +47,15 @@ public MappingModelExpressible resolveFunctionArgumentType( } } else if ( argumentIndex == 1 ) { + final SqmTypedNode nodeToResolve = function.getArguments().get( 1 ); + if ( nodeToResolve.getExpressible() instanceof MappingModelExpressible ) { + // If the node already has suitable type, don't infer it to be treated as an array + return null; + } final SqmTypedNode node = function.getArguments().get( 0 ); if ( node instanceof SqmExpression ) { final MappingModelExpressible expressible = converter.determineValueMapping( (SqmExpression) node ); - if ( expressible != null ) { + if ( expressible instanceof BasicPluralType ) { return expressible; } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/ArrayToStringFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/ArrayToStringFunction.java index 18e06e88093d..badeb8ca9db1 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/ArrayToStringFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/ArrayToStringFunction.java @@ -34,7 +34,7 @@ public ArrayToStringFunction(TypeConfiguration typeConfiguration) { "array_to_string", FunctionKind.NORMAL, StandardArgumentsValidators.composite( - new ArgumentTypesValidator( StandardArgumentsValidators.between( 2, 3 ), ANY, STRING, ANY ), + new ArgumentTypesValidator( StandardArgumentsValidators.between( 2, 3 ), ANY, STRING, STRING ), new ArrayAndElementArgumentValidator( 0, 2 ) ), StandardFunctionReturnTypeResolvers.invariant( @@ -42,7 +42,7 @@ public ArrayToStringFunction(TypeConfiguration typeConfiguration) { ), StandardFunctionArgumentTypeResolvers.composite( new ArrayAndElementArgumentTypeResolver( 0, 2 ), - StandardFunctionArgumentTypeResolvers.invariant( typeConfiguration, ANY, STRING ) + StandardFunctionArgumentTypeResolvers.invariant( typeConfiguration, ANY, STRING, STRING ) ) ); } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/H2ArrayToStringFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/H2ArrayToStringFunction.java index 5e02c2a67765..914dc461c064 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/H2ArrayToStringFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/H2ArrayToStringFunction.java @@ -9,10 +9,18 @@ import java.util.List; import org.hibernate.query.ReturnableType; +import org.hibernate.query.sqm.function.SelfRenderingOrderedSetAggregateFunctionSqlAstExpression; +import org.hibernate.sql.ast.Clause; +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.FunctionExpression; +import org.hibernate.sql.ast.tree.predicate.Predicate; +import org.hibernate.sql.ast.tree.select.SortSpecification; +import org.hibernate.type.BasicPluralType; +import org.hibernate.type.SqlTypes; import org.hibernate.type.spi.TypeConfiguration; /** @@ -38,26 +46,99 @@ public void render( final Expression arrayExpression = (Expression) sqlAstArguments.get( 0 ); final Expression separatorExpression = (Expression) sqlAstArguments.get( 1 ); final Expression defaultExpression = sqlAstArguments.size() > 2 ? (Expression) sqlAstArguments.get( 2 ) : null; - sqlAppender.append( "case when " ); - arrayExpression.accept( walker ); - sqlAppender.append( " is not null then coalesce((select listagg(" ); - if ( defaultExpression != null ) { - sqlAppender.append( "coalesce(" ); + final BasicPluralType pluralType = (BasicPluralType) arrayExpression.getExpressionType().getSingleJdbcMapping(); + final int ddlTypeCode = pluralType.getElementType().getJdbcType().getDdlTypeCode(); + final boolean needsCast = !SqlTypes.isStringType( ddlTypeCode ); + if ( arrayExpression instanceof SelfRenderingOrderedSetAggregateFunctionSqlAstExpression + && ArrayAggFunction.FUNCTION_NAME.equals( ( (FunctionExpression) arrayExpression ).getFunctionName() ) ) { + final SelfRenderingOrderedSetAggregateFunctionSqlAstExpression functionExpression + = (SelfRenderingOrderedSetAggregateFunctionSqlAstExpression) arrayExpression; + // When the array argument is an aggregate expression, we access its contents directly + final Expression arrayElementExpression = (Expression) functionExpression.getArguments().get( 0 ); + final List withinGroup = functionExpression.getWithinGroup(); + final Predicate filter = functionExpression.getFilter(); + + sqlAppender.append( "listagg(" ); + if ( defaultExpression != null ) { + sqlAppender.append( "coalesce(" ); + } + if ( needsCast ) { + if ( ddlTypeCode == SqlTypes.BOOLEAN ) { + // By default, H2 uses upper case, so lower it for a consistent experience + sqlAppender.append( "lower(" ); + } + sqlAppender.append( "cast(" ); + } + arrayElementExpression.accept( walker ); + if ( needsCast ) { + sqlAppender.append( " as varchar)" ); + if ( ddlTypeCode == SqlTypes.BOOLEAN ) { + sqlAppender.append( ')' ); + } + } + if ( defaultExpression != null ) { + sqlAppender.append( ',' ); + defaultExpression.accept( walker ); + sqlAppender.append( ')' ); + } + sqlAppender.append( "," ); + walker.render( separatorExpression, SqlAstNodeRenderingMode.DEFAULT ); + sqlAppender.appendSql( ')' ); + + if ( withinGroup != null && !withinGroup.isEmpty() ) { + walker.getCurrentClauseStack().push( Clause.WITHIN_GROUP ); + sqlAppender.appendSql( " within group (order by " ); + withinGroup.get( 0 ).accept( walker ); + for ( int i = 1; i < withinGroup.size(); i++ ) { + sqlAppender.appendSql( ',' ); + withinGroup.get( i ).accept( walker ); + } + sqlAppender.appendSql( ')' ); + walker.getCurrentClauseStack().pop(); + } + if ( filter != null ) { + walker.getCurrentClauseStack().push( Clause.WHERE ); + sqlAppender.appendSql( " filter (where " ); + filter.accept( walker ); + sqlAppender.appendSql( ')' ); + walker.getCurrentClauseStack().pop(); + } } - sqlAppender.append( "array_get(" ); - arrayExpression.accept( walker ); - sqlAppender.append(",i.idx)" ); - if ( defaultExpression != null ) { + else { + sqlAppender.append( "case when " ); + arrayExpression.accept( walker ); + sqlAppender.append( " is not null then coalesce((select listagg(" ); + if ( defaultExpression != null ) { + sqlAppender.append( "coalesce(" ); + } + if ( needsCast ) { + if ( ddlTypeCode == SqlTypes.BOOLEAN ) { + // By default, H2 uses upper case, so lower it for a consistent experience + sqlAppender.append( "lower(" ); + } + sqlAppender.append( "cast(" ); + } + sqlAppender.append( "array_get(" ); + arrayExpression.accept( walker ); + sqlAppender.append( ",i.idx)" ); + if ( needsCast ) { + sqlAppender.append( " as varchar)" ); + if ( ddlTypeCode == SqlTypes.BOOLEAN ) { + sqlAppender.append( ')' ); + } + } + if ( defaultExpression != null ) { + sqlAppender.append( ',' ); + defaultExpression.accept( walker ); + sqlAppender.append( ')' ); + } sqlAppender.append( "," ); - defaultExpression.accept( walker ); - sqlAppender.append( ")" ); + walker.render( separatorExpression, SqlAstNodeRenderingMode.DEFAULT ); + sqlAppender.append( ") within group (order by i.idx) from system_range(1," ); + sqlAppender.append( Integer.toString( maximumArraySize ) ); + sqlAppender.append( ") i(idx) where i.idx<=coalesce(cardinality(" ); + arrayExpression.accept( walker ); + sqlAppender.append( "),0)),'') end" ); } - sqlAppender.append("," ); - separatorExpression.accept( walker ); - sqlAppender.append( ") within group (order by i.idx) from system_range(1,"); - sqlAppender.append( Integer.toString( maximumArraySize ) ); - sqlAppender.append( ") i(idx) where i.idx<=coalesce(cardinality("); - arrayExpression.accept( walker ); - sqlAppender.append("),0)),'') end" ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/HSQLArrayPositionFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/HSQLArrayPositionFunction.java index e936a19ac0df..1034a95c2f17 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/HSQLArrayPositionFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/HSQLArrayPositionFunction.java @@ -9,6 +9,7 @@ import java.util.List; import org.hibernate.query.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; @@ -37,7 +38,7 @@ public void render( sqlAppender.append( " is not null then coalesce((select t.idx from unnest("); arrayExpression.accept( walker ); sqlAppender.append(") with ordinality t(val,idx) where t.val is not distinct from " ); - elementExpression.accept( walker ); + walker.render( elementExpression, SqlAstNodeRenderingMode.NO_PLAIN_PARAMETER ); if ( sqlAstArguments.size() > 2 ) { sqlAppender.append( " and t.idx>=" ); sqlAstArguments.get( 2 ).accept( walker ); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/HSQLArrayPositionsFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/HSQLArrayPositionsFunction.java index 85149b6378d4..35acf858b927 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/HSQLArrayPositionsFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/HSQLArrayPositionsFunction.java @@ -9,6 +9,7 @@ import java.util.List; import org.hibernate.query.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; @@ -37,7 +38,7 @@ public void render( sqlAppender.append( " is not null then coalesce((select array_agg(t.idx) from unnest("); arrayExpression.accept( walker ); sqlAppender.append(") with ordinality t(val,idx) where t.val is not distinct from " ); - elementExpression.accept( walker ); + walker.render( elementExpression, SqlAstNodeRenderingMode.NO_PLAIN_PARAMETER ); sqlAppender.append( "),cast(array[] as integer array)) end" ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/HSQLArrayToStringFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/HSQLArrayToStringFunction.java index 2b0149f70d2e..1d2aa0bb64a4 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/HSQLArrayToStringFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/HSQLArrayToStringFunction.java @@ -9,11 +9,18 @@ import java.util.List; import org.hibernate.query.ReturnableType; +import org.hibernate.query.sqm.function.SelfRenderingOrderedSetAggregateFunctionSqlAstExpression; +import org.hibernate.sql.ast.Clause; 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.FunctionExpression; +import org.hibernate.sql.ast.tree.predicate.Predicate; +import org.hibernate.sql.ast.tree.select.SortSpecification; +import org.hibernate.type.BasicPluralType; +import org.hibernate.type.SqlTypes; import org.hibernate.type.spi.TypeConfiguration; /** @@ -34,23 +41,96 @@ public void render( final Expression arrayExpression = (Expression) sqlAstArguments.get( 0 ); final Expression separatorExpression = (Expression) sqlAstArguments.get( 1 ); final Expression defaultExpression = sqlAstArguments.size() > 2 ? (Expression) sqlAstArguments.get( 2 ) : null; - sqlAppender.append( "case when " ); - arrayExpression.accept( walker ); - sqlAppender.append( " is not null then coalesce((select group_concat(" ); - if ( defaultExpression != null ) { - sqlAppender.append( "coalesce(" ); + final BasicPluralType pluralType = (BasicPluralType) arrayExpression.getExpressionType().getSingleJdbcMapping(); + final int ddlTypeCode = pluralType.getElementType().getJdbcType().getDdlTypeCode(); + final boolean needsCast = !SqlTypes.isStringType( ddlTypeCode ); + if ( arrayExpression instanceof SelfRenderingOrderedSetAggregateFunctionSqlAstExpression + && ArrayAggFunction.FUNCTION_NAME.equals( ( (FunctionExpression) arrayExpression ).getFunctionName() ) ) { + final SelfRenderingOrderedSetAggregateFunctionSqlAstExpression functionExpression + = (SelfRenderingOrderedSetAggregateFunctionSqlAstExpression) arrayExpression; + // When the array argument is an aggregate expression, we access its contents directly + final Expression arrayElementExpression = (Expression) functionExpression.getArguments().get( 0 ); + final List withinGroup = functionExpression.getWithinGroup(); + final Predicate filter = functionExpression.getFilter(); + + sqlAppender.append( "group_concat(" ); + if ( defaultExpression != null ) { + sqlAppender.append( "coalesce(" ); + } + if ( needsCast ) { + if ( ddlTypeCode == SqlTypes.BOOLEAN ) { + // By default, HSQLDB uses upper case, so lower it for a consistent experience + sqlAppender.append( "lower(" ); + } + sqlAppender.append( "cast(" ); + } + arrayElementExpression.accept( walker ); + if ( needsCast ) { + sqlAppender.append( " as longvarchar)" ); + if ( ddlTypeCode == SqlTypes.BOOLEAN ) { + sqlAppender.append( ')' ); + } + } + if ( defaultExpression != null ) { + sqlAppender.append( "," ); + defaultExpression.accept( walker ); + sqlAppender.append( ")" ); + } + + if ( withinGroup != null && !withinGroup.isEmpty() ) { + walker.getCurrentClauseStack().push( Clause.WITHIN_GROUP ); + sqlAppender.appendSql( " order by " ); + withinGroup.get( 0 ).accept( walker ); + for ( int i = 1; i < withinGroup.size(); i++ ) { + sqlAppender.appendSql( ',' ); + withinGroup.get( i ).accept( walker ); + } + walker.getCurrentClauseStack().pop(); + } + sqlAppender.append( " separator " ); + // HSQLDB doesn't like non-literals as separator + walker.render( separatorExpression, SqlAstNodeRenderingMode.INLINE_PARAMETERS ); + sqlAppender.appendSql( ')' ); + if ( filter != null ) { + walker.getCurrentClauseStack().push( Clause.WHERE ); + sqlAppender.appendSql( " filter (where " ); + filter.accept( walker ); + sqlAppender.appendSql( ')' ); + walker.getCurrentClauseStack().pop(); + } } - sqlAppender.append( "t.val" ); - if ( defaultExpression != null ) { - sqlAppender.append( "," ); - defaultExpression.accept( walker ); - sqlAppender.append( ")" ); + else { + sqlAppender.append( "case when " ); + arrayExpression.accept( walker ); + sqlAppender.append( " is not null then coalesce((select group_concat(" ); + if ( defaultExpression != null ) { + sqlAppender.append( "coalesce(" ); + } + if ( needsCast ) { + if ( ddlTypeCode == SqlTypes.BOOLEAN ) { + // By default, HSQLDB uses upper case, so lower it for a consistent experience + sqlAppender.append( "lower(" ); + } + sqlAppender.append( "cast(" ); + } + sqlAppender.append( "t.val" ); + if ( needsCast ) { + sqlAppender.append( " as longvarchar)" ); + if ( ddlTypeCode == SqlTypes.BOOLEAN ) { + sqlAppender.append( ')' ); + } + } + if ( defaultExpression != null ) { + sqlAppender.append( "," ); + defaultExpression.accept( walker ); + sqlAppender.append( ")" ); + } + sqlAppender.append( " order by t.idx separator " ); + // HSQLDB doesn't like non-literals as separator + walker.render( separatorExpression, SqlAstNodeRenderingMode.INLINE_PARAMETERS ); + sqlAppender.append( ") from unnest(" ); + arrayExpression.accept( walker ); + sqlAppender.append( ") with ordinality t(val,idx)),'') end" ); } - sqlAppender.append( " order by t.idx separator " ); - // HSQLDB doesn't like non-literals as separator - walker.render( separatorExpression, SqlAstNodeRenderingMode.INLINE_PARAMETERS ); - sqlAppender.append( ") from unnest("); - arrayExpression.accept( walker ); - sqlAppender.append(") with ordinality t(val,idx)),'') end" ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/OracleArrayToStringFunction.java b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/OracleArrayToStringFunction.java index 2681a885d365..95f15380b776 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/function/array/OracleArrayToStringFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/function/array/OracleArrayToStringFunction.java @@ -8,19 +8,21 @@ import java.util.List; +import org.hibernate.metamodel.mapping.JdbcMappingContainer; import org.hibernate.query.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.query.sqm.function.SelfRenderingOrderedSetAggregateFunctionSqlAstExpression; +import org.hibernate.sql.ast.Clause; 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.FunctionExpression; +import org.hibernate.sql.ast.tree.predicate.Predicate; +import org.hibernate.sql.ast.tree.select.SortSpecification; +import org.hibernate.type.SqlTypes; import org.hibernate.type.spi.TypeConfiguration; -import static org.hibernate.query.sqm.produce.function.FunctionParameterType.ANY; -import static org.hibernate.query.sqm.produce.function.FunctionParameterType.INTEGER; +import org.checkerframework.checker.nullness.qual.Nullable; /** * Oracle array_to_string function. @@ -37,22 +39,89 @@ public void render( List sqlAstArguments, ReturnableType returnType, SqlAstTranslator walker) { - final String arrayTypeName = DdlTypeHelper.getTypeName( - ( (Expression) sqlAstArguments.get( 0 ) ).getExpressionType(), - walker.getSessionFactory().getTypeConfiguration() - ); - sqlAppender.append( arrayTypeName ); - sqlAppender.append( "_to_string(" ); - sqlAstArguments.get( 0 ).accept( walker ); - sqlAppender.append( ',' ); - sqlAstArguments.get( 1 ).accept( walker ); - if ( sqlAstArguments.size() > 2 ) { + final Expression arrayExpression = (Expression) sqlAstArguments.get( 0 ); + final JdbcMappingContainer expressionType = (arrayExpression).getExpressionType(); + if ( arrayExpression instanceof SelfRenderingOrderedSetAggregateFunctionSqlAstExpression + && ArrayAggFunction.FUNCTION_NAME.equals( ( (FunctionExpression) arrayExpression ).getFunctionName() ) ) { + final SelfRenderingOrderedSetAggregateFunctionSqlAstExpression functionExpression + = (SelfRenderingOrderedSetAggregateFunctionSqlAstExpression) arrayExpression; + // When the array argument is an aggregate expression, we access its contents directly + final Expression arrayElementExpression = (Expression) functionExpression.getArguments().get( 0 ); + final @Nullable Expression defaultExpression = + sqlAstArguments.size() > 2 ? (Expression) sqlAstArguments.get( 2 ) : null; + final List withinGroup = functionExpression.getWithinGroup(); + final Predicate filter = functionExpression.getFilter(); + + sqlAppender.append( "listagg(" ); + if ( filter != null ) { + sqlAppender.appendSql( "case when " ); + walker.getCurrentClauseStack().push( Clause.WHERE ); + filter.accept( walker ); + walker.getCurrentClauseStack().pop(); + sqlAppender.appendSql( " then " ); + } + if ( defaultExpression != null ) { + sqlAppender.append( "coalesce(" ); + } + arrayElementExpression.accept( walker ); + if ( defaultExpression != null ) { + sqlAppender.append( ',' ); + defaultExpression.accept( walker ); + sqlAppender.append( ')' ); + } + if ( filter != null ) { + sqlAppender.appendSql( " else null end" ); + } sqlAppender.append( ',' ); - sqlAstArguments.get( 2 ).accept( walker ); + sqlAstArguments.get( 1 ).accept( walker ); + sqlAppender.appendSql( ')' ); + + if ( withinGroup != null && !withinGroup.isEmpty() ) { + walker.getCurrentClauseStack().push( Clause.WITHIN_GROUP ); + sqlAppender.appendSql( " within group (order by " ); + withinGroup.get( 0 ).accept( walker ); + for ( int i = 1; i < withinGroup.size(); i++ ) { + sqlAppender.appendSql( ',' ); + withinGroup.get( i ).accept( walker ); + } + sqlAppender.appendSql( ')' ); + walker.getCurrentClauseStack().pop(); + } + } + else if ( expressionType.getSingleJdbcMapping().getJdbcType().getDefaultSqlTypeCode() == SqlTypes.JSON ) { + sqlAppender.append( "(select listagg(" ); + if ( sqlAstArguments.size() > 2 ) { + sqlAppender.append( "coalesce(t.v," ); + sqlAstArguments.get( 2 ).accept( walker ); + sqlAppender.append( ")," ); + } + else { + sqlAppender.append( "t.v," ); + } + + sqlAstArguments.get( 1 ).accept( walker ); + sqlAppender.append( ") from json_table(" ); + sqlAstArguments.get( 0 ).accept( walker ); + sqlAppender.append( ",'$[*]' columns (v path '$')) t)" ); } else { - sqlAppender.append( ",null" ); + final String arrayTypeName = DdlTypeHelper.getTypeName( + expressionType, + walker.getSessionFactory().getTypeConfiguration() + ); + sqlAppender.append( arrayTypeName ); + sqlAppender.append( "_to_string(" ); + sqlAstArguments.get( 0 ).accept( walker ); + sqlAppender.append( ',' ); + sqlAstArguments.get( 1 ).accept( walker ); + if ( sqlAstArguments.size() > 2 ) { + sqlAppender.append( ',' ); + sqlAstArguments.get( 2 ).accept( walker ); + } + else { + sqlAppender.append( ",null" ); + } + sqlAppender.append( ')' ); } - sqlAppender.append( ')' ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/hint/IndexQueryHintHandler.java b/hibernate-core/src/main/java/org/hibernate/dialect/hint/IndexQueryHintHandler.java index 9a70d556786c..ae4134de5e10 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/hint/IndexQueryHintHandler.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/hint/IndexQueryHintHandler.java @@ -25,8 +25,7 @@ public class IndexQueryHintHandler implements QueryHintHandler { public static final IndexQueryHintHandler INSTANCE = new IndexQueryHintHandler(); - private static final Pattern QUERY_PATTERN = Pattern.compile( "^\\s*(select\\b.+?\\bfrom\\b.+?)(\\bwhere\\b.+?)$" ); - + private static final Pattern QUERY_PATTERN = Pattern.compile( "^\\s*(select\\b.+?\\bfrom\\b.+?)(\\b(where|join)\\b.+?)$" ); @Override public String addQueryHints(String query, String hints) { Matcher matcher = QUERY_PATTERN.matcher( query ); diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/temptable/TemporaryTableColumn.java b/hibernate-core/src/main/java/org/hibernate/dialect/temptable/TemporaryTableColumn.java index b46c49faa07a..68e564e04ecd 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/temptable/TemporaryTableColumn.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/temptable/TemporaryTableColumn.java @@ -8,6 +8,9 @@ import org.hibernate.engine.jdbc.Size; import org.hibernate.metamodel.mapping.JdbcMapping; +import org.hibernate.metamodel.mapping.SqlTypedMapping; + +import org.checkerframework.checker.nullness.qual.Nullable; /** * A column in a IdTable. As these columns mirror the entity id columns, we know a few things about it inherently, @@ -15,7 +18,7 @@ * * @author Steve Ebersole */ -public class TemporaryTableColumn { +public class TemporaryTableColumn implements SqlTypedMapping { private final TemporaryTable containingTable; private final String columnName; private final JdbcMapping jdbcMapping; @@ -59,6 +62,7 @@ public String getColumnName() { return columnName; } + @Override public JdbcMapping getJdbcMapping() { return jdbcMapping; } @@ -82,4 +86,29 @@ public boolean isNullable() { public boolean isPrimaryKey() { return primaryKey; } + + @Override + public @Nullable String getColumnDefinition() { + return sqlTypeName; + } + + @Override + public @Nullable Long getLength() { + return size.getLength(); + } + + @Override + public @Nullable Integer getPrecision() { + return size.getPrecision(); + } + + @Override + public @Nullable Integer getScale() { + return size.getScale(); + } + + @Override + public @Nullable Integer getTemporalPrecision() { + return getJdbcMapping().getJdbcType().isTemporal() ? size.getPrecision() : null; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/dialect/temptable/TemporaryTableHelper.java b/hibernate-core/src/main/java/org/hibernate/dialect/temptable/TemporaryTableHelper.java index 1f1c6e93f2f4..43cd8d4dc46d 100644 --- a/hibernate-core/src/main/java/org/hibernate/dialect/temptable/TemporaryTableHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/dialect/temptable/TemporaryTableHelper.java @@ -14,6 +14,7 @@ import java.util.function.Function; import org.hibernate.engine.jdbc.internal.FormatStyle; +import org.hibernate.engine.jdbc.spi.JdbcCoordinator; import org.hibernate.engine.jdbc.spi.JdbcServices; import org.hibernate.engine.jdbc.spi.SqlExceptionHelper; import org.hibernate.engine.jdbc.spi.SqlStatementLogger; @@ -149,18 +150,19 @@ public static void cleanTemporaryTableRows( TemporaryTableExporter exporter, Function sessionUidAccess, SharedSessionContractImplementor session) { + final JdbcCoordinator jdbcCoordinator = session.getJdbcCoordinator(); PreparedStatement ps = null; try { final String sql = exporter.getSqlTruncateCommand( temporaryTable, sessionUidAccess, session ); - ps = session.getJdbcCoordinator().getStatementPreparer().prepareStatement( sql, false ); + ps = jdbcCoordinator.getStatementPreparer().prepareStatement( sql, false ); if ( temporaryTable.getSessionUidColumn() != null ) { final String sessionUid = sessionUidAccess.apply( session ); ps.setString( 1, sessionUid ); } - session.getJdbcCoordinator().getResultSetReturn().executeUpdate( ps, sql ); + jdbcCoordinator.getResultSetReturn().executeUpdate( ps, sql ); } catch( Throwable t ) { log.unableToCleanupTemporaryIdTable(t); @@ -168,11 +170,12 @@ public static void cleanTemporaryTableRows( finally { if ( ps != null ) { try { - session.getJdbcCoordinator().getLogicalConnection().getResourceRegistry().release( ps ); + jdbcCoordinator.getLogicalConnection().getResourceRegistry().release( ps ); } catch( Throwable ignore ) { // ignore } + jdbcCoordinator.afterStatementExecution(); } } } diff --git a/hibernate-core/src/main/java/org/hibernate/engine/internal/Cascade.java b/hibernate-core/src/main/java/org/hibernate/engine/internal/Cascade.java index ecf12dcb2af6..c5cde212c9bf 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/internal/Cascade.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/internal/Cascade.java @@ -10,15 +10,18 @@ import java.util.Collection; import java.util.Iterator; import java.util.List; +import java.util.Set; import org.hibernate.HibernateException; import org.hibernate.bytecode.enhance.spi.interceptor.LazyAttributeLoadingInterceptor; +import org.hibernate.annotations.OnDeleteAction; import org.hibernate.collection.spi.PersistentCollection; import org.hibernate.engine.spi.CascadeStyle; import org.hibernate.engine.spi.CascadingAction; import org.hibernate.engine.spi.CollectionEntry; import org.hibernate.engine.spi.EntityEntry; import org.hibernate.engine.spi.PersistenceContext; +import org.hibernate.engine.spi.SelfDirtinessTracker; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.engine.spi.Status; import org.hibernate.event.spi.DeleteContext; @@ -27,19 +30,22 @@ import org.hibernate.internal.CoreMessageLogger; import org.hibernate.persister.collection.CollectionPersister; import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.type.AnyType; import org.hibernate.type.AssociationType; import org.hibernate.type.CollectionType; import org.hibernate.type.ComponentType; import org.hibernate.type.CompositeType; import org.hibernate.type.EntityType; +import org.hibernate.type.ForeignKeyDirection; import org.hibernate.type.ManyToOneType; import org.hibernate.type.OneToOneType; import org.hibernate.type.Type; +import static org.hibernate.engine.internal.ManagedTypeHelper.asManagedEntity; +import static org.hibernate.engine.internal.ManagedTypeHelper.asSelfDirtinessTrackerOrNull; import static org.hibernate.engine.internal.ManagedTypeHelper.isHibernateProxy; import static org.hibernate.engine.spi.CascadingActions.CHECK_ON_FLUSH; import static org.hibernate.pretty.MessageHelper.infoString; -import static org.hibernate.type.ForeignKeyDirection.TO_PARENT; /** * Delegate responsible for, in conjunction with the various @@ -92,15 +98,29 @@ public static void cascade( final PersistenceContext persistenceContext = eventSource.getPersistenceContextInternal(); final boolean enhancedForLazyLoading = persister.getBytecodeEnhancementMetadata().isEnhancedForLazyLoading(); final EntityEntry entry; + final Set dirtyAttributes; if ( enhancedForLazyLoading ) { entry = persistenceContext.getEntry( parent ); - if ( entry != null - && entry.getLoadedState() == null - && entry.getStatus() == Status.MANAGED ) { - return; + if ( entry != null && entry.getLoadedState() == null && entry.getStatus() == Status.MANAGED ) { + final SelfDirtinessTracker selfDirtinessTracker = asSelfDirtinessTrackerOrNull( parent ); + if ( selfDirtinessTracker == null ) { + return; + } + else { + if ( asManagedEntity( parent ).$$_hibernate_useTracker() ) { + dirtyAttributes = Set.of( selfDirtinessTracker.$$_hibernate_getDirtyAttributes() ); + } + else { + dirtyAttributes = null; + } + } + } + else { + dirtyAttributes = null; } } else { + dirtyAttributes = null; entry = null; } final Type[] types = persister.getPropertyTypes(); @@ -109,12 +129,16 @@ public static void cascade( final boolean hasUninitializedLazyProperties = persister.hasUninitializedLazyProperties( parent ); for ( int i = 0; i < types.length; i++) { - final CascadeStyle style = cascadeStyles[ i ]; final String propertyName = propertyNames[ i ]; + if ( dirtyAttributes != null && !dirtyAttributes.contains( propertyName ) ) { + return; + } + final CascadeStyle style = cascadeStyles[ i ]; final Type type = types[i]; final boolean isUninitializedProperty = hasUninitializedLazyProperties && !persister.getBytecodeEnhancementMetadata().isAttributeLoaded( parent, propertyName ); + final boolean isCascadeDeleteEnabled = cascadeDeleteEnabled( action, persister, i ); if ( style.doCascade( action ) ) { final Object child; @@ -130,7 +154,7 @@ public static void cascade( // parent was not in the PersistenceContext continue; } - if ( type.isCollectionType() ) { + if ( type instanceof CollectionType ) { // CollectionType#getCollection gets the PersistentCollection // that corresponds to the uninitialized collection from the // PersistenceContext. If not present, an uninitialized @@ -145,13 +169,13 @@ public static void cascade( null ); } - else if ( type.isComponentType() ) { + else if ( type instanceof AnyType || type instanceof ComponentType ) { // Hibernate does not support lazy embeddables, so this shouldn't happen. throw new UnsupportedOperationException( "Lazy components are not supported." ); } - else if ( action.performOnLazyProperty() && type.isEntityType() ) { + else if ( action.performOnLazyProperty() && type instanceof EntityType ) { // Only need to initialize a lazy entity attribute when action.performOnLazyProperty() // returns true. LazyAttributeLoadingInterceptor interceptor = persister.getBytecodeEnhancementMetadata() @@ -178,7 +202,7 @@ else if ( action.performOnLazyProperty() && type.isEntityType() ) { style, propertyName, anything, - false + isCascadeDeleteEnabled ); } else { @@ -193,7 +217,7 @@ else if ( action.performOnLazyProperty() && type.isEntityType() ) { type, style, propertyName, - false + isCascadeDeleteEnabled ); } } @@ -222,7 +246,7 @@ private static void cascadeProperty( final boolean isCascadeDeleteEnabled) throws HibernateException { if ( child != null ) { - if ( type.isAssociationType() ) { + if ( type instanceof EntityType || type instanceof CollectionType || type instanceof AnyType ) { final AssociationType associationType = (AssociationType) type; final boolean unownedTransient = eventSource.getSessionFactory() .getSessionFactoryOptions() @@ -242,7 +266,7 @@ private static void cascadeProperty( ); } } - else if ( type.isComponentType() ) { + else if ( type instanceof ComponentType ) { if ( componentPath == null && propertyName != null ) { componentPath = new ArrayList<>(); } @@ -359,8 +383,8 @@ private static void cascadeLogicalOneToOneOrphanRemoval( ); } - if ( type.isAssociationType() - && ( (AssociationType) type ).getForeignKeyDirection().equals(TO_PARENT) ) { + if ( type instanceof CollectionType + || type instanceof OneToOneType && ( (OneToOneType) type ).getForeignKeyDirection() == ForeignKeyDirection.TO_PARENT ) { // If FK direction is to-parent, we must remove the orphan *before* the queued update(s) // occur. Otherwise, replacing the association on a managed entity, without manually // nulling and flushing, causes FK constraint violations. @@ -400,19 +424,17 @@ private static boolean cascadeAssociationNow( } private static boolean isUnownedAssociation(AssociationType associationType, SessionFactoryImplementor factory) { - if ( associationType.isEntityType() ) { - if ( associationType instanceof ManyToOneType ) { - final ManyToOneType manyToOne = (ManyToOneType) associationType; - // logical one-to-one + non-null unique key property name indicates unowned - return manyToOne.isLogicalOneToOne() && manyToOne.getRHSUniqueKeyPropertyName() != null; - } - else if ( associationType instanceof OneToOneType ) { - final OneToOneType oneToOne = (OneToOneType) associationType; - // constrained false + non-null unique key property name indicates unowned - return oneToOne.isNullable() && oneToOne.getRHSUniqueKeyPropertyName() != null; - } + if ( associationType instanceof ManyToOneType ) { + final ManyToOneType manyToOne = (ManyToOneType) associationType; + // logical one-to-one + non-null unique key property name indicates unowned + return manyToOne.isLogicalOneToOne() && manyToOne.getRHSUniqueKeyPropertyName() != null; + } + else if ( associationType instanceof OneToOneType ) { + final OneToOneType oneToOne = (OneToOneType) associationType; + // constrained false + non-null unique key property name indicates unowned + return oneToOne.isNullable() && oneToOne.getRHSUniqueKeyPropertyName() != null; } - else if ( associationType.isCollectionType() ) { + else if ( associationType instanceof CollectionType ) { // for collections, we can ask the persister if we're on the inverse side return ( (CollectionType) associationType ).isInverse( factory ); } @@ -451,8 +473,8 @@ private static void cascadeComponent( componentPropertyStyle, subPropertyName, anything, - false - ); + cascadeDeleteEnabled( action, componentType, i ) + ); } } } @@ -468,10 +490,10 @@ private static void cascadeAssociation( final CascadeStyle style, final T anything, final boolean isCascadeDeleteEnabled) { - if ( type.isEntityType() || type.isAnyType() ) { + if ( type instanceof EntityType || type instanceof AnyType ) { cascadeToOne( action, eventSource, parent, child, type, style, anything, isCascadeDeleteEnabled ); } - else if ( type.isCollectionType() ) { + else if ( type instanceof CollectionType ) { cascadeCollection( action, cascadePoint, @@ -510,7 +532,7 @@ private static void cascadeCollection( } //cascade to current collection elements - if ( elemType.isEntityType() || elemType.isAnyType() || elemType.isComponentType() ) { + if ( elemType instanceof EntityType || elemType instanceof AnyType || elemType instanceof ComponentType ) { cascadeCollectionElements( action, elementsCascadePoint, @@ -522,7 +544,7 @@ private static void cascadeCollection( style, elemType, anything, - persister.isCascadeDeleteEnabled() + cascadeDeleteEnabled( action, persister ) ); } } @@ -539,7 +561,7 @@ private static void cascadeToOne( final CascadeStyle style, final T anything, final boolean isCascadeDeleteEnabled) { - final String entityName = type.isEntityType() + final String entityName = type instanceof EntityType ? ( (EntityType) type ).getAssociatedEntityName() : null; if ( style.reallyDoCascade( action ) ) { @@ -601,12 +623,22 @@ private static void cascadeCollectionElements( } } + // a newly instantiated collection can't have orphans + final PersistentCollection persistentCollection; + if ( child instanceof PersistentCollection ) { + persistentCollection = (PersistentCollection) child; + } + else { + persistentCollection = eventSource.getPersistenceContext() + .getCollectionHolder( child ); + } + final boolean deleteOrphans = style.hasOrphanDelete() && action.deleteOrphans() - && elemType.isEntityType() - && child instanceof PersistentCollection + && elemType instanceof EntityType + && persistentCollection != null // a newly instantiated collection can't have orphans - && ! ( (PersistentCollection) child ).isNewlyInstantiated(); + && !persistentCollection.isNewlyInstantiated(); if ( deleteOrphans ) { final boolean traceEnabled = LOG.isTraceEnabled(); @@ -617,7 +649,7 @@ private static void cascadeCollectionElements( // 1. newly instantiated collections // 2. arrays (we can't track orphans for detached arrays) final String entityName = collectionType.getAssociatedEntityName( eventSource.getFactory() ); - deleteOrphans( eventSource, entityName, (PersistentCollection) child ); + deleteOrphans( eventSource, entityName, persistentCollection ); if ( traceEnabled ) { LOG.tracev( "Done deleting orphans for collection: {0}", collectionType.getRole() ); @@ -648,4 +680,19 @@ private static void deleteOrphans(EventSource eventSource, String entityName, Pe } } } + + private static boolean cascadeDeleteEnabled(CascadingAction action, CollectionPersister persister) { + return action.directionAffectedByCascadeDelete() == ForeignKeyDirection.FROM_PARENT + && persister.isCascadeDeleteEnabled(); + } + + private static boolean cascadeDeleteEnabled(CascadingAction action, EntityPersister persister, int i) { + return action.directionAffectedByCascadeDelete() == ForeignKeyDirection.TO_PARENT + && persister.getEntityMetamodel().getPropertyOnDeleteActions()[i] == OnDeleteAction.CASCADE; + } + + private static boolean cascadeDeleteEnabled(CascadingAction action, CompositeType componentType, int i) { + return action.directionAffectedByCascadeDelete() == ForeignKeyDirection.TO_PARENT + && componentType.getOnDeleteAction( i ) == OnDeleteAction.CASCADE; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/engine/internal/ForeignKeys.java b/hibernate-core/src/main/java/org/hibernate/engine/internal/ForeignKeys.java index bbf6216e56de..e9d515208c59 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/internal/ForeignKeys.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/internal/ForeignKeys.java @@ -16,7 +16,8 @@ import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.proxy.LazyInitializer; -import org.hibernate.type.CompositeType; +import org.hibernate.type.AnyType; +import org.hibernate.type.ComponentType; import org.hibernate.type.EntityType; import org.hibernate.type.Type; @@ -106,21 +107,21 @@ private Object nullifyTransient(Object value, String propertyName, Type type) { if ( value == null ) { return null; } - else if ( type.isEntityType() ) { + else if ( type instanceof EntityType ) { return nullifyEntityType( value, propertyName, (EntityType) type ); } - else if ( type.isAnyType() ) { + else if ( type instanceof AnyType ) { return isNullifiable( null, value) ? null : value; } - else if ( type.isComponentType() ) { - return nullifyCompositeType( value, propertyName, (CompositeType) type ); + else if ( type instanceof ComponentType ) { + return nullifyCompositeType( value, propertyName, (ComponentType) type ); } else { return value; } } - private Object nullifyCompositeType(Object value, String propertyName, CompositeType compositeType) { + private Object nullifyCompositeType(Object value, String propertyName, ComponentType compositeType) { final Object[] subvalues = compositeType.getPropertyValues(value, session ); final Type[] subtypes = compositeType.getSubtypes(); final String[] subPropertyNames = compositeType.getPropertyNames(); @@ -194,7 +195,7 @@ private boolean initializationIsNecessary(Object value, Type type) { // more than just initializing the associated entity. return isDelete && value == UNFETCHED_PROPERTY - && type.isEntityType() + && type instanceof EntityType && !session.getPersistenceContextInternal().isNullifiableEntityKeysEmpty(); } @@ -225,7 +226,7 @@ private boolean isNullifiable(final String entityName, Object object) // an unloaded proxy might be scheduled for deletion return persistenceContext.containsDeletedUnloadedEntityKey( session.generateEntityKey( - lazyInitializer.getIdentifier(), + lazyInitializer.getInternalIdentifier(), session.getFactory().getMappingMetamodel() .getEntityDescriptor( lazyInitializer.getEntityName() ) ) @@ -370,6 +371,21 @@ public static Object getEntityIdentifierIfNotUnsaved( } } + public static Object getEntityIdentifier( + final String entityName, + final Object object, + final SharedSessionContractImplementor session) { + if ( object == null ) { + return null; + } + else { + final Object id = session.getContextEntityIdentifier( object ); + return id == null + ? session.getEntityPersister( entityName, object ).getIdentifier( object, session ) + : id; + } + } + private static void throwIfTransient(String entityName, Object object, SharedSessionContractImplementor session) { if ( isTransient( entityName, object, Boolean.FALSE, session ) ) { throw new TransientObjectException( @@ -432,7 +448,7 @@ private static void collectNonNullableTransientEntities( if ( value == null ) { // do nothing } - else if ( type.isEntityType() ) { + else if ( type instanceof EntityType ) { final EntityType entityType = (EntityType) type; if ( !isNullable && !entityType.isOneToOne() @@ -440,13 +456,13 @@ else if ( type.isEntityType() ) { nonNullableTransientEntities.add( propertyName, value ); } } - else if ( type.isAnyType() ) { + else if ( type instanceof AnyType ) { if ( !isNullable && nullifier.isNullifiable( null, value ) ) { nonNullableTransientEntities.add( propertyName, value ); } } - else if ( type.isComponentType() ) { - final CompositeType compositeType = (CompositeType) type; + else if ( type instanceof ComponentType ) { + final ComponentType compositeType = (ComponentType) type; final boolean[] subValueNullability = compositeType.getPropertyNullability(); if ( subValueNullability != null ) { final String[] subPropertyNames = compositeType.getPropertyNames(); diff --git a/hibernate-core/src/main/java/org/hibernate/engine/internal/ManagedTypeHelper.java b/hibernate-core/src/main/java/org/hibernate/engine/internal/ManagedTypeHelper.java index 5c849ad63d2a..3b280aa78900 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/internal/ManagedTypeHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/internal/ManagedTypeHelper.java @@ -390,6 +390,15 @@ public static SelfDirtinessTracker asSelfDirtinessTracker(final Object entity) { throw new ClassCastException( "Object of type '" + entity.getClass() + "' can't be cast to SelfDirtinessTracker" ); } + public static SelfDirtinessTracker asSelfDirtinessTrackerOrNull(final Object entity) { + Objects.requireNonNull( entity ); + if ( entity instanceof PrimeAmongSecondarySupertypes ) { + PrimeAmongSecondarySupertypes t = (PrimeAmongSecondarySupertypes) entity; + return t.asSelfDirtinessTracker(); + } + return null; + } + /** * Cast the object to an HibernateProxy, or return null in case it is not an instance of HibernateProxy * @param entity the entity to cast diff --git a/hibernate-core/src/main/java/org/hibernate/engine/internal/Nullability.java b/hibernate-core/src/main/java/org/hibernate/engine/internal/Nullability.java index a2ab620a39c8..33cf8067ae1a 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/internal/Nullability.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/internal/Nullability.java @@ -14,7 +14,9 @@ import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.generator.Generator; +import org.hibernate.type.AnyType; import org.hibernate.type.CollectionType; +import org.hibernate.type.ComponentType; import org.hibernate.type.CompositeType; import org.hibernate.type.Type; @@ -144,16 +146,19 @@ private static boolean generated(Generator generator) { * @throws HibernateException error while getting subcomponent values */ private String checkSubElementsNullability(Type propertyType, Object value) throws HibernateException { - if ( propertyType.isComponentType() ) { - return checkComponentNullability( value, (CompositeType) propertyType ); + if ( propertyType instanceof AnyType ) { + return checkComponentNullability( value, (AnyType) propertyType ); + } + if ( propertyType instanceof ComponentType ) { + return checkComponentNullability( value, (ComponentType) propertyType ); } - if ( propertyType.isCollectionType() ) { + if ( propertyType instanceof CollectionType ) { // persistent collections may have components final CollectionType collectionType = (CollectionType) propertyType; final Type collectionElementType = collectionType.getElementType( session.getFactory() ); - if ( collectionElementType.isComponentType() ) { + if ( collectionElementType instanceof ComponentType || collectionElementType instanceof AnyType ) { // check for all components values in the collection final CompositeType componentType = (CompositeType) collectionElementType; final Iterator itr = getLoadedElementsIterator( session, collectionType, value ); @@ -189,7 +194,7 @@ private String checkComponentNullability(Object value, CompositeType compositeTy // // The more correct fix would be to cascade saves of the many-to-any elements before the Nullability checking - if ( compositeType.isAnyType() ) { + if ( compositeType instanceof AnyType ) { return null; } diff --git a/hibernate-core/src/main/java/org/hibernate/engine/internal/StatefulPersistenceContext.java b/hibernate-core/src/main/java/org/hibernate/engine/internal/StatefulPersistenceContext.java index f22ad6c35db0..63bcc03169ca 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/internal/StatefulPersistenceContext.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/internal/StatefulPersistenceContext.java @@ -78,6 +78,7 @@ import static org.hibernate.engine.internal.ManagedTypeHelper.asManagedEntity; import static org.hibernate.engine.internal.ManagedTypeHelper.asPersistentAttributeInterceptable; import static org.hibernate.engine.internal.ManagedTypeHelper.isPersistentAttributeInterceptable; +import static org.hibernate.proxy.HibernateProxy.extractLazyInitializer; /** * A stateful implementation of the {@link PersistenceContext} contract, meaning that we maintain this @@ -231,8 +232,14 @@ public void clear() { if ( entitiesByKey != null ) { //Strictly avoid lambdas in this case for ( EntityHolderImpl value : entitiesByKey.values() ) { - if ( value != null && value.proxy != null ) { - HibernateProxy.extractLazyInitializer( value.proxy ).unsetSession(); + if ( value != null ) { + value.state = EntityHolderState.DETACHED; + if ( value.proxy != null ) { + final LazyInitializer lazyInitializer = extractLazyInitializer( value.proxy ); + if ( lazyInitializer != null ) { + lazyInitializer.unsetSession(); + } + } } } } @@ -2243,6 +2250,11 @@ public boolean isEventuallyInitialized() { return state == EntityHolderState.INITIALIZED || entityInitializer != null; } + @Override + public boolean isDetached() { + return state == EntityHolderState.DETACHED; + } + public static EntityHolderImpl forProxy(EntityKey entityKey, EntityPersister descriptor, Object proxy) { return new EntityHolderImpl( entityKey, descriptor, null, proxy ); } @@ -2255,7 +2267,8 @@ public static EntityHolderImpl forEntity(EntityKey entityKey, EntityPersister de enum EntityHolderState { UNINITIALIZED, ENHANCED_PROXY, - INITIALIZED + INITIALIZED, + DETACHED } // NATURAL ID RESOLUTION HANDLING ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -2271,4 +2284,13 @@ public NaturalIdResolutions getNaturalIdResolutions() { return naturalIdResolutions; } + @Override + public EntityHolder detachEntity(EntityKey key) { + final EntityHolderImpl entityHolder = removeEntityHolder( key ); + if ( entityHolder != null ) { + entityHolder.state = EntityHolderState.DETACHED; + } + return entityHolder; + } + } diff --git a/hibernate-core/src/main/java/org/hibernate/engine/internal/StatisticalLoggingSessionEventListener.java b/hibernate-core/src/main/java/org/hibernate/engine/internal/StatisticalLoggingSessionEventListener.java index 8a6b06744112..374bce7df35e 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/internal/StatisticalLoggingSessionEventListener.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/internal/StatisticalLoggingSessionEventListener.java @@ -250,10 +250,10 @@ public void prePartialFlushStart() { @Override public void prePartialFlushEnd() { - assert prePartialFlushStart > 0 : "Unexpected call to partialFlushEnd; expecting partialFlushStart"; + assert prePartialFlushStart > 0 : "Unexpected call to prePartialFlushEnd; expecting prePartialFlushStart"; prePartialFlushCount++; - prePartialFlushTime += ( System.nanoTime() - partialFlushStart ); + prePartialFlushTime += ( System.nanoTime() - prePartialFlushStart ); prePartialFlushStart = -1; } diff --git a/hibernate-core/src/main/java/org/hibernate/engine/internal/TwoPhaseLoad.java b/hibernate-core/src/main/java/org/hibernate/engine/internal/TwoPhaseLoad.java index 1e6c5109348b..9791b4098a24 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/internal/TwoPhaseLoad.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/internal/TwoPhaseLoad.java @@ -7,10 +7,16 @@ package org.hibernate.engine.internal; import org.hibernate.LockMode; +import org.hibernate.engine.spi.EntityEntry; +import org.hibernate.engine.spi.EntityHolder; import org.hibernate.engine.spi.EntityKey; +import org.hibernate.engine.spi.PersistenceContext; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.engine.spi.Status; import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.proxy.LazyInitializer; + +import static org.hibernate.proxy.HibernateProxy.extractLazyInitializer; /** * Functionality relating to the Hibernate two-phase loading process, that may be reused by persisters @@ -39,16 +45,27 @@ public static void addUninitializedCachedEntity( final LockMode lockMode, final Object version, final SharedSessionContractImplementor session) { - session.getPersistenceContextInternal().addEntity( + final PersistenceContext persistenceContext = session.getPersistenceContextInternal(); + final EntityHolder entityHolder = persistenceContext.addEntityHolder( key, object ); + final EntityEntry entityEntry = persistenceContext.addEntry( object, Status.LOADING, null, - key, + null, + key.getIdentifier(), version, lockMode, true, persister, false ); + entityHolder.setEntityEntry( entityEntry ); + final Object proxy = entityHolder.getProxy(); + if ( proxy != null ) { + // there is already a proxy for this impl + final LazyInitializer lazyInitializer = extractLazyInitializer( proxy ); + assert lazyInitializer != null; + lazyInitializer.setImplementation( object ); + } } } diff --git a/hibernate-core/src/main/java/org/hibernate/engine/internal/UnsavedValueFactory.java b/hibernate-core/src/main/java/org/hibernate/engine/internal/UnsavedValueFactory.java index 3df891f301cd..e582ad0ab0ad 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/internal/UnsavedValueFactory.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/internal/UnsavedValueFactory.java @@ -21,6 +21,8 @@ import org.hibernate.type.descriptor.java.VersionJavaType; import org.hibernate.type.descriptor.java.spi.PrimitiveJavaType; +import static org.hibernate.engine.internal.Versioning.isNullInitialVersion; + /** * Helper for dealing with unsaved value handling * @@ -82,25 +84,20 @@ else if ( "any".equals( unsavedValue ) ) { public static VersionValue getUnsavedVersionValue( KeyValue bootVersionMapping, VersionJavaType jtd, - Long length, - Integer precision, - Integer scale, Getter getter, - Supplier templateInstanceAccess, - SessionFactoryImplementor sessionFactory) { + Supplier templateInstanceAccess) { final String unsavedValue = bootVersionMapping.getNullValue(); if ( unsavedValue == null ) { if ( getter != null && templateInstanceAccess != null ) { - Object templateInstance = templateInstanceAccess.get(); + final Object templateInstance = templateInstanceAccess.get(); @SuppressWarnings("unchecked") final T defaultValue = (T) getter.get( templateInstance ); - - // if the version of a newly instantiated object is not the same - // as the version seed value, use that as the unsaved-value - final T seedValue = jtd.seed( length, precision, scale, mockSession( sessionFactory ) ); - return jtd.areEqual( seedValue, defaultValue ) - ? VersionValue.UNDEFINED - : new VersionValue( defaultValue ); + // if the version of a newly instantiated object is null + // or a negative number, use that value as the unsaved-value, + // otherwise assume it's the initial version set by program + return isNullInitialVersion( defaultValue ) + ? new VersionValue( defaultValue ) + : VersionValue.UNDEFINED; } else { return VersionValue.UNDEFINED; diff --git a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/BlobProxy.java b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/BlobProxy.java index fa70329ce446..556768b41ac9 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/BlobProxy.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/BlobProxy.java @@ -38,6 +38,8 @@ public final class BlobProxy implements Blob, BlobImplementer { // no longer necessary. The class name could be updated to reflect this but that would break APIs. private final BinaryStream binaryStream; + private final int markBytes; + private boolean resetAllowed; private boolean needsReset; /** @@ -47,7 +49,9 @@ public final class BlobProxy implements Blob, BlobImplementer { * @see #generateProxy(byte[]) */ private BlobProxy(byte[] bytes) { - binaryStream = new BinaryStreamImpl( bytes ); + binaryStream = new BinaryStreamImpl(bytes); + markBytes = bytes.length + 1; + setStreamMark(); } /** @@ -59,6 +63,19 @@ private BlobProxy(byte[] bytes) { */ private BlobProxy(InputStream stream, long length) { this.binaryStream = new StreamBackedBinaryStream( stream, length ); + this.markBytes = (int) length + 1; + setStreamMark(); + } + + private void setStreamMark() { + final InputStream inputStream = binaryStream.getInputStream(); + if ( inputStream != null && inputStream.markSupported() ) { + inputStream.mark( markBytes ); + resetAllowed = true; + } + else { + resetAllowed = false; + } } private InputStream getStream() throws SQLException { @@ -73,7 +90,14 @@ public BinaryStream getUnderlyingStream() throws SQLException { private void resetIfNeeded() throws SQLException { try { if ( needsReset ) { - binaryStream.getInputStream().reset(); + final InputStream inputStream = binaryStream.getInputStream(); + if ( !resetAllowed && inputStream != null) { + throw new SQLException( "Underlying stream does not allow reset" ); + } + if ( inputStream != null ) { + inputStream.reset(); + setStreamMark(); + } } } catch ( IOException ioe) { @@ -96,6 +120,11 @@ public static Blob generateProxy(byte[] bytes) { /** * Generates a BlobImpl proxy using a given number of bytes from an InputStream. * + * Be aware that certain database drivers will automatically close the provided InputStream after the + * contents have been written to the database. This may cause unintended side effects if the entity + * is also audited by Envers. In this case, it's recommended to use {@link #generateProxy(byte[])} + * instead as it isn't affected by this non-standard behavior. + * * @param stream The input stream of bytes to be created as a Blob. * @param length The number of bytes from stream to be written to the Blob. * diff --git a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/batch/JdbcBatchLogging.java b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/batch/JdbcBatchLogging.java index 10de5317c012..11edcd01c15e 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/batch/JdbcBatchLogging.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/batch/JdbcBatchLogging.java @@ -15,8 +15,8 @@ import org.jboss.logging.annotations.MessageLogger; import org.jboss.logging.annotations.ValidIdRange; -import static org.jboss.logging.Logger.Level.ERROR; import static org.jboss.logging.Logger.Level.INFO; +import static org.jboss.logging.Logger.Level.WARN; /** * Sub-system logging related to JDBC batch execution @@ -35,11 +35,7 @@ public interface JdbcBatchLogging extends BasicLogger { Logger BATCH_LOGGER = Logger.getLogger( NAME ); JdbcBatchLogging BATCH_MESSAGE_LOGGER = Logger.getMessageLogger( JdbcBatchLogging.class, NAME ); - @LogMessage(level = ERROR) - @Message(id = 100501, value = "Exception executing batch [%s], SQL: %s") - void unableToExecuteBatch(Exception e, String sql ); - - @LogMessage(level = ERROR) + @LogMessage(level = WARN) @Message(id = 100502, value = "Unable to release batch statement...") void unableToReleaseBatchStatement(); diff --git a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/batch/internal/BatchImpl.java b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/batch/internal/BatchImpl.java index 2dcd5363d88d..51c17304cfad 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/batch/internal/BatchImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/batch/internal/BatchImpl.java @@ -290,12 +290,10 @@ protected void performExecution() { } catch (SQLException e) { abortBatch( e ); - BATCH_MESSAGE_LOGGER.unableToExecuteBatch( e, sql ); throw sqlExceptionHelper.convert( e, "could not execute batch", sql ); } catch (RuntimeException re) { abortBatch( re ); - BATCH_MESSAGE_LOGGER.unableToExecuteBatch( re, sql ); throw re; } } ); diff --git a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/connections/internal/DatasourceConnectionProviderImpl.java b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/connections/internal/DatasourceConnectionProviderImpl.java index 76bdbd7f731e..fea57b07fef1 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/connections/internal/DatasourceConnectionProviderImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/connections/internal/DatasourceConnectionProviderImpl.java @@ -139,7 +139,7 @@ public boolean supportsAggressiveRelease() { @Override public DatabaseConnectionInfo getDatabaseConnectionInfo(Dialect dialect) { return new DatabaseConnectionInfoImpl( - "Connecting through datasource" + dataSourceJndiName, + "Connecting through datasource '" + (dataSourceJndiName != null ? dataSourceJndiName : dataSource) + "'", null, dialect.getVersion(), null, diff --git a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/connections/internal/DriverManagerConnectionProviderImpl.java b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/connections/internal/DriverManagerConnectionProviderImpl.java index 6bc006024660..e6cdc2a49ea9 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/connections/internal/DriverManagerConnectionProviderImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/connections/internal/DriverManagerConnectionProviderImpl.java @@ -472,7 +472,9 @@ protected void closeConnection(Connection conn, Throwable t) { } } finally { - allConnections.remove( conn ); + if ( !allConnections.remove( conn ) ) { + ConnectionInfoLogger.INSTANCE.debug( "Connection remove failed." ); + } } } diff --git a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/env/internal/JdbcEnvironmentInitiator.java b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/env/internal/JdbcEnvironmentInitiator.java index 7038b1aa9fc3..8588501c8786 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/env/internal/JdbcEnvironmentInitiator.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/env/internal/JdbcEnvironmentInitiator.java @@ -19,7 +19,6 @@ import org.hibernate.dialect.Dialect; import org.hibernate.engine.config.spi.ConfigurationService; import org.hibernate.engine.jdbc.batch.spi.BatchBuilder; -import org.hibernate.engine.jdbc.connections.internal.ConnectionProviderInitiator; import org.hibernate.engine.jdbc.connections.internal.DatabaseConnectionInfoImpl; import org.hibernate.engine.jdbc.connections.spi.ConnectionProvider; import org.hibernate.engine.jdbc.connections.spi.DatabaseConnectionInfo; @@ -154,10 +153,14 @@ else if ( explicitDialectConfiguration( explicitDatabaseName, configurationValue databaseConnectionInfo = buildDbInfo( configurationValues, jdbcEnvironment.getDialect() ); } + logConnectionInfo( databaseConnectionInfo ); + return jdbcEnvironment; + } + + // For Hibernate Reactive: it needs to disable or customize the log + protected void logConnectionInfo(DatabaseConnectionInfo databaseConnectionInfo) { // Standardized DB info logging ConnectionInfoLogger.INSTANCE.logConnectionInfoDetails( databaseConnectionInfo.toInfoString() ); - - return jdbcEnvironment; } private DatabaseConnectionInfo buildDbInfo(ServiceRegistryImplementor registry, Dialect dialect) { @@ -175,7 +178,8 @@ private DatabaseConnectionInfo buildDbInfo(Map configurationValu return new DatabaseConnectionInfoImpl( configurationValues, dialect ); } - private static JdbcEnvironmentImpl getJdbcEnvironmentWithDefaults( + // Used by Hibernate Reactive + protected JdbcEnvironmentImpl getJdbcEnvironmentWithDefaults( Map configurationValues, ServiceRegistryImplementor registry, DialectFactory dialectFactory) { @@ -183,7 +187,8 @@ private static JdbcEnvironmentImpl getJdbcEnvironmentWithDefaults( return new JdbcEnvironmentImpl( registry, dialect ); } - private static JdbcEnvironmentImpl getJdbcEnvironmentWithExplicitConfiguration( + // Used by Hibernate Reactive + protected JdbcEnvironmentImpl getJdbcEnvironmentWithExplicitConfiguration( Map configurationValues, ServiceRegistryImplementor registry, DialectFactory dialectFactory, @@ -204,6 +209,15 @@ private static JdbcEnvironmentImpl getJdbcEnvironmentWithExplicitConfiguration( null, configurationValues ); + return getJdbcEnvironmentWithExplicitConfiguration( configurationValues, registry, dialectFactory, dialectResolutionInfo ); + } + + // Used by Hibernate Reactive + protected JdbcEnvironmentImpl getJdbcEnvironmentWithExplicitConfiguration( + Map configurationValues, + ServiceRegistryImplementor registry, + DialectFactory dialectFactory, + DialectResolutionInfo dialectResolutionInfo) { final Dialect dialect = dialectFactory.buildDialect( configurationValues, () -> dialectResolutionInfo ); return new JdbcEnvironmentImpl( registry, dialect ); } @@ -216,7 +230,7 @@ private static JdbcEnvironmentImpl getJdbcEnvironmentWithExplicitConfiguration( * * @see JdbcSettings#ALLOW_METADATA_ON_BOOT */ - private static boolean allowJdbcMetadataAccess(Map configurationValues) { + public static boolean allowJdbcMetadataAccess(Map configurationValues) { final Boolean allow = getBooleanWrapper( ALLOW_METADATA_ON_BOOT, configurationValues, null ); if ( allow != null ) { return allow; @@ -296,7 +310,8 @@ private static String getExplicitDatabaseName(Map configurationV ); } - private JdbcEnvironmentImpl getJdbcEnvironmentUsingJdbcMetadata( + // Used by Hibernate Reactive + protected JdbcEnvironmentImpl getJdbcEnvironmentUsingJdbcMetadata( Map configurationValues, ServiceRegistryImplementor registry, DialectFactory dialectFactory, String explicitDatabaseName, @@ -311,13 +326,11 @@ private JdbcEnvironmentImpl getJdbcEnvironmentUsingJdbcMetadata( new SqlExceptionHelper( false ), registry ); - temporaryJdbcSessionOwner.transactionCoordinator = registry.requireService( TransactionCoordinatorBuilder.class ) - .buildTransactionCoordinator( - new JdbcCoordinatorImpl( null, temporaryJdbcSessionOwner, jdbcServices ), - () -> false - ); + final JdbcCoordinatorImpl jdbcCoordinator = new JdbcCoordinatorImpl( null, temporaryJdbcSessionOwner, jdbcServices ); try { + temporaryJdbcSessionOwner.transactionCoordinator = registry.requireService( TransactionCoordinatorBuilder.class ) + .buildTransactionCoordinator( jdbcCoordinator, () -> false ); return temporaryJdbcSessionOwner.transactionCoordinator.createIsolationDelegate().delegateWork( new AbstractReturningWork<>() { @Override @@ -389,6 +402,10 @@ private int databaseMicroVersion(DatabaseMetaData metadata) throws SQLException catch ( Exception e ) { log.unableToObtainConnectionToQueryMetadata( e ); } + finally { + //noinspection resource + jdbcCoordinator.close(); + } // accessing the JDBC metadata failed return getJdbcEnvironmentWithDefaults( configurationValues, registry, dialectFactory ); } diff --git a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/mutation/internal/PreparedStatementDetailsStandard.java b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/mutation/internal/PreparedStatementDetailsStandard.java index 1732484d5694..f4b04aae4ab2 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/jdbc/mutation/internal/PreparedStatementDetailsStandard.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/jdbc/mutation/internal/PreparedStatementDetailsStandard.java @@ -12,6 +12,7 @@ import org.hibernate.engine.jdbc.mutation.group.PreparedStatementDetails; import org.hibernate.engine.jdbc.mutation.group.PreparedStatementGroup; +import org.hibernate.engine.jdbc.spi.JdbcCoordinator; import org.hibernate.engine.jdbc.spi.JdbcServices; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.jdbc.Expectation; @@ -66,8 +67,10 @@ public TableMapping getMutatingTableDetails() { @Override public void releaseStatement(SharedSessionContractImplementor session) { if ( statement != null ) { - session.getJdbcCoordinator().getLogicalConnection().getResourceRegistry().release( statement ); + final JdbcCoordinator jdbcCoordinator = session.getJdbcCoordinator(); + jdbcCoordinator.getLogicalConnection().getResourceRegistry().release( statement ); statement = null; + jdbcCoordinator.afterStatementExecution(); } } diff --git a/hibernate-core/src/main/java/org/hibernate/engine/profile/FetchProfile.java b/hibernate-core/src/main/java/org/hibernate/engine/profile/FetchProfile.java index 0cd02922b83c..965cc4e86e71 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/profile/FetchProfile.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/profile/FetchProfile.java @@ -14,6 +14,7 @@ import org.hibernate.internal.CoreMessageLogger; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.type.BagType; +import org.hibernate.type.CollectionType; import org.hibernate.type.Type; import org.checkerframework.checker.nullness.qual.Nullable; @@ -98,7 +99,7 @@ public void addFetch(final Fetch fetch) { final String role = association.getRole(); final Type associationType = association.getOwner().getPropertyType( association.getAssociationPath() ); - if ( associationType.isCollectionType() ) { + if ( associationType instanceof CollectionType ) { LOG.tracev( "Handling request to add collection fetch [{0}]", role ); // couple of things for which to account in the case of collection diff --git a/hibernate-core/src/main/java/org/hibernate/engine/spi/ActionQueue.java b/hibernate-core/src/main/java/org/hibernate/engine/spi/ActionQueue.java index 332bcefa9b96..9922ac4c1725 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/spi/ActionQueue.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/spi/ActionQueue.java @@ -52,7 +52,7 @@ import org.hibernate.persister.entity.EntityPersister; import org.hibernate.proxy.LazyInitializer; import org.hibernate.type.CollectionType; -import org.hibernate.type.CompositeType; +import org.hibernate.type.ComponentType; import org.hibernate.type.EntityType; import org.hibernate.type.ForeignKeyDirection; import org.hibernate.type.Type; @@ -1167,7 +1167,10 @@ public void addTransitiveDependencies(InsertInfo origin, Set visited } private void addDirectDependency(Type type, @Nullable Object value, IdentityHashMap insertInfosByEntity) { - if ( type.isEntityType() && value != null ) { + if ( value == null ) { + return; + } + if ( type instanceof EntityType ) { final EntityType entityType = (EntityType) type; final InsertInfo insertInfo = insertInfosByEntity.get( value ); if ( insertInfo != null ) { @@ -1188,7 +1191,7 @@ private void addDirectDependency(Type type, @Nullable Object value, IdentityHash } } } - else if ( type.isCollectionType() && value != null ) { + else if ( type instanceof CollectionType ) { CollectionType collectionType = (CollectionType) type; final PluralAttributeMapping pluralAttributeMapping = insertAction.getSession() .getFactory() @@ -1212,9 +1215,9 @@ else if ( type.isCollectionType() && value != null ) { } } } - else if ( type.isComponentType() && value != null ) { + else if ( type instanceof ComponentType ) { // Support recursive checks of composite type properties for associations and collections. - final CompositeType compositeType = (CompositeType) type; + ComponentType compositeType = (ComponentType) type; final SharedSessionContractImplementor session = insertAction.getSession(); final Object[] componentValues = compositeType.getPropertyValues( value, session ); for ( int j = 0; j < componentValues.length; ++j ) { diff --git a/hibernate-core/src/main/java/org/hibernate/engine/spi/CascadingAction.java b/hibernate-core/src/main/java/org/hibernate/engine/spi/CascadingAction.java index 36cbc840fdf9..22a2de1304f4 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/spi/CascadingAction.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/spi/CascadingAction.java @@ -7,16 +7,22 @@ package org.hibernate.engine.spi; import java.util.Iterator; +import java.util.List; +import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.HibernateException; +import org.hibernate.Incubating; import org.hibernate.event.spi.EventSource; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.type.CollectionType; +import org.hibernate.type.ForeignKeyDirection; import org.hibernate.type.Type; /** * A session action that may be cascaded from parent entity to its children * + * @param The type of some context propagated with the cascading action + * * @author Gavin King * @author Steve Ebersole */ @@ -27,10 +33,9 @@ public interface CascadingAction { * * @param session The session within which the cascade is occurring. * @param child The child to which cascading should be performed. - * @param entityName The child's entity name - * @param anything Anything ;) Typically some form of cascade-local cache - * which is specific to each CascadingAction type - * @param isCascadeDeleteEnabled Are cascading deletes enabled. + * @param anything Some context specific to the kind of {@link CascadingAction} + * @param isCascadeDeleteEnabled Whether the foreign key is declared with + * {@link org.hibernate.annotations.OnDeleteAction#CASCADE on delete cascade}. */ void cascade( EventSource session, @@ -92,4 +97,18 @@ default void noCascade(EventSource session, Object parent, EntityPersister persi * Should this action be performed (or noCascade consulted) in the case of lazy properties. */ boolean performOnLazyProperty(); + + /** + * The cascade direction in which we care whether the foreign key is declared with + * {@link org.hibernate.annotations.OnDeleteAction#CASCADE on delete cascade}. + * + * @apiNote This allows us to reuse the long-existing boolean parameter of + * {@link #cascade(EventSource, Object, String, Object, boolean)} + * for multiple purposes. + * + */ + @Incubating @Nullable + default ForeignKeyDirection directionAffectedByCascadeDelete(){ + return null; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/engine/spi/CascadingActions.java b/hibernate-core/src/main/java/org/hibernate/engine/spi/CascadingActions.java index f58cda812fbf..a49da298f4c5 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/spi/CascadingActions.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/spi/CascadingActions.java @@ -6,6 +6,7 @@ */ package org.hibernate.engine.spi; +import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.HibernateException; import org.hibernate.Internal; import org.hibernate.LockMode; @@ -20,6 +21,7 @@ import org.hibernate.event.spi.RefreshContext; import org.hibernate.internal.CoreMessageLogger; import org.hibernate.type.CollectionType; +import org.hibernate.type.ForeignKeyDirection; import org.jboss.logging.Logger; import java.util.Iterator; @@ -73,6 +75,11 @@ public boolean deleteOrphans() { return true; } + @Override + public ForeignKeyDirection directionAffectedByCascadeDelete() { + return ForeignKeyDirection.FROM_PARENT; + } + @Override public String toString() { return "ACTION_DELETE"; @@ -378,7 +385,7 @@ public void cascade( Void context, boolean isCascadeDeleteEnabled) throws HibernateException { - if ( child != null && isChildTransient( session, child, entityName ) ) { + if ( child != null && isChildTransient( session, child, entityName, isCascadeDeleteEnabled ) ) { throw new TransientObjectException( "persistent instance references an unsaved transient instance of '" + entityName + "' (save the transient instance before flushing)" ); //TODO: should be TransientPropertyValueException @@ -419,13 +426,18 @@ public boolean performOnLazyProperty() { return false; } + @Override + public ForeignKeyDirection directionAffectedByCascadeDelete() { + return ForeignKeyDirection.TO_PARENT; + } + @Override public String toString() { return "ACTION_CHECK_ON_FLUSH"; } }; - private static boolean isChildTransient(EventSource session, Object child, String entityName) { + private static boolean isChildTransient(EventSource session, Object child, String entityName, boolean isCascadeDeleteEnabled) { if ( isHibernateProxy( child ) ) { // a proxy is always non-transient // and ForeignKeys.isTransient() @@ -440,7 +452,11 @@ private static boolean isChildTransient(EventSource session, Object child, Strin // we are good, even if it's not yet // inserted, since ordering problems // are detected and handled elsewhere - return entry.getStatus().isDeletedOrGone(); + return entry.getStatus().isDeletedOrGone() + // if the foreign key is 'on delete cascade' + // we don't have to throw because the database + // will delete the parent for us + && !isCascadeDeleteEnabled; } else { // TODO: check if it is a merged entity which has not yet been flushed @@ -495,6 +511,11 @@ public abstract static class BaseCascadingAction implements CascadingAction 1 || persister.isBatchLoadable(); + return persister.isBatchLoadable() || effectiveBatchSize( persister ) > 1; } public int effectiveBatchSize(EntityPersister persister) { @@ -336,7 +336,7 @@ public int effectiveBatchSize(EntityPersister persister) { } public boolean effectivelyBatchLoadable(EntityPersister persister) { - return batchSize > 1 || persister.isBatchLoadable(); + return persister.isBatchLoadable() || effectiveBatchSize( persister ) > 1; } public boolean getSubselectFetchEnabled() { diff --git a/hibernate-core/src/main/java/org/hibernate/engine/spi/PersistenceContext.java b/hibernate-core/src/main/java/org/hibernate/engine/spi/PersistenceContext.java index b60436330bc5..b5198bf3a4a1 100644 --- a/hibernate-core/src/main/java/org/hibernate/engine/spi/PersistenceContext.java +++ b/hibernate-core/src/main/java/org/hibernate/engine/spi/PersistenceContext.java @@ -864,4 +864,12 @@ EntityHolder claimEntityHolderIfPossible( * @return This persistence context's natural-id helper */ NaturalIdResolutions getNaturalIdResolutions(); + + /** + Remove the {@link EntityHolder} and set its state to DETACHED + */ + default @Nullable EntityHolder detachEntity(EntityKey key) { + EntityHolder entityHolder = removeEntityHolder( key ); + return entityHolder; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/event/internal/AbstractSaveEventListener.java b/hibernate-core/src/main/java/org/hibernate/event/internal/AbstractSaveEventListener.java index fd218493ebbd..d03b2ae90e5a 100644 --- a/hibernate-core/src/main/java/org/hibernate/event/internal/AbstractSaveEventListener.java +++ b/hibernate-core/src/main/java/org/hibernate/event/internal/AbstractSaveEventListener.java @@ -152,7 +152,8 @@ private static Object generateId( EventSource source, BeforeExecutionGenerator generator, EntityPersister persister) { - final Object id = generator.generate( source, entity, null, INSERT ); + final Object currentValue = generator.allowAssignedIdentifiers() ? persister.getIdentifier( entity, source ) : null; + final Object id = generator.generate( source, entity, currentValue, INSERT ); if ( id == null ) { throw new IdentifierGenerationException( "Null id generated for entity '" + persister.getEntityName() + "'" ); } diff --git a/hibernate-core/src/main/java/org/hibernate/event/internal/AbstractVisitor.java b/hibernate-core/src/main/java/org/hibernate/event/internal/AbstractVisitor.java index 19e2d9768556..1cda6c54bdbd 100644 --- a/hibernate-core/src/main/java/org/hibernate/event/internal/AbstractVisitor.java +++ b/hibernate-core/src/main/java/org/hibernate/event/internal/AbstractVisitor.java @@ -10,7 +10,9 @@ import org.hibernate.bytecode.enhance.spi.LazyPropertyInitializer; import org.hibernate.event.spi.EventSource; import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.type.AnyType; import org.hibernate.type.CollectionType; +import org.hibernate.type.ComponentType; import org.hibernate.type.CompositeType; import org.hibernate.type.EntityType; import org.hibernate.type.Type; @@ -87,15 +89,18 @@ Object processComponent(Object component, CompositeType componentType) throws Hi */ final Object processValue(Object value, Type type) throws HibernateException { - if ( type.isCollectionType() ) { + if ( type instanceof CollectionType ) { //even process null collections return processCollection( value, (CollectionType) type ); } - else if ( type.isEntityType() ) { + else if ( type instanceof EntityType ) { return processEntity( value, (EntityType) type ); } - else if ( type.isComponentType() ) { - return processComponent( value, (CompositeType) type ); + else if ( type instanceof ComponentType ) { + return processComponent( value, (ComponentType) type ); + } + else if ( type instanceof AnyType ) { + return processComponent( value, (AnyType) type ); } else { return null; diff --git a/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultDeleteEventListener.java b/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultDeleteEventListener.java index 0c5c4eaad3aa..498b051bf103 100644 --- a/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultDeleteEventListener.java +++ b/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultDeleteEventListener.java @@ -47,6 +47,7 @@ import org.hibernate.proxy.HibernateProxy; import org.hibernate.proxy.LazyInitializer; import org.hibernate.type.CollectionType; +import org.hibernate.type.ComponentType; import org.hibernate.type.CompositeType; import org.hibernate.type.Type; import org.hibernate.type.TypeHelper; @@ -108,7 +109,7 @@ private boolean optimizeUnloadedDelete(DeleteEvent event) { final EventSource source = event.getSession(); final EntityPersister persister = event.getFactory().getMappingMetamodel() .findEntityDescriptor( lazyInitializer.getEntityName() ); - final Object id = lazyInitializer.getIdentifier(); + final Object id = lazyInitializer.getInternalIdentifier(); final EntityKey key = source.generateEntityKey( id, persister ); final PersistenceContext persistenceContext = source.getPersistenceContextInternal(); if ( !persistenceContext.containsEntity( key ) @@ -137,15 +138,15 @@ && canBeDeletedWithoutLoading( source, persister ) ) { private static void deleteOwnedCollections(Type type, Object key, EventSource session) { final MappingMetamodelImplementor mappingMetamodel = session.getFactory().getMappingMetamodel(); final ActionQueue actionQueue = session.getActionQueue(); - if ( type.isCollectionType() ) { + if ( type instanceof CollectionType ) { final String role = ( (CollectionType) type ).getRole(); final CollectionPersister persister = mappingMetamodel.getCollectionDescriptor(role); if ( !persister.isInverse() && !skipRemoval( session, persister, key ) ) { actionQueue.addAction( new CollectionRemoveAction( persister, key, session ) ); } } - else if ( type.isComponentType() ) { - final Type[] subtypes = ( (CompositeType) type ).getSubtypes(); + else if ( type instanceof ComponentType ) { + final Type[] subtypes = ( (ComponentType) type ).getSubtypes(); for ( Type subtype : subtypes ) { deleteOwnedCollections( subtype, key, session ); } @@ -459,7 +460,7 @@ private Object[] createDeletedState( final String[] propertyNames = persister.getPropertyNames(); final BytecodeEnhancementMetadata enhancementMetadata = persister.getBytecodeEnhancementMetadata(); for ( int i = 0; i < types.length; i++) { - if ( types[i].isCollectionType() && !enhancementMetadata.isAttributeLoaded( parent, propertyNames[i] ) ) { + if ( types[i] instanceof CollectionType && !enhancementMetadata.isAttributeLoaded( parent, propertyNames[i] ) ) { final CollectionType collectionType = (CollectionType) types[i]; final CollectionPersister collectionDescriptor = persister.getFactory().getMappingMetamodel() .getCollectionDescriptor( collectionType.getRole() ); diff --git a/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultEvictEventListener.java b/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultEvictEventListener.java index 9b6f9714552e..c22929a5c4d1 100644 --- a/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultEvictEventListener.java +++ b/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultEvictEventListener.java @@ -59,7 +59,7 @@ public void onEvict(EvictEvent event) throws HibernateException { .getMappingMetamodel() .getEntityDescriptor( lazyInitializer.getEntityName() ); final EntityKey key = source.generateEntityKey( id, persister ); - final EntityHolder holder = persistenceContext.removeEntityHolder( key ); + final EntityHolder holder = persistenceContext.detachEntity( key ); // if the entity has been evicted then its holder is null if ( holder != null && !lazyInitializer.isUninitialized() ) { final Object entity = holder.getEntity(); diff --git a/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultFlushEntityEventListener.java b/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultFlushEntityEventListener.java index 59d2739ef589..26a9d2ddffd4 100644 --- a/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultFlushEntityEventListener.java +++ b/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultFlushEntityEventListener.java @@ -71,33 +71,26 @@ public void injectCallbackRegistry(CallbackRegistry callbackRegistry) { /** * make sure user didn't mangle the id */ - public void checkId(Object object, EntityPersister persister, Object id, SessionImplementor session) + public void checkId(Object object, EntityPersister persister, EntityEntry entry, SessionImplementor session) throws HibernateException { - - if ( id instanceof DelayedPostInsertIdentifier ) { - // this is a situation where the entity id is assigned by a post-insert generator - // and was saved outside the transaction forcing it to be delayed - return; - } - - final Object oid = persister.getIdentifier( object, session ); - - if ( id == null ) { - throw new AssertionFailure( "null id in " + persister.getEntityName() - + " entry (don't flush the Session after an exception occurs)" ); - } - - //Small optimisation: always try to avoid getIdentifierType().isEqual(..) when possible. - //(However it's not safe to invoke the equals() method as it might trigger side-effects) - if ( id == oid ) { - //No further checks necessary: - return; - } - - if ( !persister.getIdentifierType().isEqual( id, oid, session.getFactory() ) ) { - throw new HibernateException( "identifier of an instance of " + persister.getEntityName() - + " was altered from " + oid + " to " + id ); + final Object entryId = entry.getId(); + if ( entryId == null ) { + throw new AssertionFailure( "Entry for instance of '" + persister.getEntityName() + + "' has a null identifier (this can happen if the session is flushed after an exception occurs)" ); + } + if ( !(entryId instanceof DelayedPostInsertIdentifier) ) { + final Object currentId = persister.getIdentifier( object, session ); + // Small optimisation: always try to avoid getIdentifierType().isEqual(..) when possible. + // (However it's not safe to invoke the equals() method as it might trigger side effects.) + if ( entryId != currentId + && !entry.getStatus().isDeletedOrGone() + && !persister.getIdentifierType().isEqual( entryId, currentId, session.getFactory() ) ) { + throw new HibernateException( "Identifier of an instance of '" + persister.getEntityName() + + "' was altered from " + entryId + " to " + currentId ); + } } + // else this is a situation where the entity id is assigned by a post-insert + // generator and was saved outside the transaction, forcing it to be delayed } private void checkNaturalId( @@ -179,7 +172,7 @@ else if ( !mightBeDirty && loadedState != null ) { } else { final EntityPersister persister = entry.getPersister(); - checkId( entity, persister, entry.getId(), session ); + checkId( entity, persister, entry, session ); // grab its current state Object[] values = persister.getValues( entity ); checkNaturalId( persister, entity, entry, values, loadedState, session ); diff --git a/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultMergeEventListener.java b/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultMergeEventListener.java index 24f7af0f2f6d..3dbcb8dc1174 100644 --- a/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultMergeEventListener.java +++ b/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultMergeEventListener.java @@ -41,7 +41,9 @@ import org.hibernate.proxy.HibernateProxy; import org.hibernate.proxy.LazyInitializer; import org.hibernate.stat.spi.StatisticsImplementor; +import org.hibernate.type.AnyType; import org.hibernate.type.CollectionType; +import org.hibernate.type.ComponentType; import org.hibernate.type.CompositeType; import org.hibernate.type.EntityType; import org.hibernate.type.ForeignKeyDirection; @@ -162,17 +164,17 @@ private void merge(MergeEvent event, MergeContext copiedAlready, Object entity) final Object originalId; if ( entry == null ) { final EntityPersister persister = source.getEntityPersister( event.getEntityName(), entity ); - originalId = persister.getIdentifier( entity, source ); + originalId = persister.getIdentifier( entity, copiedAlready ); if ( originalId != null ) { final EntityKey entityKey; - if ( persister.getIdentifierType().isComponentType() ) { + if ( persister.getIdentifierType() instanceof ComponentType ) { /* this is needed in case of composite id containing an association with a generated identifier, in such a case generating the EntityKey will cause a NPE when trying to get the hashcode of the null id */ copiedId = copyCompositeTypeId( originalId, - (CompositeType) persister.getIdentifierType(), + (ComponentType) persister.getIdentifierType(), source, copiedAlready ); @@ -250,7 +252,7 @@ private static Object copyCompositeTypeId( final Object[] copyValues = compositeType.getPropertyValues( idCopy ); for ( int i = 0; i < subtypes.length; i++ ) { final Type subtype = subtypes[i]; - if ( subtype.isEntityType() ) { + if ( subtype instanceof EntityType ) { // the value of the copy in the MergeContext has the id assigned final Object o = mergeContext.get( propertyValues[i] ); if ( o != null ) { @@ -260,8 +262,11 @@ private static Object copyCompositeTypeId( copyValues[i] = subtype.deepCopy( propertyValues[i], sessionFactory ); } } - else if ( subtype.isComponentType() ) { - copyValues[i] = copyCompositeTypeId( propertyValues[i], (CompositeType) subtype, session, mergeContext ); + else if ( subtype instanceof AnyType ) { + copyValues[i] = copyCompositeTypeId( propertyValues[i], (AnyType) subtype, session, mergeContext ); + } + else if ( subtype instanceof ComponentType ) { + copyValues[i] = copyCompositeTypeId( propertyValues[i], (ComponentType) subtype, session, mergeContext ); } else { copyValues[i] = subtype.deepCopy( propertyValues[i], sessionFactory ); diff --git a/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultRefreshEventListener.java b/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultRefreshEventListener.java index a01cd40015bb..797f28b70972 100644 --- a/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultRefreshEventListener.java +++ b/hibernate-core/src/main/java/org/hibernate/event/internal/DefaultRefreshEventListener.java @@ -12,6 +12,8 @@ import org.hibernate.NonUniqueObjectException; import org.hibernate.TransientObjectException; import org.hibernate.UnresolvableObjectException; +import org.hibernate.bytecode.enhance.spi.interceptor.LazyAttributeLoadingInterceptor; +import org.hibernate.bytecode.spi.BytecodeEnhancementMetadata; import org.hibernate.cache.spi.access.CollectionDataAccess; import org.hibernate.cache.spi.access.EntityDataAccess; import org.hibernate.cache.spi.access.SoftLock; @@ -36,6 +38,7 @@ import org.hibernate.proxy.HibernateProxy; import org.hibernate.proxy.LazyInitializer; import org.hibernate.type.CollectionType; +import org.hibernate.type.ComponentType; import org.hibernate.type.CompositeType; import org.hibernate.type.Type; @@ -193,6 +196,14 @@ private static void refresh( EntityEntry entry, Object id, PersistenceContext persistenceContext) { + final BytecodeEnhancementMetadata instrumentationMetadata = persister.getInstrumentationMetadata(); + if ( object != null && instrumentationMetadata.isEnhancedForLazyLoading() ) { + final LazyAttributeLoadingInterceptor interceptor = instrumentationMetadata.extractInterceptor( object ); + if ( interceptor != null ) { + // The list of initialized lazy fields have to be cleared in order to refresh them from the database. + interceptor.clearInitializedLazyFields(); + } + } final Object result = source.getLoadQueryInfluencers().fromInternalFetchProfile( CascadingFetchProfile.REFRESH, @@ -315,7 +326,7 @@ private static void evictCachedCollections(Type[] types, Object id, EventSource final SessionFactoryImplementor factory = source.getFactory(); final MappingMetamodelImplementor metamodel = factory.getRuntimeMetamodels().getMappingMetamodel(); for ( Type type : types ) { - if ( type.isCollectionType() ) { + if ( type instanceof CollectionType ) { final String role = ((CollectionType) type).getRole(); final CollectionPersister collectionPersister = metamodel.getCollectionDescriptor( role ); if ( collectionPersister.hasCache() ) { @@ -331,8 +342,9 @@ private static void evictCachedCollections(Type[] types, Object id, EventSource actionQueue.registerProcess( (success, session) -> cache.unlockItem( session, ck, lock ) ); } } - else if ( type.isComponentType() ) { - final CompositeType compositeType = (CompositeType) type; + else if ( type instanceof ComponentType ) { + // Only components can contain collections + ComponentType compositeType = (ComponentType) type; evictCachedCollections( compositeType.getSubtypes(), id, source ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/event/spi/MergeContext.java b/hibernate-core/src/main/java/org/hibernate/event/spi/MergeContext.java index c9a83e3e45ab..17cf332e617a 100644 --- a/hibernate-core/src/main/java/org/hibernate/event/spi/MergeContext.java +++ b/hibernate-core/src/main/java/org/hibernate/event/spi/MergeContext.java @@ -366,4 +366,8 @@ private String printEntity(Object entity) { // Entity was not found in current persistence context. Use Object#toString() method. return "[" + entity + "]"; } + + public EventSource getEventSource() { + return session; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/generator/GeneratorCreationContext.java b/hibernate-core/src/main/java/org/hibernate/generator/GeneratorCreationContext.java index 094f0c1e2c9b..e47b146ad8dc 100644 --- a/hibernate-core/src/main/java/org/hibernate/generator/GeneratorCreationContext.java +++ b/hibernate-core/src/main/java/org/hibernate/generator/GeneratorCreationContext.java @@ -12,15 +12,41 @@ import org.hibernate.mapping.Property; import org.hibernate.service.ServiceRegistry; +/** + * Access to information useful during {@linkplain Generator} creation and initialization. + * + * @see AnnotationBasedGenerator + * @see org.hibernate.id.Configurable#create(GeneratorCreationContext) + */ @Incubating public interface GeneratorCreationContext { + /** + * View of the relational database objects (tables, sequences, ...) and namespaces (catalogs and schemas). + */ Database getDatabase(); + + /** + * Access to available services. + */ ServiceRegistry getServiceRegistry(); + /** + * The default catalog name, if one. + */ String getDefaultCatalog(); + + /** + * The default schema name, if one. + */ String getDefaultSchema(); + /** + * Mapping details for the entity; may be null in the case of and id-bag id generator. + */ PersistentClass getPersistentClass(); + /** + * The entity identifier or id-bag property details. + */ Property getProperty(); } diff --git a/hibernate-core/src/main/java/org/hibernate/generator/values/internal/GeneratedValuesMappingProducer.java b/hibernate-core/src/main/java/org/hibernate/generator/values/internal/GeneratedValuesMappingProducer.java index 9af06d67f36e..4c73e44e49d2 100644 --- a/hibernate-core/src/main/java/org/hibernate/generator/values/internal/GeneratedValuesMappingProducer.java +++ b/hibernate-core/src/main/java/org/hibernate/generator/values/internal/GeneratedValuesMappingProducer.java @@ -51,6 +51,7 @@ public JdbcValuesMapping resolve( null, sqlSelections::add, loadQueryInfluencers, + false, sessionFactory ); diff --git a/hibernate-core/src/main/java/org/hibernate/id/Assigned.java b/hibernate-core/src/main/java/org/hibernate/id/Assigned.java index ffce159958ee..22ebc92abccb 100644 --- a/hibernate-core/src/main/java/org/hibernate/id/Assigned.java +++ b/hibernate-core/src/main/java/org/hibernate/id/Assigned.java @@ -43,4 +43,9 @@ public void configure(Type type, Properties parameters, ServiceRegistry serviceR throw new MappingException("no entity name"); } } + + @Override + public boolean allowAssignedIdentifiers() { + return true; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/id/CompositeNestedGeneratedValueGenerator.java b/hibernate-core/src/main/java/org/hibernate/id/CompositeNestedGeneratedValueGenerator.java index d463248b4868..b140f45b4e44 100644 --- a/hibernate-core/src/main/java/org/hibernate/id/CompositeNestedGeneratedValueGenerator.java +++ b/hibernate-core/src/main/java/org/hibernate/id/CompositeNestedGeneratedValueGenerator.java @@ -16,10 +16,13 @@ import org.hibernate.boot.model.relational.ExportableProducer; import org.hibernate.boot.model.relational.SqlStringGenerationContext; import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.generator.BeforeExecutionGenerator; import org.hibernate.id.factory.spi.StandardGenerator; import org.hibernate.property.access.spi.Setter; import org.hibernate.type.CompositeType; +import static org.hibernate.generator.EventType.INSERT; + /** * For composite identifiers, defines a number of "nested" generations that * need to happen to "fill" the identifier property(s). @@ -74,7 +77,6 @@ public interface GenerationContextLocator { * determined {@linkplain GenerationContextLocator#locateGenerationContext context} */ public interface GenerationPlan extends ExportableProducer { - /** * Initializes this instance, in particular pre-generates SQL as necessary. *

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

      + *
    • {@linkplain org.hibernate.annotations.IdGeneratorType}
    • , or + *
    • {@linkplain org.hibernate.generator.AnnotationBasedGenerator} (in either implementation or constructor-injection form)
    • + *
    + * * @author Gavin King * @author Steve Ebersole */ public interface Configurable { + /** + * Called before {@link #configure(Type, Properties, ServiceRegistry)}, + * with an instance of {@link GeneratorCreationContext}. + * + * @since 6.6 + * @deprecated Added in 6.6 as a short-term work-around, but should really use on + * of the alternatives discussed at {@linkplain Configurable}. + * See HHH-18615. + */ + @Deprecated(forRemoval = true) + default void create(GeneratorCreationContext creationContext) throws MappingException {} + /** * Configure this instance, given the value of parameters * specified by the user as XML {@code } elements and diff --git a/hibernate-core/src/main/java/org/hibernate/id/ForeignGenerator.java b/hibernate-core/src/main/java/org/hibernate/id/ForeignGenerator.java index b02bf8746bf8..aa53cdaced8d 100644 --- a/hibernate-core/src/main/java/org/hibernate/id/ForeignGenerator.java +++ b/hibernate-core/src/main/java/org/hibernate/id/ForeignGenerator.java @@ -99,7 +99,7 @@ public Object generate(SharedSessionContractImplementor sessionImplementor, Obje final EntityType foreignValueSourceType; final Type propertyType = entityDescriptor.getPropertyType( propertyName ); - if ( propertyType.isEntityType() ) { + if ( propertyType instanceof EntityType ) { // the normal case foreignValueSourceType = (EntityType) propertyType; } diff --git a/hibernate-core/src/main/java/org/hibernate/id/IdentityGenerator.java b/hibernate-core/src/main/java/org/hibernate/id/IdentityGenerator.java index 407cab115e84..f07d21409f60 100644 --- a/hibernate-core/src/main/java/org/hibernate/id/IdentityGenerator.java +++ b/hibernate-core/src/main/java/org/hibernate/id/IdentityGenerator.java @@ -6,9 +6,10 @@ */ package org.hibernate.id; +import java.util.List; + import org.hibernate.boot.spi.SessionFactoryOptions; import org.hibernate.dialect.Dialect; -import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.generator.OnExecutionGenerator; import org.hibernate.id.factory.spi.StandardGenerator; import org.hibernate.id.insert.BasicSelectingDelegate; @@ -16,6 +17,7 @@ import org.hibernate.id.insert.InsertGeneratedIdentifierDelegate; import org.hibernate.id.insert.InsertReturningDelegate; import org.hibernate.id.insert.UniqueKeySelectingDelegate; +import org.hibernate.metamodel.mapping.ModelPart; import static org.hibernate.generator.EventType.INSERT; import static org.hibernate.generator.internal.NaturalIdHelper.getNaturalIdPropertyNames; @@ -54,11 +56,16 @@ public String[] getReferencedColumnValues(Dialect dialect) { @Override public InsertGeneratedIdentifierDelegate getGeneratedIdentifierDelegate(PostInsertIdentityPersister persister) { - final SessionFactoryImplementor factory = persister.getFactory(); - final Dialect dialect = factory.getJdbcServices().getDialect(); + final Dialect dialect = persister.getFactory().getJdbcServices().getDialect(); + final SessionFactoryOptions sessionFactoryOptions = persister.getFactory().getSessionFactoryOptions(); + final List generatedProperties = persister.getGeneratedProperties( INSERT ); + if ( generatedProperties.size() == 1 && sessionFactoryOptions.isGetGeneratedKeysEnabled() ) { + // Use Connection#prepareStatement(sql, Statement.RETURN_GENERATED_KEYS) when only retrieving identity + assert generatedProperties.get( 0 ).isEntityIdentifierMapping(); + return dialect.getIdentityColumnSupport().buildGetGeneratedKeysDelegate( persister ); + } // Try to use generic delegates if the dialects supports them - final SessionFactoryOptions sessionFactoryOptions = factory.getSessionFactoryOptions(); - if ( dialect.supportsInsertReturningGeneratedKeys() && sessionFactoryOptions.isGetGeneratedKeysEnabled() ) { + else if ( dialect.supportsInsertReturningGeneratedKeys() && sessionFactoryOptions.isGetGeneratedKeysEnabled() ) { return new GetGeneratedKeysDelegate( persister, false, INSERT ); } else if ( dialect.supportsInsertReturning() && noCustomSql( persister, INSERT ) ) { diff --git a/hibernate-core/src/main/java/org/hibernate/id/enhanced/SequenceStyleGenerator.java b/hibernate-core/src/main/java/org/hibernate/id/enhanced/SequenceStyleGenerator.java index 1e60c4ec7d10..329ccb17713c 100644 --- a/hibernate-core/src/main/java/org/hibernate/id/enhanced/SequenceStyleGenerator.java +++ b/hibernate-core/src/main/java/org/hibernate/id/enhanced/SequenceStyleGenerator.java @@ -15,6 +15,8 @@ import org.hibernate.HibernateException; import org.hibernate.MappingException; import org.hibernate.boot.model.naming.Identifier; +import org.hibernate.boot.model.naming.ObjectNameNormalizer; +import org.hibernate.boot.model.naming.PhysicalNamingStrategy; import org.hibernate.boot.model.relational.Database; import org.hibernate.boot.model.relational.QualifiedName; import org.hibernate.boot.model.relational.QualifiedNameParser; @@ -26,6 +28,7 @@ import org.hibernate.engine.config.spi.ConfigurationService; import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment; import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.generator.GeneratorCreationContext; import org.hibernate.id.BulkInsertionCapableIdentifierGenerator; import org.hibernate.id.IdentifierGenerator; import org.hibernate.id.PersistentIdentifierGenerator; @@ -163,6 +166,8 @@ public class SequenceStyleGenerator private Optimizer optimizer; private Type identifierType; + private PhysicalNamingStrategy physicalNamingStrategy; + /** * Getter for property 'databaseStructure'. * @@ -194,6 +199,11 @@ public Type getIdentifierType() { // Configurable implementation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + @Override + public void create(GeneratorCreationContext creationContext) throws MappingException { + physicalNamingStrategy = creationContext.getDatabase().getPhysicalNamingStrategy(); + } + @Override public void configure(Type type, Properties parameters, ServiceRegistry serviceRegistry) throws MappingException { final JdbcEnvironment jdbcEnvironment = serviceRegistry.requireService( JdbcEnvironment.class ); @@ -242,6 +252,9 @@ public void configure(Type type, Properties parameters, ServiceRegistry serviceR getInt( INITIAL_PARAM, parameters, -1 ) ); this.databaseStructure.configure( optimizer ); + + // we don't want or need this after initialization is complete + physicalNamingStrategy = null; } private int adjustIncrementSize( @@ -262,7 +275,9 @@ private int adjustIncrementSize( if ( sequenceMismatchStrategy != SequenceMismatchStrategy.NONE && optimizationStrategy.isPooled() && physicalSequence ) { - final String databaseSequenceName = sequenceName.getObjectName().getText(); + final String databaseSequenceName = physicalNamingStrategy != null + ? physicalNamingStrategy.toPhysicalSequenceName( sequenceName.getObjectName(), jdbcEnvironment ).getText() + : sequenceName.getObjectName().getText(); final Number databaseIncrementValue = isSchemaToBeRecreated( contributor, configurationService ) ? null : getSequenceIncrementValue( jdbcEnvironment, databaseSequenceName ); if ( databaseIncrementValue != null && databaseIncrementValue.intValue() != incrementSize) { final int dbIncrementValue = databaseIncrementValue.intValue(); diff --git a/hibernate-core/src/main/java/org/hibernate/id/factory/IdentifierGeneratorFactory.java b/hibernate-core/src/main/java/org/hibernate/id/factory/IdentifierGeneratorFactory.java index ec21727bbf2a..af06fb73e8ce 100644 --- a/hibernate-core/src/main/java/org/hibernate/id/factory/IdentifierGeneratorFactory.java +++ b/hibernate-core/src/main/java/org/hibernate/id/factory/IdentifierGeneratorFactory.java @@ -9,7 +9,7 @@ import java.util.Properties; import org.hibernate.Incubating; -import org.hibernate.dialect.Dialect; +import org.hibernate.generator.GeneratorCreationContext; import org.hibernate.id.IdentifierGenerator; import org.hibernate.id.factory.spi.GeneratorDefinitionResolver; import org.hibernate.service.Service; @@ -71,5 +71,21 @@ Generator createIdentifierGenerator( * @deprecated use {@link #createIdentifierGenerator(GenerationType, String, String, JavaType, Properties, GeneratorDefinitionResolver)} */ @Deprecated(since = "6.0") - Generator createIdentifierGenerator(String strategy, Type type, Properties parameters); + Generator createIdentifierGenerator(String strategy, Type type, GeneratorCreationContext creationContext, Properties parameters); + + /** + * Given a strategy, retrieve the appropriate identifier generator instance. + * + * @param strategy The generation strategy. + * @param type The mapping type for the identifier values. + * @param parameters Any parameters properties given in the generator mapping. + * + * @return The appropriate generator instance. + * + * @deprecated use {@link #createIdentifierGenerator(GenerationType, String, String, JavaType, Properties, GeneratorDefinitionResolver)} + */ + @Deprecated(since = "6.0") + default Generator createIdentifierGenerator(String strategy, Type type, Properties parameters) { + return createIdentifierGenerator( strategy, type, null, parameters ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/id/factory/internal/IdentifierGeneratorUtil.java b/hibernate-core/src/main/java/org/hibernate/id/factory/internal/IdentifierGeneratorUtil.java index 212cb3970a9e..b2f08bd9e1aa 100644 --- a/hibernate-core/src/main/java/org/hibernate/id/factory/internal/IdentifierGeneratorUtil.java +++ b/hibernate-core/src/main/java/org/hibernate/id/factory/internal/IdentifierGeneratorUtil.java @@ -6,10 +6,12 @@ */ package org.hibernate.id.factory.internal; +import org.hibernate.boot.model.relational.Database; import org.hibernate.cfg.AvailableSettings; import org.hibernate.dialect.Dialect; import org.hibernate.engine.config.spi.ConfigurationService; import org.hibernate.engine.config.spi.StandardConverters; +import org.hibernate.generator.GeneratorCreationContext; import org.hibernate.id.IdentifierGenerator; import org.hibernate.id.OptimizableGenerator; import org.hibernate.id.PersistentIdentifierGenerator; @@ -17,10 +19,13 @@ import org.hibernate.id.enhanced.SingleNamingStrategy; import org.hibernate.id.factory.IdentifierGeneratorFactory; import org.hibernate.mapping.Column; +import org.hibernate.mapping.PersistentClass; +import org.hibernate.mapping.Property; import org.hibernate.mapping.RootClass; import org.hibernate.mapping.SimpleValue; import org.hibernate.mapping.Table; import org.hibernate.generator.Generator; +import org.hibernate.service.ServiceRegistry; import java.util.Map; import java.util.Properties; @@ -37,6 +42,32 @@ public static Generator createLegacyIdentifierGenerator( return identifierGeneratorFactory.createIdentifierGenerator( simpleValue.getIdentifierGeneratorStrategy(), simpleValue.getType(), + new GeneratorCreationContext() { + @Override + public Database getDatabase() { + return simpleValue.getMetadata().getDatabase(); + } + @Override + public ServiceRegistry getServiceRegistry() { + return simpleValue.getServiceRegistry(); + } + @Override + public String getDefaultCatalog() { + return null; + } + @Override + public String getDefaultSchema() { + return null; + } + @Override + public PersistentClass getPersistentClass() { + return rootClass; + } + @Override + public Property getProperty() { + return rootClass.getIdentifierProperty(); + } + }, collectParameters( simpleValue, dialect, defaultCatalog, defaultSchema, rootClass ) ); } @@ -48,7 +79,7 @@ public static Properties collectParameters( String defaultSchema, RootClass rootClass) { final ConfigurationService configService = - simpleValue.getMetadata().getMetadataBuildingOptions().getServiceRegistry() + simpleValue.getServiceRegistry() .requireService( ConfigurationService.class ); final Properties params = new Properties(); diff --git a/hibernate-core/src/main/java/org/hibernate/id/factory/internal/StandardIdentifierGeneratorFactory.java b/hibernate-core/src/main/java/org/hibernate/id/factory/internal/StandardIdentifierGeneratorFactory.java index ed30caee7f94..c24de0b5d99f 100644 --- a/hibernate-core/src/main/java/org/hibernate/id/factory/internal/StandardIdentifierGeneratorFactory.java +++ b/hibernate-core/src/main/java/org/hibernate/id/factory/internal/StandardIdentifierGeneratorFactory.java @@ -19,6 +19,7 @@ import org.hibernate.engine.config.spi.ConfigurationService; import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment; import org.hibernate.generator.Generator; +import org.hibernate.generator.GeneratorCreationContext; import org.hibernate.id.Assigned; import org.hibernate.id.Configurable; import org.hibernate.id.ForeignGenerator; @@ -206,7 +207,8 @@ private Dialect getDialect() { } @Override @Deprecated - public Generator createIdentifierGenerator(String strategy, Type type, Properties parameters) { + public Generator createIdentifierGenerator( + String strategy, Type type, GeneratorCreationContext creationContext, Properties parameters) { try { final Class clazz = getIdentifierGeneratorClass( strategy ); final Generator identifierGenerator; @@ -222,7 +224,11 @@ public Generator createIdentifierGenerator(String strategy, Type type, Propertie } if ( identifierGenerator instanceof Configurable ) { - ( (Configurable) identifierGenerator ).configure( type, parameters, serviceRegistry ); + final Configurable configurable = (Configurable) identifierGenerator; + if ( creationContext != null ) { + configurable.create( creationContext ); + } + configurable.configure( type, parameters, serviceRegistry ); } return identifierGenerator; } diff --git a/hibernate-core/src/main/java/org/hibernate/id/insert/AbstractSelectingDelegate.java b/hibernate-core/src/main/java/org/hibernate/id/insert/AbstractSelectingDelegate.java index 4c2ad98a2539..74013f1fafa8 100644 --- a/hibernate-core/src/main/java/org/hibernate/id/insert/AbstractSelectingDelegate.java +++ b/hibernate-core/src/main/java/org/hibernate/id/insert/AbstractSelectingDelegate.java @@ -106,7 +106,6 @@ public GeneratedValues performMutation( statementDetails.releaseStatement( session ); } jdbcValueBindings.afterStatement( statementDetails.getMutatingTableDetails() ); - session.getJdbcCoordinator().afterStatementExecution(); } // the insert is complete, select the generated id... diff --git a/hibernate-core/src/main/java/org/hibernate/id/insert/GetGeneratedKeysDelegate.java b/hibernate-core/src/main/java/org/hibernate/id/insert/GetGeneratedKeysDelegate.java index 63d8c2089453..3d823a9cf9c5 100644 --- a/hibernate-core/src/main/java/org/hibernate/id/insert/GetGeneratedKeysDelegate.java +++ b/hibernate-core/src/main/java/org/hibernate/id/insert/GetGeneratedKeysDelegate.java @@ -153,7 +153,6 @@ public GeneratedValues performMutation( statementDetails.releaseStatement( session ); } jdbcValueBindings.afterStatement( statementDetails.getMutatingTableDetails() ); - jdbcCoordinator.afterStatementExecution(); } } diff --git a/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java b/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java index 3a3e80c088dc..e24c4a54d7c1 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/AbstractSharedSessionContract.java @@ -19,6 +19,7 @@ import java.util.function.Function; import jakarta.persistence.EntityGraph; +import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.CacheMode; import org.hibernate.EntityNameResolver; import org.hibernate.Filter; @@ -891,22 +892,8 @@ public QueryImplementor createQuery(String queryString, Class expected // dynamic native (SQL) query handling @Override @SuppressWarnings("rawtypes") - public NativeQueryImpl createNativeQuery(String sqlString) { - checkOpen(); - pulseTransactionCoordinator(); - delayedAfterCompletion(); - - try { - final NativeQueryImpl query = new NativeQueryImpl<>( sqlString, this ); - if ( isEmpty( query.getComment() ) ) { - query.setComment( "dynamic native SQL query" ); - } - applyQuerySettingsAndHints( query ); - return query; - } - catch (RuntimeException he) { - throw getExceptionConverter().convert( he ); - } + public NativeQueryImplementor createNativeQuery(String sqlString) { + return createNativeQuery( sqlString, (Class) null ); } @Override @SuppressWarnings("rawtypes") @@ -939,12 +926,28 @@ protected NamedResultSetMappingMemento getResultSetMappingMemento(String resultS @Override @SuppressWarnings({"rawtypes", "unchecked"}) //note: we're doing something a bit funny here to work around // the clashing signatures declared by the supertypes - public NativeQueryImplementor createNativeQuery(String sqlString, Class resultClass) { - final NativeQueryImpl query = createNativeQuery( sqlString ); - addResultType( resultClass, query ); - return query; + public NativeQueryImplementor createNativeQuery(String sqlString, @Nullable Class resultClass) { + checkOpen(); + pulseTransactionCoordinator(); + delayedAfterCompletion(); + + try { + final NativeQueryImpl query = new NativeQueryImpl<>( sqlString, resultClass, this ); + if ( isEmpty( query.getComment() ) ) { + query.setComment( "dynamic native SQL query" ); + } + applyQuerySettingsAndHints( query ); + return query; + } + catch (RuntimeException he) { + throw getExceptionConverter().convert( he ); + } } + /** + * @deprecated Use {@link NativeQueryImpl#NativeQueryImpl(String, Class, SharedSessionContractImplementor)} instead + */ + @Deprecated(forRemoval = true) protected void addResultType(Class resultClass, NativeQueryImplementor query) { if ( Tuple.class.equals( resultClass ) ) { query.setTupleTransformer( NativeQueryTupleTransformer.INSTANCE ); diff --git a/hibernate-core/src/main/java/org/hibernate/internal/CoordinatingEntityNameResolver.java b/hibernate-core/src/main/java/org/hibernate/internal/CoordinatingEntityNameResolver.java index d325f36cb528..2ba0bfb3e1c4 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/CoordinatingEntityNameResolver.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/CoordinatingEntityNameResolver.java @@ -30,15 +30,15 @@ public String resolveEntityName(Object entity) { return entityName; } - final MappingMetamodelImplementor mappingMetamodel = sessionFactory.getRuntimeMetamodels().getMappingMetamodel(); - for ( EntityNameResolver resolver : mappingMetamodel.getEntityNameResolvers() ) { + for ( EntityNameResolver resolver : sessionFactory.getSessionFactoryOptions().getEntityNameResolvers() ) { entityName = resolver.resolveEntityName( entity ); if ( entityName != null ) { return entityName; } } - for ( EntityNameResolver resolver : sessionFactory.getSessionFactoryOptions().getEntityNameResolvers() ) { + final MappingMetamodelImplementor mappingMetamodel = sessionFactory.getRuntimeMetamodels().getMappingMetamodel(); + for ( EntityNameResolver resolver : mappingMetamodel.getEntityNameResolvers() ) { entityName = resolver.resolveEntityName( entity ); if ( entityName != null ) { return entityName; diff --git a/hibernate-core/src/main/java/org/hibernate/internal/SessionFactoryImpl.java b/hibernate-core/src/main/java/org/hibernate/internal/SessionFactoryImpl.java index 4420b3e672b9..9ca7b6b571eb 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/SessionFactoryImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/SessionFactoryImpl.java @@ -76,6 +76,7 @@ import org.hibernate.jpa.internal.ExceptionMapperLegacyJpaImpl; import org.hibernate.jpa.internal.PersistenceUnitUtilImpl; import org.hibernate.mapping.Collection; +import org.hibernate.mapping.KeyValue; import org.hibernate.mapping.PersistentClass; import org.hibernate.mapping.RootClass; import org.hibernate.mapping.SimpleValue; @@ -462,7 +463,8 @@ private static Map createGenerators( bootMetamodel.getEntityBindings().stream() .filter( model -> !model.isInherited() ) .forEach( model -> { - final Generator generator = model.getIdentifier().createGenerator( + final KeyValue id = model.getIdentifier(); + final Generator generator = id.createGenerator( bootstrapContext.getIdentifierGeneratorFactory(), jdbcServices.getJdbcEnvironment().getDialect(), (RootClass) model @@ -471,8 +473,11 @@ private static Map createGenerators( final Configurable identifierGenerator = (Configurable) generator; identifierGenerator.initialize( sqlStringGenerationContext ); } - if ( generator.allowAssignedIdentifiers() ) { - ( (SimpleValue) model.getIdentifier() ).setNullValue( "undefined" ); + if ( generator.allowAssignedIdentifiers() && id instanceof SimpleValue ) { + final SimpleValue simpleValue = (SimpleValue) id; + if ( simpleValue.getNullValue() == null ) { + simpleValue.setNullValue( "undefined" ); + } } generators.put( model.getEntityName(), generator ); } ); diff --git a/hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java b/hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java index e636475e7a2e..ad797db04652 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/SessionImpl.java @@ -1788,7 +1788,7 @@ public T getReference(T object) { checkOpen(); final LazyInitializer lazyInitializer = extractLazyInitializer( object ); if ( lazyInitializer != null ) { - return (T) getReference( lazyInitializer.getPersistentClass(), lazyInitializer.getIdentifier() ); + return (T) getReference( lazyInitializer.getPersistentClass(), lazyInitializer.getInternalIdentifier() ); } else { EntityPersister persister = getEntityPersister( null, object ); diff --git a/hibernate-core/src/main/java/org/hibernate/internal/StatelessSessionImpl.java b/hibernate-core/src/main/java/org/hibernate/internal/StatelessSessionImpl.java index a7fa4669aa51..86bc34112719 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/StatelessSessionImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/StatelessSessionImpl.java @@ -131,7 +131,8 @@ public Object insert(String entityName, Object entity) { } final Generator generator = persister.getGenerator(); if ( !generator.generatedOnExecution( entity, this ) ) { - id = ( (BeforeExecutionGenerator) generator).generate( this, entity, null, INSERT ); + final Object currentValue = generator.allowAssignedIdentifiers() ? persister.getIdentifier( entity, this ) : null; + id = ( (BeforeExecutionGenerator) generator ).generate( this, entity, currentValue, INSERT ); if ( firePreInsert(entity, id, state, persister) ) { return id; } @@ -702,7 +703,7 @@ public void fetch(Object association) { if ( initializer != null ) { if ( initializer.isUninitialized() ) { final String entityName = initializer.getEntityName(); - final Object id = initializer.getIdentifier(); + final Object id = initializer.getInternalIdentifier(); initializer.setSession( this ); persistenceContext.beforeLoad(); try { diff --git a/hibernate-core/src/main/java/org/hibernate/internal/util/EntityPrinter.java b/hibernate-core/src/main/java/org/hibernate/internal/util/EntityPrinter.java index 12ccc44062e5..78618d829302 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/util/EntityPrinter.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/util/EntityPrinter.java @@ -15,6 +15,7 @@ import org.hibernate.engine.spi.EntityHolder; import org.hibernate.engine.spi.EntityKey; import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.engine.spi.TypedValue; import org.hibernate.internal.CoreLogging; import org.hibernate.internal.CoreMessageLogger; @@ -53,7 +54,7 @@ public String toString(String entityName, Object entity) throws HibernateExcepti result.put( entityPersister.getIdentifierPropertyName(), entityPersister.getIdentifierType().toLoggableString( - entityPersister.getIdentifier( entity, null ), + entityPersister.getIdentifier( entity, (SharedSessionContractImplementor) null ), factory ) ); diff --git a/hibernate-core/src/main/java/org/hibernate/internal/util/config/ConfigurationHelper.java b/hibernate-core/src/main/java/org/hibernate/internal/util/config/ConfigurationHelper.java index 4fd922b427cb..13bfa18c4a82 100644 --- a/hibernate-core/src/main/java/org/hibernate/internal/util/config/ConfigurationHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/internal/util/config/ConfigurationHelper.java @@ -262,7 +262,7 @@ public static Map clone(Map configurationValues) { /** - * replace a property by a starred version + * Replace a property by a starred version * * @param props properties to check * @param key property to mask @@ -270,16 +270,30 @@ public static Map clone(Map configurationValues) { * @return cloned and masked properties */ public static Properties maskOut(Properties props, String key) { - Properties clone = ( Properties ) props.clone(); + Properties clone = (Properties) props.clone(); if ( clone.get( key ) != null ) { clone.setProperty( key, "****" ); } return clone; } - - - + /** + * Replace properties by starred version + * + * @param props properties to check + * @param keys properties to mask + * + * @return cloned and masked properties + */ + public static Properties maskOut(Properties props, String... keys) { + Properties clone = (Properties) props.clone(); + for ( String key : keys ) { + if ( clone.get( key ) != null ) { + clone.setProperty( key, "****" ); + } + } + return clone; + } /** * Extract a property value by name from the given properties object. diff --git a/hibernate-core/src/main/java/org/hibernate/jpa/boot/internal/EntityManagerFactoryBuilderImpl.java b/hibernate-core/src/main/java/org/hibernate/jpa/boot/internal/EntityManagerFactoryBuilderImpl.java index c089097949a0..ff2cbe9d3cf4 100644 --- a/hibernate-core/src/main/java/org/hibernate/jpa/boot/internal/EntityManagerFactoryBuilderImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/jpa/boot/internal/EntityManagerFactoryBuilderImpl.java @@ -57,6 +57,7 @@ import org.hibernate.bytecode.enhance.spi.EnhancementException; import org.hibernate.bytecode.enhance.spi.UnloadedClass; import org.hibernate.bytecode.enhance.spi.UnloadedField; +import org.hibernate.bytecode.spi.BytecodeProvider; import org.hibernate.bytecode.spi.ClassTransformer; import org.hibernate.cfg.AvailableSettings; import org.hibernate.cfg.Environment; @@ -128,6 +129,7 @@ import static org.hibernate.cfg.AvailableSettings.TRANSACTION_COORDINATOR_STRATEGY; import static org.hibernate.cfg.AvailableSettings.URL; import static org.hibernate.cfg.AvailableSettings.USER; +import static org.hibernate.cfg.BytecodeSettings.BYTECODE_PROVIDER_INSTANCE; import static org.hibernate.cfg.BytecodeSettings.ENHANCER_ENABLE_ASSOCIATION_MANAGEMENT; import static org.hibernate.cfg.BytecodeSettings.ENHANCER_ENABLE_DIRTY_TRACKING; import static org.hibernate.cfg.BytecodeSettings.ENHANCER_ENABLE_LAZY_INITIALIZATION; @@ -443,6 +445,11 @@ protected EnhancementContext getEnhancementContext( final boolean dirtyTrackingEnabled, final boolean lazyInitializationEnabled, final boolean associationManagementEnabled ) { + final Object propValue = configurationValues.get( BYTECODE_PROVIDER_INSTANCE ); + if ( propValue != null && ( ! ( propValue instanceof BytecodeProvider ) ) ) { + throw persistenceException( "Property " + BYTECODE_PROVIDER_INSTANCE + " was set to '" + propValue + "', which is not compatible with the expected type " + BytecodeProvider.class ); + } + final BytecodeProvider overriddenBytecodeProvider = (BytecodeProvider) propValue; return new DefaultEnhancementContext() { @Override @@ -483,6 +490,10 @@ public boolean doExtendedEnhancement(UnloadedClass classDescriptor) { return false; } + @Override + public BytecodeProvider getBytecodeProvider() { + return overriddenBytecodeProvider; + } }; } diff --git a/hibernate-core/src/main/java/org/hibernate/jpa/boot/internal/PersistenceUnitInfoDescriptor.java b/hibernate-core/src/main/java/org/hibernate/jpa/boot/internal/PersistenceUnitInfoDescriptor.java index 8b5e2b2a79f0..b3803166cef7 100644 --- a/hibernate-core/src/main/java/org/hibernate/jpa/boot/internal/PersistenceUnitInfoDescriptor.java +++ b/hibernate-core/src/main/java/org/hibernate/jpa/boot/internal/PersistenceUnitInfoDescriptor.java @@ -7,26 +7,29 @@ package org.hibernate.jpa.boot.internal; import java.net.URL; -import java.util.ArrayList; -import java.util.Collection; import java.util.List; import java.util.Properties; +import org.hibernate.bytecode.enhance.spi.EnhancementContext; +import org.hibernate.bytecode.spi.ClassTransformer; +import org.hibernate.internal.CoreLogging; +import org.hibernate.internal.CoreMessageLogger; +import org.hibernate.jpa.boot.spi.PersistenceUnitDescriptor; +import org.hibernate.jpa.internal.enhance.EnhancingClassTransformerImpl; + import jakarta.persistence.PersistenceException; import jakarta.persistence.SharedCacheMode; import jakarta.persistence.ValidationMode; import jakarta.persistence.spi.PersistenceUnitInfo; import jakarta.persistence.spi.PersistenceUnitTransactionType; -import org.hibernate.bytecode.enhance.spi.EnhancementContext; -import org.hibernate.bytecode.spi.ClassTransformer; -import org.hibernate.jpa.boot.spi.PersistenceUnitDescriptor; -import org.hibernate.jpa.internal.enhance.EnhancingClassTransformerImpl; - /** * @author Steve Ebersole */ public class PersistenceUnitInfoDescriptor implements PersistenceUnitDescriptor { + + private static final CoreMessageLogger LOGGER = CoreLogging.messageLogger( PersistenceUnitInfoDescriptor.class ); + private final PersistenceUnitInfo persistenceUnitInfo; private ClassTransformer classTransformer; @@ -121,6 +124,9 @@ public void pushClassTransformer(EnhancementContext enhancementContext) { } // During testing, we will return a null temp class loader in cases where we don't care about enhancement if ( persistenceUnitInfo.getNewTempClassLoader() != null ) { + if ( LOGGER.isDebugEnabled() ) { + LOGGER.debug( "Pushing class transformers for PU named '" + getName() + "' on loading classloader " + enhancementContext.getLoadingClassLoader() ); + } final EnhancingClassTransformerImpl classTransformer = new EnhancingClassTransformerImpl( enhancementContext ); this.classTransformer = classTransformer; persistenceUnitInfo.addTransformer( classTransformer ); diff --git a/hibernate-core/src/main/java/org/hibernate/jpa/internal/PersistenceUnitUtilImpl.java b/hibernate-core/src/main/java/org/hibernate/jpa/internal/PersistenceUnitUtilImpl.java index 2cfbe054cdf8..b3572934bcd7 100644 --- a/hibernate-core/src/main/java/org/hibernate/jpa/internal/PersistenceUnitUtilImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/jpa/internal/PersistenceUnitUtilImpl.java @@ -13,6 +13,7 @@ import org.hibernate.MappingException; import org.hibernate.engine.spi.EntityEntry; import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.jpa.internal.util.PersistenceUtilHelper; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.proxy.LazyInitializer; @@ -106,7 +107,7 @@ private Object getIdentifierFromPersister(Object entity) { catch (MappingException ex) { throw new IllegalArgumentException( entityClass.getName() + " is not an entity", ex ); } - return persister.getIdentifier( entity, null ); + return persister.getIdentifier( entity, (SharedSessionContractImplementor) null ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/jpa/internal/enhance/EnhancingClassTransformerImpl.java b/hibernate-core/src/main/java/org/hibernate/jpa/internal/enhance/EnhancingClassTransformerImpl.java index 77383de47070..bf5368927212 100644 --- a/hibernate-core/src/main/java/org/hibernate/jpa/internal/enhance/EnhancingClassTransformerImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/jpa/internal/enhance/EnhancingClassTransformerImpl.java @@ -11,6 +11,7 @@ import java.util.Objects; import java.util.concurrent.locks.ReentrantLock; +import org.hibernate.bytecode.enhance.internal.bytebuddy.CorePrefixFilter; import org.hibernate.bytecode.enhance.spi.EnhancementContext; import org.hibernate.bytecode.enhance.spi.EnhancementContextWrapper; import org.hibernate.bytecode.enhance.spi.Enhancer; @@ -31,13 +32,11 @@ public class EnhancingClassTransformerImpl implements ClassTransformer { private final ReentrantLock lock = new ReentrantLock(); private volatile WeakReference entryReference; - //This list is matching the constants used by CoreTypePool's default constructor - private static final String[] NO_TRANSFORM_PREFIXES = { "jakarta/", "java/", "org/hibernate/annotations/" }; - public EnhancingClassTransformerImpl(EnhancementContext enhancementContext) { Objects.requireNonNull( enhancementContext ); this.enhancementContext = enhancementContext; - this.bytecodeProvider = BytecodeProviderInitiator.buildDefaultBytecodeProvider(); + final BytecodeProvider overriddenProvider = enhancementContext.getBytecodeProvider(); + this.bytecodeProvider = overriddenProvider == null ? BytecodeProviderInitiator.buildDefaultBytecodeProvider() : overriddenProvider; } @Override @@ -48,14 +47,13 @@ public byte[] transform( ProtectionDomain protectionDomain, byte[] classfileBuffer) throws TransformerException { - //Take care to not transform certain types; this is both an optimisation (we can skip this unnecessary work) - //and a safety precaution as we otherwise risk attempting to redefine classes which have already been loaded: - //see https://hibernate.atlassian.net/browse/HHH-18108 - //N.B. this className doesn't use the dot-format but the slashes for package separators. - for ( String prefix : NO_TRANSFORM_PREFIXES ) { - if ( className.startsWith( prefix ) ) { - return null; - } + //N.B. the "className" argument doesn't use the dot-format but the slashes for package separators. + final String classNameDotFormat = className.replace( '/', '.' ); + if ( CorePrefixFilter.DEFAULT_INSTANCE.isCoreClassName( classNameDotFormat ) ) { + //Take care to not transform certain types; this is both an optimisation (we can skip this unnecessary work) + //and a safety precaution as we otherwise risk attempting to redefine classes which have already been loaded: + //see https://hibernate.atlassian.net/browse/HHH-18108 + return null; } try { @@ -64,9 +62,6 @@ public byte[] transform( catch (final Exception e) { throw new TransformerException( "Error performing enhancement of " + className, e ); } - finally { - bytecodeProvider.resetCaches(); - } } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/jpa/spi/NativeQueryArrayTransformer.java b/hibernate-core/src/main/java/org/hibernate/jpa/spi/NativeQueryArrayTransformer.java new file mode 100644 index 000000000000..bcadd9d5b36e --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/jpa/spi/NativeQueryArrayTransformer.java @@ -0,0 +1,22 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.jpa.spi; + +import org.hibernate.query.TupleTransformer; + +/** + * A {@link TupleTransformer} for handling {@code Object[]} results from native queries. + */ +public class NativeQueryArrayTransformer implements TupleTransformer { + + public static final NativeQueryArrayTransformer INSTANCE = new NativeQueryArrayTransformer(); + + @Override + public Object[] transformTuple(Object[] tuple, String[] aliases) { + return tuple; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/jpa/spi/NativeQueryConstructorTransformer.java b/hibernate-core/src/main/java/org/hibernate/jpa/spi/NativeQueryConstructorTransformer.java index b9414bae993b..e0e82a92d5b7 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 @@ -20,7 +20,7 @@ public class NativeQueryConstructorTransformer implements TupleTransformer { private final Class resultClass; - private Constructor constructor; + private transient Constructor constructor; private Constructor constructor(Object[] elements) { if ( constructor == null ) { @@ -71,4 +71,21 @@ public T transformTuple(Object[] tuple, String[] aliases) { throw new InstantiationException( "Cannot instantiate query result type", resultClass, e ); } } + + @Override + public boolean equals(Object obj) { + if ( this == obj ) { + return true; + } + if ( obj == null || getClass() != obj.getClass() ) { + return false; + } + final NativeQueryConstructorTransformer that = (NativeQueryConstructorTransformer) obj; + return resultClass.equals( that.resultClass ); + } + + @Override + public int hashCode() { + return resultClass.hashCode(); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/CacheEntityLoaderHelper.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/CacheEntityLoaderHelper.java index 833cc62b96f4..424c78789c70 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/CacheEntityLoaderHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/CacheEntityLoaderHelper.java @@ -35,7 +35,7 @@ import org.hibernate.internal.CoreMessageLogger; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.pretty.MessageHelper; -import org.hibernate.proxy.HibernateProxy; +import org.hibernate.proxy.LazyInitializer; import org.hibernate.sql.results.LoadingLogger; import org.hibernate.stat.internal.StatsHelper; import org.hibernate.stat.spi.StatisticsImplementor; @@ -47,6 +47,7 @@ import static org.hibernate.engine.internal.ManagedTypeHelper.isPersistentAttributeInterceptable; import static org.hibernate.engine.internal.Versioning.getVersion; import static org.hibernate.loader.ast.internal.LoaderHelper.upgradeLock; +import static org.hibernate.proxy.HibernateProxy.extractLazyInitializer; /** * @author Vlad Mihalcea @@ -388,7 +389,6 @@ private Object convertCacheEntryToEntity( EntityKey entityKey) { final SessionFactoryImplementor factory = source.getFactory(); - final EntityPersister subclassPersister; if ( LOG.isTraceEnabled() ) { LOG.tracef( @@ -398,19 +398,20 @@ private Object convertCacheEntryToEntity( ); } - final Object entity; + final EntityPersister subclassPersister = + factory.getRuntimeMetamodels().getMappingMetamodel() + .getEntityDescriptor( entry.getSubclass() ); + final PersistenceContext persistenceContext = source.getPersistenceContextInternal(); + final EntityHolder oldHolder = persistenceContext.getEntityHolder( entityKey ); - subclassPersister = factory.getRuntimeMetamodels() - .getMappingMetamodel() - .getEntityDescriptor( entry.getSubclass() ); + final Object entity; if ( instanceToLoad != null ) { entity = instanceToLoad; } else { - final EntityHolder holder = source.getPersistenceContextInternal().getEntityHolder( entityKey ); - if ( holder != null && holder.getEntity() != null ) { + if ( oldHolder != null && oldHolder.getEntity() != null ) { // Use the entity which might already be - entity = holder.getEntity(); + entity = oldHolder.getEntity(); } else { entity = source.instantiate( subclassPersister, entityId ); @@ -430,23 +431,39 @@ private Object convertCacheEntryToEntity( } // make it circular-reference safe - TwoPhaseLoad.addUninitializedCachedEntity( - entityKey, - entity, - subclassPersister, - LockMode.NONE, - entry.getVersion(), - source - ); - - final PersistenceContext persistenceContext = source.getPersistenceContext(); - final Object[] values; - final Object version; + final EntityHolder holder = persistenceContext.addEntityHolder( entityKey, entity ); + final Object proxy = holder.getProxy(); final boolean isReadOnly; + if ( proxy != null ) { + // there is already a proxy for this impl + // only set the status to read-only if the proxy is read-only + final LazyInitializer lazyInitializer = extractLazyInitializer( proxy ); + assert lazyInitializer != null; + lazyInitializer.setImplementation( entity ); + + isReadOnly = lazyInitializer.isReadOnly(); + } + else { + isReadOnly = source.isDefaultReadOnly(); + } + holder.setEntityEntry( + persistenceContext.addEntry( + entity, + Status.LOADING, + null, + null, + entityKey.getIdentifier(), + entry.getVersion(), + LockMode.NONE, + true, + persister, + false + ) + ); final Type[] types = subclassPersister.getPropertyTypes(); // initializes the entity by (desired) side-effect - values = ( (StandardCacheEntryImpl) entry ).assemble( + final Object[] values = ( (StandardCacheEntryImpl) entry ).assemble( entity, entityId, subclassPersister, @@ -462,32 +479,23 @@ private Object convertCacheEntryToEntity( source ); } - version = getVersion( values, subclassPersister ); + final Object version = getVersion( values, subclassPersister ); LOG.tracef( "Cached Version : %s", version ); - final Object proxy = persistenceContext.getProxy( entityKey ); - if ( proxy != null ) { - // there is already a proxy for this impl - // only set the status to read-only if the proxy is read-only - isReadOnly = HibernateProxy.extractLazyInitializer( proxy ).isReadOnly(); - } - else { - isReadOnly = source.isDefaultReadOnly(); - } - - EntityEntry entityEntry = persistenceContext.addEntry( - entity, - ( isReadOnly ? Status.READ_ONLY : Status.MANAGED ), - values, - null, - entityId, - version, - LockMode.NONE, - true, - subclassPersister, - false + holder.setEntityEntry( + persistenceContext.addEntry( + entity, + isReadOnly ? Status.READ_ONLY : Status.MANAGED, + values, + null, + entityId, + version, + LockMode.NONE, + true, + subclassPersister, + false + ) ); - persistenceContext.getEntityHolder( entityKey ).setEntityEntry( entityEntry ); subclassPersister.afterInitialize( entity, source ); persistenceContext.initializeNonLazyCollections(); diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/CollectionBatchLoaderArrayParam.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/CollectionBatchLoaderArrayParam.java index fb578add5306..7cce9d59c762 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 @@ -19,10 +19,10 @@ import org.hibernate.loader.ast.spi.SqlArrayMultiKeyLoader; import org.hibernate.metamodel.mapping.ForeignKeyDescriptor; import org.hibernate.metamodel.mapping.JdbcMapping; -import org.hibernate.metamodel.mapping.NonAggregatedIdentifierMapping; import org.hibernate.metamodel.mapping.PluralAttributeMapping; -import org.hibernate.metamodel.mapping.ValuedModelPart; -import org.hibernate.metamodel.mapping.internal.IdClassEmbeddable; +import org.hibernate.metamodel.mapping.SelectableMapping; +import org.hibernate.metamodel.mapping.SqlTypedMapping; +import org.hibernate.metamodel.mapping.internal.SqlTypedMappingImpl; import org.hibernate.query.spi.QueryOptions; import org.hibernate.sql.ast.tree.expression.JdbcParameter; import org.hibernate.sql.ast.tree.from.TableGroup; @@ -30,7 +30,7 @@ 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.JdbcParameterImpl; +import org.hibernate.sql.exec.internal.SqlTypedMappingJdbcParameter; import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; import org.hibernate.sql.exec.spi.JdbcParameterBindings; import org.hibernate.sql.exec.spi.JdbcParametersList; @@ -51,7 +51,7 @@ public class CollectionBatchLoaderArrayParam extends AbstractCollectionBatchLoader implements SqlArrayMultiKeyLoader { private final Class keyDomainType; - private final JdbcMapping arrayJdbcMapping; + private final SqlTypedMapping arraySqlTypedMapping; private final JdbcParameter jdbcParameter; private final SelectStatement sqlSelect; private final JdbcOperationQuerySelect jdbcSelectOperation; @@ -72,7 +72,8 @@ public CollectionBatchLoaderArrayParam( } final ForeignKeyDescriptor keyDescriptor = getLoadable().getKeyDescriptor(); - final JdbcMapping jdbcMapping = keyDescriptor.getSingleJdbcMapping(); + final SelectableMapping selectable = keyDescriptor.getSelectable( 0 ); + final JdbcMapping jdbcMapping = selectable.getJdbcMapping(); final Class jdbcArrayClass = Array.newInstance( jdbcMapping.getJdbcJavaType().getJavaTypeClass(), 0 ) .getClass(); keyDomainType = getKeyType( keyDescriptor.getKeyPart() ); @@ -80,14 +81,22 @@ public CollectionBatchLoaderArrayParam( final BasicType arrayBasicType = getSessionFactory().getTypeConfiguration() .getBasicTypeRegistry() .getRegisteredType( jdbcArrayClass ); - arrayJdbcMapping = MultiKeyLoadHelper.resolveArrayJdbcMapping( - arrayBasicType, - jdbcMapping, - jdbcArrayClass, - getSessionFactory() + + arraySqlTypedMapping = new SqlTypedMappingImpl( + selectable.getColumnDefinition(), + selectable.getLength(), + selectable.getPrecision(), + selectable.getScale(), + selectable.getTemporalPrecision(), + MultiKeyLoadHelper.resolveArrayJdbcMapping( + arrayBasicType, + jdbcMapping, + jdbcArrayClass, + getSessionFactory() + ) ); - jdbcParameter = new JdbcParameterImpl( arrayJdbcMapping ); + jdbcParameter = new SqlTypedMappingJdbcParameter( arraySqlTypedMapping ); sqlSelect = LoaderSelectBuilder.createSelectBySingleArrayParameter( getLoadable(), keyDescriptor.getKeyPart(), @@ -107,12 +116,14 @@ public CollectionBatchLoaderArrayParam( .buildSelectTranslator( getSessionFactory(), sqlSelect ) .translate( JdbcParameterBindings.NO_BINDINGS, QueryOptions.NONE ); } + @Override public PersistentCollection load(Object keyBeingLoaded, SharedSessionContractImplementor session) { final ForeignKeyDescriptor keyDescriptor = getLoadable().getKeyDescriptor(); - if ( keyDescriptor.isEmbedded() ) { + if ( keyDescriptor.isEmbedded() + || keyDescriptor.getKeyPart().getSingleJdbcMapping().getValueConverter() != null ) { assert keyDescriptor.getJdbcTypeCount() == 1; - return loadEmbeddable( keyBeingLoaded, session, keyDescriptor ); + return loadWithConversion( keyBeingLoaded, session, keyDescriptor ); } else { return super.load( keyBeingLoaded, session ); @@ -120,7 +131,7 @@ public PersistentCollection load(Object keyBeingLoaded, SharedSessionContract } - private PersistentCollection loadEmbeddable( + private PersistentCollection loadWithConversion( Object keyBeingLoaded, SharedSessionContractImplementor session, ForeignKeyDescriptor keyDescriptor) { @@ -140,14 +151,14 @@ private PersistentCollection loadEmbeddable( .getComponentType(), length ); - final Object[] embeddedKeys = (Object[]) Array.newInstance( keyDomainType, length ); + final Object[] domainKeys = (Object[]) Array.newInstance( keyDomainType, length ); session.getPersistenceContextInternal().getBatchFetchQueue() .collectBatchLoadableCollectionKeys( length, (index, key) -> keyDescriptor.forEachJdbcValue( key, (i, value, jdbcMapping) -> { keysToInitialize[index] = value; - embeddedKeys[index] = key; + domainKeys[index] = key; }, session ) , keyBeingLoaded, @@ -162,7 +173,7 @@ private PersistentCollection loadEmbeddable( initializeKeys( keyBeingLoaded, keys, session ); - for ( Object initializedKey : embeddedKeys ) { + for ( Object initializedKey : domainKeys ) { if ( initializedKey != null ) { finishInitializingKey( initializedKey, session ); } @@ -191,7 +202,7 @@ void initializeKeys(Object key, Object[] keysToInitialize, SharedSessionContract final JdbcParameterBindings jdbcParameterBindings = new JdbcParameterBindingsImpl(1); jdbcParameterBindings.addBinding( jdbcParameter, - new JdbcParameterBindingImpl( arrayJdbcMapping, keysToInitialize ) + new JdbcParameterBindingImpl( arraySqlTypedMapping.getJdbcMapping(), keysToInitialize ) ); final SubselectFetch.RegistrationHandler subSelectFetchableKeysHandler = SubselectFetch.createRegistrationHandler( @@ -222,25 +233,9 @@ void finishInitializingKeys(Object[] keys, SharedSessionContractImplementor sess @Override Object[] resolveKeysToInitialize(Object keyBeingLoaded, SharedSessionContractImplementor session) { - final ForeignKeyDescriptor keyDescriptor = getLoadable().getKeyDescriptor(); - if( keyDescriptor.isEmbedded()){ - assert keyDescriptor.getJdbcTypeCount() == 1; - final int length = getDomainBatchSize(); - final Object[] keysToInitialize = (Object[]) Array.newInstance( keyDescriptor.getSingleJdbcMapping().getJdbcJavaType().getJavaTypeClass(), length ); - session.getPersistenceContextInternal().getBatchFetchQueue() - .collectBatchLoadableCollectionKeys( - length, - (index, key) -> - keyDescriptor.forEachJdbcValue( key, (i, value, jdbcMapping) -> { - keysToInitialize[index] = value; - }, session ) - , - keyBeingLoaded, - getLoadable() - ); - // now trim down the array to the number of keys we found - return trimIdBatch( length, keysToInitialize ); - } + assert !getLoadable().getKeyDescriptor().isEmbedded() + && getLoadable().getKeyDescriptor().getKeyPart().getSingleJdbcMapping().getValueConverter() == null + : "Should use loadWithConversion() instead"; return super.resolveKeysToInitialize( keyBeingLoaded, session ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/CompoundNaturalIdLoader.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/CompoundNaturalIdLoader.java index 442b21f4f4f4..13173f8dd476 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/CompoundNaturalIdLoader.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/CompoundNaturalIdLoader.java @@ -25,6 +25,7 @@ import org.hibernate.sql.ast.tree.predicate.Predicate; import org.hibernate.sql.exec.internal.JdbcParameterBindingImpl; import org.hibernate.sql.exec.internal.JdbcParameterImpl; +import org.hibernate.sql.exec.internal.SqlTypedMappingJdbcParameter; import org.hibernate.sql.exec.spi.JdbcParameterBinding; /** @@ -83,7 +84,7 @@ protected void applyNaturalIdRestriction( predicateConsumer.accept( new NullnessPredicate( columnReference ) ); } else { - final JdbcParameter jdbcParameter = new JdbcParameterImpl( jdbcValueMapping.getJdbcMapping() ); + final JdbcParameter jdbcParameter = new SqlTypedMappingJdbcParameter( jdbcValueMapping ); final ComparisonPredicate predicate = new ComparisonPredicate( columnReference, ComparisonOperator.EQUAL, 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 66843671169a..c429229b242b 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 @@ -35,6 +35,7 @@ import org.hibernate.sql.exec.internal.BaseExecutionContext; import org.hibernate.sql.exec.internal.JdbcParameterBindingsImpl; import org.hibernate.sql.exec.internal.JdbcParameterImpl; +import org.hibernate.sql.exec.internal.SqlTypedMappingJdbcParameter; import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; import org.hibernate.sql.exec.spi.JdbcParameterBindings; import org.hibernate.sql.exec.spi.JdbcParametersList; @@ -112,7 +113,7 @@ class DatabaseSnapshotExecutor { selection.getContainingTableExpression() ); - final JdbcParameter jdbcParameter = new JdbcParameterImpl( selection.getJdbcMapping() ); + final JdbcParameter jdbcParameter = new SqlTypedMappingJdbcParameter( selection ); jdbcParametersBuilder.add( jdbcParameter ); final ColumnReference columnReference = (ColumnReference) sqlExpressionResolver 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 5ffff9fd24d7..22cc1c70b210 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 @@ -12,17 +12,19 @@ import org.hibernate.LockOptions; import org.hibernate.engine.spi.LoadQueryInfluencers; -import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.loader.ast.spi.SqlArrayMultiKeyLoader; -import org.hibernate.metamodel.mapping.BasicEntityIdentifierMapping; import org.hibernate.metamodel.mapping.EntityIdentifierMapping; import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.mapping.JdbcMapping; +import org.hibernate.metamodel.mapping.SelectableMapping; +import org.hibernate.metamodel.mapping.SqlTypedMapping; +import org.hibernate.metamodel.mapping.internal.SqlTypedMappingImpl; import org.hibernate.query.spi.QueryOptions; import org.hibernate.sql.ast.tree.expression.JdbcParameter; import org.hibernate.sql.ast.tree.select.SelectStatement; import org.hibernate.sql.exec.internal.JdbcParameterImpl; +import org.hibernate.sql.exec.internal.SqlTypedMappingJdbcParameter; import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; import org.hibernate.sql.exec.spi.JdbcParameterBindings; @@ -43,8 +45,8 @@ public class EntityBatchLoaderArrayParam private final int domainBatchSize; private final LoadQueryInfluencers loadQueryInfluencers; - private final BasicEntityIdentifierMapping identifierMapping; - private final JdbcMapping arrayJdbcMapping; + private final EntityIdentifierMapping identifierMapping; + private final SqlTypedMapping arraySqlTypedMapping; private final JdbcParameter jdbcParameter; private final SelectStatement sqlAst; private final JdbcOperationQuerySelect jdbcSelectOperation; @@ -77,17 +79,26 @@ public EntityBatchLoaderArrayParam( ); } - identifierMapping = (BasicEntityIdentifierMapping) getLoadable().getIdentifierMapping(); - final Class arrayClass = - Array.newInstance( identifierMapping.getJavaType().getJavaTypeClass(), 0 ).getClass(); - arrayJdbcMapping = MultiKeyLoadHelper.resolveArrayJdbcMapping( - sessionFactory.getTypeConfiguration().getBasicTypeRegistry().getRegisteredType( arrayClass ), - identifierMapping.getJdbcMapping(), - arrayClass, - sessionFactory + identifierMapping = getLoadable().getIdentifierMapping(); + final SelectableMapping selectable = identifierMapping.getSelectable( 0 ); + final JdbcMapping jdbcMapping = selectable.getJdbcMapping(); + final Class jdbcArrayClass = Array.newInstance( jdbcMapping.getJdbcJavaType().getJavaTypeClass(), 0 ) + .getClass(); + arraySqlTypedMapping = new SqlTypedMappingImpl( + selectable.getColumnDefinition(), + selectable.getLength(), + selectable.getPrecision(), + selectable.getScale(), + selectable.getTemporalPrecision(), + MultiKeyLoadHelper.resolveArrayJdbcMapping( + sessionFactory.getTypeConfiguration().getBasicTypeRegistry().getRegisteredType( jdbcArrayClass ), + jdbcMapping, + jdbcArrayClass, + sessionFactory + ) ); - jdbcParameter = new JdbcParameterImpl( arrayJdbcMapping ); + jdbcParameter = new SqlTypedMappingJdbcParameter( arraySqlTypedMapping ); sqlAst = LoaderSelectBuilder.createSelectBySingleArrayParameter( getLoadable(), identifierMapping, @@ -148,7 +159,7 @@ protected void initializeEntities( sqlAst, jdbcSelectOperation, jdbcParameter, - arrayJdbcMapping, + arraySqlTypedMapping.getJdbcMapping(), id, entityInstance, getLoadable().getRootEntityDescriptor(), diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/LoaderSelectBuilder.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/LoaderSelectBuilder.java index 0aacc3f7dc24..d4d88f0ac953 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/LoaderSelectBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/LoaderSelectBuilder.java @@ -30,7 +30,6 @@ import org.hibernate.metamodel.mapping.AttributeMapping; import org.hibernate.metamodel.mapping.CollectionPart; import org.hibernate.metamodel.mapping.EntityIdentifierMapping; -import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.mapping.EntityValuedModelPart; import org.hibernate.metamodel.mapping.ForeignKeyDescriptor; import org.hibernate.metamodel.mapping.ModelPart; @@ -73,6 +72,7 @@ import org.hibernate.sql.ast.tree.select.QuerySpec; import org.hibernate.sql.ast.tree.select.SelectStatement; import org.hibernate.sql.exec.internal.JdbcParameterImpl; +import org.hibernate.sql.exec.internal.SqlTypedMappingJdbcParameter; import org.hibernate.sql.results.graph.BiDirectionalFetch; import org.hibernate.sql.results.graph.DomainResult; import org.hibernate.sql.results.graph.EntityGraphTraversalState; @@ -625,7 +625,7 @@ private void applyRestriction( selection ); if ( numberOfKeysToLoad == 1 ) { - final JdbcParameter jdbcParameter = new JdbcParameterImpl( selection.getJdbcMapping() ); + final JdbcParameter jdbcParameter = new SqlTypedMappingJdbcParameter( selection ); jdbcParameterConsumer.accept( jdbcParameter ); rootQuerySpec.applyPredicate( @@ -635,11 +635,9 @@ private void applyRestriction( else { final InListPredicate predicate = new InListPredicate( columnRef ); for ( int i = 0; i < numberOfKeysToLoad; i++ ) { - for ( int j = 0; j < numberColumns; j++ ) { - final JdbcParameter jdbcParameter = new JdbcParameterImpl( columnRef.getJdbcMapping() ); - jdbcParameterConsumer.accept( jdbcParameter ); - predicate.addExpression( jdbcParameter ); - } + final JdbcParameter jdbcParameter = new SqlTypedMappingJdbcParameter( selection ); + jdbcParameterConsumer.accept( jdbcParameter ); + predicate.addExpression( jdbcParameter ); } rootQuerySpec.applyPredicate( predicate ); } @@ -667,12 +665,13 @@ private void applyRestriction( for ( int i = 0; i < numberOfKeysToLoad; i++ ) { final List tupleParams = new ArrayList<>( numberColumns ); - for ( int j = 0; j < numberColumns; j++ ) { - final ColumnReference columnReference = columnReferences.get( j ); - final JdbcParameter jdbcParameter = new JdbcParameterImpl( columnReference.getJdbcMapping() ); - jdbcParameterConsumer.accept( jdbcParameter ); - tupleParams.add( jdbcParameter ); - } + restrictedPart.forEachSelectable( + (columnIndex, selection) -> { + final JdbcParameter jdbcParameter = new SqlTypedMappingJdbcParameter( selection ); + jdbcParameterConsumer.accept( jdbcParameter ); + tupleParams.add( jdbcParameter ); + } + ); final SqlTuple paramTuple = new SqlTuple( tupleParams, restrictedPart ); predicate.addExpression( paramTuple ); } @@ -963,19 +962,6 @@ else if ( fetchDepth > maximumFetchDepth + 1 ) { creationState ); - if ( fetch.getTiming() == FetchTiming.IMMEDIATE && joined ) { - if ( isFetchablePluralAttributeMapping ) { - final PluralAttributeMapping pluralAttributeMapping = (PluralAttributeMapping) fetchable; - final QuerySpec querySpec = creationState.getInflightQueryPart().getFirstQuerySpec(); - applyOrdering( - querySpec, - fetchablePath, - pluralAttributeMapping, - creationState - ); - } - } - fetches.add( fetch ); } finally { @@ -1010,19 +996,6 @@ else if ( fetchable instanceof PluralAttributeMapping ) { return true; } - private void applyOrdering( - QuerySpec querySpec, - NavigablePath navigablePath, - PluralAttributeMapping pluralAttributeMapping, - LoaderSqlAstCreationState sqlAstCreationState) { - assert pluralAttributeMapping.getAttributeName().equals( navigablePath.getLocalName() ); - - final TableGroup tableGroup = sqlAstCreationState.getFromClauseAccess().getTableGroup( navigablePath ); - assert tableGroup != null; - - applyOrdering( querySpec, tableGroup, pluralAttributeMapping, sqlAstCreationState ); - } - private SelectStatement generateSelect(SubselectFetch subselect) { // todo (6.0) : we could even convert this to a join by piecing together diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/LoaderSqlAstCreationState.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/LoaderSqlAstCreationState.java index ae03685bcf61..f7377d4dcd1c 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/LoaderSqlAstCreationState.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/LoaderSqlAstCreationState.java @@ -24,6 +24,7 @@ import org.hibernate.metamodel.mapping.AssociationKey; import org.hibernate.metamodel.mapping.ForeignKeyDescriptor; import org.hibernate.metamodel.mapping.ModelPart; +import org.hibernate.metamodel.mapping.ordering.OrderByFragment; import org.hibernate.query.spi.Limit; import org.hibernate.query.sqm.tree.from.SqmFrom; import org.hibernate.spi.NavigablePath; @@ -41,8 +42,10 @@ import org.hibernate.sql.ast.spi.SqlAstQueryPartProcessingState; import org.hibernate.sql.ast.spi.SqlExpressionResolver; import org.hibernate.sql.ast.tree.from.FromClause; +import org.hibernate.sql.ast.tree.from.TableGroup; import org.hibernate.sql.ast.tree.predicate.Predicate; import org.hibernate.sql.ast.tree.select.QueryPart; +import org.hibernate.sql.ast.tree.select.QuerySpec; import org.hibernate.sql.results.graph.DomainResultCreationState; import org.hibernate.sql.results.graph.FetchParent; import org.hibernate.sql.results.graph.internal.ImmutableFetchList; @@ -94,6 +97,13 @@ public LoaderSqlAstCreationState( ); } + @Override + public void applyOrdering(TableGroup tableGroup, OrderByFragment orderByFragment) { + final QuerySpec querySpec = getInflightQueryPart().getFirstQuerySpec(); + assert querySpec.isRoot() : "Illegal attempt to apply order-by fragment to a non-root query spec"; + orderByFragment.apply( querySpec, tableGroup, this ); + } + @Override public SqlAstCreationContext getCreationContext() { return sf; diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/MultiIdEntityLoaderArrayParam.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/MultiIdEntityLoaderArrayParam.java index c2fd880413ae..ae36e32d94a5 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 @@ -27,14 +27,18 @@ import org.hibernate.loader.ast.spi.MultiIdLoadOptions; import org.hibernate.loader.ast.spi.SqlArrayMultiKeyLoader; import org.hibernate.metamodel.mapping.BasicEntityIdentifierMapping; +import org.hibernate.metamodel.mapping.EntityIdentifierMapping; import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.mapping.JdbcMapping; +import org.hibernate.metamodel.mapping.SelectableMapping; +import org.hibernate.metamodel.mapping.SqlTypedMapping; +import org.hibernate.metamodel.mapping.internal.SqlTypedMappingImpl; import org.hibernate.query.spi.QueryOptions; 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.JdbcParameterImpl; +import org.hibernate.sql.exec.internal.SqlTypedMappingJdbcParameter; import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; import org.hibernate.sql.exec.spi.JdbcParameterBindings; import org.hibernate.sql.exec.spi.JdbcParametersList; @@ -49,19 +53,30 @@ * @author Steve Ebersole */ public class MultiIdEntityLoaderArrayParam extends AbstractMultiIdEntityLoader implements SqlArrayMultiKeyLoader { - private final JdbcMapping arrayJdbcMapping; + private final SqlTypedMapping arraySqlTypedMapping; private final JdbcParameter jdbcParameter; public MultiIdEntityLoaderArrayParam(EntityMappingType entityDescriptor, SessionFactoryImplementor sessionFactory) { super( entityDescriptor, sessionFactory ); - final Class arrayClass = createTypedArray( 0 ).getClass(); - arrayJdbcMapping = MultiKeyLoadHelper.resolveArrayJdbcMapping( - getSessionFactory().getTypeConfiguration().getBasicTypeRegistry().getRegisteredType( arrayClass ), - getIdentifierMapping().getJdbcMapping(), - arrayClass, - getSessionFactory() + final EntityIdentifierMapping identifierMapping = entityDescriptor.getIdentifierMapping(); + final SelectableMapping selectable = identifierMapping.getSelectable( 0 ); + final JdbcMapping jdbcMapping = selectable.getJdbcMapping(); + final Class jdbcArrayClass = Array.newInstance( jdbcMapping.getJdbcJavaType().getJavaTypeClass(), 0 ) + .getClass(); + arraySqlTypedMapping = new SqlTypedMappingImpl( + selectable.getColumnDefinition(), + selectable.getLength(), + selectable.getPrecision(), + selectable.getScale(), + selectable.getTemporalPrecision(), + MultiKeyLoadHelper.resolveArrayJdbcMapping( + sessionFactory.getTypeConfiguration().getBasicTypeRegistry().getRegisteredType( jdbcArrayClass ), + jdbcMapping, + jdbcArrayClass, + sessionFactory + ) ); - jdbcParameter = new JdbcParameterImpl( arrayJdbcMapping ); + jdbcParameter = new SqlTypedMappingJdbcParameter( arraySqlTypedMapping ); } @Override @@ -177,7 +192,10 @@ protected List performOrderedMultiLoad(K[] ids, MultiIdLoadOptions loadOp final JdbcParameterBindings jdbcParameterBindings = new JdbcParameterBindingsImpl(1); jdbcParameterBindings.addBinding( jdbcParameter, - new JdbcParameterBindingImpl( arrayJdbcMapping, idsToLoadFromDatabase.toArray( createTypedArray(0 ) ) ) + new JdbcParameterBindingImpl( + arraySqlTypedMapping.getJdbcMapping(), + idsToLoadFromDatabase.toArray( createTypedArray( 0 ) ) + ) ); final PersistenceContext persistenceContext = session.getPersistenceContext(); @@ -274,7 +292,7 @@ protected List performUnorderedMultiLoad( sqlAst, jdbcSelectOperation, jdbcParameter, - arrayJdbcMapping, + arraySqlTypedMapping.getJdbcMapping(), null, null, null, @@ -395,6 +413,12 @@ protected final K[] processResolvableEntities( private X[] createTypedArray(@SuppressWarnings("SameParameterValue") int length) { //noinspection unchecked - return (X[]) Array.newInstance( getIdentifierMapping().getJavaType().getJavaTypeClass(), length ); + return (X[]) Array.newInstance( + getIdentifierMapping().getSelectable( 0 ) + .getJdbcMapping() + .getJdbcJavaType() + .getJavaTypeClass(), + length + ); } } 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 17116a311ce7..af8990c9299b 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 @@ -6,6 +6,7 @@ */ package org.hibernate.loader.ast.internal; +import java.lang.reflect.Array; import java.util.Collections; import java.util.List; @@ -18,16 +19,17 @@ import org.hibernate.loader.ast.spi.SqlArrayMultiKeyLoader; import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.mapping.JdbcMapping; +import org.hibernate.metamodel.mapping.SelectableMapping; +import org.hibernate.metamodel.mapping.SqlTypedMapping; import org.hibernate.metamodel.mapping.internal.BasicAttributeMapping; import org.hibernate.metamodel.mapping.internal.SimpleNaturalIdMapping; +import org.hibernate.metamodel.mapping.internal.SqlTypedMappingImpl; import org.hibernate.query.spi.QueryOptions; import org.hibernate.sql.ast.tree.expression.JdbcParameter; import org.hibernate.sql.ast.tree.select.SelectStatement; -import org.hibernate.sql.exec.internal.JdbcParameterImpl; +import org.hibernate.sql.exec.internal.SqlTypedMappingJdbcParameter; import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; import org.hibernate.sql.exec.spi.JdbcParameterBindings; -import org.hibernate.type.BasicType; -import org.hibernate.type.BasicTypeRegistry; /** * Standard MultiNaturalIdLoader implementation @@ -79,15 +81,25 @@ public List multiLoad(K[] naturalIds, MultiNaturalIdLoadOptions loadOptio ? new LockOptions( LockMode.NONE ) : loadOptions.getLockOptions(); - final BasicTypeRegistry basicTypeRegistry = sessionFactory.getTypeConfiguration().getBasicTypeRegistry(); - final BasicType arrayBasicType = basicTypeRegistry.getRegisteredType( keyArrayClass ); - final JdbcMapping arrayJdbcMapping = MultiKeyLoadHelper.resolveArrayJdbcMapping( - arrayBasicType, - getNaturalIdMapping().getSingleJdbcMapping(), - keyArrayClass, - sessionFactory + final SelectableMapping selectable = getNaturalIdAttribute().getSelectable( 0 ); + final JdbcMapping jdbcMapping = selectable.getJdbcMapping(); + final Class jdbcArrayClass = Array.newInstance( jdbcMapping.getJdbcJavaType().getJavaTypeClass(), 0 ) + .getClass(); + + final SqlTypedMapping arraySqlTypedMapping = new SqlTypedMappingImpl( + selectable.getColumnDefinition(), + selectable.getLength(), + selectable.getPrecision(), + selectable.getScale(), + selectable.getTemporalPrecision(), + MultiKeyLoadHelper.resolveArrayJdbcMapping( + sessionFactory.getTypeConfiguration().getBasicTypeRegistry().getRegisteredType( jdbcArrayClass ), + jdbcMapping, + jdbcArrayClass, + sessionFactory + ) ); - final JdbcParameter jdbcParameter = new JdbcParameterImpl( arrayJdbcMapping ); + final JdbcParameter jdbcParameter = new SqlTypedMappingJdbcParameter( arraySqlTypedMapping ); final SelectStatement sqlAst = LoaderSelectBuilder.createSelectBySingleArrayParameter( getLoadable(), @@ -108,7 +120,7 @@ public List multiLoad(K[] naturalIds, MultiNaturalIdLoadOptions loadOptio sqlAst, jdbcSelectOperation, jdbcParameter, - arrayJdbcMapping, + arraySqlTypedMapping.getJdbcMapping(), null, null, null, diff --git a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/SimpleNaturalIdLoader.java b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/SimpleNaturalIdLoader.java index 9b77932d4e16..e1bca8b7a7e6 100644 --- a/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/SimpleNaturalIdLoader.java +++ b/hibernate-core/src/main/java/org/hibernate/loader/ast/internal/SimpleNaturalIdLoader.java @@ -21,6 +21,7 @@ import org.hibernate.sql.ast.tree.predicate.Predicate; import org.hibernate.sql.exec.internal.JdbcParameterBindingImpl; import org.hibernate.sql.exec.internal.JdbcParameterImpl; +import org.hibernate.sql.exec.internal.SqlTypedMappingJdbcParameter; import org.hibernate.sql.exec.spi.JdbcParameterBinding; /** @@ -74,7 +75,7 @@ protected void applyNaturalIdRestriction( predicateConsumer.accept( new NullnessPredicate( columnReference ) ); } else { - final JdbcParameter jdbcParameter = new JdbcParameterImpl( jdbcValueMapping.getJdbcMapping() ); + final JdbcParameter jdbcParameter = new SqlTypedMappingJdbcParameter( jdbcValueMapping ); final ComparisonPredicate predicate = new ComparisonPredicate( columnReference, ComparisonOperator.EQUAL, diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/BasicValue.java b/hibernate-core/src/main/java/org/hibernate/mapping/BasicValue.java index e9abeef45b90..697abf1bf8a3 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/BasicValue.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/BasicValue.java @@ -40,7 +40,6 @@ import org.hibernate.engine.jdbc.Size; import org.hibernate.internal.CoreLogging; import org.hibernate.internal.CoreMessageLogger; -import org.hibernate.internal.util.ReflectHelper; import org.hibernate.internal.util.StringHelper; import org.hibernate.internal.util.collections.CollectionHelper; import org.hibernate.metamodel.mapping.JdbcMapping; @@ -71,6 +70,7 @@ import org.hibernate.type.descriptor.jdbc.JdbcType; import org.hibernate.type.descriptor.jdbc.JdbcTypeIndicators; import org.hibernate.type.internal.BasicTypeImpl; +import org.hibernate.type.internal.ConvertedBasicTypeImpl; import org.hibernate.type.spi.TypeConfiguration; import org.hibernate.type.spi.TypeConfigurationAware; import org.hibernate.usertype.DynamicParameterizedType; @@ -82,6 +82,7 @@ import jakarta.persistence.TemporalType; import static java.lang.Boolean.parseBoolean; +import static org.hibernate.internal.util.ReflectHelper.reflectedPropertyType; import static org.hibernate.internal.util.collections.CollectionHelper.isNotEmpty; import static org.hibernate.mapping.MappingHelper.injectParameters; @@ -103,6 +104,7 @@ public class BasicValue extends SimpleValue implements JdbcTypeIndicators, Resol private Function implicitJavaTypeAccess; private EnumType enumerationStyle; + @SuppressWarnings("deprecation") private TemporalType temporalPrecision; private TimeZoneStorageType timeZoneStorageType; private boolean isSoftDelete; @@ -234,8 +236,7 @@ public java.lang.reflect.Type getResolvedJavaType() { public long getColumnLength() { final Selectable selectable = getColumn(); if ( selectable instanceof Column ) { - final Column column = (Column) selectable; - final Long length = column.getLength(); + final Long length = ( (Column) selectable ).getLength(); return length == null ? NO_COLUMN_LENGTH : length; } else { @@ -248,8 +249,9 @@ public int getColumnPrecision() { final Selectable selectable = getColumn(); if ( selectable instanceof Column ) { final Column column = (Column) selectable; - if ( column.getTemporalPrecision() != null ) { - return column.getTemporalPrecision(); + final Integer temporalPrecision = column.getTemporalPrecision(); + if ( temporalPrecision != null ) { + return temporalPrecision; } final Integer precision = column.getPrecision(); return precision == null ? NO_COLUMN_PRECISION : precision; @@ -263,8 +265,7 @@ public int getColumnPrecision() { public int getColumnScale() { final Selectable selectable = getColumn(); if ( selectable instanceof Column ) { - final Column column = (Column) selectable; - final Integer scale = column.getScale(); + final Integer scale = ( (Column) selectable ).getScale(); return scale == null ? NO_COLUMN_SCALE : scale; } else { @@ -284,8 +285,9 @@ public void copyTypeFrom(SimpleValue sourceValue) { super.copyTypeFrom( sourceValue ); if ( sourceValue instanceof BasicValue ) { final BasicValue basicValue = (BasicValue) sourceValue; - this.resolution = basicValue.resolution; - this.implicitJavaTypeAccess = (typeConfiguration) -> basicValue.implicitJavaTypeAccess.apply( typeConfiguration ); + resolution = basicValue.resolution; + implicitJavaTypeAccess = + typeConfiguration -> basicValue.implicitJavaTypeAccess.apply( typeConfiguration ); } } @@ -447,55 +449,71 @@ && parseBoolean( typeParameters.getProperty(DynamicParameterizedType.IS_DYNAMIC) getBuildingContext() ); } - - if ( isVersion() ) { + else if ( isVersion() ) { return VersionResolution.from( implicitJavaTypeAccess, timeZoneStorageType, getBuildingContext() ); } + else { + // determine JavaType if we can + final BasicJavaType explicitJavaType = getExplicitJavaType(); + final JavaType javaType = determineJavaType( explicitJavaType ); + final ConverterDescriptor converterDescriptor = getConverterDescriptor( javaType ); + return converterDescriptor != null + ? converterResolution( javaType, converterDescriptor ) + : resolution( explicitJavaType, javaType ); + } + } - // determine JavaType if we can - final BasicJavaType explicitJavaType = explicitJavaTypeAccess == null - ? null + private BasicJavaType getExplicitJavaType() { + return explicitJavaTypeAccess == null ? null : explicitJavaTypeAccess.apply( getTypeConfiguration() ); + } - JavaType javaType = determineJavaType( explicitJavaType ); - ConverterDescriptor attributeConverterDescriptor = getAttributeConverterDescriptor(); - + private ConverterDescriptor getConverterDescriptor(JavaType javaType) { + final ConverterDescriptor converterDescriptor = getAttributeConverterDescriptor(); if ( isSoftDelete() ) { - assert attributeConverterDescriptor != null; - final boolean conversionWasUnspecified = SoftDelete.UnspecifiedConversion.class.equals( attributeConverterDescriptor.getAttributeConverterClass() ); - if ( conversionWasUnspecified ) { - final JdbcType jdbcType = BooleanJdbcType.INSTANCE.resolveIndicatedType( this, javaType ); - if ( jdbcType.isNumber() ) { - attributeConverterDescriptor = new InstanceBasedConverterDescriptor( - NumericBooleanConverter.INSTANCE, - getBuildingContext().getBootstrapContext().getClassmateContext() - ); - } - else if ( jdbcType.isString() ) { - // here we pick 'T' / 'F' storage, though 'Y' / 'N' is equally valid - its 50/50 - attributeConverterDescriptor = new InstanceBasedConverterDescriptor( - TrueFalseConverter.INSTANCE, - getBuildingContext().getBootstrapContext().getClassmateContext() - ); - } - else { - // should indicate BIT or BOOLEAN == no conversion needed - // - we still create the converter to properly set up JDBC type, etc - attributeConverterDescriptor = new InstanceBasedConverterDescriptor( - PassThruSoftDeleteConverter.INSTANCE, - getBuildingContext().getBootstrapContext().getClassmateContext() - ); - } - } + assert converterDescriptor != null; + final ConverterDescriptor softDeleteConverterDescriptor = + getSoftDeleteConverterDescriptor( converterDescriptor, javaType); + return getSoftDeleteStrategy() == SoftDeleteType.ACTIVE + ? new ReversedConverterDescriptor<>( softDeleteConverterDescriptor ) + : softDeleteConverterDescriptor; + } + else { + return converterDescriptor; + } + } - if ( getSoftDeleteStrategy() == SoftDeleteType.ACTIVE ) { - attributeConverterDescriptor = new ReversedConverterDescriptor<>( attributeConverterDescriptor ); + private ConverterDescriptor getSoftDeleteConverterDescriptor( + ConverterDescriptor attributeConverterDescriptor, JavaType javaType) { + final boolean conversionWasUnspecified = + SoftDelete.UnspecifiedConversion.class.equals( attributeConverterDescriptor.getAttributeConverterClass() ); + if ( conversionWasUnspecified ) { + final JdbcType jdbcType = BooleanJdbcType.INSTANCE.resolveIndicatedType( this, javaType); + if ( jdbcType.isNumber() ) { + return new InstanceBasedConverterDescriptor( + NumericBooleanConverter.INSTANCE, + getBuildingContext().getBootstrapContext().getClassmateContext() + ); + } + else if ( jdbcType.isString() ) { + // here we pick 'T' / 'F' storage, though 'Y' / 'N' is equally valid - its 50/50 + return new InstanceBasedConverterDescriptor( + TrueFalseConverter.INSTANCE, + getBuildingContext().getBootstrapContext().getClassmateContext() + ); + } + else { + // should indicate BIT or BOOLEAN == no conversion needed + // - we still create the converter to properly set up JDBC type, etc + return new InstanceBasedConverterDescriptor( + PassThruSoftDeleteConverter.INSTANCE, + getBuildingContext().getBootstrapContext().getClassmateContext() + ); } } - - return attributeConverterDescriptor != null - ? converterResolution( javaType, attributeConverterDescriptor ) - : resolution( explicitJavaType, javaType ); + else { + return attributeConverterDescriptor; + } } private static class ReversedConverterDescriptor implements ConverterDescriptor { @@ -680,11 +698,10 @@ private Resolution converterResolution(JavaType javaType, ConverterDescrip && !attributeConverterDescriptor.getDomainValueResolvedType().getErasedType() .isAssignableFrom( javaType.getJavaTypeClass() ) ) { // In this case, the converter applies to the element of a BasicPluralJavaType - final BasicPluralJavaType containerJtd = (BasicPluralJavaType) javaType; final BasicType registeredElementType = converterResolution.getLegacyResolvedBasicType(); final Selectable column = getColumn(); final BasicType registeredType = registeredElementType == null ? null - : containerJtd.resolveType( + : ( (BasicPluralJavaType) javaType ).resolveType( getTypeConfiguration(), getDialect(), registeredElementType, @@ -730,66 +747,76 @@ private JavaType determineJavaType(JavaType explicitJavaType) { } private JavaType determineReflectedJavaType() { - final java.lang.reflect.Type impliedJavaType; - final TypeConfiguration typeConfiguration = getTypeConfiguration(); + final java.lang.reflect.Type impliedJavaType = impliedJavaType( typeConfiguration ); + if ( impliedJavaType == null ) { + return null; + } + else { + resolvedJavaType = impliedJavaType; + return javaType( typeConfiguration, impliedJavaType ); + } + } + + private java.lang.reflect.Type impliedJavaType(TypeConfiguration typeConfiguration) { if ( resolvedJavaType != null ) { - impliedJavaType = resolvedJavaType; + return resolvedJavaType; } else if ( implicitJavaTypeAccess != null ) { - impliedJavaType = implicitJavaTypeAccess.apply( typeConfiguration ); + return implicitJavaTypeAccess.apply(typeConfiguration); } else if ( ownerName != null && propertyName != null ) { - impliedJavaType = ReflectHelper.reflectedPropertyType( - ownerName, - propertyName, - getServiceRegistry().requireService( ClassLoaderService.class ) - ); + return reflectedPropertyType( ownerName, propertyName, + getServiceRegistry().requireService( ClassLoaderService.class ) ); } else { return null; } + } - resolvedJavaType = impliedJavaType; + private JavaType javaType(TypeConfiguration typeConfiguration, java.lang.reflect.Type impliedJavaType) { + final JavaType javaType = typeConfiguration.getJavaTypeRegistry().findDescriptor( impliedJavaType ); + return javaType == null ? specialJavaType( typeConfiguration, impliedJavaType ) : javaType; + } - if ( impliedJavaType == null ) { - return null; + private JavaType specialJavaType( + TypeConfiguration typeConfiguration, + java.lang.reflect.Type impliedJavaType) { + final JavaTypeRegistry javaTypeRegistry = typeConfiguration.getJavaTypeRegistry(); + if ( jdbcTypeCode != null ) { + // Construct special JavaType instances for JSON/XML types which can report recommended JDBC types + // and implement toString/fromString as well as copying based on FormatMapper operations + switch ( jdbcTypeCode ) { + case SqlTypes.JSON: + final JavaType jsonJavaType = + new JsonJavaType<>( impliedJavaType, + mutabilityPlan( typeConfiguration, impliedJavaType ), + typeConfiguration ); + javaTypeRegistry.addDescriptor( jsonJavaType ); + return jsonJavaType; + case SqlTypes.SQLXML: + final JavaType xmlJavaType = + new XmlJavaType<>( impliedJavaType, + mutabilityPlan( typeConfiguration, impliedJavaType ), + typeConfiguration ); + javaTypeRegistry.addDescriptor( xmlJavaType ); + return xmlJavaType; + } } + return javaTypeRegistry.resolveDescriptor( impliedJavaType ); + } - final JavaTypeRegistry javaTypeRegistry = typeConfiguration.getJavaTypeRegistry(); - final JavaType javaType = javaTypeRegistry.findDescriptor( impliedJavaType ); - final MutabilityPlan explicitMutabilityPlan = explicitMutabilityPlanAccess != null - ? explicitMutabilityPlanAccess.apply( typeConfiguration ) - : null; - final MutabilityPlan determinedMutabilityPlan = explicitMutabilityPlan != null + private MutabilityPlan mutabilityPlan( + TypeConfiguration typeConfiguration, java.lang.reflect.Type impliedJavaType) { + final MutabilityPlan explicitMutabilityPlan = getExplicitMutabilityPlan(); + return explicitMutabilityPlan != null ? explicitMutabilityPlan : RegistryHelper.INSTANCE.determineMutabilityPlan( impliedJavaType, typeConfiguration ); - if ( javaType == null ) { - if ( jdbcTypeCode != null ) { - // Construct special JavaType instances for JSON/XML types which can report recommended JDBC types - // and implement toString/fromString as well as copying based on FormatMapper operations - switch ( jdbcTypeCode ) { - case SqlTypes.JSON: - final JavaType jsonJavaType = new JsonJavaType<>( - impliedJavaType, - determinedMutabilityPlan, - typeConfiguration - ); - javaTypeRegistry.addDescriptor( jsonJavaType ); - return jsonJavaType; - case SqlTypes.SQLXML: - final JavaType xmlJavaType = new XmlJavaType<>( - impliedJavaType, - determinedMutabilityPlan, - typeConfiguration - ); - javaTypeRegistry.addDescriptor( xmlJavaType ); - return xmlJavaType; - } - } - return javaTypeRegistry.resolveDescriptor( impliedJavaType ); - } - return javaType; + } + + private MutabilityPlan getExplicitMutabilityPlan() { + return explicitMutabilityPlanAccess == null ? null + : explicitMutabilityPlanAccess.apply( getTypeConfiguration() ); } private static Resolution interpretExplicitlyNamedType( @@ -842,7 +869,8 @@ public TypeConfiguration getTypeConfiguration() { // return EnumeratedValueResolution.fromName( name, stdIndicators, context ); // } - if ( name.startsWith( BasicTypeImpl.EXTERNALIZED_PREFIX ) ) { + if ( name.startsWith( BasicTypeImpl.EXTERNALIZED_PREFIX ) + || name.startsWith( ConvertedBasicTypeImpl.EXTERNALIZED_PREFIX ) ) { final BasicType basicType = context.getBootstrapContext().resolveAdHocBasicType( name ); return new NamedBasicTypeResolution<>( basicType.getJavaTypeDescriptor(), @@ -1066,8 +1094,7 @@ private UserType getConfiguredUserTypeBean(Class> expli : getUserTypeBean( explicitCustomType, properties ).getBeanInstance(); if ( typeInstance instanceof TypeConfigurationAware ) { - final TypeConfigurationAware configurationAware = (TypeConfigurationAware) typeInstance; - configurationAware.setTypeConfiguration( getTypeConfiguration() ); + ( (TypeConfigurationAware) typeInstance ).setTypeConfiguration( getTypeConfiguration() ); } if ( typeInstance instanceof DynamicParameterizedType ) { @@ -1097,11 +1124,12 @@ private ManagedBean getUserTypeBean(Class explicitCustomType, Properti } } + @SuppressWarnings("deprecation") public void setTemporalPrecision(TemporalType temporalPrecision) { this.temporalPrecision = temporalPrecision; } - @Override + @Override @SuppressWarnings("deprecation") public TemporalType getTemporalPrecision() { return temporalPrecision; } diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/Column.java b/hibernate-core/src/main/java/org/hibernate/mapping/Column.java index 71a3034eb365..d040f47a0a2e 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/Column.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/Column.java @@ -350,7 +350,7 @@ private String getSqlTypeName(DdlTypeRegistry ddlTypeRegistry, Dialect dialect, } private static Type getUnderlyingType(Mapping mapping, Type type, int typeIndex) { - if ( type.isComponentType() ) { + if ( type instanceof ComponentType ) { final ComponentType componentType = (ComponentType) type; int cols = 0; for ( Type subtype : componentType.getSubtypes() ) { @@ -362,7 +362,7 @@ private static Type getUnderlyingType(Mapping mapping, Type type, int typeIndex) } throw new IndexOutOfBoundsException(); } - else if ( type.isEntityType() ) { + else if ( type instanceof EntityType ) { final EntityType entityType = (EntityType) type; final Type idType = entityType.getIdentifierOrUniqueKeyType( mapping ); return getUnderlyingType( mapping, idType, typeIndex ); diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/Component.java b/hibernate-core/src/main/java/org/hibernate/mapping/Component.java index 431099e0ca69..8891b5219cac 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/Component.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/Component.java @@ -62,7 +62,6 @@ import static java.util.Collections.unmodifiableList; import static java.util.stream.Collectors.toList; -import static org.hibernate.generator.EventType.INSERT; import static org.hibernate.internal.util.StringHelper.qualify; import static org.hibernate.mapping.MappingHelper.checkPropertyColumnDuplication; import static org.hibernate.metamodel.mapping.EntityDiscriminatorMapping.DISCRIMINATOR_ROLE_NAME; @@ -719,7 +718,8 @@ else if ( rootClass.getIdentifierProperty() != null ) { if ( property.getValue().isSimpleValue() ) { final SimpleValue value = (SimpleValue) property.getValue(); - if ( !DEFAULT_ID_GEN_STRATEGY.equals( value.getIdentifierGeneratorStrategy() ) ) { + if ( !DEFAULT_ID_GEN_STRATEGY.equals( value.getIdentifierGeneratorStrategy() ) + || value.getCustomIdGeneratorCreator() != null ) { // skip any 'assigned' generators, they would have been handled by // the StandardGenerationContextLocator generator.addGeneratedValuePlan( new ValueGenerationPlan( @@ -797,10 +797,9 @@ public int getPropertyIndex() { } @Override - public Object execute(SharedSessionContractImplementor session, Object incomingObject) { - if ( !subgenerator.generatedOnExecution( incomingObject, session ) ) { - return ( (BeforeExecutionGenerator) subgenerator) - .generate( session, incomingObject, null, INSERT ); + public BeforeExecutionGenerator getGenerator() { + if ( subgenerator instanceof BeforeExecutionGenerator ) { + return (BeforeExecutionGenerator) subgenerator; } else { throw new IdentifierGenerationException( "Identity generation isn't supported for composite ids" ); diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/DenormalizedTable.java b/hibernate-core/src/main/java/org/hibernate/mapping/DenormalizedTable.java index 0eb210f38a17..b1668d2b71e6 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/DenormalizedTable.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/DenormalizedTable.java @@ -70,10 +70,12 @@ public void createForeignKeys(MetadataBuildingContext context) { if ( foreignKey.getReferencedTable() == null ) { foreignKey.setReferencedTable( referencedClass.getTable() ); } + + final ForeignKey denormalizedForeignKey = createDenormalizedForeignKey( foreignKey ); createForeignKey( context.getBuildingOptions() .getImplicitNamingStrategy() - .determineForeignKeyName( new ForeignKeyNameSource( foreignKey, this, context ) ) + .determineForeignKeyName( new ForeignKeyNameSource( denormalizedForeignKey, this, context ) ) .render( context.getMetadataCollector().getDatabase().getDialect() ), foreignKey.getColumns(), foreignKey.getReferencedEntityName(), @@ -83,6 +85,18 @@ public void createForeignKeys(MetadataBuildingContext context) { } } + private ForeignKey createDenormalizedForeignKey(ForeignKey includedTableFk) { + final ForeignKey denormalizedForeignKey = new ForeignKey(); + denormalizedForeignKey.setReferencedEntityName( includedTableFk.getReferencedEntityName() ); + denormalizedForeignKey.setKeyDefinition( includedTableFk.getKeyDefinition() ); + denormalizedForeignKey.setReferencedTable( includedTableFk.getReferencedTable() ); + denormalizedForeignKey.addReferencedColumns( includedTableFk.getReferencedColumns() ); + for ( Column keyColumn : includedTableFk.getColumns() ) { + denormalizedForeignKey.addColumn( keyColumn ); + } + return denormalizedForeignKey; + } + @Override public Column getColumn(Column column) { Column superColumn = super.getColumn( column ); diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/PersistentClass.java b/hibernate-core/src/main/java/org/hibernate/mapping/PersistentClass.java index e3faa1fcad73..9d4890ddb2f2 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/PersistentClass.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/PersistentClass.java @@ -287,6 +287,20 @@ public void addProperty(Property property) { property.setPersistentClass( this ); } + @Internal + public void movePropertyToJoin(Property property, Join join) { + assert joins.contains( join ); + assert property.getPersistentClass() == this; + properties.remove( property ); + declaredProperties.remove( property ); + join.addProperty( property ); + } + + @Internal + protected void moveSubclassPropertyToJoin(Property property) { + subclassProperties.remove( property ); + } + @Override public boolean contains(Property property) { return properties.contains( property ); diff --git a/hibernate-core/src/main/java/org/hibernate/mapping/Property.java b/hibernate-core/src/main/java/org/hibernate/mapping/Property.java index 55bf308400e8..543d891afb60 100644 --- a/hibernate-core/src/main/java/org/hibernate/mapping/Property.java +++ b/hibernate-core/src/main/java/org/hibernate/mapping/Property.java @@ -14,6 +14,7 @@ import org.hibernate.HibernateException; import org.hibernate.Internal; import org.hibernate.MappingException; +import org.hibernate.annotations.OnDeleteAction; import org.hibernate.boot.model.relational.Database; import org.hibernate.bytecode.enhance.spi.interceptor.EnhancementHelper; import org.hibernate.cfg.AvailableSettings; @@ -30,6 +31,9 @@ import org.hibernate.service.ServiceRegistry; import org.hibernate.generator.Generator; import org.hibernate.generator.GeneratorCreationContext; +import org.hibernate.type.AnyType; +import org.hibernate.type.CollectionType; +import org.hibernate.type.ComponentType; import org.hibernate.type.CompositeType; import org.hibernate.type.Type; import org.hibernate.type.WrapperArrayHandling; @@ -83,7 +87,7 @@ public boolean isSynthetic() { public Type getType() throws MappingException { return value.getType(); } - + public int getColumnSpan() { return value.getColumnSpan(); } @@ -103,11 +107,11 @@ public java.util.List getSelectables() { public java.util.List getColumns() { return value.getColumns(); } - + public String getName() { return name; } - + public boolean isComposite() { return value instanceof Component; } @@ -134,6 +138,10 @@ public void resetOptional(boolean optional) { } } + public OnDeleteAction getOnDeleteAction() { + return value instanceof ToOne ? ( (ToOne) value ).getOnDeleteAction() : null; + } + /** * @deprecated this method is no longer used */ @@ -144,22 +152,31 @@ public boolean isPrimitive(Class clazz) { public CascadeStyle getCascadeStyle() throws MappingException { final Type type = value.getType(); - if ( type.isComponentType() ) { - return getCompositeCascadeStyle( (CompositeType) type, cascade ); + if ( type instanceof AnyType ) { + return getCascadeStyle( cascade ); + } + if ( type instanceof ComponentType ) { + return getCompositeCascadeStyle( (ComponentType) type, cascade ); } - else if ( type.isCollectionType() ) { + else if ( type instanceof CollectionType ) { final Collection collection = (Collection) value; return getCollectionCascadeStyle( collection.getElement().getType(), cascade ); } else { - return getCascadeStyle( cascade ); + return getCascadeStyle( cascade ); } } private static CascadeStyle getCompositeCascadeStyle(CompositeType compositeType, String cascade) { - if ( compositeType.isAnyType() ) { + if ( compositeType instanceof AnyType ) { return getCascadeStyle( cascade ); } + else { + return getCompositeCascadeStyle( (ComponentType) compositeType, cascade ); + } + } + + private static CascadeStyle getCompositeCascadeStyle(ComponentType compositeType, String cascade) { final int length = compositeType.getSubtypes().length; for ( int i=0; i predicateConsumer, + @Nullable Consumer predicateConsumer, SqlAstCreationState creationState) { return new StandardVirtualTableGroup( navigablePath, this, lhs, fetched ); } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/internal/AttributeFactory.java b/hibernate-core/src/main/java/org/hibernate/metamodel/internal/AttributeFactory.java index 351500a46392..3744a92b682a 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/internal/AttributeFactory.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/internal/AttributeFactory.java @@ -61,6 +61,9 @@ import org.hibernate.property.access.internal.PropertyAccessMapImpl; import org.hibernate.property.access.spi.Getter; import org.hibernate.type.AnyType; +import org.hibernate.type.BasicType; +import org.hibernate.type.CollectionType; +import org.hibernate.type.ComponentType; import org.hibernate.type.BasicPluralType; import org.hibernate.type.EntityType; import org.hibernate.type.descriptor.java.JavaType; @@ -461,7 +464,7 @@ private static AttributeMetadata determineAttributeMetadata( final org.hibernate.type.Type type = value.getType(); LOG.tracef( " Determined type [name=%s, class=%s]", type.getName(), type.getClass().getName() ); - if ( type.isAnyType() ) { + if ( type instanceof AnyType ) { return new SingularAttributeMetadataImpl<>( propertyMapping, attributeContext.getOwnerType(), @@ -470,18 +473,17 @@ private static AttributeMetadata determineAttributeMetadata( context ); } - else if ( type.isAssociationType() ) { - // collection or entity - if ( type.isEntityType() ) { - // entity - return new SingularAttributeMetadataImpl<>( - propertyMapping, - attributeContext.getOwnerType(), - member, - determineSingularAssociationClassification( member ), - context - ); - } + else if ( type instanceof EntityType ) { + // entity + return new SingularAttributeMetadataImpl<>( + propertyMapping, + attributeContext.getOwnerType(), + member, + determineSingularAssociationClassification( member ), + context + ); + } + else if ( type instanceof CollectionType ) { // collection if ( value instanceof Collection ) { final Collection collValue = (Collection) value; @@ -519,7 +521,7 @@ else if ( value instanceof OneToMany ) { // ); } } - else if ( propertyMapping.isComposite() ) { + else if ( type instanceof ComponentType ) { // component return new SingularAttributeMetadataImpl<>( propertyMapping, @@ -530,6 +532,7 @@ else if ( propertyMapping.isComposite() ) { ); } else { + assert type instanceof BasicType; // basic type return new SingularAttributeMetadataImpl<>( propertyMapping, @@ -557,14 +560,15 @@ else if ( value instanceof List ) { private static AttributeClassification elementClassification( org.hibernate.type.Type elementType, Value elementValue, boolean isManyToMany) { - final AttributeClassification elementClassification; - if ( elementType.isAnyType() ) { + // First, determine the type of the elements and use that to help determine the + // collection type + if ( elementType instanceof AnyType ) { return AttributeClassification.ANY; } - else if ( elementValue instanceof Component ) { + else if ( elementType instanceof ComponentType ) { return AttributeClassification.EMBEDDED; } - else if ( elementType.isAssociationType() ) { + else if ( elementType instanceof EntityType ) { return isManyToMany ? AttributeClassification.MANY_TO_MANY : AttributeClassification.ONE_TO_MANY; @@ -576,13 +580,7 @@ else if ( elementType.isAssociationType() ) { private static AttributeClassification collectionClassification( org.hibernate.type.Type elementType, Value elementValue, boolean isManyToMany) { - if ( elementType.isAnyType() ) { - return AttributeClassification.ELEMENT_COLLECTION; - } - else if ( elementValue instanceof Component ) { - return AttributeClassification.ELEMENT_COLLECTION; - } - else if ( elementType.isAssociationType() ) { + if ( elementType instanceof EntityType ) { return isManyToMany ? AttributeClassification.MANY_TO_MANY : AttributeClassification.ONE_TO_MANY; @@ -593,13 +591,13 @@ else if ( elementType.isAssociationType() ) { } private static AttributeClassification keyClassification(org.hibernate.type.Type keyType, Value keyValue) { - if ( keyType.isAnyType() ) { + if ( keyType instanceof AnyType ) { return AttributeClassification.ANY; } - else if ( keyValue instanceof Component ) { + else if ( keyType instanceof ComponentType ) { return AttributeClassification.EMBEDDED; } - else if ( keyType.isAssociationType() ) { + else if ( keyType instanceof EntityType ) { return AttributeClassification.MANY_TO_ONE; } else { @@ -778,72 +776,63 @@ private static Member resolveMappedSuperclassMember( Property property, MappedSuperclassDomainType ownerType, MetadataContext metadataContext) { - final EntityPersister declaringEntity = getDeclaringEntity( (AbstractIdentifiableType) ownerType, metadataContext ); - if ( declaringEntity != null ) { - return resolveEntityMember( property, declaringEntity ); - } - else { - final ManagedDomainType subType = ownerType.getSubTypes().iterator().next(); - final Type.PersistenceType persistenceType = subType.getPersistenceType(); - if ( persistenceType == Type.PersistenceType.ENTITY ) { - return resolveEntityMember( property, getDeclaringEntity( (AbstractIdentifiableType) subType, metadataContext ) ); - } - else if ( persistenceType == Type.PersistenceType.EMBEDDABLE ) { - return resolveEmbeddedMember( property, (EmbeddableDomainType) subType, metadataContext ); - } - else if ( persistenceType == Type.PersistenceType.MAPPED_SUPERCLASS ) { - return resolveMappedSuperclassMember( - property, - (MappedSuperclassDomainType) subType, - metadataContext - ); - } - else { - throw new IllegalArgumentException( "Unexpected sub-type: " + persistenceType ); - } - } + return property.getGetter( ownerType.getJavaType() ).getMember(); } private final MemberResolver identifierMemberResolver = (attributeContext, metadataContext) -> { final AbstractIdentifiableType identifiableType = (AbstractIdentifiableType) attributeContext.getOwnerType(); - final EntityPersister declaringEntityMapping = getDeclaringEntity( identifiableType, metadataContext ); - final EntityIdentifierMapping identifierMapping = declaringEntityMapping.getIdentifierMapping(); - final Property propertyMapping = attributeContext.getPropertyMapping(); - if ( !propertyMapping.getName().equals( identifierMapping.getAttributeName() ) ) { - // this *should* indicate processing part of an IdClass... - return virtualIdentifierMemberResolver.resolveMember( attributeContext, metadataContext ); - } - - final Getter getter = getter( declaringEntityMapping, propertyMapping ); - if ( getter instanceof PropertyAccessMapImpl.GetterImpl ) { - return new MapMember( identifierMapping.getAttributeName(), identifierMapping.getJavaType().getJavaTypeClass() ); + if ( identifiableType instanceof MappedSuperclassDomainType ) { + return attributeContext.getPropertyMapping() + .getGetter( identifiableType.getJavaType() ) + .getMember(); } else { - return getter.getMember(); + final EntityPersister declaringEntityMapping = getDeclaringEntity( identifiableType, metadataContext ); + final EntityIdentifierMapping identifierMapping = declaringEntityMapping.getIdentifierMapping(); + final Property propertyMapping = attributeContext.getPropertyMapping(); + if ( !propertyMapping.getName().equals( identifierMapping.getAttributeName() ) ) { + // this *should* indicate processing part of an IdClass... + return virtualIdentifierMemberResolver.resolveMember( attributeContext, metadataContext ); + } + + final Getter getter = getter( declaringEntityMapping, propertyMapping ); + if ( getter instanceof PropertyAccessMapImpl.GetterImpl ) { + return new MapMember( identifierMapping.getAttributeName(), identifierMapping.getJavaType().getJavaTypeClass() ); + } + else { + return getter.getMember(); + } } }; private final MemberResolver versionMemberResolver = (attributeContext, metadataContext) -> { final AbstractIdentifiableType identifiableType = (AbstractIdentifiableType) attributeContext.getOwnerType(); - final EntityPersister entityPersister = getDeclaringEntity( identifiableType, metadataContext ); - final EntityVersionMapping versionMapping = entityPersister.getVersionMapping(); - assert entityPersister.isVersioned(); - assert versionMapping != null; - - final String versionPropertyName = attributeContext.getPropertyMapping().getName(); - if ( !versionPropertyName.equals( versionMapping.getVersionAttribute().getAttributeName() ) ) { - // this should never happen, but to be safe... - throw new IllegalArgumentException( "Given property did not match declared version property" ); - } - - final Getter getter = getter( entityPersister, attributeContext.getPropertyMapping() ); - if ( getter instanceof PropertyAccessMapImpl.GetterImpl ) { - return new MapMember( versionPropertyName, versionMapping.getJavaType().getJavaTypeClass() ); + if ( identifiableType instanceof MappedSuperclassDomainType ) { + return attributeContext.getPropertyMapping() + .getGetter( identifiableType.getJavaType() ) + .getMember(); } else { - return getter.getMember(); + final EntityPersister entityPersister = getDeclaringEntity( identifiableType, metadataContext ); + final EntityVersionMapping versionMapping = entityPersister.getVersionMapping(); + assert entityPersister.isVersioned(); + assert versionMapping != null; + + final String versionPropertyName = attributeContext.getPropertyMapping().getName(); + if ( !versionPropertyName.equals( versionMapping.getVersionAttribute().getAttributeName() ) ) { + // this should never happen, but to be safe... + throw new IllegalArgumentException( "Given property did not match declared version property" ); + } + + final Getter getter = getter( entityPersister, attributeContext.getPropertyMapping() ); + if ( getter instanceof PropertyAccessMapImpl.GetterImpl ) { + return new MapMember( versionPropertyName, versionMapping.getJavaType().getJavaTypeClass() ); + } + else { + return getter.getMember(); + } } }; diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/internal/BaseAttributeMetadata.java b/hibernate-core/src/main/java/org/hibernate/metamodel/internal/BaseAttributeMetadata.java index 5cfea74ddc66..a12ce485bb7d 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/internal/BaseAttributeMetadata.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/internal/BaseAttributeMetadata.java @@ -14,6 +14,7 @@ import org.hibernate.metamodel.AttributeClassification; import org.hibernate.metamodel.model.domain.ManagedDomainType; import org.hibernate.metamodel.model.domain.internal.MapMember; +import org.hibernate.type.CollectionType; /** * @author Steve Ebersole @@ -87,7 +88,7 @@ public ManagedDomainType getOwnerType() { } public boolean isPlural() { - return propertyMapping.getType().isCollectionType(); + return propertyMapping.getType() instanceof CollectionType; } public Property getPropertyMapping() { diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/internal/MetadataContext.java b/hibernate-core/src/main/java/org/hibernate/metamodel/internal/MetadataContext.java index 864af9d455d1..be53ad3a5425 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/internal/MetadataContext.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/internal/MetadataContext.java @@ -25,6 +25,7 @@ import org.hibernate.internal.EntityManagerMessageLogger; import org.hibernate.internal.HEMLogging; import org.hibernate.internal.util.ReflectHelper; +import org.hibernate.internal.util.collections.ArrayHelper; import org.hibernate.internal.util.collections.CollectionHelper; import org.hibernate.mapping.Component; import org.hibernate.mapping.MappedSuperclass; @@ -276,8 +277,10 @@ public Map> getIdentifiableTypesByName() { attribute = factoryFunction.apply( entityType, genericProperty ); if ( !property.isGeneric() ) { final PersistentAttribute concreteAttribute = factoryFunction.apply( entityType, property ); - //noinspection unchecked - ( (AttributeContainer) entityType ).getInFlightAccess().addConcreteGenericAttribute( concreteAttribute ); + if ( concreteAttribute != null ) { + //noinspection unchecked + ( (AttributeContainer) entityType ).getInFlightAccess().addConcreteGenericAttribute( concreteAttribute ); + } } } else { @@ -362,7 +365,13 @@ else if ( MappedSuperclass.class.isAssignableFrom( mapping.getClass() ) ) { // applyNaturalIdAttribute( safeMapping, jpaType ); for ( Property property : safeMapping.getDeclaredProperties() ) { - if ( safeMapping.isVersioned() && property == safeMapping.getVersion() ) { + if ( isIdentifierProperty( property, safeMapping ) ) { + // property represents special handling for id-class mappings but we have already + // accounted for the embedded property mappings in #applyIdMetadata && + // #buildIdClassAttributes + continue; + } + else if ( safeMapping.isVersioned() && property == safeMapping.getVersion() ) { // skip the version property, it was already handled previously. continue; } @@ -374,7 +383,7 @@ else if ( MappedSuperclass.class.isAssignableFrom( mapping.getClass() ) ) { if ( attribute != null ) { addAttribute( jpaType, attribute ); if ( property.isNaturalIdentifier() ) { - ( ( AttributeContainer) jpaType ).getInFlightAccess() + ( (AttributeContainer) jpaType ).getInFlightAccess() .applyNaturalIdAttribute( attribute ); } } @@ -415,7 +424,16 @@ else if ( MappedSuperclass.class.isAssignableFrom( mapping.getClass() ) ) { final PersistentAttribute attribute = attributeFactory.buildAttribute( (ManagedDomainType) embeddable, property ); if ( attribute != null ) { - addAttribute( embeddable, attribute ); + final Property superclassProperty = getMappedSuperclassProperty( + property.getName(), + component.getMappedSuperclass() + ); + if ( superclassProperty != null && superclassProperty.isGeneric() ) { + ( (AttributeContainer) embeddable ).getInFlightAccess().addConcreteGenericAttribute( attribute ); + } + else { + addAttribute( embeddable, attribute ); + } } } @@ -433,6 +451,14 @@ else if ( MappedSuperclass.class.isAssignableFrom( mapping.getClass() ) ) { } } + private static boolean isIdentifierProperty(Property property, MappedSuperclass mappedSuperclass) { + final Component identifierMapper = mappedSuperclass.getIdentifierMapper(); + return identifierMapper != null && ArrayHelper.contains( + identifierMapper.getPropertyNames(), + property.getName() + ); + } + private void addAttribute(ManagedDomainType type, PersistentAttribute attribute) { //noinspection unchecked AttributeContainer container = (AttributeContainer) type; @@ -549,15 +575,24 @@ private Property getMappedSuperclassIdentifier(PersistentClass persistentClass) return null; } - private EmbeddableTypeImpl applyIdClassMetadata(Component idClassComponent) { - final JavaTypeRegistry registry = getTypeConfiguration() - .getJavaTypeRegistry(); - final Class componentClass = idClassComponent.getComponentClass(); - final JavaType javaType = registry.resolveManagedTypeDescriptor( componentClass ); + private EmbeddableTypeImpl applyIdClassMetadata(Component idClassComponent) { + final JavaType javaType = + getTypeConfiguration().getJavaTypeRegistry() + .resolveManagedTypeDescriptor( idClassComponent.getComponentClass() ); + + final MappedSuperclass mappedSuperclass = idClassComponent.getMappedSuperclass(); + final MappedSuperclassDomainType superType; + if ( mappedSuperclass != null ) { + //noinspection unchecked + superType = (MappedSuperclassDomainType) locateMappedSuperclassType( mappedSuperclass ); + } + else { + superType = null; + } - final EmbeddableTypeImpl embeddableType = new EmbeddableTypeImpl<>( + final EmbeddableTypeImpl embeddableType = new EmbeddableTypeImpl<>( javaType, - null, + superType, null, false, getJpaMetamodel() @@ -567,27 +602,29 @@ private EmbeddableTypeImpl applyIdClassMetadata(Component idClassComponent) { } private void applyIdMetadata(MappedSuperclass mappingType, MappedSuperclassDomainType jpaMappingType) { + @SuppressWarnings("unchecked") + final AttributeContainer attributeContainer = (AttributeContainer) jpaMappingType; if ( mappingType.hasIdentifierProperty() ) { final Property declaredIdentifierProperty = mappingType.getDeclaredIdentifierProperty(); if ( declaredIdentifierProperty != null ) { - //noinspection unchecked - final SingularPersistentAttribute attribute = (SingularPersistentAttribute) buildAttribute( - declaredIdentifierProperty, - jpaMappingType, - attributeFactory::buildIdAttribute - ); - //noinspection unchecked - ( (AttributeContainer) jpaMappingType ).getInFlightAccess().applyIdAttribute( attribute ); + final SingularPersistentAttribute attribute = + (SingularPersistentAttribute) + buildAttribute( + declaredIdentifierProperty, + jpaMappingType, + attributeFactory::buildIdAttribute + ); + attributeContainer.getInFlightAccess().applyIdAttribute( attribute ); } } //a MappedSuperclass can have no identifier if the id is set below in the hierarchy else if ( mappingType.getIdentifierMapper() != null ) { - Set> attributes = buildIdClassAttributes( - jpaMappingType, - mappingType.getIdentifierMapper().getProperties() - ); - //noinspection unchecked - ( ( AttributeContainer) jpaMappingType ).getInFlightAccess().applyIdClassAttributes( attributes ); + final Set> attributes = + buildIdClassAttributes( + jpaMappingType, + mappingType.getIdentifierMapper().getProperties() + ); + attributeContainer.getInFlightAccess().applyIdClassAttributes( attributes ); } } @@ -643,6 +680,32 @@ private MappedSuperclass getMappedSuperclass(MappedSuperclass mappedSuperclass) : getMappedSuperclass( mappedSuperclass.getSuperPersistentClass() ); } + private Property getMappedSuperclassProperty(String propertyName, MappedSuperclass mappedSuperclass) { + if ( mappedSuperclass == null ) { + return null; + } + + for ( Property property : mappedSuperclass.getDeclaredProperties() ) { + if ( property.getName().equals( propertyName ) ) { + return property; + } + } + + final Property property = getMappedSuperclassProperty( + propertyName, + mappedSuperclass.getSuperMappedSuperclass() + ); + if ( property != null ) { + return property; + } + + if ( mappedSuperclass.getSuperPersistentClass() != null ) { + return mappedSuperclass.getSuperPersistentClass().getProperty( propertyName ); + } + + return null; + } + private Set> buildIdClassAttributes( IdentifiableDomainType ownerType, List properties) { @@ -770,15 +833,28 @@ private static void injectField( // + "; expected type : " + attribute.getClass().getName() // + "; encountered type : " + field.getType().getName() // ); - LOG.illegalArgumentOnStaticMetamodelFieldInjection( - metamodelClass.getName(), - name, - model.getClass().getName(), - field.getType().getName() - ); + // Avoid logging an error for Enver's default revision classes that are both entities and mapped-supers. + // This is a workaround for https://hibernate.atlassian.net/browse/HHH-17612 + if ( !isDefaultEnversRevisionType( metamodelClass ) ) { + LOG.illegalArgumentOnStaticMetamodelFieldInjection( + metamodelClass.getName(), + name, + model.getClass().getName(), + field.getType().getName() + ); + } } } + private static boolean isDefaultEnversRevisionType(Class metamodelClass) { + return Set.of( + "org.hibernate.envers.DefaultRevisionEntity_", + "org.hibernate.envers.DefaultTrackingModifiedEntitiesRevisionEntity_", + "org.hibernate.envers.enhanced.SequenceIdRevisionEntity_", + "org.hibernate.envers.enhanced.SequenceIdTrackingModifiedEntitiesRevisionEntity_" + ).contains( metamodelClass.getName() ); + } + public MappedSuperclassDomainType locateMappedSuperclassType(MappedSuperclass mappedSuperclass) { return mappedSuperclassByMappedSuperclassMapping.get( mappedSuperclass ); } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/EntityIdentifierMapping.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/EntityIdentifierMapping.java index f6612255e8aa..92ea4430b7f8 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/EntityIdentifierMapping.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/EntityIdentifierMapping.java @@ -10,6 +10,7 @@ import org.hibernate.engine.internal.ForeignKeys; import org.hibernate.engine.spi.IdentifierValue; import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.event.spi.MergeContext; import jakarta.persistence.EmbeddedId; import jakarta.persistence.Id; @@ -68,6 +69,15 @@ default String getPartName() { */ Object getIdentifier(Object entity); + /** + * Extract the identifier from an instance of the entity + * + * It's supposed to be use during the merging process + */ + default Object getIdentifier(Object entity, MergeContext mergeContext){ + return getIdentifier( entity ); + } + /** * Return the identifier of the persistent or transient object, or throw * an exception if the instance is "unsaved" diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/EntityMappingType.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/EntityMappingType.java index 7c7b0071b56c..8de1d5886ea9 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/EntityMappingType.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/EntityMappingType.java @@ -321,6 +321,14 @@ default void pruneForSubclasses(TableGroup tableGroup, Set treatedEntity */ EntityIdentifierMapping getIdentifierMapping(); + /** + * Mapping details for the entity's identifier. This is shared across all + * entity mappings within an inheritance hierarchy. + */ + default EntityIdentifierMapping getIdentifierMappingForJoin() { + return getIdentifierMapping(); + } + /** * Mapping details for the entity's discriminator. This is shared across all * entity mappings within an inheritance hierarchy. diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/MappedDiscriminatorConverter.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/MappedDiscriminatorConverter.java index b7aa1a857ac3..17e877d05f03 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/MappedDiscriminatorConverter.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/MappedDiscriminatorConverter.java @@ -13,7 +13,9 @@ import org.hibernate.metamodel.model.domain.NavigableRole; import org.hibernate.metamodel.spi.MappingMetamodelImplementor; import org.hibernate.type.BasicType; +import org.hibernate.type.descriptor.java.CharacterJavaType; import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.descriptor.java.StringJavaType; import java.util.List; import java.util.Map; @@ -113,6 +115,23 @@ public DiscriminatorValueDetails getDetailsForDiscriminatorValue(Object value) { return notNullMatch; } + if ( value.getClass().isEnum() ) { + final Object enumValue; + if ( getRelationalJavaType() instanceof StringJavaType ) { + enumValue = ( (Enum) value ).name(); + } + else if ( getRelationalJavaType() instanceof CharacterJavaType ) { + enumValue = ( (Enum) value ).name().charAt( 0 ); + } + else { + enumValue = ( (Enum) value ).ordinal(); + } + final DiscriminatorValueDetails enumMatch = discriminatorValueToEntityNameMap.get( enumValue ); + if ( enumMatch != null ) { + return enumMatch; + } + } + throw new HibernateException( "Unrecognized discriminator value: " + value ); } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/SqlTypedMapping.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/SqlTypedMapping.java index 3f110c24c363..b1e4d7969c03 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/SqlTypedMapping.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/SqlTypedMapping.java @@ -8,17 +8,20 @@ import org.hibernate.engine.jdbc.Size; +import org.checkerframework.checker.nullness.qual.Nullable; + /** * Models the type of a thing that can be used as an expression in a SQL query * * @author Christian Beikov */ public interface SqlTypedMapping { + @Nullable String getColumnDefinition(); - Long getLength(); - Integer getPrecision(); - Integer getScale(); - Integer getTemporalPrecision(); + @Nullable Long getLength(); + @Nullable Integer getPrecision(); + @Nullable Integer getScale(); + @Nullable Integer getTemporalPrecision(); default boolean isLob() { return getJdbcMapping().getJdbcType().isLob(); } 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 7fd9d2d0d306..5bf61af5087d 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 @@ -245,6 +245,7 @@ else if ( attributeMapping instanceof ToOneAttributeMapping ) { creationProcess ) ); + toOne.setupCircularFetchModelPart( creationProcess ); attributeMapping = toOne; currentIndex += attributeMapping.getJdbcTypeCount(); diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/AbstractEntityCollectionPart.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/AbstractEntityCollectionPart.java index a7d354025f87..e5f7d5faae32 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/AbstractEntityCollectionPart.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/AbstractEntityCollectionPart.java @@ -45,6 +45,7 @@ import org.hibernate.sql.results.graph.collection.internal.EagerCollectionFetch; import org.hibernate.sql.results.graph.entity.EntityFetch; import org.hibernate.sql.results.graph.entity.internal.EntityFetchJoinedImpl; +import org.hibernate.type.ComponentType; import org.hibernate.type.CompositeType; import org.hibernate.type.Type; @@ -389,8 +390,8 @@ private static Set resolveTargetKeyPropertyNames( propertyType = entityBinding.getIdentifierMapper().getType(); } if ( entityBinding.getIdentifierProperty() == null ) { - final CompositeType compositeType; - if ( propertyType.isComponentType() && ( compositeType = (CompositeType) propertyType ).isEmbedded() + final ComponentType compositeType; + if ( propertyType instanceof ComponentType && ( compositeType = (ComponentType) propertyType ).isEmbedded() && compositeType.getPropertyNames().length == 1 ) { ToOneAttributeMapping.addPrefixedPropertyPaths( targetKeyPropertyNames, @@ -443,8 +444,8 @@ else if ( bootModelValue instanceof OneToMany ) { } else { final Type propertyType = entityBinding.getRecursiveProperty( referencedPropertyName ).getType(); - final CompositeType compositeType; - if ( propertyType.isComponentType() && ( compositeType = (CompositeType) propertyType ).isEmbedded() + final ComponentType compositeType; + if ( propertyType instanceof ComponentType && ( compositeType = (ComponentType) propertyType ).isEmbedded() && compositeType.getPropertyNames().length == 1 ) { final Set targetKeyPropertyNames = new HashSet<>( 2 ); ToOneAttributeMapping.addPrefixedPropertyPaths( diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/AnyDiscriminatorPart.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/AnyDiscriminatorPart.java index 85f3ea113cc6..21f6badfd8b5 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 @@ -25,6 +25,7 @@ import org.hibernate.metamodel.mapping.MappedDiscriminatorConverter; import org.hibernate.metamodel.mapping.MappingType; import org.hibernate.metamodel.mapping.SelectableConsumer; +import org.hibernate.metamodel.mapping.SelectablePath; import org.hibernate.metamodel.model.domain.NavigableRole; import org.hibernate.metamodel.spi.MappingMetamodelImplementor; import org.hibernate.spi.NavigablePath; @@ -58,6 +59,7 @@ public class AnyDiscriminatorPart implements DiscriminatorMapping, FetchOptions private final String table; private final String column; + private final SelectablePath selectablePath; private final String customReadExpression; private final String customWriteExpression; private final String columnDefinition; @@ -76,7 +78,10 @@ public AnyDiscriminatorPart( NavigableRole partRole, DiscriminatedAssociationModelPart declaringType, String table, - String column, String customReadExpression, String customWriteExpression, + String column, + SelectablePath selectablePath, + String customReadExpression, + String customWriteExpression, String columnDefinition, Long length, Integer precision, @@ -91,6 +96,7 @@ public AnyDiscriminatorPart( this.declaringType = declaringType; this.table = table; this.column = column; + this.selectablePath = selectablePath; this.customReadExpression = customReadExpression; this.customWriteExpression = customWriteExpression; this.columnDefinition = columnDefinition; @@ -136,6 +142,16 @@ public String getSelectionExpression() { return column; } + @Override + public String getSelectableName() { + return selectablePath.getSelectableName(); + } + + @Override + public SelectablePath getSelectablePath() { + return selectablePath; + } + @Override public boolean isFormula() { return false; 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 4c20f90e6008..191b23e07c3d 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 @@ -19,6 +19,7 @@ import org.hibernate.metamodel.mapping.JdbcMapping; import org.hibernate.metamodel.mapping.MappingType; import org.hibernate.metamodel.mapping.SelectableConsumer; +import org.hibernate.metamodel.mapping.SelectablePath; import org.hibernate.metamodel.model.domain.NavigableRole; import org.hibernate.spi.NavigablePath; import org.hibernate.sql.ast.spi.FromClauseAccess; @@ -47,6 +48,7 @@ public class AnyKeyPart implements BasicValuedModelPart, FetchOptions { private final NavigableRole navigableRole; private final String table; private final String column; + private final SelectablePath selectablePath; private final DiscriminatedAssociationModelPart anyPart; private final String customReadExpression; private final String customWriteExpression; @@ -65,6 +67,7 @@ public AnyKeyPart( DiscriminatedAssociationModelPart anyPart, String table, String column, + SelectablePath selectablePath, String customReadExpression, String customWriteExpression, String columnDefinition, @@ -79,6 +82,7 @@ public AnyKeyPart( this.navigableRole = navigableRole; this.table = table; this.column = column; + this.selectablePath = selectablePath; this.anyPart = anyPart; this.customReadExpression = customReadExpression; this.customWriteExpression = customWriteExpression; @@ -103,6 +107,16 @@ public String getSelectionExpression() { return column; } + @Override + public String getSelectableName() { + return selectablePath.getSelectableName(); + } + + @Override + public SelectablePath getSelectablePath() { + return selectablePath; + } + @Override public boolean isFormula() { return false; 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 5b518b683428..59a153da41c4 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 @@ -153,7 +153,7 @@ public IdentifierValue getUnsavedStrategy() { public Object getIdentifier(Object entity) { final LazyInitializer lazyInitializer = HibernateProxy.extractLazyInitializer( entity ); if ( lazyInitializer != null ) { - return lazyInitializer.getIdentifier(); + return lazyInitializer.getInternalIdentifier(); } return propertyAccess.getGetter().get( entity ); } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/CaseStatementDiscriminatorMappingImpl.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/CaseStatementDiscriminatorMappingImpl.java index 08b66db39c1d..892754ab1791 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/CaseStatementDiscriminatorMappingImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/CaseStatementDiscriminatorMappingImpl.java @@ -6,7 +6,9 @@ */ package org.hibernate.metamodel.mapping.internal; +import java.util.ArrayList; import java.util.LinkedHashMap; +import java.util.List; import org.hibernate.engine.FetchTiming; import org.hibernate.engine.spi.SessionFactoryImplementor; @@ -255,6 +257,24 @@ public CaseStatementDiscriminatorExpression(TableGroup entityTableGroup) { this.entityTableGroup = entityTableGroup; } + public List getUsedTableReferences() { + final ArrayList usedTableReferences = new ArrayList<>( tableDiscriminatorDetailsMap.size() ); + tableDiscriminatorDetailsMap.forEach( + (tableName, tableDiscriminatorDetails) -> { + final TableReference tableReference = entityTableGroup.getTableReference( + entityTableGroup.getNavigablePath(), + tableName, + false + ); + + if ( tableReference != null ) { + usedTableReferences.add( tableReference ); + } + } + ); + return usedTableReferences; + } + @Override public void renderToSql( SqlAppender sqlAppender, diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/CompoundNaturalIdMapping.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/CompoundNaturalIdMapping.java index 37c931e9499e..0083b7a40ba9 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/CompoundNaturalIdMapping.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/CompoundNaturalIdMapping.java @@ -7,7 +7,6 @@ package org.hibernate.metamodel.mapping.internal; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import java.util.Map; import java.util.function.BiConsumer; @@ -61,6 +60,11 @@ public class CompoundNaturalIdMapping extends AbstractNaturalIdMapping implement private final List attributes; private List jdbcMappings; + /* + This value is used to determine the size of the array used to create the ImmutableFetchList (see org.hibernate.sql.results.graph.internal.ImmutableFetchList#Builder) + The Fetch is inserted into the array at a position corresponding to its Fetchable key value. + */ + private final int maxFetchableKeyIndex; public CompoundNaturalIdMapping( EntityMappingType declaringType, @@ -69,6 +73,14 @@ public CompoundNaturalIdMapping( super( declaringType, isMutable( attributes ) ); this.attributes = attributes; + int maxIndex = 0; + for ( SingularAttributeMapping attribute : attributes ) { + if ( attribute.getFetchableKey() > maxIndex ) { + maxIndex = attribute.getFetchableKey(); + } + } + this.maxFetchableKeyIndex = maxIndex + 1; + creationProcess.registerInitializationCallback( "Determine compound natural-id JDBC mappings ( " + declaringType.getEntityName() + ")", () -> { @@ -235,7 +247,15 @@ public void verifyFlushState(Object id, Object[] currentState, Object[] loadedSt @Override public boolean areEqual(Object one, Object other, SharedSessionContractImplementor session) { - return Arrays.equals( (Object[]) one, (Object[]) other ); + final Object[] one1 = (Object[]) one; + final Object[] other1 = (Object[]) other; + final List naturalIdAttributes = getNaturalIdAttributes(); + for ( int i = 0; i < naturalIdAttributes.size(); i++ ) { + if ( !naturalIdAttributes.get( i ).areEqual( one1[i], other1[i], session ) ) { + return false; + } + } + return true; } @Override @@ -531,6 +551,10 @@ public void visitSubParts(Consumer consumer, EntityMappingType treatT attributes.forEach( consumer ); } + @Override + public int getNumberOfFetchableKeys() { + return maxFetchableKeyIndex; + } public static class DomainResultImpl implements DomainResult, FetchParent { private final NavigablePath navigablePath; diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/DiscriminatedAssociationAttributeMapping.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/DiscriminatedAssociationAttributeMapping.java index 3c40af99c00d..c539daeaaba5 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/DiscriminatedAssociationAttributeMapping.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/DiscriminatedAssociationAttributeMapping.java @@ -52,6 +52,8 @@ import org.hibernate.type.descriptor.java.JavaType; import org.hibernate.type.descriptor.java.MutabilityPlan; +import org.checkerframework.checker.nullness.qual.Nullable; + /** * Singular, any-valued attribute * @@ -487,9 +489,9 @@ public Object assemble(Serializable cached, SharedSessionContract session) { public TableGroupJoin createTableGroupJoin( NavigablePath navigablePath, TableGroup lhs, - String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, - SqlAstJoinType requestedJoinType, + @Nullable String explicitSourceAlias, + @Nullable SqlAliasBase explicitSqlAliasBase, + @Nullable SqlAstJoinType requestedJoinType, boolean fetched, boolean addsPredicate, SqlAstCreationState creationState) { @@ -512,11 +514,11 @@ public TableGroupJoin createTableGroupJoin( public TableGroup createRootTableGroupJoin( NavigablePath navigablePath, TableGroup lhs, - String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, - SqlAstJoinType sqlAstJoinType, + @Nullable String explicitSourceAlias, + @Nullable SqlAliasBase explicitSqlAliasBase, + @Nullable SqlAstJoinType sqlAstJoinType, boolean fetched, - Consumer predicateConsumer, + @Nullable Consumer predicateConsumer, SqlAstCreationState creationState) { return new StandardVirtualTableGroup( navigablePath, this, lhs, fetched ); } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/DiscriminatedAssociationMapping.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/DiscriminatedAssociationMapping.java index 43dbd65b91ca..a45a15c90355 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 @@ -25,6 +25,7 @@ import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.mapping.MappingType; import org.hibernate.metamodel.mapping.ModelPart; +import org.hibernate.metamodel.mapping.SelectablePath; import org.hibernate.metamodel.model.domain.NavigableRole; import org.hibernate.spi.NavigablePath; import org.hibernate.sql.ast.tree.from.TableGroup; @@ -73,13 +74,18 @@ public static DiscriminatedAssociationMapping from( assert !keySelectable.isFormula(); final Column metaColumn = (Column) metaSelectable; final Column keyColumn = (Column) keySelectable; + final SelectablePath parentSelectablePath = declaringModelPart.asAttributeMapping() != null + ? MappingModelCreationHelper.getSelectablePath( declaringModelPart.asAttributeMapping().getDeclaringType() ) + : null; final MetaType metaType = (MetaType) anyType.getDiscriminatorType(); final AnyDiscriminatorPart discriminatorPart = new AnyDiscriminatorPart( - containerRole.append( AnyDiscriminatorPart.ROLE_NAME), + containerRole.append( AnyDiscriminatorPart.ROLE_NAME ), declaringModelPart, tableName, metaColumn.getText( dialect ), + parentSelectablePath != null ? parentSelectablePath.append( metaColumn.getQuotedName( dialect ) ) + : new SelectablePath( metaColumn.getQuotedName( dialect ) ), metaColumn.getCustomReadExpression(), metaColumn.getCustomWriteExpression(), metaColumn.getSqlType(), @@ -101,6 +107,8 @@ public static DiscriminatedAssociationMapping from( declaringModelPart, tableName, keyColumn.getText( dialect ), + parentSelectablePath != null ? parentSelectablePath.append( keyColumn.getQuotedName( dialect ) ) + : new SelectablePath( keyColumn.getQuotedName( dialect ) ), keyColumn.getCustomReadExpression(), keyColumn.getCustomWriteExpression(), keyColumn.getSqlType(), diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/DiscriminatedCollectionPart.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/DiscriminatedCollectionPart.java index 280c782c9cd2..81d86e6f5b3e 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/DiscriminatedCollectionPart.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/DiscriminatedCollectionPart.java @@ -45,6 +45,10 @@ import org.hibernate.type.AnyType; import org.hibernate.type.descriptor.java.JavaType; +import org.checkerframework.checker.nullness.qual.Nullable; + +import static java.util.Objects.requireNonNullElse; + /** * @author Steve Ebersole */ @@ -343,19 +347,13 @@ public int forEachJdbcType(int offset, IndexedConsumer action) { public TableGroupJoin createTableGroupJoin( NavigablePath navigablePath, TableGroup lhs, - String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, - SqlAstJoinType requestedJoinType, + @Nullable String explicitSourceAlias, + @Nullable SqlAliasBase explicitSqlAliasBase, + @Nullable SqlAstJoinType requestedJoinType, boolean fetched, boolean addsPredicate, SqlAstCreationState creationState) { - final SqlAstJoinType joinType; - if ( requestedJoinType == null ) { - joinType = SqlAstJoinType.INNER; - } - else { - joinType = requestedJoinType; - } + final SqlAstJoinType joinType = requireNonNullElse( requestedJoinType, SqlAstJoinType.INNER ); final TableGroup tableGroup = createRootTableGroupJoin( navigablePath, lhs, @@ -374,11 +372,11 @@ public TableGroupJoin createTableGroupJoin( public TableGroup createRootTableGroupJoin( NavigablePath navigablePath, TableGroup lhs, - String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, - SqlAstJoinType sqlAstJoinType, + @Nullable String explicitSourceAlias, + @Nullable SqlAliasBase explicitSqlAliasBase, + @Nullable SqlAstJoinType sqlAstJoinType, boolean fetched, - Consumer predicateConsumer, + @Nullable Consumer predicateConsumer, SqlAstCreationState creationState) { return new StandardVirtualTableGroup( navigablePath, this, lhs, fetched ); } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/EmbeddableMappingTypeImpl.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/EmbeddableMappingTypeImpl.java index 3c0af66c2580..5e9b280e2ca6 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/EmbeddableMappingTypeImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/EmbeddableMappingTypeImpl.java @@ -764,7 +764,7 @@ private EmbeddableDiscriminatorMapping generateDiscriminatorMapping( return new ExplicitColumnDiscriminatorMappingImpl( this, name, - bootDescriptor.getTable().getName(), + bootDescriptor.getTable().getQualifiedName( creationContext.getSqlStringGenerationContext() ), discriminatorColumnExpression, isFormula, !isFormula, diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/EmbeddedAttributeMapping.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/EmbeddedAttributeMapping.java index 9308988f039f..a47872909d0d 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/EmbeddedAttributeMapping.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/EmbeddedAttributeMapping.java @@ -53,6 +53,10 @@ import org.hibernate.sql.results.graph.embeddable.internal.EmbeddableFetchImpl; import org.hibernate.sql.results.graph.embeddable.internal.EmbeddableResultImpl; +import org.checkerframework.checker.nullness.qual.Nullable; + +import static java.util.Objects.requireNonNullElse; + /** * @author Steve Ebersole */ @@ -318,13 +322,13 @@ public SqlTuple toSqlExpression( public TableGroupJoin createTableGroupJoin( NavigablePath navigablePath, TableGroup lhs, - String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, - SqlAstJoinType requestedJoinType, + @Nullable String explicitSourceAlias, + @Nullable SqlAliasBase explicitSqlAliasBase, + @Nullable SqlAstJoinType requestedJoinType, boolean fetched, boolean addsPredicate, SqlAstCreationState creationState) { - final SqlAstJoinType joinType = requestedJoinType == null ? SqlAstJoinType.INNER : requestedJoinType; + final SqlAstJoinType joinType = requireNonNullElse( requestedJoinType, SqlAstJoinType.INNER ); final TableGroup tableGroup = createRootTableGroupJoin( navigablePath, lhs, @@ -343,11 +347,11 @@ public TableGroupJoin createTableGroupJoin( public TableGroup createRootTableGroupJoin( NavigablePath navigablePath, TableGroup lhs, - String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, - SqlAstJoinType sqlAstJoinType, + @Nullable String explicitSourceAlias, + @Nullable SqlAliasBase explicitSqlAliasBase, + @Nullable SqlAstJoinType sqlAstJoinType, boolean fetched, - Consumer predicateConsumer, + @Nullable Consumer predicateConsumer, SqlAstCreationState creationState) { return new StandardVirtualTableGroup( navigablePath, this, lhs, fetched ); } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/EmbeddedCollectionPart.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/EmbeddedCollectionPart.java index 190b5fc13865..aa49eb298cd1 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/EmbeddedCollectionPart.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/EmbeddedCollectionPart.java @@ -13,7 +13,6 @@ import org.hibernate.engine.FetchStyle; import org.hibernate.engine.FetchTiming; -import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.metamodel.mapping.CollectionPart; import org.hibernate.metamodel.mapping.EmbeddableMappingType; import org.hibernate.metamodel.mapping.EntityMappingType; @@ -21,7 +20,6 @@ import org.hibernate.metamodel.mapping.MappingType; import org.hibernate.metamodel.mapping.ModelPart; import org.hibernate.metamodel.mapping.PluralAttributeMapping; -import org.hibernate.metamodel.mapping.SelectableConsumer; import org.hibernate.metamodel.model.domain.NavigableRole; import org.hibernate.persister.collection.CollectionPersister; import org.hibernate.property.access.internal.PropertyAccessStrategyBasicImpl; @@ -41,7 +39,6 @@ import org.hibernate.sql.ast.tree.from.StandardVirtualTableGroup; import org.hibernate.sql.ast.tree.from.TableGroup; import org.hibernate.sql.ast.tree.from.TableGroupJoin; -import org.hibernate.sql.ast.tree.from.TableGroupProducer; import org.hibernate.sql.ast.tree.from.TableReference; import org.hibernate.sql.ast.tree.predicate.Predicate; import org.hibernate.sql.results.graph.DomainResult; @@ -49,12 +46,15 @@ import org.hibernate.sql.results.graph.Fetch; import org.hibernate.sql.results.graph.FetchOptions; import org.hibernate.sql.results.graph.FetchParent; -import org.hibernate.sql.results.graph.Fetchable; import org.hibernate.sql.results.graph.embeddable.EmbeddableValuedFetchable; import org.hibernate.sql.results.graph.embeddable.internal.EmbeddableFetchImpl; import org.hibernate.sql.results.graph.embeddable.internal.EmbeddableResultImpl; import org.hibernate.type.descriptor.java.JavaType; +import org.checkerframework.checker.nullness.qual.Nullable; + +import static java.util.Objects.requireNonNullElse; + /** * @author Steve Ebersole */ @@ -231,19 +231,13 @@ public SqlTuple toSqlExpression( public TableGroupJoin createTableGroupJoin( NavigablePath navigablePath, TableGroup lhs, - String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, - SqlAstJoinType requestedJoinType, + @Nullable String explicitSourceAlias, + @Nullable SqlAliasBase explicitSqlAliasBase, + @Nullable SqlAstJoinType requestedJoinType, boolean fetched, boolean addsPredicate, SqlAstCreationState creationState) { - final SqlAstJoinType joinType; - if ( requestedJoinType == null ) { - joinType = SqlAstJoinType.INNER; - } - else { - joinType = requestedJoinType; - } + final SqlAstJoinType joinType = requireNonNullElse( requestedJoinType, SqlAstJoinType.INNER ); final TableGroup tableGroup = createRootTableGroupJoin( navigablePath, lhs, @@ -262,11 +256,11 @@ public TableGroupJoin createTableGroupJoin( public TableGroup createRootTableGroupJoin( NavigablePath navigablePath, TableGroup lhs, - String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, - SqlAstJoinType sqlAstJoinType, + @Nullable String explicitSourceAlias, + @Nullable SqlAliasBase explicitSqlAliasBase, + @Nullable SqlAstJoinType sqlAstJoinType, boolean fetched, - Consumer predicateConsumer, + @Nullable Consumer predicateConsumer, SqlAstCreationState creationState) { assert lhs.getModelPart() instanceof PluralAttributeMapping; return new StandardVirtualTableGroup( navigablePath, this, lhs, fetched ); diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/EmbeddedIdentifierMappingImpl.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/EmbeddedIdentifierMappingImpl.java index 4681a7de842d..17bd7c71b293 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 @@ -87,7 +87,7 @@ public void applySqlSelections( public Object getIdentifier(Object entity) { final LazyInitializer lazyInitializer = HibernateProxy.extractLazyInitializer( entity ); if ( lazyInitializer != null ) { - return lazyInitializer.getIdentifier(); + return lazyInitializer.getInternalIdentifier(); } return propertyAccess.getGetter().get( entity ); } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/EntityVersionMappingImpl.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/EntityVersionMappingImpl.java index 87d40f704166..59465c4c4f69 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/EntityVersionMappingImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/EntityVersionMappingImpl.java @@ -54,7 +54,7 @@ public class EntityVersionMappingImpl implements EntityVersionMapping, FetchOpti private final Integer scale; private final Integer temporalPrecision; - private final BasicType versionBasicType; + private final BasicType versionBasicType; private final VersionValue unsavedValueStrategy; @@ -90,15 +90,10 @@ public EntityVersionMappingImpl( unsavedValueStrategy = UnsavedValueFactory.getUnsavedVersionValue( (KeyValue) bootEntityDescriptor.getVersion().getValue(), (VersionJavaType) versionBasicType.getJavaTypeDescriptor(), - length, - precision, - scale, - declaringType - .getRepresentationStrategy() + declaringType.getRepresentationStrategy() .resolvePropertyAccess( bootEntityDescriptor.getVersion() ) .getGetter(), - templateInstanceAccess, - creationProcess.getCreationContext().getSessionFactory() + templateInstanceAccess ); } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/FetchOptionsHelper.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/FetchOptionsHelper.java index 8c856a9121dd..602bc1cabf8b 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/FetchOptionsHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/FetchOptionsHelper.java @@ -16,7 +16,10 @@ import org.hibernate.persister.collection.CollectionPersister; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.sql.results.graph.FetchOptions; +import org.hibernate.type.AnyType; import org.hibernate.type.AssociationType; +import org.hibernate.type.CollectionType; +import org.hibernate.type.EntityType; /** * @author Steve Ebersole @@ -40,7 +43,7 @@ public static FetchStyle determineFetchStyleByMetadata( FetchMode mappingFetchMode, AssociationType type, SessionFactoryImplementor sessionFactory) { - if ( !type.isEntityType() && !type.isCollectionType() ) { + if ( !( type instanceof EntityType ) && !( type instanceof CollectionType ) ) { return FetchStyle.SELECT; } @@ -48,7 +51,7 @@ public static FetchStyle determineFetchStyleByMetadata( return FetchStyle.JOIN; } - if ( type.isEntityType() ) { + if ( type instanceof EntityType ) { EntityPersister persister = (EntityPersister) type.getAssociatedJoinable( sessionFactory ); if ( persister.isBatchLoadable() ) { return FetchStyle.BATCH; @@ -116,11 +119,11 @@ public static FetchTiming determineFetchTiming( } private static boolean isSubsequentSelectDelayed(AssociationType type, SessionFactoryImplementor sessionFactory) { - if ( type.isAnyType() ) { + if ( type instanceof AnyType ) { // we'd need more context here. this is only kept as part of the property state on the owning entity return false; } - else if ( type.isEntityType() ) { + else if ( type instanceof EntityType ) { final EntityPersister entityPersister = (EntityPersister) type.getAssociatedJoinable( sessionFactory ); return entityPersister.getEntityMetamodel().isLazy(); } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/GeneratedValuesProcessor.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/GeneratedValuesProcessor.java index a4a67035ddf3..ab0ad491c558 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/GeneratedValuesProcessor.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/GeneratedValuesProcessor.java @@ -153,8 +153,8 @@ public void processGeneratedValues( assert results.size() == 1; setEntityAttributes( entity, state, results.get( 0 ) ); } - else { - castNonNull( generatedValues ); + else if ( generatedValues != null ) { + // can be null when an update action resulted in a no-op (e.g. only changes to unowned association) final List results = generatedValues.getGeneratedValues( generatedValuesToSelect ); setEntityAttributes( entity, state, results.toArray( new Object[0] ) ); } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/InverseNonAggregatedIdentifierMapping.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/InverseNonAggregatedIdentifierMapping.java index 5e47f34d21a8..46589709d974 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/InverseNonAggregatedIdentifierMapping.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/InverseNonAggregatedIdentifierMapping.java @@ -14,6 +14,7 @@ import org.hibernate.engine.spi.EntityKey; import org.hibernate.engine.spi.PersistenceContext; import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.event.spi.MergeContext; import org.hibernate.internal.util.collections.CollectionHelper; import org.hibernate.metamodel.mapping.AttributeMapping; import org.hibernate.metamodel.mapping.EmbeddableMappingType; @@ -196,6 +197,11 @@ public SqlTuple toSqlExpression( @Override public Object getIdentifier(Object entity) { + return getIdentifier( entity, null ); + } + + @Override + public Object getIdentifier(Object entity, MergeContext mergeContext) { if ( hasContainingClass() ) { final Object id = identifierValueMapper.getRepresentationStrategy().getInstantiator().instantiate( null, @@ -218,16 +224,17 @@ public Object getIdentifier(Object entity) { //JPA 2 @MapsId + @IdClass points to the pk of the entity else if ( attributeMapping instanceof ToOneAttributeMapping && !( identifierValueMapper.getAttributeMapping( i ) instanceof ToOneAttributeMapping ) ) { + final Object toOne = getIfMerged( o, mergeContext ); final ToOneAttributeMapping toOneAttributeMapping = (ToOneAttributeMapping) attributeMapping; final ModelPart targetPart = toOneAttributeMapping.getForeignKeyDescriptor().getPart( toOneAttributeMapping.getSideNature().inverse() ); if ( targetPart.isEntityIdentifierMapping() ) { - propertyValues[i] = ( (EntityIdentifierMapping) targetPart ).getIdentifier( o ); + propertyValues[i] = ( (EntityIdentifierMapping) targetPart ) + .getIdentifier( toOne, mergeContext ); } else { - propertyValues[i] = o; - assert false; + propertyValues[i] = toOne; } } else { @@ -242,6 +249,16 @@ else if ( attributeMapping instanceof ToOneAttributeMapping } } + private static Object getIfMerged(Object o, MergeContext mergeContext) { + if ( mergeContext != null ) { + final Object merged = mergeContext.get( o ); + if ( merged != null ) { + return merged; + } + } + return o; + } + @Override public void setIdentifier(Object entity, Object id, SharedSessionContractImplementor session) { final Object[] propertyValues = new Object[identifierValueMapper.getNumberOfAttributeMappings()]; diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/ManyToManyCollectionPart.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/ManyToManyCollectionPart.java index 4e262fc3fc71..f0818f97df87 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/ManyToManyCollectionPart.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/ManyToManyCollectionPart.java @@ -56,6 +56,8 @@ import org.hibernate.sql.ast.tree.predicate.Predicate; import org.hibernate.type.EntityType; +import org.checkerframework.checker.nullness.qual.Nullable; + import static java.util.Objects.requireNonNullElse; import static org.hibernate.metamodel.mapping.internal.MappingModelCreationHelper.createInverseModelPart; import static org.hibernate.metamodel.mapping.internal.MappingModelCreationHelper.getPropertyOrder; @@ -258,14 +260,13 @@ public ModelPart getKeyTargetMatchPart() { public TableGroupJoin createTableGroupJoin( NavigablePath navigablePath, TableGroup lhs, - String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, - SqlAstJoinType requestedJoinType, + @Nullable String explicitSourceAlias, + @Nullable SqlAliasBase explicitSqlAliasBase, + @Nullable SqlAstJoinType requestedJoinType, boolean fetched, boolean addsPredicate, SqlAstCreationState creationState) { final SqlAstJoinType joinType = requireNonNullElse( requestedJoinType, SqlAstJoinType.INNER ); - final LazyTableGroup lazyTableGroup = createRootTableGroupJoin( navigablePath, lhs, @@ -302,11 +303,11 @@ public TableGroupJoin createTableGroupJoin( public LazyTableGroup createRootTableGroupJoin( NavigablePath navigablePath, TableGroup lhs, - String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, - SqlAstJoinType requestedJoinType, + @Nullable String explicitSourceAlias, + @Nullable SqlAliasBase explicitSqlAliasBase, + @Nullable SqlAstJoinType requestedJoinType, boolean fetched, - Consumer predicateConsumer, + @Nullable Consumer predicateConsumer, SqlAstCreationState creationState) { final SqlAstJoinType joinType = requireNonNullElse( requestedJoinType, SqlAstJoinType.INNER ); final boolean canUseInnerJoin = joinType == SqlAstJoinType.INNER || lhs.canUseInnerJoins(); @@ -393,7 +394,8 @@ else if ( getNature() == Nature.INDEX ) { fkTargetModelPart = resolveNamedTargetPart( mapKeyPropertyName, entityMappingType, collectionDescriptor ); } else { - fkTargetModelPart = getAssociatedEntityMappingType().getIdentifierMapping(); + fkTargetModelPart = getAssociatedEntityMappingType().getIdentifierMappingForJoin(); +// fkTargetModelPart = getAssociatedEntityMappingType().getIdentifierMapping(); } } else if ( StringHelper.isNotEmpty( bootCollectionDescriptor.getMappedByProperty() ) ) { @@ -449,7 +451,8 @@ else if ( StringHelper.isNotEmpty( bootCollectionDescriptor.getMappedByProperty( } else { // non-inverse @ManyToMany - fkTargetModelPart = getAssociatedEntityMappingType().getIdentifierMapping(); + fkTargetModelPart = getAssociatedEntityMappingType().getIdentifierMappingForJoin(); +// fkTargetModelPart = getAssociatedEntityMappingType().getIdentifierMapping(); } if ( getNature() == Nature.ELEMENT ) { diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/MappingModelCreationHelper.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/MappingModelCreationHelper.java index 0d9d96459aab..17cf1a06cc56 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/MappingModelCreationHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/MappingModelCreationHelper.java @@ -120,6 +120,8 @@ import org.hibernate.type.descriptor.java.spi.JavaTypeRegistry; import org.hibernate.type.spi.TypeConfiguration; +import org.checkerframework.checker.nullness.qual.Nullable; + import static org.hibernate.internal.util.NullnessUtil.castNonNull; import static org.hibernate.metamodel.mapping.MappingModelCreationLogging.MAPPING_MODEL_CREATION_MESSAGE_LOGGER; import static org.hibernate.sql.ast.spi.SqlExpressionResolver.createColumnReferenceKey; @@ -769,7 +771,7 @@ private static void interpretPluralAttributeMappingKeyDescriptor( final ManagedMappingType keyDeclaringType; final String collectionTableName = ((AbstractCollectionPersister) collectionDescriptor).getTableName(); - if ( collectionDescriptor.getElementType().isEntityType() ) { + if ( collectionDescriptor.getElementType() instanceof EntityType ) { keyDeclaringType = ( (QueryableCollection) collectionDescriptor ).getElementPersister(); } else { @@ -781,7 +783,8 @@ private static void interpretPluralAttributeMappingKeyDescriptor( } if ( isReferenceToPrimaryKey ) { - fkTargetPart = collectionDescriptor.getOwnerEntityPersister().getIdentifierMapping(); + fkTargetPart = collectionDescriptor.getOwnerEntityPersister().getIdentifierMappingForJoin(); +// fkTargetPart = collectionDescriptor.getOwnerEntityPersister().getIdentifierMapping(); } else { fkTargetPart = declaringType.findContainingEntityMapping().findSubPart( lhsPropertyName ); @@ -890,7 +893,8 @@ public static boolean interpretToOneKeyDescriptor( return interpretNestedToOneKeyDescriptor( referencedEntityDescriptor, referencedPropertyName, - attributeMapping + attributeMapping, + creationProcess ); } @@ -918,6 +922,7 @@ else if ( modelPart instanceof EmbeddableValuedModelPart ) { creationProcess ); attributeMapping.setForeignKeyDescriptor( embeddedForeignKeyDescriptor ); + attributeMapping.setupCircularFetchModelPart( creationProcess ); } else if ( modelPart == null ) { throw new IllegalArgumentException( "Unable to find attribute " + bootProperty.getPersistentClass() @@ -934,7 +939,8 @@ else if ( modelPart == null ) { final ModelPart fkTarget; if ( bootValueMapping.isReferenceToPrimaryKey() ) { - fkTarget = referencedEntityDescriptor.getIdentifierMapping(); + fkTarget = referencedEntityDescriptor.getIdentifierMappingForJoin(); +// fkTarget = referencedEntityDescriptor.getIdentifierMapping(); } else { fkTarget = referencedEntityDescriptor.findByPath( bootValueMapping.getReferencedPropertyName() ); @@ -966,6 +972,7 @@ else if ( modelPart == null ) { ( (PropertyBasedMapping) simpleFkTarget ).getPropertyAccess() ); } + final SelectablePath parentSelectablePath = getSelectablePath( attributeMapping.getDeclaringType() ); final SelectableMapping keySelectableMapping; int i = 0; final Value value = bootProperty.getValue(); @@ -973,6 +980,7 @@ else if ( modelPart == null ) { keySelectableMapping = SelectableMappingImpl.from( tableExpression, columnIterator.next(), + parentSelectablePath, simpleFkTarget.getJdbcMapping(), creationProcess.getCreationContext().getTypeConfiguration(), value.isColumnInsertable( i ), @@ -989,6 +997,7 @@ else if ( modelPart == null ) { keySelectableMapping = SelectableMappingImpl.from( tableExpression, table.getPrimaryKey().getColumn( 0 ), + parentSelectablePath, simpleFkTarget.getJdbcMapping(), creationProcess.getCreationContext().getTypeConfiguration(), value.isColumnInsertable( 0 ), @@ -1010,6 +1019,7 @@ else if ( modelPart == null ) { swapDirection ); attributeMapping.setForeignKeyDescriptor( foreignKeyDescriptor ); + attributeMapping.setupCircularFetchModelPart( creationProcess ); creationProcess.registerForeignKey( attributeMapping, foreignKeyDescriptor ); } else if ( fkTarget instanceof EmbeddableValuedModelPart ) { @@ -1026,6 +1036,7 @@ else if ( fkTarget instanceof EmbeddableValuedModelPart ) { creationProcess ); attributeMapping.setForeignKeyDescriptor( embeddedForeignKeyDescriptor ); + attributeMapping.setupCircularFetchModelPart( creationProcess ); creationProcess.registerForeignKey( attributeMapping, embeddedForeignKeyDescriptor ); } else { @@ -1046,13 +1057,15 @@ else if ( fkTarget instanceof EmbeddableValuedModelPart ) { * @param referencedEntityDescriptor The entity which contains the inverse property * @param referencedPropertyName The inverse property name path * @param attributeMapping The attribute for which we try to set the foreign key + * @param creationProcess The creation process * @return true if the foreign key is actually set */ private static boolean interpretNestedToOneKeyDescriptor( EntityPersister referencedEntityDescriptor, String referencedPropertyName, - ToOneAttributeMapping attributeMapping) { - String[] propertyPath = StringHelper.split( ".", referencedPropertyName ); + ToOneAttributeMapping attributeMapping, + MappingModelCreationProcess creationProcess) { + final String[] propertyPath = StringHelper.split( ".", referencedPropertyName ); EmbeddableValuedModelPart lastEmbeddableModelPart = null; for ( int i = 0; i < propertyPath.length; i++ ) { @@ -1077,6 +1090,7 @@ private static boolean interpretNestedToOneKeyDescriptor( } attributeMapping.setForeignKeyDescriptor( foreignKeyDescriptor ); + attributeMapping.setupCircularFetchModelPart( creationProcess ); return true; } if ( modelPart instanceof EmbeddableValuedModelPart ) { @@ -1125,6 +1139,7 @@ private static EmbeddedForeignKeyDescriptor buildEmbeddableForeignKeyDescriptor( boolean[] updateable, Dialect dialect, MappingModelCreationProcess creationProcess) { + final SelectablePath parentSelectablePath = getSelectablePath( keyDeclaringType ); final boolean hasConstraint; final SelectableMappings keySelectableMappings; if ( bootValueMapping instanceof Collection ) { @@ -1138,6 +1153,7 @@ private static EmbeddedForeignKeyDescriptor buildEmbeddableForeignKeyDescriptor( keyTableExpression, collectionBootValueMapping.getKey(), getPropertyOrder( bootValueMapping, creationProcess ), + parentSelectablePath, creationProcess.getCreationContext().getMetadata(), creationProcess.getCreationContext().getTypeConfiguration(), insertable, @@ -1163,6 +1179,7 @@ private static EmbeddedForeignKeyDescriptor buildEmbeddableForeignKeyDescriptor( keyTableExpression, bootValueMapping, getPropertyOrder( bootValueMapping, creationProcess ), + parentSelectablePath, creationProcess.getCreationContext().getMetadata(), creationProcess.getCreationContext().getTypeConfiguration(), insertable, @@ -1210,6 +1227,15 @@ private static EmbeddedForeignKeyDescriptor buildEmbeddableForeignKeyDescriptor( } } + public static @Nullable SelectablePath getSelectablePath(ManagedMappingType type) { + if ( type instanceof EmbeddableMappingType ) { + final EmbeddableMappingType embeddableType = (EmbeddableMappingType) type; + return embeddableType.getAggregateMapping() != null + ? embeddableType.getAggregateMapping().getSelectablePath() : null; + } + return null; + } + public static int[] getPropertyOrder(Value bootValueMapping, MappingModelCreationProcess creationProcess) { final ComponentType componentType; final boolean sorted; @@ -1683,9 +1709,9 @@ public boolean isSimpleJoinPredicate(Predicate predicate) { public TableGroupJoin createTableGroupJoin( NavigablePath navigablePath, TableGroup lhs, - String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, - SqlAstJoinType sqlAstJoinType, + @Nullable String explicitSourceAlias, + @Nullable SqlAliasBase explicitSqlAliasBase, + @Nullable SqlAstJoinType sqlAstJoinType, boolean fetched, boolean addsPredicate, SqlAstCreationState creationState) { @@ -1696,11 +1722,11 @@ public TableGroupJoin createTableGroupJoin( public TableGroup createRootTableGroupJoin( NavigablePath navigablePath, TableGroup lhs, - String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, - SqlAstJoinType sqlAstJoinType, + @Nullable String explicitSourceAlias, + @Nullable SqlAliasBase explicitSqlAliasBase, + @Nullable SqlAstJoinType sqlAstJoinType, boolean fetched, - Consumer predicateConsumer, + @Nullable Consumer predicateConsumer, SqlAstCreationState creationState) { return null; } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/NonAggregatedIdentifierMappingImpl.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/NonAggregatedIdentifierMappingImpl.java index 54871a77f99e..abfd43f0170f 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/NonAggregatedIdentifierMappingImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/NonAggregatedIdentifierMappingImpl.java @@ -15,6 +15,7 @@ import org.hibernate.engine.spi.EntityKey; import org.hibernate.engine.spi.PersistenceContext; import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.event.spi.MergeContext; import org.hibernate.internal.util.collections.CollectionHelper; import org.hibernate.mapping.Component; import org.hibernate.mapping.RootClass; @@ -237,10 +238,15 @@ public String getAttributeName() { @Override public Object getIdentifier(Object entity) { + return getIdentifier( entity, null ); + } + + @Override + public Object getIdentifier(Object entity, MergeContext mergeContext) { if ( hasContainingClass() ) { final LazyInitializer lazyInitializer = HibernateProxy.extractLazyInitializer( entity ); if ( lazyInitializer != null ) { - return lazyInitializer.getIdentifier(); + return lazyInitializer.getInternalIdentifier(); } final EmbeddableMappingType embeddableTypeDescriptor = getEmbeddableTypeDescriptor(); final Object[] propertyValues = new Object[embeddableTypeDescriptor.getNumberOfAttributeMappings()]; @@ -259,16 +265,16 @@ public Object getIdentifier(Object entity) { //JPA 2 @MapsId + @IdClass points to the pk of the entity else if ( attributeMapping instanceof ToOneAttributeMapping && !( identifierValueMapper.getAttributeMapping( i ) instanceof ToOneAttributeMapping ) ) { + final Object toOne = getIfMerged( o, mergeContext ); final ToOneAttributeMapping toOneAttributeMapping = (ToOneAttributeMapping) attributeMapping; final ModelPart targetPart = toOneAttributeMapping.getForeignKeyDescriptor().getPart( toOneAttributeMapping.getSideNature().inverse() ); if ( targetPart.isEntityIdentifierMapping() ) { - propertyValues[i] = ( (EntityIdentifierMapping) targetPart ).getIdentifier( o ); + propertyValues[i] = ( (EntityIdentifierMapping) targetPart ).getIdentifier( toOne, mergeContext ); } else { - propertyValues[i] = o; - assert false; + propertyValues[i] = toOne; } } else { @@ -285,6 +291,16 @@ else if ( attributeMapping instanceof ToOneAttributeMapping } } + private static Object getIfMerged(Object o, MergeContext mergeContext) { + if ( mergeContext != null ) { + final Object merged = mergeContext.get( o ); + if ( merged != null ) { + return merged; + } + } + return o; + } + @Override public void setIdentifier(Object entity, Object id, SharedSessionContractImplementor session) { final Object[] propertyValues = new Object[identifierValueMapper.getNumberOfAttributeMappings()]; diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/OneToManyCollectionPart.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/OneToManyCollectionPart.java index d6a082027d57..495641d6c657 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/OneToManyCollectionPart.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/OneToManyCollectionPart.java @@ -31,6 +31,8 @@ import org.hibernate.sql.ast.tree.from.TableGroupJoinProducer; import org.hibernate.sql.ast.tree.predicate.Predicate; +import org.checkerframework.checker.nullness.qual.Nullable; + import static java.util.Objects.requireNonNullElse; /** @@ -154,9 +156,9 @@ public boolean isSimpleJoinPredicate(Predicate predicate) { public TableGroupJoin createTableGroupJoin( NavigablePath navigablePath, TableGroup lhs, - String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, - SqlAstJoinType requestedJoinType, + @Nullable String explicitSourceAlias, + @Nullable SqlAliasBase explicitSqlAliasBase, + @Nullable SqlAstJoinType requestedJoinType, boolean fetched, boolean addsPredicate, SqlAstCreationState creationState) { @@ -193,11 +195,11 @@ public TableGroupJoin createTableGroupJoin( public TableGroup createRootTableGroupJoin( NavigablePath navigablePath, TableGroup lhs, - String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, - SqlAstJoinType sqlAstJoinType, + @Nullable String explicitSourceAlias, + @Nullable SqlAliasBase explicitSqlAliasBase, + @Nullable SqlAstJoinType sqlAstJoinType, boolean fetched, - Consumer predicateConsumer, + @Nullable Consumer predicateConsumer, SqlAstCreationState creationState) { return createTableGroupInternal( true, diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/PluralAttributeMappingImpl.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/PluralAttributeMappingImpl.java index 5e60be959f0e..16c8c3360d92 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 @@ -39,6 +39,7 @@ import org.hibernate.metamodel.mapping.SelectableMapping; import org.hibernate.metamodel.mapping.SoftDeleteMapping; import org.hibernate.metamodel.mapping.TableDetails; +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; @@ -76,6 +77,8 @@ import org.jboss.logging.Logger; +import org.checkerframework.checker.nullness.qual.Nullable; + import static org.hibernate.boot.model.internal.SoftDeleteHelper.createNonSoftDeletedRestriction; import static org.hibernate.boot.model.internal.SoftDeleteHelper.resolveSoftDeleteMapping; @@ -250,12 +253,32 @@ private static void injectAttributeMapping( @Override public boolean isBidirectionalAttributeName(NavigablePath fetchablePath, ToOneAttributeMapping modelPart) { - if ( 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 - return fkDescriptor.getTargetPart() == modelPart.getForeignKeyDescriptor().getTargetPart(); + return bidirectionalAttributeName == null + // 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 SelectableMapping selectable1 = part1.getSelectable( i ); + final SelectableMapping selectable2 = part2.getSelectable( i ); + if ( selectable1.getJdbcMapping() != selectable2.getJdbcMapping() + || !selectable1.getContainingTableExpression().equals( selectable2.getContainingTableExpression() ) + || !selectable1.getSelectionExpression().equals( selectable2.getSelectionExpression() ) ) { + return false; + } } - return fetchablePath.getLocalName().endsWith( bidirectionalAttributeName ); + return true; } @SuppressWarnings("unused") @@ -684,9 +707,9 @@ public boolean isSimpleJoinPredicate(Predicate predicate) { public TableGroupJoin createTableGroupJoin( NavigablePath navigablePath, TableGroup lhs, - String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, - SqlAstJoinType requestedJoinType, + @Nullable String explicitSourceAlias, + @Nullable SqlAliasBase explicitSqlAliasBase, + @Nullable SqlAstJoinType requestedJoinType, boolean fetched, boolean addsPredicate, SqlAstCreationState creationState) { @@ -736,6 +759,16 @@ public TableGroupJoin createTableGroupJoin( creationState ); + if ( fetched ) { + if ( orderByFragment != null ) { + creationState.applyOrdering( tableGroup, orderByFragment ); + } + + if ( manyToManyOrderByFragment != null ) { + creationState.applyOrdering( tableGroup, manyToManyOrderByFragment ); + } + } + final TableGroupJoin tableGroupJoin = new TableGroupJoin( navigablePath, determineSqlJoinType( lhs, requestedJoinType, fetched ), @@ -803,7 +836,7 @@ private void applySoftDeleteRestriction( } } - public SqlAstJoinType determineSqlJoinType(TableGroup lhs, SqlAstJoinType requestedJoinType, boolean fetched) { + public SqlAstJoinType determineSqlJoinType(TableGroup lhs, @Nullable SqlAstJoinType requestedJoinType, boolean fetched) { if ( hasSoftDelete() ) { return SqlAstJoinType.LEFT; } @@ -825,11 +858,11 @@ public SqlAstJoinType determineSqlJoinType(TableGroup lhs, SqlAstJoinType reques public TableGroup createRootTableGroupJoin( NavigablePath navigablePath, TableGroup lhs, - String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, - SqlAstJoinType requestedJoinType, + @Nullable String explicitSourceAlias, + @Nullable SqlAliasBase explicitSqlAliasBase, + @Nullable SqlAstJoinType requestedJoinType, boolean fetched, - Consumer predicateConsumer, + @Nullable Consumer predicateConsumer, SqlAstCreationState creationState) { return createRootTableGroupJoin( navigablePath, @@ -862,8 +895,10 @@ private TableGroup createRootTableGroupJoin( if ( collectionDescriptor.isOneToMany() ) { tableGroup = createOneToManyTableGroup( lhs.canUseInnerJoins() && joinType == SqlAstJoinType.INNER, + joinType, navigablePath, fetched, + addsPredicate, explicitSourceAlias, sqlAliasBase, creationState @@ -897,8 +932,10 @@ public void setForeignKeyDescriptor(ForeignKeyDescriptor fkDescriptor) { private TableGroup createOneToManyTableGroup( boolean canUseInnerJoins, + SqlAstJoinType joinType, NavigablePath navigablePath, boolean fetched, + boolean addsPredicate, String sourceAlias, SqlAliasBase explicitSqlAliasBase, SqlAstCreationState creationState) { @@ -921,6 +958,12 @@ private TableGroup createOneToManyTableGroup( elementTableGroup, creationState.getCreationContext().getSessionFactory() ); + // For inner joins we never need join nesting + final boolean nestedJoin = joinType != SqlAstJoinType.INNER + // For outer joins we need nesting if there might be an on-condition that refers to the element table + && ( addsPredicate + || isAffectedByEnabledFilters( creationState.getLoadQueryInfluencers(), creationState.applyOnlyLoadByKeyFilters() ) + || collectionDescriptor.hasWhereRestrictions() ); if ( indexDescriptor instanceof TableGroupJoinProducer ) { final TableGroupJoin tableGroupJoin = ( (TableGroupJoinProducer) indexDescriptor ).createTableGroupJoin( @@ -928,12 +971,12 @@ private TableGroup createOneToManyTableGroup( tableGroup, null, sqlAliasBase, - SqlAstJoinType.INNER, + joinType, fetched, false, creationState ); - tableGroup.registerIndexTableGroup( tableGroupJoin ); + tableGroup.registerIndexTableGroup( tableGroupJoin, nestedJoin ); } return tableGroup; @@ -1023,8 +1066,10 @@ public TableGroup createRootTableGroup( if ( getCollectionDescriptor().isOneToMany() ) { return createOneToManyTableGroup( canUseInnerJoins, + SqlAstJoinType.INNER, navigablePath, false, + false, explicitSourceAlias, explicitSqlAliasBase, creationState diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/SelectableMappingImpl.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/SelectableMappingImpl.java index f39e8ab78e30..fe2de5416e06 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 @@ -8,6 +8,7 @@ import java.util.Locale; +import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.dialect.Dialect; import org.hibernate.mapping.Column; import org.hibernate.mapping.Selectable; @@ -126,7 +127,7 @@ public static SelectableMapping from( public static SelectableMapping from( final String containingTableExpression, final Selectable selectable, - final SelectablePath parentPath, + @Nullable final SelectablePath parentPath, final JdbcMapping jdbcMapping, final TypeConfiguration typeConfiguration, boolean insertable, @@ -154,7 +155,7 @@ public static SelectableMapping from( public static SelectableMapping from( final String containingTableExpression, final Selectable selectable, - final SelectablePath parentPath, + @Nullable final SelectablePath parentPath, final JdbcMapping jdbcMapping, final TypeConfiguration typeConfiguration, boolean insertable, 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 7ff282dd7851..227ca356de9e 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 @@ -9,6 +9,7 @@ import java.util.ArrayList; import java.util.List; +import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.dialect.Dialect; import org.hibernate.engine.spi.Mapping; import org.hibernate.internal.util.collections.CollectionHelper; @@ -19,6 +20,7 @@ import org.hibernate.metamodel.mapping.SelectableConsumer; import org.hibernate.metamodel.mapping.SelectableMapping; import org.hibernate.metamodel.mapping.SelectableMappings; +import org.hibernate.metamodel.mapping.SelectablePath; import org.hibernate.metamodel.spi.RuntimeModelCreationContext; import org.hibernate.query.sqm.function.SqmFunctionRegistry; import org.hibernate.type.CompositeType; @@ -67,48 +69,30 @@ public static SelectableMappings from( Dialect dialect, SqmFunctionRegistry sqmFunctionRegistry, RuntimeModelCreationContext creationContext) { - if ( insertable.length == 0 ) { - return from( - containingTableExpression, - value, - propertyOrder, - mapping, - typeConfiguration, - dialect, - sqmFunctionRegistry, - creationContext - ); - } - final List jdbcMappings = new ArrayList<>(); - resolveJdbcMappings( jdbcMappings, mapping, value.getType() ); - - final List selectables = value.getVirtualSelectables(); - - final SelectableMapping[] selectableMappings = new SelectableMapping[jdbcMappings.size()]; - for ( int i = 0; i < selectables.size(); i++ ) { - selectableMappings[propertyOrder[i]] = SelectableMappingImpl.from( - containingTableExpression, - selectables.get( i ), - jdbcMappings.get( propertyOrder[i] ), - typeConfiguration, - insertable[i], - updateable[i], - false, - dialect, - sqmFunctionRegistry, - creationContext - ); - } - - return new SelectableMappingsImpl( selectableMappings ); + return from( + containingTableExpression, + value, + propertyOrder, + null, + mapping, + typeConfiguration, + insertable, + updateable, + dialect, + sqmFunctionRegistry, + creationContext + ); } - private static SelectableMappings from( + public static SelectableMappings from( String containingTableExpression, Value value, int[] propertyOrder, + @Nullable SelectablePath parentSelectablePath, Mapping mapping, TypeConfiguration typeConfiguration, + boolean[] insertable, + boolean[] updateable, Dialect dialect, SqmFunctionRegistry sqmFunctionRegistry, RuntimeModelCreationContext creationContext) { @@ -122,10 +106,11 @@ private static SelectableMappings from( selectableMappings[propertyOrder[i]] = SelectableMappingImpl.from( containingTableExpression, selectables.get( i ), + parentSelectablePath, jdbcMappings.get( propertyOrder[i] ), typeConfiguration, - false, - false, + i < insertable.length && insertable[i], + i < updateable.length && updateable[i], false, dialect, sqmFunctionRegistry, 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 e54bfbf1d007..69f9a0b361ca 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 @@ -500,7 +500,7 @@ public Object getAssociationKeyFromSide( final LazyInitializer lazyInitializer = HibernateProxy.extractLazyInitializer( targetObject ); if ( lazyInitializer != null ) { if ( refersToPrimaryKey ) { - return lazyInitializer.getIdentifier(); + return lazyInitializer.getInternalIdentifier(); } else { targetObject = lazyInitializer.getImplementation(); diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/SqlTypedMappingImpl.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/SqlTypedMappingImpl.java index 07d3bb3a5d55..0d29429116e8 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/SqlTypedMappingImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/SqlTypedMappingImpl.java @@ -9,24 +9,30 @@ import org.hibernate.metamodel.mapping.JdbcMapping; import org.hibernate.metamodel.mapping.SqlTypedMapping; +import org.checkerframework.checker.nullness.qual.Nullable; + /** * @author Christian Beikov */ public class SqlTypedMappingImpl implements SqlTypedMapping { - private final String columnDefinition; - private final Long length; - private final Integer precision; - private final Integer scale; - private final Integer temporalPrecision; + private final @Nullable String columnDefinition; + private final @Nullable Long length; + private final @Nullable Integer precision; + private final @Nullable Integer scale; + private final @Nullable Integer temporalPrecision; private final JdbcMapping jdbcMapping; + public SqlTypedMappingImpl(JdbcMapping jdbcMapping) { + this( null, null, null, null, null, jdbcMapping ); + } + public SqlTypedMappingImpl( - String columnDefinition, - Long length, - Integer precision, - Integer scale, - Integer temporalPrecision, + @Nullable String columnDefinition, + @Nullable Long length, + @Nullable Integer precision, + @Nullable Integer scale, + @Nullable Integer temporalPrecision, JdbcMapping jdbcMapping) { // Save memory by using interned strings. Probability is high that we have multiple duplicate strings this.columnDefinition = columnDefinition == null ? null : columnDefinition.intern(); @@ -38,27 +44,27 @@ public SqlTypedMappingImpl( } @Override - public String getColumnDefinition() { + public @Nullable String getColumnDefinition() { return columnDefinition; } @Override - public Long getLength() { + public @Nullable Long getLength() { return length; } @Override - public Integer getPrecision() { + public @Nullable Integer getPrecision() { return precision; } @Override - public Integer getTemporalPrecision() { + public @Nullable Integer getTemporalPrecision() { return temporalPrecision; } @Override - public Integer getScale() { + public @Nullable Integer getScale() { return scale; } diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/ToOneAttributeMapping.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/internal/ToOneAttributeMapping.java index 66eda808aed8..613f33ed7853 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 @@ -41,6 +41,7 @@ import org.hibernate.metamodel.mapping.AttributeMapping; import org.hibernate.metamodel.mapping.AttributeMetadata; import org.hibernate.metamodel.mapping.CollectionPart; +import org.hibernate.metamodel.mapping.CompositeIdentifierMapping; import org.hibernate.metamodel.mapping.EmbeddableValuedModelPart; import org.hibernate.metamodel.mapping.EntityAssociationMapping; import org.hibernate.metamodel.mapping.EntityIdentifierMapping; @@ -67,6 +68,7 @@ import org.hibernate.property.access.spi.PropertyAccess; import org.hibernate.proxy.HibernateProxy; import org.hibernate.proxy.LazyInitializer; +import org.hibernate.query.sqm.sql.SqmToSqlAstConverter; import org.hibernate.spi.EntityIdentifierNavigablePath; import org.hibernate.spi.NavigablePath; import org.hibernate.spi.TreatedNavigablePath; @@ -114,6 +116,8 @@ import org.hibernate.type.Type; import org.hibernate.type.descriptor.java.JavaType; +import org.checkerframework.checker.nullness.qual.Nullable; + import static org.hibernate.boot.model.internal.SoftDeleteHelper.createNonSoftDeletedRestriction; /** @@ -175,6 +179,7 @@ public class Entity1 { private ForeignKeyDescriptor.Nature sideNature; private String identifyingColumnsTableExpression; private boolean canUseParentTableGroup; + private @Nullable EmbeddableValuedModelPart circularFetchModelPart; /** * For Hibernate Reactive @@ -458,8 +463,8 @@ the navigable path is NavigablePath(Card.fields.{element}.{id}.card) and it does propertyType = entityBinding.getIdentifierMapper().getType(); } if ( entityBinding.getIdentifierProperty() == null ) { - final CompositeType compositeType; - if ( propertyType.isComponentType() && ( compositeType = (CompositeType) propertyType ).isEmbedded() + final ComponentType compositeType; + if ( propertyType instanceof ComponentType && ( compositeType = (ComponentType) propertyType ).isEmbedded() && compositeType.getPropertyNames().length == 1 ) { this.targetKeyPropertyName = compositeType.getPropertyNames()[0]; addPrefixedPropertyPaths( @@ -518,8 +523,8 @@ the navigable path is NavigablePath(Card.fields.{element}.{id}.card) and it does this.targetKeyPropertyNames = targetKeyPropertyNames; } else { - final CompositeType compositeType; - if ( propertyType.isComponentType() && ( compositeType = (CompositeType) propertyType ).isEmbedded() + final ComponentType compositeType; + if ( propertyType instanceof ComponentType && ( compositeType = (ComponentType) propertyType ).isEmbedded() && compositeType.getPropertyNames().length == 1 ) { final Set targetKeyPropertyNames = new HashSet<>( 2 ); this.targetKeyPropertyName = compositeType.getPropertyNames()[0]; @@ -769,7 +774,7 @@ public static void addPrefixedPropertyNames( if ( prefix != null ) { targetKeyPropertyNames.add( prefix ); } - if ( type.isComponentType() ) { + if ( type instanceof ComponentType ) { final CompositeType componentType = (CompositeType) type; final String[] propertyNames = componentType.getPropertyNames(); final Type[] componentTypeSubtypes = componentType.getSubtypes(); @@ -784,7 +789,7 @@ public static void addPrefixedPropertyNames( addPrefixedPropertyNames( targetKeyPropertyNames, newPrefix, componentTypeSubtypes[i], factory ); } } - else if ( type.isEntityType() ) { + else if ( type instanceof EntityType ) { final EntityType entityType = (EntityType) type; final Type identifierOrUniqueKeyType = entityType.getIdentifierOrUniqueKeyType( factory ); final String propertyName; @@ -858,12 +863,36 @@ public void setForeignKeyDescriptor(ForeignKeyDescriptor foreignKeyDescriptor) { // * the association does not force a join (`@NotFound`, nullable 1-1, ...) // Otherwise we need to join to the associated entity table(s) final boolean forceJoin = hasNotFoundAction() + || entityMappingType.getSoftDeleteMapping() != null || ( cardinality == Cardinality.ONE_TO_ONE && isNullable() ); this.canUseParentTableGroup = ! forceJoin && sideNature == ForeignKeyDescriptor.Nature.KEY && declaringTableGroupProducer.containsTableReference( identifyingColumnsTableExpression ); } + public void setupCircularFetchModelPart(MappingModelCreationProcess creationProcess) { + final EntityIdentifierMapping entityIdentifierMapping = getAssociatedEntityMappingType().getIdentifierMapping(); + if ( sideNature == ForeignKeyDescriptor.Nature.TARGET + && entityIdentifierMapping instanceof CompositeIdentifierMapping + && foreignKeyDescriptor.getKeyPart() != entityIdentifierMapping ) { + // Setup a special embeddable model part for fetching the key object for a circular fetch. + // 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 + final CompositeIdentifierMapping identifierMapping = (CompositeIdentifierMapping) entityIdentifierMapping; + this.circularFetchModelPart = MappingModelCreationHelper.createInverseModelPart( + identifierMapping, + getDeclaringType(), + this, + foreignKeyDescriptor.getTargetPart(), + creationProcess + ); + } + else { + this.circularFetchModelPart = null; + } + } + public String getIdentifyingColumnsTableExpression() { return identifyingColumnsTableExpression; } @@ -914,6 +943,10 @@ public Cardinality getCardinality() { return cardinality; } + public boolean hasJoinTable() { + return hasJoinTable; + } + @Override public EntityMappingType getMappedType() { return getEntityMappingType(); @@ -1043,34 +1076,6 @@ class Mother { We have a circularity but it is not bidirectional */ - final TableGroup parentTableGroup = creationState - .getSqlAstCreationState() - .getFromClauseAccess() - .getTableGroup( fetchParent.getNavigablePath() ); - final DomainResult foreignKeyDomainResult; - assert !creationState.isResolvingCircularFetch(); - try { - creationState.setResolvingCircularFetch( true ); - if ( sideNature == ForeignKeyDescriptor.Nature.KEY ) { - foreignKeyDomainResult = foreignKeyDescriptor.createKeyDomainResult( - fetchablePath, - createTableGroupForDelayedFetch( fetchablePath, parentTableGroup, null, creationState ), - fetchParent, - creationState - ); - } - else { - foreignKeyDomainResult = foreignKeyDescriptor.createTargetDomainResult( - fetchablePath, - parentTableGroup, - fetchParent, - creationState - ); - } - } - finally { - creationState.setResolvingCircularFetch( false ); - } return new CircularFetchImpl( this, fetchTiming, @@ -1078,13 +1083,52 @@ class Mother { fetchParent, isSelectByUniqueKey( sideNature ), parentNavigablePath, - foreignKeyDomainResult, + determineCircularKeyResult( fetchParent, fetchablePath, creationState ), creationState ); } return null; } + private DomainResult determineCircularKeyResult( + FetchParent fetchParent, + NavigablePath fetchablePath, + DomainResultCreationState creationState) { + final FromClauseAccess fromClauseAccess = creationState.getSqlAstCreationState().getFromClauseAccess(); + final TableGroup parentTableGroup = fromClauseAccess.getTableGroup( fetchParent.getNavigablePath() ); + assert !creationState.isResolvingCircularFetch(); + try { + creationState.setResolvingCircularFetch( true ); + if ( circularFetchModelPart != null ) { + return circularFetchModelPart.createDomainResult( + fetchablePath, + createTableGroupForDelayedFetch( fetchablePath, parentTableGroup, null, creationState ), + null, + creationState + ); + } + else if ( sideNature == ForeignKeyDescriptor.Nature.KEY ) { + return foreignKeyDescriptor.createKeyDomainResult( + fetchablePath, + createTableGroupForDelayedFetch( fetchablePath, parentTableGroup, null, creationState ), + fetchParent, + creationState + ); + } + else { + return foreignKeyDescriptor.createTargetDomainResult( + fetchablePath, + parentTableGroup, + fetchParent, + creationState + ); + } + } + finally { + creationState.setResolvingCircularFetch( false ); + } + } + protected boolean isBidirectionalAttributeName( NavigablePath parentNavigablePath, ModelPart parentModelPart, @@ -1330,14 +1374,7 @@ else if ( CollectionPart.Nature.fromNameExact( parentNavigablePath.getLocalName( // We get here is this is a lazy collection initialization for which we know the owner is in the PC // So we create a delayed fetch, as we are sure to find the entity in the PC final FromClauseAccess fromClauseAccess = creationState.getSqlAstCreationState().getFromClauseAccess(); - final NavigablePath realParent; - if ( CollectionPart.Nature.fromNameExact( parentNavigablePath.getLocalName() ) != null ) { - realParent = parentNavigablePath.getParent(); - } - else { - realParent = parentNavigablePath; - } - final TableGroup parentTableGroup = fromClauseAccess.getTableGroup( realParent ); + final TableGroup parentTableGroup = fromClauseAccess.getTableGroup( parentNavigablePath ); final DomainResult domainResult; if ( sideNature == ForeignKeyDescriptor.Nature.KEY ) { domainResult = foreignKeyDescriptor.createKeyDomainResult( @@ -1560,7 +1597,19 @@ public static class EntityB { having the left join we don't want to add an extra implicit join that will be translated into an SQL inner join (see HHH-15342) */ - if ( fetchTiming == FetchTiming.IMMEDIATE && selected ) { + + final ForeignKeyDescriptor.Nature resolvingKeySideOfForeignKey = creationState.getCurrentlyResolvingForeignKeyPart(); + final ForeignKeyDescriptor.Nature side; + if ( resolvingKeySideOfForeignKey == ForeignKeyDescriptor.Nature.KEY && this.sideNature == ForeignKeyDescriptor.Nature.TARGET ) { + // If we are currently resolving the key part of a foreign key we do not want to add joins. + // So if the lhs of this association is the target of the FK, we have to use the KEY part to avoid a join + side = ForeignKeyDescriptor.Nature.KEY; + } + else { + side = this.sideNature; + } + if ( fetchTiming == FetchTiming.IMMEDIATE && selected + || !creationState.getSqlAstCreationState().isProcedureOrNativeQuery() && needsJoinFetch( side ) ) { final TableGroup tableGroup = determineTableGroupForFetch( fetchablePath, fetchParent, @@ -1590,8 +1639,7 @@ public static class EntityB { } } else if ( hasNotFoundAction() - || getAssociatedEntityMappingType().getSoftDeleteMapping() != null - || affectedByEnabledFilters ) { + || getAssociatedEntityMappingType().getSoftDeleteMapping() != null ) { // For the target side only add keyResult when a not-found action is present keyResult = foreignKeyDescriptor.createTargetDomainResult( fetchablePath, @@ -1639,16 +1687,6 @@ else if ( hasNotFoundAction() */ - final ForeignKeyDescriptor.Nature resolvingKeySideOfForeignKey = creationState.getCurrentlyResolvingForeignKeyPart(); - final ForeignKeyDescriptor.Nature side; - if ( resolvingKeySideOfForeignKey == ForeignKeyDescriptor.Nature.KEY && this.sideNature == ForeignKeyDescriptor.Nature.TARGET ) { - // If we are currently resolving the key part of a foreign key we do not want to add joins. - // So if the lhs of this association is the target of the FK, we have to use the KEY part to avoid a join - side = ForeignKeyDescriptor.Nature.KEY; - } - else { - side = this.sideNature; - } final DomainResult keyResult; if ( side == ForeignKeyDescriptor.Nature.KEY ) { final TableGroup tableGroup = sideNature == ForeignKeyDescriptor.Nature.KEY @@ -1700,6 +1738,25 @@ else if ( hasNotFoundAction() ); } + 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). + final EntityIdentifierMapping identifier = entityMappingType.getIdentifierMapping(); + if ( identifier instanceof BasicEntityIdentifierMappingImpl ) { + return false; + } + final ValuedModelPart targetPart = foreignKeyDescriptor.getTargetPart(); + if ( identifier != targetPart ) { + // If the identifier and the target part of the same class, we can preserve laziness as deferred loading will still work + return identifier.getExpressibleJavaType().getJavaTypeClass() != targetPart.getExpressibleJavaType() + .getJavaTypeClass(); + } + } + + return false; + } + private boolean isAffectedByEnabledFilters(DomainResultCreationState creationState) { final LoadQueryInfluencers loadQueryInfluencers = creationState.getSqlAstCreationState() .getLoadQueryInfluencers(); @@ -1981,9 +2038,9 @@ public Fetchable getFetchable(int position) { public TableGroupJoin createTableGroupJoin( NavigablePath navigablePath, TableGroup lhs, - String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, - SqlAstJoinType requestedJoinType, + @Nullable String explicitSourceAlias, + @Nullable SqlAliasBase explicitSqlAliasBase, + @Nullable SqlAstJoinType requestedJoinType, boolean fetched, boolean addsPredicate, SqlAstCreationState creationState) { @@ -2075,7 +2132,11 @@ public TableGroupJoin createTableGroupJoin( final TableGroupJoin join = new TableGroupJoin( navigablePath, - joinType, + // Avoid checking for nested joins in here again, since this is already done in createRootTableGroupJoin + // and simply rely on the canUseInnerJoins flag instead for override the join type to LEFT + requestedJoinType == null && !lazyTableGroup.canUseInnerJoins() + ? SqlAstJoinType.LEFT + : joinType, lazyTableGroup, null ); @@ -2147,7 +2208,7 @@ public TableGroupJoin createTableGroupJoin( } @Override - public SqlAstJoinType determineSqlJoinType(TableGroup lhs, SqlAstJoinType requestedJoinType, boolean fetched) { + public SqlAstJoinType determineSqlJoinType(TableGroup lhs, @Nullable SqlAstJoinType requestedJoinType, boolean fetched) { if ( requestedJoinType != null ) { return requestedJoinType; } @@ -2163,11 +2224,11 @@ public SqlAstJoinType determineSqlJoinType(TableGroup lhs, SqlAstJoinType reques public LazyTableGroup createRootTableGroupJoin( NavigablePath navigablePath, TableGroup lhs, - String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, - SqlAstJoinType requestedJoinType, + @Nullable String explicitSourceAlias, + @Nullable SqlAliasBase explicitSqlAliasBase, + @Nullable SqlAstJoinType requestedJoinType, boolean fetched, - Consumer predicateConsumer, + @Nullable Consumer predicateConsumer, SqlAstCreationState creationState) { final SqlAliasBase sqlAliasBase = SqlAliasBase.from( explicitSqlAliasBase, @@ -2177,18 +2238,16 @@ public LazyTableGroup createRootTableGroupJoin( ); final SoftDeleteMapping softDeleteMapping = getAssociatedEntityMappingType().getSoftDeleteMapping(); - final boolean canUseInnerJoin; - if ( ! lhs.canUseInnerJoins() ) { - canUseInnerJoin = false; - } - else if ( isNullable - || hasNotFoundAction() - || softDeleteMapping != null ) { + final SqlAstJoinType currentlyProcessingJoinType = creationState instanceof SqmToSqlAstConverter + ? ( (SqmToSqlAstConverter) creationState ).getCurrentlyProcessingJoinType() + : null; + if ( currentlyProcessingJoinType != null && currentlyProcessingJoinType != SqlAstJoinType.INNER ) { + // Don't change the join type though, as that has implications for eager initialization of a LazyTableGroup canUseInnerJoin = false; } else { - canUseInnerJoin = requestedJoinType == SqlAstJoinType.INNER; + canUseInnerJoin = determineSqlJoinType( lhs, requestedJoinType, fetched ) == SqlAstJoinType.INNER; } TableGroup realParentTableGroup = lhs; diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/ordering/OrderByFragmentTranslator.java b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/ordering/OrderByFragmentTranslator.java index 1f33aadcfbbb..a90a177b03d6 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/ordering/OrderByFragmentTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/mapping/ordering/OrderByFragmentTranslator.java @@ -75,8 +75,13 @@ private static OrderingParser.OrderByFragmentContext buildParseTree(TranslationC return parser.orderByFragment(); } 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 + // CommonTokenStream and cause a wrong parse + // lexer.reset(); + // reset the input token stream and parser state - lexer.reset(); parser.reset(); // fall back to LL(k)-based parsing diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/AbstractIdentifiableType.java b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/AbstractIdentifiableType.java index 7686ca7e3eb3..8115f5b29889 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/AbstractIdentifiableType.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/AbstractIdentifiableType.java @@ -420,11 +420,10 @@ else if ( id != null ) { } else { assert type instanceof EmbeddableDomainType; - final EmbeddableDomainType compositeType = (EmbeddableDomainType) type; return new EmbeddedSqmPathSource<>( EntityIdentifierMapping.ID_ROLE_NAME, (SqmPathSource) id, - compositeType, + (EmbeddableDomainType) type, Bindable.BindableType.SINGULAR_ATTRIBUTE, id.isGeneric() ); diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AbstractPluralAttribute.java b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AbstractPluralAttribute.java index 116230ccf1f1..f7c9f2aa8cf9 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AbstractPluralAttribute.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/AbstractPluralAttribute.java @@ -153,9 +153,12 @@ public Class getBindableJavaType() { return getElementType().getJavaType(); } + @SuppressWarnings("unchecked") @Override public SqmPath createSqmPath(SqmPath lhs, SqmPathSource intermediatePathSource) { - return new SqmPluralValuedSimplePath<>( + // We need an unchecked cast here : PluralPersistentAttribute implements path source with its element type + // but resolving paths from it must produce collection-typed expressions. + return (SqmPath) new SqmPluralValuedSimplePath<>( PathHelper.append( lhs, this, intermediatePathSource ), this, lhs, diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/JpaMetamodelImpl.java b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/JpaMetamodelImpl.java index 4367821420c9..a53d5921c62b 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/JpaMetamodelImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/JpaMetamodelImpl.java @@ -289,7 +289,21 @@ public Set> getEmbeddables() { @Override public EnumJavaType getEnumType(String className) { - return enumJavaTypes.get( className ); + final EnumJavaType enumJavaType = enumJavaTypes.get( className ); + if ( enumJavaType != null ) { + return enumJavaType; + } + final ClassLoaderService classLoaderService = serviceRegistry.getService( ClassLoaderService.class ); + try { + final Class clazz = classLoaderService.classForName( className ); + if ( clazz == null || !clazz.isEnum() ) { + return null; + } + return new EnumJavaType( clazz ); + } + catch (ClassLoadingException e) { + throw new RuntimeException( e ); + } } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/MappingMetamodelImpl.java b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/MappingMetamodelImpl.java index 8a73cad47ec2..05f3d784bab4 100644 --- a/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/MappingMetamodelImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/metamodel/model/domain/internal/MappingMetamodelImpl.java @@ -47,6 +47,7 @@ import org.hibernate.metamodel.mapping.MappingModelExpressible; import org.hibernate.metamodel.mapping.internal.MappingModelCreationProcess; import org.hibernate.metamodel.model.domain.BasicDomainType; +import org.hibernate.metamodel.model.domain.DomainType; import org.hibernate.metamodel.model.domain.EmbeddableDomainType; import org.hibernate.metamodel.model.domain.EntityDomainType; import org.hibernate.metamodel.model.domain.ManagedDomainType; @@ -316,7 +317,7 @@ private void processBootCollections( ); collectionPersisterMap.put( model.getRole(), persister ); Type indexType = persister.getIndexType(); - if ( indexType != null && indexType.isEntityType() && !indexType.isAnyType() ) { + if ( indexType instanceof org.hibernate.type.EntityType ) { String entityName = ( (org.hibernate.type.EntityType) indexType ).getAssociatedEntityName(); Set roles = collectionRolesByEntityParticipant.get( entityName ); //noinspection Java8MapApi @@ -327,7 +328,7 @@ private void processBootCollections( roles.add( persister.getRole() ); } Type elementType = persister.getElementType(); - if ( elementType.isEntityType() && !elementType.isAnyType() ) { + if ( elementType instanceof org.hibernate.type.EntityType ) { String entityName = ( (org.hibernate.type.EntityType) elementType ).getAssociatedEntityName(); Set roles = collectionRolesByEntityParticipant.get( entityName ); //noinspection Java8MapApi @@ -791,8 +792,12 @@ private String[] doGetImplementors(Class clazz) throws MappingException { public MappingModelExpressible resolveMappingExpressible( SqmExpressible sqmExpressible, Function tableGroupLocator) { - if ( sqmExpressible instanceof SqmPath ) { + if ( sqmExpressible instanceof SqmPath ) { final SqmPath sqmPath = (SqmPath) sqmExpressible; + final DomainType sqmPathType = sqmPath.getResolvedModel().getSqmPathType(); + if ( sqmPathType instanceof MappingModelExpressible ) { + return (MappingModelExpressible) sqmPathType; + } final NavigablePath navigablePath = sqmPath.getNavigablePath(); if ( navigablePath.getParent() != null ) { final TableGroup parentTableGroup = tableGroupLocator.apply( navigablePath.getParent() ); diff --git a/hibernate-core/src/main/java/org/hibernate/persister/collection/AbstractCollectionPersister.java b/hibernate-core/src/main/java/org/hibernate/persister/collection/AbstractCollectionPersister.java index 55a16ed66a1e..fc4628d84126 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/collection/AbstractCollectionPersister.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/collection/AbstractCollectionPersister.java @@ -140,6 +140,7 @@ import org.hibernate.type.AnyType; import org.hibernate.type.BasicType; import org.hibernate.type.CollectionType; +import org.hibernate.type.ComponentType; import org.hibernate.type.CompositeType; import org.hibernate.type.EntityType; import org.hibernate.type.MetaType; @@ -386,7 +387,7 @@ public AbstractCollectionPersister( // ELEMENT - if ( elementType.isEntityType() ) { + if ( elementType instanceof EntityType ) { String entityName = ( (EntityType) elementType ).getAssociatedEntityName(); elementPersister = creationContext.getDomainModel().getEntityDescriptor( entityName ); // NativeSQL: collect element column and auto-aliases @@ -437,7 +438,7 @@ public AbstractCollectionPersister( creationContext.getFunctionRegistry() ); elementColumnIsGettable[j] = true; - if ( elementType.isComponentType() ) { + if ( elementType instanceof ComponentType || elementType instanceof AnyType ) { // Implements desired behavior specifically for @ElementCollection mappings. elementColumnIsSettable[j] = columnInsertability[j]; } @@ -555,7 +556,7 @@ else if ( indexedCollection instanceof org.hibernate.mapping.Map elementClass = null; // elementType.returnedClass(); } - if ( elementType.isComponentType() ) { + if ( elementType instanceof ComponentType || elementType instanceof AnyType ) { elementPropertyMapping = new CompositeElementPropertyMapping( elementColumnNames, elementColumnReaders, @@ -565,7 +566,7 @@ else if ( indexedCollection instanceof org.hibernate.mapping.Map creationContext.getMetadata() ); } - else if ( !elementType.isEntityType() ) { + else if ( !( elementType instanceof EntityType ) ) { elementPropertyMapping = new ElementPropertyMapping( elementColumnNames, elementType ); } else { @@ -1534,7 +1535,7 @@ private void initCollectionPropertyMap(String aliasName, Type type, String[] col collectionPropertyColumnAliases.put( aliasName, columnAliases ); //TODO: this code is almost certainly obsolete and can be removed - if ( type.isComponentType() ) { + if ( type instanceof ComponentType || type instanceof AnyType ) { CompositeType ct = (CompositeType) type; String[] propertyNames = ct.getPropertyNames(); for ( int i = 0; i < propertyNames.length; i++ ) { diff --git a/hibernate-core/src/main/java/org/hibernate/persister/collection/BasicCollectionPersister.java b/hibernate-core/src/main/java/org/hibernate/persister/collection/BasicCollectionPersister.java index e948281080bb..873d8bcbb7df 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/collection/BasicCollectionPersister.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/collection/BasicCollectionPersister.java @@ -60,6 +60,7 @@ import org.hibernate.sql.model.ast.builder.TableUpdateBuilderStandard; import org.hibernate.sql.model.internal.TableUpdateStandard; import org.hibernate.sql.model.jdbc.JdbcMutationOperation; +import org.hibernate.type.EntityType; import static org.hibernate.internal.util.collections.CollectionHelper.arrayList; import static org.hibernate.sql.model.ModelMutationLogging.MODEL_MUTATION_LOGGER; @@ -770,7 +771,7 @@ public boolean isOneToMany() { @Override public boolean isManyToMany() { - return elementType.isEntityType(); //instanceof AssociationType; + return elementType instanceof EntityType; //instanceof AssociationType; } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java index f06efdcaf3d8..6a1efd8764bb 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractEntityPersister.java @@ -100,6 +100,7 @@ import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.event.spi.EventSource; import org.hibernate.event.spi.LoadEvent; +import org.hibernate.event.spi.MergeContext; import org.hibernate.generator.BeforeExecutionGenerator; import org.hibernate.generator.EventType; import org.hibernate.generator.Generator; @@ -293,8 +294,10 @@ import org.hibernate.type.AssociationType; import org.hibernate.type.BasicType; import org.hibernate.type.CollectionType; +import org.hibernate.type.ComponentType; import org.hibernate.type.CompositeType; import org.hibernate.type.EntityType; +import org.hibernate.type.ManyToOneType; import org.hibernate.type.Type; import org.hibernate.type.descriptor.java.JavaType; import org.hibernate.type.descriptor.java.MutabilityPlan; @@ -330,6 +333,7 @@ import static org.hibernate.internal.util.collections.CollectionHelper.toSmallList; import static org.hibernate.loader.ast.internal.MultiKeyLoadHelper.supportsSqlArrayType; import static org.hibernate.metamodel.RepresentationMode.POJO; +import static org.hibernate.metamodel.mapping.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; @@ -396,6 +400,7 @@ public abstract class AbstractEntityPersister private final int[] lazyPropertyNumbers; private final Type[] lazyPropertyTypes; private final String[][] lazyPropertyColumnAliases; + private final Set nonLazyPropertyNames; //information about all properties in class hierarchy private final String[] subclassPropertyNameClosure; @@ -494,6 +499,7 @@ public abstract class AbstractEntityPersister private final boolean implementsLifecycle; private List uniqueKeyEntries = null; //lazily initialized + private ConcurrentHashMap nonLazyPropertyLoadPlansByName; @Deprecated(since = "6.0") public AbstractEntityPersister( @@ -644,6 +650,7 @@ public AbstractEntityPersister( propertyColumnUpdateable = new boolean[hydrateSpan][]; propertyColumnInsertable = new boolean[hydrateSpan][]; sharedColumnNames = new HashSet<>(); + nonLazyPropertyNames = new HashSet<>(); final HashSet thisClassProperties = new HashSet<>(); final ArrayList lazyNames = new ArrayList<>(); @@ -709,6 +716,9 @@ public AbstractEntityPersister( lazyTypes.add( prop.getValue().getType() ); lazyColAliases.add( colAliases ); } + else { + nonLazyPropertyNames.add( prop.getName() ); + } propertyColumnUpdateable[i] = prop.getValue().getColumnUpdateability(); propertyColumnInsertable[i] = prop.getValue().getColumnInsertability(); @@ -738,6 +748,15 @@ public AbstractEntityPersister( final ArrayList definedBySubclass = new ArrayList<>(); final ArrayList propNullables = new ArrayList<>(); + if ( persistentClass.hasSubclasses() ) { + for ( Selectable selectable : persistentClass.getIdentifier().getSelectables() ) { + if ( !selectable.isFormula() ) { + // Identifier columns are always shared between subclasses + sharedColumnNames.add( ( (Column) selectable ).getQuotedName( dialect ) ); + } + } + } + for ( Property prop : persistentClass.getSubclassPropertyClosure() ) { names.add( prop.getName() ); types.add( prop.getType() ); @@ -963,7 +982,7 @@ else if ( entityMetamodel.isMutable() ) { // 2) have no associations. // Eventually we want to be a little more lenient with associations. for ( Type type : getSubclassPropertyTypeClosure() ) { - if ( type.isAssociationType() ) { + if ( type instanceof AnyType || type instanceof CollectionType || type instanceof EntityType ) { return false; } } @@ -1294,6 +1313,17 @@ private static List initUniqueKeyEntries(final AbstractEntityPer uniqueKeys.add( new UniqueKeyEntry( ukName, index, type ) ); } } + else if ( associationType instanceof ManyToOneType ) { + final ManyToOneType manyToOneType = ( (ManyToOneType) associationType ); + if ( manyToOneType.isLogicalOneToOne() && manyToOneType.isReferenceToPrimaryKey() ) { + final AttributeMapping attributeMapping = aep.findAttributeMapping( manyToOneType.getPropertyName() ); + if ( attributeMapping != null ) { + final int index = attributeMapping.getStateArrayPosition(); + final Type type = aep.getPropertyTypes()[index]; + uniqueKeys.add( new UniqueKeyEntry( manyToOneType.getPropertyName(), index, type ) ); + } + } + } } } return CollectionHelper.toSmallList( uniqueKeys ); @@ -1329,6 +1359,10 @@ private SingleIdArrayLoadPlan createLazyLoadPlan(List f partsToSelect.add( getAttributeMapping( getSubclassPropertyIndex( lazyAttributeDescriptor.getName() ) ) ); } + return createLazyLoanPlan( partsToSelect ); + } + + private SingleIdArrayLoadPlan createLazyLoanPlan(List partsToSelect) { if ( partsToSelect.isEmpty() ) { // only one-to-one is lazily fetched return null; @@ -1540,7 +1574,7 @@ public Object initializeLazyProperty(String fieldName, Object entity, SharedSess if ( hasCollections() ) { final Type type = getPropertyType( fieldName ); - if ( type.isCollectionType() ) { + if ( type instanceof CollectionType ) { // we have a condition where a collection attribute is being access via enhancement: // we can circumvent all the rest and just return the PersistentCollection final CollectionType collectionType = (CollectionType) type; @@ -1654,75 +1688,117 @@ protected Object initializeLazyPropertiesFromDatastore( final EntityEntry entry, final String fieldName, final SharedSessionContractImplementor session) { - - if ( !hasLazyProperties() ) { - throw new AssertionFailure( "no lazy properties" ); + if ( nonLazyPropertyNames.contains( fieldName ) ) { + // An eager property can be lazy because of an applied EntityGraph + final List partsToSelect = new ArrayList<>(1); + int propertyIndex = getPropertyIndex( fieldName ); + partsToSelect.add( getAttributeMapping( propertyIndex ) ); + SingleIdArrayLoadPlan lazyLoanPlan; + ConcurrentHashMap propertyLoadPlansByName = this.nonLazyPropertyLoadPlansByName; + if ( propertyLoadPlansByName == null ) { + propertyLoadPlansByName = new ConcurrentHashMap<>(); + lazyLoanPlan = createLazyLoanPlan( partsToSelect ); + propertyLoadPlansByName.put( fieldName, lazyLoanPlan ); + this.nonLazyPropertyLoadPlansByName = propertyLoadPlansByName; + } + else { + lazyLoanPlan = nonLazyPropertyLoadPlansByName.get( fieldName ); + if ( lazyLoanPlan == null ) { + lazyLoanPlan = createLazyLoanPlan( partsToSelect ); + nonLazyPropertyLoadPlansByName.put( fieldName, lazyLoanPlan ); + } + } + try { + final Object[] values = lazyLoanPlan.load( id, session ); + final Object selectedValue = values[0]; + initializeLazyProperty( + entity, + entry, + selectedValue, + propertyIndex, + getPropertyTypes()[propertyIndex] + ); + return selectedValue; + } + catch (JDBCException ex) { + throw session.getJdbcServices().getSqlExceptionHelper().convert( + ex.getSQLException(), + "could not initialize lazy properties: " + infoString( this, id, getFactory() ), + lazyLoanPlan.getJdbcSelect().getSqlString() + ); + } } + else { + if ( !hasLazyProperties() ) { + throw new AssertionFailure( "no lazy properties" ); + } - final PersistentAttributeInterceptor interceptor = asPersistentAttributeInterceptable( entity ).$$_hibernate_getInterceptor(); - assert interceptor != null : "Expecting bytecode interceptor to be non-null"; + final PersistentAttributeInterceptor interceptor = asPersistentAttributeInterceptable( entity ).$$_hibernate_getInterceptor(); + assert interceptor != null : "Expecting bytecode interceptor to be non-null"; - LOG.tracef( "Initializing lazy properties from datastore (triggered for `%s`)", fieldName ); + LOG.tracef( "Initializing lazy properties from datastore (triggered for `%s`)", fieldName ); - final String fetchGroup = getEntityMetamodel().getBytecodeEnhancementMetadata() - .getLazyAttributesMetadata() - .getFetchGroupName( fieldName ); - final List fetchGroupAttributeDescriptors = getEntityMetamodel().getBytecodeEnhancementMetadata() - .getLazyAttributesMetadata() - .getFetchGroupAttributeDescriptors( fetchGroup ); + final String fetchGroup = getEntityMetamodel().getBytecodeEnhancementMetadata() + .getLazyAttributesMetadata() + .getFetchGroupName( fieldName ); + final List fetchGroupAttributeDescriptors = getEntityMetamodel().getBytecodeEnhancementMetadata() + .getLazyAttributesMetadata() + .getFetchGroupAttributeDescriptors( fetchGroup ); - final Set initializedLazyAttributeNames = interceptor.getInitializedLazyAttributeNames(); + final Set initializedLazyAttributeNames = interceptor.getInitializedLazyAttributeNames(); - final SingleIdArrayLoadPlan lazySelect = getSQLLazySelectLoadPlan( fetchGroup ); + final SingleIdArrayLoadPlan lazySelect = getSQLLazySelectLoadPlan( fetchGroup ); - try { - Object result = null; - final Object[] values = lazySelect.load( id, session ); - int i = 0; - for ( LazyAttributeDescriptor fetchGroupAttributeDescriptor : fetchGroupAttributeDescriptors ) { - final boolean previousInitialized = initializedLazyAttributeNames.contains( fetchGroupAttributeDescriptor.getName() ); - - if ( previousInitialized ) { - // todo : one thing we should consider here is potentially un-marking an attribute as dirty based on the selected value - // we know the current value - getPropertyValue( entity, fetchGroupAttributeDescriptor.getAttributeIndex() ); - // we know the selected value (see selectedValue below) - // we can use the attribute Type to tell us if they are the same - // - // assuming entity is a SelfDirtinessTracker we can also know if the attribute is - // currently considered dirty, and if really not dirty we would do the un-marking - // - // of course that would mean a new method on SelfDirtinessTracker to allow un-marking - - // its already been initialized (e.g. by a write) so we don't want to overwrite - i++; - continue; - } + try { + Object result = null; + final Object[] values = lazySelect.load( id, session ); + int i = 0; + for ( LazyAttributeDescriptor fetchGroupAttributeDescriptor : fetchGroupAttributeDescriptors ) { + final boolean previousInitialized = initializedLazyAttributeNames.contains( + fetchGroupAttributeDescriptor.getName() ); + + if ( previousInitialized ) { + // todo : one thing we should consider here is potentially un-marking an attribute as dirty based on the selected value + // we know the current value - getPropertyValue( entity, fetchGroupAttributeDescriptor.getAttributeIndex() ); + // we know the selected value (see selectedValue below) + // we can use the attribute Type to tell us if they are the same + // + // assuming entity is a SelfDirtinessTracker we can also know if the attribute is + // currently considered dirty, and if really not dirty we would do the un-marking + // + // of course that would mean a new method on SelfDirtinessTracker to allow un-marking + + // its already been initialized (e.g. by a write) so we don't want to overwrite + i++; + continue; + } - final Object selectedValue = values[i++]; - final boolean set = initializeLazyProperty( - fieldName, - entity, - entry, - fetchGroupAttributeDescriptor.getLazyIndex(), - selectedValue - ); - if ( set ) { - result = selectedValue; - interceptor.attributeInitialized( fetchGroupAttributeDescriptor.getName() ); + final Object selectedValue = values[i++]; + final boolean set = initializeLazyProperty( + fieldName, + entity, + entry, + fetchGroupAttributeDescriptor, + selectedValue + ); + if ( set ) { + result = selectedValue; + interceptor.attributeInitialized( fetchGroupAttributeDescriptor.getName() ); + } } - } + LOG.trace( "Done initializing lazy properties" ); - LOG.trace( "Done initializing lazy properties" ); + return result; - return result; - } - catch ( JDBCException ex ) { - throw session.getJdbcServices().getSqlExceptionHelper().convert( - ex.getSQLException(), - "could not initialize lazy properties: " + infoString( this, id, getFactory() ), - lazySelect.getJdbcSelect().getSqlString() - ); + } + catch (JDBCException ex) { + throw session.getJdbcServices().getSqlExceptionHelper().convert( + ex.getSQLException(), + "could not initialize lazy properties: " + infoString( this, id, getFactory() ), + lazySelect.getJdbcSelect().getSqlString() + ); + } } } @@ -1781,6 +1857,43 @@ protected boolean initializeLazyProperty( return fieldName.equals( lazyPropertyNames[index] ); } + + + protected boolean initializeLazyProperty( + final String fieldName, + final Object entity, + final EntityEntry entry, + LazyAttributeDescriptor fetchGroupAttributeDescriptor, + final Object propValue) { + final String name = fetchGroupAttributeDescriptor.getName(); + initializeLazyProperty( + entity, + entry, + propValue, + getPropertyIndex( name ), + fetchGroupAttributeDescriptor.getType() + ); + return fieldName.equals( name ); + } + + private void initializeLazyProperty(Object entity, EntityEntry entry, Object propValue, int index, Type type) { + setPropertyValue( entity, index, propValue ); + if ( entry.getLoadedState() != null ) { + // object have been loaded with setReadOnly(true); HHH-2236 + entry.getLoadedState()[index] = type.deepCopy( + propValue, + factory + ); + } + // If the entity has deleted state, then update that as well + if ( entry.getDeletedState() != null ) { + entry.getDeletedState()[index] = type.deepCopy( + propValue, + factory + ); + } + } + @Override public NavigableRole getNavigableRole() { return navigableRole; @@ -1887,61 +2000,62 @@ public String selectFragment(String alias, String suffix) { // Wrap expressions with aliases final SelectClause selectClause = rootQuerySpec.getSelectClause(); final List sqlSelections = selectClause.getSqlSelections(); + final Set processedExpressions = new HashSet<>( sqlSelections.size() ); int i = 0; - int columnIndex = 0; - final String[] columnAliases = getSubclassColumnAliasClosure(); - final int columnAliasesSize = columnAliases.length; - for ( String identifierAlias : identifierAliases ) { - sqlSelections.set( - i, - new SqlSelectionImpl( - i, - new AliasedExpression( sqlSelections.get( i ).getExpression(), identifierAlias + suffix ) - ) - ); - if ( i < columnAliasesSize && columnAliases[i].equals( identifierAlias ) ) { - columnIndex++; + final int identifierSelectionSize = identifierMapping.getJdbcTypeCount(); + for ( int j = 0; j < identifierSelectionSize; j++ ) { + final SelectableMapping selectableMapping = identifierMapping.getSelectable( j ); + if ( processedExpressions.add( selectableMapping.getSelectionExpression() ) ) { + aliasSelection( sqlSelections, i, identifierAliases[j] + suffix ); + i++; } - i++; } - if ( entityMetamodel.hasSubclasses() ) { - sqlSelections.set( - i, - new SqlSelectionImpl( - i, - new AliasedExpression( sqlSelections.get( i ).getExpression(), getDiscriminatorAlias() + suffix ) - ) - ); - i++; + if ( hasSubclasses() ) { + assert discriminatorMapping.getJdbcTypeCount() == 1; + final SelectableMapping selectableMapping = discriminatorMapping.getSelectable( 0 ); + if ( processedExpressions.add( selectableMapping.getSelectionExpression() ) ) { + aliasSelection( sqlSelections, i, getDiscriminatorAlias() + suffix ); + i++; + } } if ( hasRowId() ) { - sqlSelections.set( - i, - new SqlSelectionImpl( - i, - new AliasedExpression( sqlSelections.get( i ).getExpression(), ROWID_ALIAS + suffix ) - ) - ); - i++; + final SelectableMapping selectableMapping = rowIdMapping; + if ( processedExpressions.add( selectableMapping.getSelectionExpression() ) ) { + aliasSelection( sqlSelections, i, ROWID_ALIAS + suffix ); + i++; + } } + final String[] columnAliases = getSubclassColumnAliasClosure(); final String[] formulaAliases = getSubclassFormulaAliasClosure(); + int columnIndex = 0; int formulaIndex = 0; - for ( ; i < sqlSelections.size(); i++ ) { - final SqlSelection sqlSelection = sqlSelections.get( i ); - final ColumnReference columnReference = (ColumnReference) sqlSelection.getExpression(); - final String selectAlias = !columnReference.isColumnExpressionFormula() - ? columnAliases[columnIndex++] + suffix - : formulaAliases[formulaIndex++] + suffix; - sqlSelections.set( - i, - new SqlSelectionImpl( - sqlSelection.getValuesArrayPosition(), - new AliasedExpression( sqlSelection.getExpression(), selectAlias ) - ) - ); + final int size = getNumberOfFetchables(); + // getSubclassColumnAliasClosure contains the _identifierMapper columns when it has an id class, + // which need to be skipped + if ( identifierMapping instanceof NonAggregatedIdentifierMapping + && ( (NonAggregatedIdentifierMapping) identifierMapping ).getIdClassEmbeddable() != null ) { + columnIndex = identifierSelectionSize; + } + for ( int j = 0; j < size; j++ ) { + final AttributeMapping fetchable = getFetchable( j ); + if ( !(fetchable instanceof PluralAttributeMapping) + && !skipFetchable( fetchable, fetchable.getMappedFetchOptions().getTiming() ) + && fetchable.isSelectable() ) { + final int jdbcTypeCount = fetchable.getJdbcTypeCount(); + for ( int k = 0; k < jdbcTypeCount; k++ ) { + final SelectableMapping selectableMapping = fetchable.getSelectable( k ); + if ( processedExpressions.add( selectableMapping.getSelectionExpression() ) ) { + final String baseAlias = selectableMapping.isFormula() + ? formulaAliases[formulaIndex++] + : columnAliases[columnIndex++]; + aliasSelection( sqlSelections, i, baseAlias + suffix ); + i++; + } + } + } } final String sql = getFactory().getJdbcServices() @@ -1961,6 +2075,17 @@ public String selectFragment(String alias, String suffix) { return expression; } + private static void aliasSelection( + List sqlSelections, + int selectionIndex, + String alias) { + final Expression expression = sqlSelections.get( selectionIndex ).getExpression(); + sqlSelections.set( + selectionIndex, + new SqlSelectionImpl( selectionIndex, new AliasedExpression( expression, alias ) ) + ); + } + private ImmutableFetchList fetchProcessor(FetchParent fetchParent, LoaderSqlAstCreationState creationState) { final FetchableContainer fetchableContainer = fetchParent.getReferencedMappingContainer(); final int size = fetchableContainer.getNumberOfFetchables(); @@ -1971,45 +2096,45 @@ private ImmutableFetchList fetchProcessor(FetchParent fetchParent, LoaderSqlAstC // Ignore plural attributes if ( !( fetchable instanceof PluralAttributeMapping ) ) { final FetchTiming fetchTiming = fetchable.getMappedFetchOptions().getTiming(); - if ( fetchable.asBasicValuedModelPart() != null ) { - // Ignore lazy basic columns - if ( fetchTiming == FetchTiming.DELAYED ) { - continue; + if ( !skipFetchable( fetchable, fetchTiming ) ) { + if ( fetchTiming == null ) { + throw new AssertionFailure( "fetchTiming was null" ); } - } - else if ( fetchable instanceof Association ) { - final Association association = (Association) fetchable; - // Ignore the fetchable if the FK is on the other side - if ( association.getSideNature() == ForeignKeyDescriptor.Nature.TARGET ) { - continue; - } - // Ensure the FK comes from the root table - if ( !getRootTableName().equals( association.getForeignKeyDescriptor().getKeyTable() ) ) { - continue; + if ( fetchable.isSelectable() ) { + final Fetch fetch = fetchParent.generateFetchableFetch( + fetchable, + fetchParent.resolveNavigablePath( fetchable ), + fetchTiming, + false, + null, + creationState + ); + fetches.add( fetch ); } } - - if ( fetchTiming == null ) { - throw new AssertionFailure("fetchTiming was null"); - } - - if ( fetchable.isSelectable() ) { - final Fetch fetch = fetchParent.generateFetchableFetch( - fetchable, - fetchParent.resolveNavigablePath( fetchable ), - fetchTiming, - false, - null, - creationState - ); - fetches.add( fetch ); - } } } return fetches.build(); } + private boolean skipFetchable(Fetchable fetchable, FetchTiming fetchTiming) { + if ( fetchable.asBasicValuedModelPart() != null ) { + // Ignore lazy basic columns + return fetchTiming == FetchTiming.DELAYED; + } + else if ( fetchable instanceof Association ) { + final Association association = (Association) fetchable; + // Ignore the fetchable if the FK is on the other side + return association.getSideNature() == ForeignKeyDescriptor.Nature.TARGET + // Ensure the FK comes from the root table + || !getRootTableName().equals( association.getForeignKeyDescriptor().getKeyTable() ); + } + else { + return false; + } + } + /** * @deprecated use {@link Fetchable#isSelectable()} instead. */ @@ -2292,7 +2417,7 @@ public int getSubclassPropertyTableNumber(String propertyPath) { // // performance op to avoid the array search // return 0; // } -// else if ( type.isCollectionType() ) { +// else if ( type instanceof CollectionType ) { // // properly handle property-ref-based associations // rootPropertyName = assocType.getLHSPropertyName(); // } @@ -3226,7 +3351,7 @@ else if ( discriminatorValue == NOT_NULL_DISCRIMINATOR ) { return new NullnessPredicate( sqlExpression, true ); } else if ( hasNull ) { - junction.add( new NullnessPredicate( sqlExpression ) ); + junction.add( new NullnessPredicate( sqlExpression ) ); } junction.add( predicate ); @@ -3920,7 +4045,7 @@ public boolean isSubclassPropertyNullable(int i) { public int[] findDirty(Object[] currentState, Object[] previousState, Object entity, SharedSessionContractImplementor session) throws HibernateException { int[] props = DirtyHelper.findDirty( - entityMetamodel.getProperties(), + entityMetamodel.getDirtyCheckablePropertyTypes(), currentState, previousState, propertyColumnUpdateable, @@ -4622,6 +4747,11 @@ public Object getIdentifier(Object entity, SharedSessionContractImplementor sess return identifierMapping.getIdentifier( entity ); } + @Override + public Object getIdentifier(Object entity, MergeContext mergeContext) { + return identifierMapping.getIdentifier( entity, mergeContext ); + } + @Override public void setIdentifier(Object entity, Object id, SharedSessionContractImplementor session) { identifierMapping.setIdentifier( entity, id, session ); @@ -6090,50 +6220,30 @@ public ModelPart findSubPart(String name, EntityMappingType treatTargetType) { } } - if ( treatTargetType != null ) { - if ( ! treatTargetType.isTypeOrSuperType( this ) ) { + if ( treatTargetType == null ) { + final var subDefinedAttribute = findSubPartInSubclassMappings( name ); + if ( subDefinedAttribute != null ) { + return subDefinedAttribute; + } + } + else if ( treatTargetType != this ) { + if ( !treatTargetType.isTypeOrSuperType( this ) ) { return null; } - - if ( subclassMappingTypes != null && !subclassMappingTypes.isEmpty() ) { - for ( EntityMappingType subMappingType : subclassMappingTypes.values() ) { - if ( ! treatTargetType.isTypeOrSuperType( subMappingType ) ) { - continue; - } - - final ModelPart subDefinedAttribute = subMappingType.findSubTypesSubPart( name, treatTargetType ); - - if ( subDefinedAttribute != null ) { - return subDefinedAttribute; - } - } + // Prefer attributes defined in the treat target type or its subtypes + final var treatTypeSubPart = treatTargetType.findSubTypesSubPart( name, null ); + if ( treatTypeSubPart != null ) { + return treatTypeSubPart; } - } - else { - if ( subclassMappingTypes != null && !subclassMappingTypes.isEmpty() ) { - ModelPart attribute = null; - for ( EntityMappingType subMappingType : subclassMappingTypes.values() ) { - final ModelPart subDefinedAttribute = subMappingType.findSubTypesSubPart( name, treatTargetType ); - if ( subDefinedAttribute != null ) { - if ( attribute != null && !MappingModelHelper.isCompatibleModelPart( attribute, subDefinedAttribute ) ) { - throw new PathException( - String.format( - Locale.ROOT, - "Could not resolve attribute '%s' of '%s' due to the attribute being declared in multiple subtypes '%s' and '%s'", - name, - getJavaType().getTypeName(), - attribute.asAttributeMapping().getDeclaringType() - .getJavaType().getTypeName(), - subDefinedAttribute.asAttributeMapping().getDeclaringType() - .getJavaType().getTypeName() - ) - ); - } - attribute = subDefinedAttribute; + else { + // If not found, look in the treat target type's supertypes + EntityMappingType superType = treatTargetType.getSuperMappingType(); + while ( superType != this ) { + final var superTypeSubPart = superType.findDeclaredAttributeMapping( name ); + if ( superTypeSubPart != null ) { + return superTypeSubPart; } - } - if ( attribute != null ) { - return attribute; + superType = superType.getSuperMappingType(); } } } @@ -6156,6 +6266,29 @@ public ModelPart findSubPart(String name, EntityMappingType treatTargetType) { } } + private ModelPart findSubPartInSubclassMappings(String name) { + ModelPart attribute = null; + if ( isNotEmpty( subclassMappingTypes ) ) { + for ( var subMappingType : subclassMappingTypes.values() ) { + final var subDefinedAttribute = subMappingType.findSubTypesSubPart( name, null ); + if ( subDefinedAttribute != null ) { + if ( attribute != null && !isCompatibleModelPart( attribute, subDefinedAttribute ) ) { + throw new PathException( String.format( + Locale.ROOT, + "Could not resolve attribute '%s' of '%s' due to the attribute being declared in multiple subtypes '%s' and '%s'", + name, + getJavaType().getTypeName(), + attribute.asAttributeMapping().getDeclaringType().getJavaType().getTypeName(), + subDefinedAttribute.asAttributeMapping().getDeclaringType().getJavaType().getTypeName() + ) ); + } + attribute = subDefinedAttribute; + } + } + } + return attribute; + } + @Override public ModelPart findSubTypesSubPart(String name, EntityMappingType treatTargetType) { final AttributeMapping declaredAttribute = declaredAttributeMappings.get( name ); @@ -6163,19 +6296,12 @@ public ModelPart findSubTypesSubPart(String name, EntityMappingType treatTargetT return declaredAttribute; } else { - if ( subclassMappingTypes != null && !subclassMappingTypes.isEmpty() ) { - for ( EntityMappingType subMappingType : subclassMappingTypes.values() ) { - final ModelPart subDefinedAttribute = subMappingType.findSubTypesSubPart( name, treatTargetType ); - if ( subDefinedAttribute != null ) { - return subDefinedAttribute; - } - } - } - return null; + return findSubPartInSubclassMappings( name ); } } private ModelPart getIdentifierModelPart(String name, EntityMappingType treatTargetType) { + final EntityIdentifierMapping identifierMapping = getIdentifierMappingForJoin(); if ( identifierMapping instanceof NonAggregatedIdentifierMapping ) { NonAggregatedIdentifierMapping mapping = (NonAggregatedIdentifierMapping) identifierMapping; final ModelPart subPart = mapping.findSubPart( name, treatTargetType ); @@ -6236,7 +6362,7 @@ public Fetchable getKeyFetchable(int position) { } @Override - public Fetchable getFetchable(int position) { + public AttributeMapping getFetchable(int position) { return getStaticFetchableList().get( position ); } @@ -6776,9 +6902,9 @@ public String[] getSubclassPropertyColumnAliases(String propertyName, String suf } // aliases for composite-id's - if ( getIdentifierType().isComponentType() ) { + if ( getIdentifierType() instanceof ComponentType ) { // Fetch embedded identifiers property names from the "virtual" identifier component - final CompositeType componentId = (CompositeType) getIdentifierType(); + final ComponentType componentId = (ComponentType) getIdentifierType(); final String[] idPropertyNames = componentId.getPropertyNames(); final String[] idAliases = getIdentifierAliases(); final String[] idColumnNames = getIdentifierColumnNames(); diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractPropertyMapping.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractPropertyMapping.java index fb46b751239b..4a330b6fae0f 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractPropertyMapping.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/AbstractPropertyMapping.java @@ -27,6 +27,7 @@ import org.hibernate.type.AnyType; import org.hibernate.type.AssociationType; import org.hibernate.type.CollectionType; +import org.hibernate.type.ComponentType; import org.hibernate.type.CompositeType; import org.hibernate.type.EntityType; import org.hibernate.type.ManyToOneType; @@ -282,7 +283,7 @@ protected void initPropertyPaths( ); } - if ( type.isAssociationType() ) { + if ( type instanceof AnyType || type instanceof CollectionType || type instanceof EntityType ) { AssociationType actype = (AssociationType) type; if ( actype.useLHSPrimaryKey() ) { columns = getIdentifierColumnNames(); @@ -308,8 +309,20 @@ protected void initPropertyPaths( addPropertyPath( path, type, columns, columnReaders, columnReaderTemplates, formulaTemplates, factory ); } - if ( type.isComponentType() ) { - CompositeType actype = (CompositeType) type; + if ( type instanceof AnyType ) { + AnyType actype = (AnyType) type; + initComponentPropertyPaths( + path, + actype, + columns, + columnReaders, + columnReaderTemplates, + formulaTemplates, + factory + ); + } + else if ( type instanceof ComponentType ) { + ComponentType actype = (ComponentType) type; initComponentPropertyPaths( path, actype, @@ -331,7 +344,7 @@ protected void initPropertyPaths( ); } } - else if ( type.isEntityType() ) { + else if ( type instanceof EntityType ) { initIdentifierPropertyPaths( path, (EntityType) type, diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/DirtyHelper.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/DirtyHelper.java index 289677fcf241..124a16185199 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/DirtyHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/DirtyHelper.java @@ -11,6 +11,14 @@ import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.internal.util.collections.ArrayHelper; import org.hibernate.tuple.NonIdentifierAttribute; +import org.hibernate.type.AnyType; +import org.hibernate.type.BasicType; +import org.hibernate.type.CollectionType; +import org.hibernate.type.ComponentType; +import org.hibernate.type.ManyToOneType; +import org.hibernate.type.Type; + +import org.checkerframework.checker.nullness.qual.Nullable; /** * Operations for searching an array of property values for modified elements. @@ -72,6 +80,62 @@ else if ( previousState[i] == LazyPropertyInitializer.UNFETCHED_PROPERTY ) { } } + /** + * Determine if any of the given field values are dirty, returning an array containing + * indices of the dirty fields. + *

    + * If it is determined that no fields are dirty, null is returned. + * + * @param propertyTypes The property types that are dirty checkable. null entry for non-dirty checkable properties + * @param currentState The current state of the entity + * @param previousState The baseline state of the entity + * @param includeColumns Columns to be included in the dirty checking, per property + * @param session The session from which the dirty check request originated. + * + * @return Array containing indices of the dirty properties, or null if no properties considered dirty. + */ + public static int[] findDirty( + @Nullable Type[] propertyTypes, + final Object[] currentState, + final Object[] previousState, + final boolean[][] includeColumns, + final SharedSessionContractImplementor session) { + int[] results = null; + int count = 0; + int span = propertyTypes.length; + + for ( int i = 0; i < span; i++ ) { + + if ( isDirty( propertyTypes, currentState, previousState, includeColumns, session, i ) ) { + if ( results == null ) { + results = new int[span]; + } + results[count++] = i; + } + } + + return count == 0 ? null : ArrayHelper.trim( results, count ); + } + + private static boolean isDirty( + @Nullable Type[] propertyTypes, + Object[] currentState, + Object[] previousState, + boolean[][] includeColumns, + SharedSessionContractImplementor session, int i) { + final Type propertyType; + if ( currentState[i] == LazyPropertyInitializer.UNFETCHED_PROPERTY + || ( propertyType = propertyTypes[i] ) == null ) { + return false; + } + else if ( previousState[i] == LazyPropertyInitializer.UNFETCHED_PROPERTY ) { + return true; + } + else { + return propertyType.isDirty( previousState[i], currentState[i], includeColumns[i], session ); + } + } + /** * Determine if any of the given field values are modified, returning an array containing * indices of the modified fields. diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/EntityPersister.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/EntityPersister.java index a86e5f133dc5..05011c3b0a7a 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/EntityPersister.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/EntityPersister.java @@ -30,6 +30,7 @@ import org.hibernate.engine.spi.SessionImplementor; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.event.spi.EventSource; +import org.hibernate.event.spi.MergeContext; import org.hibernate.generator.BeforeExecutionGenerator; import org.hibernate.generator.EventType; import org.hibernate.generator.Generator; @@ -1130,8 +1131,18 @@ default Object getValue(Object object, int i) { */ Object getIdentifier(Object entity, SharedSessionContractImplementor session); - /** - * Inject the identifier value into the given entity. + /** + * Get the identifier of an instance from the object's identifier property. + * Throw an exception if it has no identifier property. + * + * It's supposed to be use during the merging process + */ + default Object getIdentifier(Object entity, MergeContext mergeContext) { + return getIdentifier( entity, mergeContext.getEventSource() ); + } + + /** + * Inject the identifier value into the given entity. */ void setIdentifier(Object entity, Object id, SharedSessionContractImplementor session); diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/JoinedSubclassEntityPersister.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/JoinedSubclassEntityPersister.java index d7e88fe0c576..ba60f50d0dc1 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/JoinedSubclassEntityPersister.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/JoinedSubclassEntityPersister.java @@ -127,7 +127,7 @@ public class JoinedSubclassEntityPersister extends AbstractEntityPersister { // the closure of all columns used by the entire hierarchy including // subclasses and superclasses of this class - private final int[] subclassColumnTableNumberClosure; + private final int[] subclassColumnNaturalOrderTableNumberClosure; // private final int[] subclassFormulaTableNumberClosure; private final String[] subclassColumnClosure; @@ -470,11 +470,12 @@ public JoinedSubclassEntityPersister( final String tableName = property.getValue().getTable(). getQualifiedName( creationContext.getSqlStringGenerationContext() ); final Integer tableNumber = getTableId( tableName, subclassTableNameClosure ); + final Integer naturalTableNumber = getTableId( tableName, naturalOrderSubclassTableNameClosure ); propTableNumbers.add( tableNumber ); for ( Selectable selectable : property.getSelectables() ) { if ( !selectable.isFormula() ) { - columnTableNumbers.add( tableNumber ); + columnTableNumbers.add( naturalTableNumber ); Column column = (Column) selectable; columns.add( column.getQuotedName( dialect ) ); } @@ -484,7 +485,7 @@ public JoinedSubclassEntityPersister( } } - subclassColumnTableNumberClosure = toIntArray( columnTableNumbers ); + subclassColumnNaturalOrderTableNumberClosure = toIntArray( columnTableNumbers ); subclassPropertyTableNumberClosure = toIntArray( propTableNumbers ); // subclassFormulaTableNumberClosure = ArrayHelper.toIntArray( formulaTableNumbers ); subclassColumnClosure = toStringArray( columns ); @@ -1069,12 +1070,12 @@ public int determineTableNumberForColumn(String columnName) { && subclassColumnClosure[i].endsWith( "\"" ); if ( quoted ) { if ( subclassColumnClosure[i].equals( columnName ) ) { - return subclassColumnTableNumberClosure[i]; + return subclassColumnNaturalOrderTableNumberClosure[i]; } } else { if ( subclassColumnClosure[i].equalsIgnoreCase( columnName ) ) { - return subclassColumnTableNumberClosure[i]; + return subclassColumnNaturalOrderTableNumberClosure[i]; } } } @@ -1421,19 +1422,31 @@ public void pruneForSubclasses(TableGroup tableGroup, Map } } + @Override + public EntityIdentifierMapping getIdentifierMappingForJoin() { + // If the joined subclass has a physical discriminator and has subtypes + // we must use the root table identifier mapping for joining to allow table group elimination to work + return isPhysicalDiscriminator() && !getSubMappingTypes().isEmpty() + ? getRootEntityDescriptor().getIdentifierMapping() + : super.getIdentifierMappingForJoin(); + } + private boolean applyDiscriminatorPredicate( TableReferenceJoin join, NamedTableReference tableReference, Map entityNameUses, MappingMetamodelImplementor metamodel) { if ( tableReference.getTableExpression().equals( getRootTableName() ) ) { - assert join.getJoinType() == SqlAstJoinType.INNER : "Found table reference join with root table of non-INNER type: " + join.getJoinType(); final String discriminatorPredicate = getPrunedDiscriminatorPredicate( entityNameUses, metamodel, - tableReference.getIdentificationVariable() + "t" ); - join.applyPredicate( new SqlFragmentPredicate( discriminatorPredicate ) ); + // null means we're filtering for all subtypes, so we don't need to apply a predicate + if ( discriminatorPredicate != null ) { + assert join.getJoinType() == SqlAstJoinType.INNER : "Found table reference join with root table of non-INNER type: " + join.getJoinType(); + tableReference.setPrunedTableExpression( "(select * from " + getRootTableName() + " t where " + discriminatorPredicate + ")" ); + } return true; } return false; 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 b45a3fa9bda0..22d1e2f68735 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 @@ -47,6 +47,7 @@ import org.hibernate.metamodel.mapping.SelectableMapping; import org.hibernate.metamodel.mapping.TableDetails; import org.hibernate.metamodel.mapping.internal.MappingModelCreationProcess; +import org.hibernate.metamodel.mapping.internal.SqlTypedMappingImpl; import org.hibernate.metamodel.spi.MappingMetamodelImplementor; import org.hibernate.metamodel.spi.RuntimeModelCreationContext; import org.hibernate.persister.spi.PersisterCreationContext; @@ -492,8 +493,7 @@ protected String generateSubquery(PersistentClass model, Metadata mapping) { subquery.append( "select " ); for ( Column col : columns ) { if ( !table.containsColumn( col ) ) { - int sqlType = col.getSqlTypeCode( mapping ); - subquery.append( dialect.getSelectClauseNullString( sqlType, getFactory().getTypeConfiguration() ) ) + subquery.append( getSelectClauseNullString( col, dialect ) ) .append(" as "); } subquery.append( col.getQuotedName( dialect ) ) @@ -508,6 +508,23 @@ protected String generateSubquery(PersistentClass model, Metadata mapping) { return subquery.append( ")" ).toString(); } + private String getSelectClauseNullString(Column col, Dialect dialect) { + return dialect.getSelectClauseNullString( + new SqlTypedMappingImpl( + col.getTypeName(), + col.getLength(), + col.getPrecision(), + col.getScale(), + col.getTemporalPrecision(), + col.getValue().getSelectableType( + col.getValue().getBuildingContext().getMetadataCollector(), + col.getTypeIndex() + ) + ), + getFactory().getTypeConfiguration() + ); + } + protected String generateSubquery(Map entityNameUses) { if ( !hasSubclasses() ) { return getTableName(); @@ -574,9 +591,7 @@ protected String generateSubquery(Map entityNameUses) { if ( selectableMapping == null ) { // If there is no selectable mapping for a table name, we render a null expression selectableMapping = selectableMappings.values().iterator().next(); - final int sqlType = selectableMapping.getJdbcMapping().getJdbcType() - .getDdlTypeCode(); - buf.append( dialect.getSelectClauseNullString( sqlType, getFactory().getTypeConfiguration() ) ) + buf.append( dialect.getSelectClauseNullString( selectableMapping, getFactory().getTypeConfiguration() ) ) .append( " as " ); } if ( selectableMapping.isFormula() ) { diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/AbstractDeleteCoordinator.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/AbstractDeleteCoordinator.java index 0e5f64267d98..a2f5bbb72a3d 100644 --- a/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/AbstractDeleteCoordinator.java +++ b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/AbstractDeleteCoordinator.java @@ -276,22 +276,25 @@ protected void doStaticDelete( session ); - mutationExecutor.execute( - entity, - null, - null, - (statementDetails, affectedRowCount, batchPosition) -> identifiedResultsCheck( - statementDetails, - affectedRowCount, - batchPosition, - entityPersister(), - id, - factory() - ), - session - ); - - mutationExecutor.release(); + try { + mutationExecutor.execute( + entity, + null, + null, + (statementDetails, affectedRowCount, batchPosition) -> identifiedResultsCheck( + statementDetails, + affectedRowCount, + batchPosition, + entityPersister(), + id, + factory() + ), + session + ); + } + finally { + mutationExecutor.release(); + } } protected void applyStaticDeleteTableDetails( diff --git a/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/AbstractMutationCoordinator.java b/hibernate-core/src/main/java/org/hibernate/persister/entity/mutation/AbstractMutationCoordinator.java index 971697403c66..38c133b689ee 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 @@ -68,7 +68,10 @@ protected BatchKeyAccess resolveBatchKeyAccess(boolean dynamicUpdate, SharedSess if ( !dynamicUpdate && !entityPersister().optimisticLockStyle().isAllOrDirty() && session.getTransactionCoordinator() != null - && session.getTransactionCoordinator().isTransactionActive() ) { + && session.getTransactionCoordinator().isTransactionActive() + && ( + session.getSessionFactory().getSessionFactoryOptions().isJdbcBatchVersionedData() + || !entityPersister().isVersioned() ) ) { return this::getBatchKey; } diff --git a/hibernate-core/src/main/java/org/hibernate/procedure/internal/NamedCallableQueryMementoImpl.java b/hibernate-core/src/main/java/org/hibernate/procedure/internal/NamedCallableQueryMementoImpl.java index d79f9d4aba49..e7ffc89325dd 100644 --- a/hibernate-core/src/main/java/org/hibernate/procedure/internal/NamedCallableQueryMementoImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/procedure/internal/NamedCallableQueryMementoImpl.java @@ -64,6 +64,7 @@ public NamedCallableQueryMementoImpl( Map hints) { super( name, + Object.class, cacheable, cacheRegion, cacheMode, diff --git a/hibernate-core/src/main/java/org/hibernate/procedure/internal/ProcedureCallImpl.java b/hibernate-core/src/main/java/org/hibernate/procedure/internal/ProcedureCallImpl.java index c39b58c05a53..65e13e0c8dd7 100644 --- a/hibernate-core/src/main/java/org/hibernate/procedure/internal/ProcedureCallImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/procedure/internal/ProcedureCallImpl.java @@ -711,6 +711,7 @@ private ProcedureOutputsImpl buildOutputs() { } catch (SQLException e) { getSession().getJdbcCoordinator().getLogicalConnection().getResourceRegistry().release( statement ); + getSession().getJdbcCoordinator().afterStatementExecution(); throw getSession().getJdbcServices().getSqlExceptionHelper().convert( e, "Error registering CallableStatement parameters", diff --git a/hibernate-core/src/main/java/org/hibernate/procedure/internal/ProcedureOutputsImpl.java b/hibernate-core/src/main/java/org/hibernate/procedure/internal/ProcedureOutputsImpl.java index a691724a59d3..e4f5d26888ed 100644 --- a/hibernate-core/src/main/java/org/hibernate/procedure/internal/ProcedureOutputsImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/procedure/internal/ProcedureOutputsImpl.java @@ -147,10 +147,4 @@ protected Output buildFunctionReturn() { return buildResultSetOutput( () -> results ); } } - - @Override - public void release() { - super.release(); - getResultContext().getSession().getJdbcCoordinator().getLogicalConnection().getResourceRegistry().release( callableStatement ); - } } diff --git a/hibernate-core/src/main/java/org/hibernate/property/access/internal/AccessStrategyHelper.java b/hibernate-core/src/main/java/org/hibernate/property/access/internal/AccessStrategyHelper.java index a46a0d2c1034..61f29f9b93b9 100644 --- a/hibernate-core/src/main/java/org/hibernate/property/access/internal/AccessStrategyHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/property/access/internal/AccessStrategyHelper.java @@ -6,14 +6,11 @@ */ package org.hibernate.property.access.internal; -import java.beans.Introspector; import java.lang.reflect.AnnotatedElement; import java.lang.reflect.Field; import java.lang.reflect.Method; import java.lang.reflect.Modifier; -import java.util.Locale; -import org.hibernate.MappingException; import org.hibernate.PropertyNotFoundException; import org.hibernate.bytecode.enhance.spi.interceptor.BytecodeLazyAttributeInterceptor; import org.hibernate.engine.spi.CompositeOwner; @@ -33,6 +30,7 @@ import static org.hibernate.engine.internal.ManagedTypeHelper.isPersistentAttributeInterceptableType; import static org.hibernate.internal.util.ReflectHelper.NO_PARAM_SIGNATURE; import static org.hibernate.internal.util.ReflectHelper.findField; +import static org.hibernate.internal.util.ReflectHelper.getterMethodOrNull; import static org.hibernate.internal.util.ReflectHelper.isRecord; /** @@ -87,89 +85,9 @@ public static AccessType getAccessType(Class containerJavaType, String proper return AccessType.FIELD; } - for ( Method method : containerClass.getDeclaredMethods() ) { - // if the method has parameters, skip it - if ( method.getParameterCount() != 0 ) { - continue; - } - - // if the method is a "bridge", skip it - if ( method.isBridge() ) { - continue; - } - - if ( method.isAnnotationPresent( Transient.class ) ) { - continue; - } - - if ( Modifier.isStatic( method.getModifiers() ) ) { - continue; - } - - final String methodName = method.getName(); - - // try "get" - if ( methodName.startsWith( "get" ) ) { - final String stemName = methodName.substring( 3 ); - final String decapitalizedStemName = Introspector.decapitalize( stemName ); - if ( stemName.equals( propertyName ) || decapitalizedStemName.equals( propertyName ) ) { - if ( method.isAnnotationPresent( Access.class ) ) { - return AccessType.PROPERTY; - } - else { - checkIsMethodVariant( containerClass, propertyName, method, stemName ); - } - } - } - - // if not "get", then try "is" - if ( methodName.startsWith( "is" ) ) { - final String stemName = methodName.substring( 2 ); - String decapitalizedStemName = Introspector.decapitalize( stemName ); - if ( stemName.equals( propertyName ) || decapitalizedStemName.equals( propertyName ) ) { - if ( method.isAnnotationPresent( Access.class ) ) { - return AccessType.PROPERTY; - } - } - } - } - - return null; - } - - private static void checkIsMethodVariant( - Class containerClass, - String propertyName, - Method method, - String stemName) { - final Method isMethodVariant = findIsMethodVariant( containerClass, stemName ); - if ( isMethodVariant == null ) { - return; - } - - if ( !isMethodVariant.isAnnotationPresent( Access.class ) ) { - throw new MappingException( - String.format( - Locale.ROOT, - "Class '%s' declares both 'get' [%s] and 'is' [%s] variants of getter for property '%s'", - containerClass.getName(), - method.toString(), - isMethodVariant, - propertyName - ) - ); - } - } - - public static @Nullable Method findIsMethodVariant(Class containerClass, String stemName) { - // verify that the Class does not also define a method with the same stem name with 'is' - try { - final Method isMethod = containerClass.getDeclaredMethod( "is" + stemName ); - if ( !Modifier.isStatic( isMethod.getModifiers() ) && isMethod.getAnnotation( Transient.class ) == null ) { - return isMethod; - } - } - catch (NoSuchMethodException ignore) { + final Method getter = getterMethodOrNull( containerClass, propertyName ); + if ( getter != null && getter.isAnnotationPresent( Access.class ) ) { + return AccessType.PROPERTY; } return null; diff --git a/hibernate-core/src/main/java/org/hibernate/property/access/internal/PropertyAccessEnhancedImpl.java b/hibernate-core/src/main/java/org/hibernate/property/access/internal/PropertyAccessEnhancedImpl.java index 3a6e3e51b476..6782d1e39ec3 100644 --- a/hibernate-core/src/main/java/org/hibernate/property/access/internal/PropertyAccessEnhancedImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/property/access/internal/PropertyAccessEnhancedImpl.java @@ -6,6 +6,9 @@ */ package org.hibernate.property.access.internal; +import jakarta.persistence.AccessType; +import org.checkerframework.checker.nullness.qual.Nullable; +import org.hibernate.property.access.spi.EnhancedGetterFieldImpl; import org.hibernate.property.access.spi.EnhancedSetterImpl; import org.hibernate.property.access.spi.EnhancedSetterMethodImpl; import org.hibernate.property.access.spi.Getter; @@ -19,9 +22,7 @@ import java.lang.reflect.Field; import java.lang.reflect.Method; -import jakarta.persistence.AccessType; -import org.checkerframework.checker.nullness.qual.Nullable; - +import static org.hibernate.internal.util.ReflectHelper.findField; import static org.hibernate.internal.util.ReflectHelper.findSetterMethod; import static org.hibernate.internal.util.ReflectHelper.getterMethodOrNull; import static org.hibernate.property.access.internal.AccessStrategyHelper.fieldOrNull; @@ -43,10 +44,12 @@ public PropertyAccessEnhancedImpl( PropertyAccessStrategy strategy, Class containerJavaType, String propertyName, - @Nullable AccessType getterAccessType) { + @Nullable AccessType classAccessType) { this.strategy = strategy; - final AccessType propertyAccessType = resolveAccessType( getterAccessType, containerJavaType, propertyName ); + final AccessType propertyAccessType = classAccessType == null ? + AccessStrategyHelper.getAccessType( containerJavaType, propertyName ) : + classAccessType; switch ( propertyAccessType ) { case FIELD: { @@ -67,10 +70,8 @@ public PropertyAccessEnhancedImpl( "Could not locate getter for property named [" + containerJavaType.getName() + "#" + propertyName + "]" ); } - final Method setterMethod = findSetterMethod( containerJavaType, propertyName, getterMethod.getReturnType() ); - - this.getter = new GetterMethodImpl( containerJavaType, propertyName, getterMethod ); - this.setter = new EnhancedSetterMethodImpl( containerJavaType, propertyName, setterMethod ); + this.getter = propertyGetter( classAccessType, containerJavaType, propertyName, getterMethod ); + this.setter = propertySetter( classAccessType, containerJavaType, propertyName, getterMethod.getReturnType() ); break; } default: { @@ -81,12 +82,31 @@ public PropertyAccessEnhancedImpl( } } - private static AccessType resolveAccessType(@Nullable AccessType getterAccessType, Class containerJavaType, String propertyName) { - if ( getterAccessType != null ) { - // this should indicate FIELD access - return getterAccessType; + private static Getter propertyGetter(@Nullable AccessType classAccessType, Class containerJavaType, String propertyName, Method getterMethod) { + if ( classAccessType != null ) { + final AccessType explicitAccessType = AccessStrategyHelper.getAccessType( containerJavaType, propertyName ); + if ( explicitAccessType == AccessType.FIELD ) { + // We need to default to FIELD unless we have an explicit AccessType to avoid unnecessary initializations + final Field field = findField( containerJavaType, propertyName ); + return new EnhancedGetterFieldImpl( containerJavaType, propertyName, field, getterMethod ); + } + } + // when classAccessType is null know PROPERTY is the explicit access type + return new GetterMethodImpl( containerJavaType, propertyName, getterMethod ); + } + + private static Setter propertySetter(@Nullable AccessType classAccessType, Class containerJavaType, String propertyName, Class fieldType) { + if ( classAccessType != null ) { + final AccessType explicitAccessType = AccessStrategyHelper.getAccessType( containerJavaType, propertyName ); + if ( explicitAccessType == AccessType.FIELD ) { + // We need to default to FIELD unless we have an explicit AccessType to avoid unnecessary initializations + final Field field = findField( containerJavaType, propertyName ); + return new EnhancedSetterImpl( containerJavaType, propertyName, field ); + } } - return AccessStrategyHelper.getAccessType( containerJavaType, propertyName ); + // when classAccessType is null know PROPERTY is the explicit access type + final Method setterMethod = findSetterMethod( containerJavaType, propertyName, fieldType ); + return new EnhancedSetterMethodImpl( containerJavaType, propertyName, setterMethod ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/property/access/internal/PropertyAccessStrategyEnhancedImpl.java b/hibernate-core/src/main/java/org/hibernate/property/access/internal/PropertyAccessStrategyEnhancedImpl.java index 33c5181552dc..9d1650521459 100644 --- a/hibernate-core/src/main/java/org/hibernate/property/access/internal/PropertyAccessStrategyEnhancedImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/property/access/internal/PropertyAccessStrategyEnhancedImpl.java @@ -21,23 +21,32 @@ */ public class PropertyAccessStrategyEnhancedImpl implements PropertyAccessStrategy { public static PropertyAccessStrategyEnhancedImpl with(AccessType getterAccessType) { - if ( getterAccessType == AccessType.FIELD ) { - return FIELD; + if ( getterAccessType == null ) { + return STANDARD; + } + + switch ( getterAccessType ) { + case FIELD: + return FIELD; + case PROPERTY: + return PROPERTY; + default: + return STANDARD; } - return STANDARD; } - private final @Nullable AccessType getterAccessType; + private final @Nullable AccessType classAccessType; public static PropertyAccessStrategyEnhancedImpl STANDARD = new PropertyAccessStrategyEnhancedImpl( null ); public static PropertyAccessStrategyEnhancedImpl FIELD = new PropertyAccessStrategyEnhancedImpl( AccessType.FIELD ); + public static PropertyAccessStrategyEnhancedImpl PROPERTY = new PropertyAccessStrategyEnhancedImpl( AccessType.PROPERTY ); - public PropertyAccessStrategyEnhancedImpl(@Nullable AccessType getterAccessType) { - this.getterAccessType = getterAccessType; + public PropertyAccessStrategyEnhancedImpl(@Nullable AccessType classAccessType) { + this.classAccessType = classAccessType; } @Override public PropertyAccess buildPropertyAccess(Class containerJavaType, final String propertyName, boolean setterRequired) { - return new PropertyAccessEnhancedImpl( this, containerJavaType, propertyName, getterAccessType ); + return new PropertyAccessEnhancedImpl( this, containerJavaType, propertyName, classAccessType ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/property/access/internal/PropertyAccessStrategyResolverStandardImpl.java b/hibernate-core/src/main/java/org/hibernate/property/access/internal/PropertyAccessStrategyResolverStandardImpl.java index 2ddb660a0c0b..4f38030ee05a 100644 --- a/hibernate-core/src/main/java/org/hibernate/property/access/internal/PropertyAccessStrategyResolverStandardImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/property/access/internal/PropertyAccessStrategyResolverStandardImpl.java @@ -8,6 +8,7 @@ import org.hibernate.HibernateException; import org.hibernate.boot.registry.selector.spi.StrategySelector; +import org.hibernate.boot.spi.AccessType; import org.hibernate.internal.util.StringHelper; import org.hibernate.metamodel.RepresentationMode; import org.hibernate.property.access.spi.BuiltInPropertyAccessStrategies; @@ -40,9 +41,12 @@ public PropertyAccessStrategy resolvePropertyAccessStrategy( || BuiltInPropertyAccessStrategies.MIXED.getExternalName().equals( explicitAccessStrategyName ) ) { //type-cache-pollution agent: it's crucial to use the ManagedTypeHelper rather than attempting a direct cast if ( isManagedType( containerClass ) ) { - if ( BuiltInPropertyAccessStrategies.FIELD.getExternalName().equals( explicitAccessStrategyName ) ) { + if ( AccessType.FIELD.getType().equals( explicitAccessStrategyName ) ) { return PropertyAccessStrategyEnhancedImpl.FIELD; } + else if ( AccessType.PROPERTY.getType().equals( explicitAccessStrategyName ) ) { + return PropertyAccessStrategyEnhancedImpl.PROPERTY; + } return PropertyAccessStrategyEnhancedImpl.STANDARD; } } diff --git a/hibernate-core/src/main/java/org/hibernate/property/access/spi/EnhancedGetterFieldImpl.java b/hibernate-core/src/main/java/org/hibernate/property/access/spi/EnhancedGetterFieldImpl.java new file mode 100644 index 000000000000..8935806d3c7f --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/property/access/spi/EnhancedGetterFieldImpl.java @@ -0,0 +1,43 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.property.access.spi; + +import org.checkerframework.checker.nullness.qual.NonNull; +import org.hibernate.Internal; + +import java.lang.reflect.Field; +import java.lang.reflect.Member; +import java.lang.reflect.Method; + +import static org.hibernate.internal.util.NullnessUtil.castNonNull; + +/** + * A specialized Getter implementation for handling getting values from + * a bytecode-enhanced Class. The reason we need specialized handling + * is to produce the correct {@link java.lang.reflect.Member} while + * using the {@link Field} to access values and ensure correct functionality. + * + * @author Steve Ebersole + * @author Luis Barreiro + */ +@Internal +public class EnhancedGetterFieldImpl extends GetterFieldImpl { + public EnhancedGetterFieldImpl(Class containerClass, String propertyName, Field field, Method getterMethod) { + super( containerClass, propertyName, field, getterMethod ); + assert getterMethod != null; + } + + @Override + public @NonNull Method getMethod() { + return castNonNull( super.getMethod() ); + } + + @Override + public Member getMember() { + return getMethod(); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/property/access/spi/GetterFieldImpl.java b/hibernate-core/src/main/java/org/hibernate/property/access/spi/GetterFieldImpl.java index 74647c50717e..f29095f2ba49 100644 --- a/hibernate-core/src/main/java/org/hibernate/property/access/spi/GetterFieldImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/property/access/spi/GetterFieldImpl.java @@ -35,11 +35,14 @@ public class GetterFieldImpl implements Getter { private final @Nullable Method getterMethod; public GetterFieldImpl(Class containerClass, String propertyName, Field field) { + this ( containerClass, propertyName, field, ReflectHelper.findGetterMethodForFieldAccess( field, propertyName ) ); + } + + GetterFieldImpl(Class containerClass, String propertyName, Field field, Method getterMethod) { this.containerClass = containerClass; this.propertyName = propertyName; this.field = field; - - this.getterMethod = ReflectHelper.findGetterMethodForFieldAccess( field, propertyName ); + this.getterMethod = getterMethod; } @Override @@ -78,9 +81,13 @@ public Type getReturnType() { return field.getGenericType(); } + public Field getField() { + return field; + } + @Override public Member getMember() { - return field; + return getField(); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/proxy/pojo/BasicLazyInitializer.java b/hibernate-core/src/main/java/org/hibernate/proxy/pojo/BasicLazyInitializer.java index 4d56431ae0d2..1a3e94f050a6 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 @@ -123,7 +123,7 @@ public Class getImplementationClass() { } final SharedSessionContractImplementor session = getSession(); if ( session == null ) { - throw new LazyInitializationException( "could not retrieve real entity class [" + getEntityName() + "#" + getIdentifier() + "] - no Session" ); + throw new LazyInitializationException( "could not retrieve real entity class [" + getEntityName() + "#" + getInternalIdentifier() + "] - no Session" ); } final SessionFactoryImplementor factory = session.getFactory(); final EntityPersister entityDescriptor = factory.getMappingMetamodel().getEntityDescriptor( getEntityName() ); diff --git a/hibernate-core/src/main/java/org/hibernate/proxy/pojo/bytebuddy/ByteBuddyProxyHelper.java b/hibernate-core/src/main/java/org/hibernate/proxy/pojo/bytebuddy/ByteBuddyProxyHelper.java index 06a3e48358ad..9916891114b1 100644 --- a/hibernate-core/src/main/java/org/hibernate/proxy/pojo/bytebuddy/ByteBuddyProxyHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/proxy/pojo/bytebuddy/ByteBuddyProxyHelper.java @@ -15,6 +15,7 @@ import java.util.HashSet; import java.util.Locale; import java.util.Set; +import java.util.function.BiFunction; import java.util.function.Function; import org.hibernate.HibernateException; @@ -27,7 +28,6 @@ import net.bytebuddy.ByteBuddy; import net.bytebuddy.NamingStrategy; -import net.bytebuddy.TypeCache; import net.bytebuddy.description.modifier.Visibility; import net.bytebuddy.description.type.TypeDefinition; import net.bytebuddy.description.type.TypeDescription; @@ -59,7 +59,8 @@ public Class buildProxy( } Collections.addAll( key, interfaces ); - return byteBuddyState.loadProxy( persistentClass, new TypeCache.SimpleKey( key ), + final String proxyClassName = persistentClass.getTypeName() + "$" + PROXY_NAMING_SUFFIX; + return byteBuddyState.loadProxy( persistentClass, proxyClassName, proxyBuilder( TypeDescription.ForLoadedType.of( persistentClass ), new TypeList.Generic.ForLoadedTypes( interfaces ) ) ); } @@ -68,7 +69,7 @@ public Class buildProxy( */ @Deprecated public DynamicType.Unloaded buildUnloadedProxy(final Class persistentClass, final Class[] interfaces) { - return byteBuddyState.make( proxyBuilder( TypeDescription.ForLoadedType.of( persistentClass ), + return byteBuddyState.make( proxyBuilderLegacy( TypeDescription.ForLoadedType.of( persistentClass ), new TypeList.Generic.ForLoadedTypes( interfaces ) ) ); } @@ -77,15 +78,24 @@ public DynamicType.Unloaded buildUnloadedProxy(final Class persistentClass */ public DynamicType.Unloaded buildUnloadedProxy(TypePool typePool, TypeDefinition persistentClass, Collection interfaces) { - return byteBuddyState.make( typePool, proxyBuilder( persistentClass, interfaces ) ); + return byteBuddyState.make( typePool, proxyBuilderLegacy( persistentClass, interfaces ) ); } - private Function> proxyBuilder(TypeDefinition persistentClass, + private Function> proxyBuilderLegacy(TypeDefinition persistentClass, + Collection interfaces) { + final BiFunction> proxyBuilder = + proxyBuilder( persistentClass, interfaces ); + final NamingStrategy.Suffixing namingStrategy = + new NamingStrategy.Suffixing( PROXY_NAMING_SUFFIX, new NamingStrategy.Suffixing.BaseNameResolver.ForFixedValue( persistentClass.getTypeName() ) ); + return byteBuddy -> proxyBuilder.apply( byteBuddy, namingStrategy ); + } + + private BiFunction> proxyBuilder(TypeDefinition persistentClass, Collection interfaces) { ByteBuddyState.ProxyDefinitionHelpers helpers = byteBuddyState.getProxyDefinitionHelpers(); - return byteBuddy -> helpers.appendIgnoreAlsoAtEnd( byteBuddy + return (byteBuddy, namingStrategy) -> helpers.appendIgnoreAlsoAtEnd( byteBuddy .ignore( helpers.getGroovyGetMetaClassFilter() ) - .with( new NamingStrategy.SuffixingRandom( PROXY_NAMING_SUFFIX, new NamingStrategy.Suffixing.BaseNameResolver.ForFixedValue( persistentClass.getTypeName() ) ) ) + .with( namingStrategy ) .subclass( interfaces.size() == 1 ? persistentClass : OBJECT, ConstructorStrategy.Default.IMITATE_SUPER_CLASS_OPENING ) .implement( interfaces ) .method( helpers.getVirtualNotFinalizerFilter() ) diff --git a/hibernate-core/src/main/java/org/hibernate/query/criteria/internal/NamedCriteriaQueryMementoImpl.java b/hibernate-core/src/main/java/org/hibernate/query/criteria/internal/NamedCriteriaQueryMementoImpl.java index 1c0ef4f2f623..049c4dab3a1d 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/criteria/internal/NamedCriteriaQueryMementoImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/criteria/internal/NamedCriteriaQueryMementoImpl.java @@ -22,6 +22,8 @@ import org.hibernate.query.sqm.spi.NamedSqmQueryMemento; import org.hibernate.query.sqm.tree.SqmStatement; +import org.checkerframework.checker.nullness.qual.Nullable; + public class NamedCriteriaQueryMementoImpl extends AbstractNamedQueryMemento implements NamedSqmQueryMemento, Serializable { private final SqmStatement sqmStatement; @@ -33,6 +35,7 @@ public class NamedCriteriaQueryMementoImpl extends AbstractNamedQueryMemento imp public NamedCriteriaQueryMementoImpl( String name, + @Nullable Class resultType, SqmStatement sqmStatement, Integer firstResult, Integer maxResults, @@ -47,7 +50,7 @@ public NamedCriteriaQueryMementoImpl( String comment, Map parameterTypes, Map hints) { - super( name, cacheable, cacheRegion, cacheMode, flushMode, readOnly, timeout, fetchSize, comment, hints ); + super( name, resultType, cacheable, cacheRegion, cacheMode, flushMode, readOnly, timeout, fetchSize, comment, hints ); this.sqmStatement = sqmStatement; this.firstResult = firstResult; this.maxResults = maxResults; @@ -114,6 +117,7 @@ public Map getParameterTypes() { public NamedSqmQueryMemento makeCopy(String name) { return new NamedCriteriaQueryMementoImpl( name, + getResultType(), sqmStatement, firstResult, maxResults, diff --git a/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleBasicEntityIdentifierMapping.java b/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleBasicEntityIdentifierMapping.java index d1f65dc0b858..f2ee788ff6d7 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleBasicEntityIdentifierMapping.java +++ b/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleBasicEntityIdentifierMapping.java @@ -9,6 +9,7 @@ import org.hibernate.Incubating; import org.hibernate.engine.spi.IdentifierValue; import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.event.spi.MergeContext; import org.hibernate.metamodel.mapping.BasicEntityIdentifierMapping; import org.hibernate.metamodel.mapping.JdbcMapping; import org.hibernate.metamodel.mapping.ManagedMappingType; @@ -51,6 +52,11 @@ public Object getIdentifier(Object entity) { return delegate.getIdentifier( entity ); } + @Override + public Object getIdentifier(Object entity, MergeContext mergeContext) { + return delegate.getIdentifier( entity, mergeContext ); + } + @Override public void setIdentifier(Object entity, Object id, SharedSessionContractImplementor session) { delegate.setIdentifier( entity, id, session ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleEmbeddableValuedModelPart.java b/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleEmbeddableValuedModelPart.java index 932baaa7175f..81a04f9333f5 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleEmbeddableValuedModelPart.java +++ b/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleEmbeddableValuedModelPart.java @@ -63,6 +63,9 @@ import org.hibernate.type.descriptor.java.JavaType; import jakarta.persistence.metamodel.Attribute; +import org.checkerframework.checker.nullness.qual.Nullable; + +import static java.util.Objects.requireNonNullElse; /** * @author Christian Beikov @@ -126,7 +129,7 @@ private Map createModelParts( sqmExpressible, attributeType, sqlSelections, - selectionIndex, + selectionIndex + index, selectionExpression + "_" + attribute.getName(), attribute.getName(), modelPartContainer.findSubPart( attribute.getName(), null ), @@ -346,13 +349,13 @@ public boolean isSimpleJoinPredicate(Predicate predicate) { public TableGroupJoin createTableGroupJoin( NavigablePath navigablePath, TableGroup lhs, - String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, - SqlAstJoinType requestedJoinType, + @Nullable String explicitSourceAlias, + @Nullable SqlAliasBase explicitSqlAliasBase, + @Nullable SqlAstJoinType requestedJoinType, boolean fetched, boolean addsPredicate, SqlAstCreationState creationState) { - final SqlAstJoinType joinType = requestedJoinType == null ? SqlAstJoinType.INNER : requestedJoinType; + final SqlAstJoinType joinType = requireNonNullElse( requestedJoinType, SqlAstJoinType.INNER ); final TableGroup tableGroup = createRootTableGroupJoin( navigablePath, lhs, @@ -371,18 +374,13 @@ public TableGroupJoin createTableGroupJoin( public TableGroup createRootTableGroupJoin( NavigablePath navigablePath, TableGroup lhs, - String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, - SqlAstJoinType sqlAstJoinType, + @Nullable String explicitSourceAlias, + @Nullable SqlAliasBase explicitSqlAliasBase, + @Nullable SqlAstJoinType sqlAstJoinType, boolean fetched, - Consumer predicateConsumer, + @Nullable Consumer predicateConsumer, SqlAstCreationState creationState) { - return new StandardVirtualTableGroup( - navigablePath, - this, - lhs, - fetched - ); + return new StandardVirtualTableGroup( navigablePath, this, lhs, fetched ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleEmbeddedEntityIdentifierMapping.java b/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleEmbeddedEntityIdentifierMapping.java index 64be6083d4d5..abd8f49da889 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleEmbeddedEntityIdentifierMapping.java +++ b/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleEmbeddedEntityIdentifierMapping.java @@ -12,6 +12,7 @@ import org.hibernate.Incubating; import org.hibernate.engine.spi.IdentifierValue; import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.event.spi.MergeContext; import org.hibernate.metamodel.mapping.CompositeIdentifierMapping; import org.hibernate.metamodel.mapping.EmbeddableMappingType; import org.hibernate.metamodel.mapping.internal.SingleAttributeIdentifierMapping; @@ -70,6 +71,11 @@ public Object getIdentifier(Object entity) { return delegate.getIdentifier( entity ); } + @Override + public Object getIdentifier(Object entity, MergeContext mergeContext) { + return delegate.getIdentifier( entity, mergeContext ); + } + @Override public void setIdentifier(Object entity, Object id, SharedSessionContractImplementor session) { delegate.setIdentifier( entity, id, session ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleEntityValuedModelPart.java b/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleEntityValuedModelPart.java index dc73c84a403a..c973837b40a1 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleEntityValuedModelPart.java +++ b/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleEntityValuedModelPart.java @@ -70,6 +70,8 @@ import org.hibernate.sql.results.graph.DomainResultCreationState; import org.hibernate.type.descriptor.java.JavaType; +import org.checkerframework.checker.nullness.qual.Nullable; + import static java.util.Objects.requireNonNullElse; import static org.hibernate.internal.util.collections.CollectionHelper.arrayList; @@ -245,14 +247,13 @@ public JavaType getMappedJavaType() { public TableGroupJoin createTableGroupJoin( NavigablePath navigablePath, TableGroup lhs, - String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, - SqlAstJoinType requestedJoinType, + @Nullable String explicitSourceAlias, + @Nullable SqlAliasBase explicitSqlAliasBase, + @Nullable SqlAstJoinType requestedJoinType, boolean fetched, boolean addsPredicate, SqlAstCreationState creationState) { final SqlAstJoinType joinType = requireNonNullElse( requestedJoinType, SqlAstJoinType.INNER ); - final LazyTableGroup lazyTableGroup = createRootTableGroupJoin( navigablePath, lhs, @@ -441,11 +442,11 @@ public TableGroup createTableGroupInternal( public LazyTableGroup createRootTableGroupJoin( NavigablePath navigablePath, TableGroup lhs, - String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, - SqlAstJoinType sqlAstJoinType, + @Nullable String explicitSourceAlias, + @Nullable SqlAliasBase explicitSqlAliasBase, + @Nullable SqlAstJoinType sqlAstJoinType, boolean fetched, - Consumer predicateConsumer, + @Nullable Consumer predicateConsumer, SqlAstCreationState creationState) { final SqlAliasBase sqlAliasBase = SqlAliasBase.from( explicitSqlAliasBase, diff --git a/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleNonAggregatedEntityIdentifierMapping.java b/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleNonAggregatedEntityIdentifierMapping.java index beee4fecd2a7..c2d7db73fb9d 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleNonAggregatedEntityIdentifierMapping.java +++ b/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleNonAggregatedEntityIdentifierMapping.java @@ -14,6 +14,7 @@ import org.hibernate.engine.FetchTiming; import org.hibernate.engine.spi.IdentifierValue; import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.event.spi.MergeContext; import org.hibernate.metamodel.mapping.EmbeddableMappingType; import org.hibernate.metamodel.mapping.NonAggregatedIdentifierMapping; import org.hibernate.metamodel.mapping.internal.IdClassEmbeddable; @@ -79,6 +80,12 @@ public Object getIdentifier(Object entity) { return delegate.getIdentifier( entity ); } + + @Override + public Object getIdentifier(Object entity, MergeContext mergeContext) { + return delegate.getIdentifier( entity, mergeContext ); + } + @Override public void setIdentifier(Object entity, Object id, SharedSessionContractImplementor session) { delegate.setIdentifier( entity, id, session ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleTableGroupProducer.java b/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleTableGroupProducer.java index f000e25a8a3c..ed67edec52f5 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleTableGroupProducer.java +++ b/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleTableGroupProducer.java @@ -6,6 +6,7 @@ */ package org.hibernate.query.derived; +import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Map; @@ -67,6 +68,8 @@ public class AnonymousTupleTableGroupProducer implements TableGroupProducer, Map private final JavaType javaTypeDescriptor; private final Map modelParts; private final Set compatibleTableExpressions; + private final int jdbcTypeCount; + private final List jdbcMappings = new ArrayList<>(); public AnonymousTupleTableGroupProducer( AnonymousTupleType tupleType, @@ -114,10 +117,14 @@ public AnonymousTupleTableGroupProducer( ); } modelParts.put( partName, modelPart ); + for ( int j = 0; j < modelPart.getJdbcTypeCount(); j++ ) { + jdbcMappings.add( modelPart.getJdbcMapping( j ) ); + } selectionIndex += modelPart.getJdbcTypeCount(); } this.modelParts = modelParts; this.compatibleTableExpressions = compatibleTableExpressions; + jdbcTypeCount = selectionIndex; } private ModelPart getModelPart(TableGroup tableGroup) { @@ -368,11 +375,16 @@ public int forEachDisassembledJdbcValue( @Override public JdbcMapping getJdbcMapping(int index) { - throw new UnsupportedOperationException( "Not yet implemented" ); + return jdbcMappings.get( index ); } @Override public int forEachJdbcType(int offset, IndexedConsumer action) { throw new UnsupportedOperationException( "Not yet implemented" ); } + + @Override + public int getJdbcTypeCount() { + return jdbcTypeCount; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleType.java b/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleType.java index be448dc0b16e..68fef456c7ef 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleType.java +++ b/hibernate-core/src/main/java/org/hibernate/query/derived/AnonymousTupleType.java @@ -11,8 +11,8 @@ import java.util.List; import java.util.Map; +import jakarta.persistence.metamodel.Bindable; import org.hibernate.Incubating; -import org.hibernate.internal.util.collections.CollectionHelper; import org.hibernate.metamodel.UnsupportedMappingException; import org.hibernate.metamodel.model.domain.DomainType; import org.hibernate.metamodel.model.domain.EntityDomainType; @@ -27,8 +27,9 @@ import org.hibernate.query.sqm.SqmPathSource; import org.hibernate.query.sqm.tree.domain.SqmPath; import org.hibernate.query.sqm.tree.select.SqmSelectClause; +import org.hibernate.query.sqm.tree.select.SqmSelectQuery; import org.hibernate.query.sqm.tree.select.SqmSelectableNode; -import org.hibernate.query.sqm.tree.select.SqmSubQuery; +import org.hibernate.query.sqm.tree.select.SqmSelection; import org.hibernate.sql.ast.spi.FromClauseAccess; import org.hibernate.sql.ast.spi.SqlSelection; import org.hibernate.type.descriptor.java.JavaType; @@ -36,6 +37,8 @@ import jakarta.persistence.metamodel.Attribute; +import static org.hibernate.internal.util.collections.CollectionHelper.linkedMapOfSize; + /** * @author Christian Beikov @@ -45,43 +48,58 @@ public class AnonymousTupleType implements TupleType, DomainType, Retur private final ObjectArrayJavaType javaTypeDescriptor; private final SqmSelectableNode[] components; + private final String[] componentNames; private final Map componentIndexMap; - public AnonymousTupleType(SqmSubQuery subQuery) { - this( extractSqmExpressibles( subQuery ) ); - } + public AnonymousTupleType(SqmSelectQuery selectQuery) { + final SqmSelectClause selectClause = selectQuery.getQueryPart() + .getFirstQuerySpec() + .getSelectClause(); - public AnonymousTupleType(SqmSelectableNode[] components) { - this.components = components; - this.javaTypeDescriptor = new ObjectArrayJavaType( getTypeDescriptors( components ) ); - final Map map = CollectionHelper.linkedMapOfSize( components.length ); - for ( int i = 0; i < components.length; i++ ) { - final SqmSelectableNode component = components[i]; - final String alias = component.getAlias(); + if ( selectClause == null || selectClause.getSelections().isEmpty() ) { + throw new IllegalArgumentException( "selectQuery has no selection items" ); + } + // todo: right now, we "snapshot" the state of the selectQuery when creating this type, but maybe we shouldn't? + // i.e. what if the selectQuery changes later on? Or should we somehow mark the selectQuery to signal, + // that changes to the select clause are invalid after a certain point? + + final List> selections = selectClause.getSelections(); + final List> selectableNodes = new ArrayList<>(); + final List aliases = new ArrayList<>(); + for ( SqmSelection selection : selections ) { + final boolean compound = selection.getSelectableNode().isCompoundSelection(); + selection.getSelectableNode().visitSubSelectableNodes( node -> { + selectableNodes.add( node ); + if ( compound ) { + aliases.add( node.getAlias() ); + } + } ); + if ( !compound ) { + // for compound selections we use the sub-selectable nodes aliases + aliases.add( selection.getAlias() ); + } + } + + components = new SqmSelectableNode[selectableNodes.size()]; + componentNames = new String[selectableNodes.size()]; + javaTypeDescriptor = new ObjectArrayJavaType( getTypeDescriptors( selectableNodes ) ); + componentIndexMap = linkedMapOfSize( selectableNodes.size() ); + for ( int i = 0; i < selectableNodes.size(); i++ ) { + components[i] = selectableNodes.get(i); + String alias = aliases.get( i ); if ( alias == null ) { throw new SemanticException( "Select item at position " + (i+1) + " in select list has no alias" + " (aliases are required in CTEs and in subqueries occurring in from clause)" ); } - map.put( alias, i ); - } - this.componentIndexMap = map; - } - - private static SqmSelectableNode[] extractSqmExpressibles(SqmSubQuery subQuery) { - final SqmSelectClause selectClause = subQuery.getQuerySpec().getSelectClause(); - if ( selectClause == null || selectClause.getSelectionItems().isEmpty() ) { - throw new IllegalArgumentException( "subquery has no selection items" ); + componentIndexMap.put( alias, i ); + componentNames[i] = alias; } - // todo: right now, we "snapshot" the state of the subquery when creating this type, but maybe we shouldn't? - // i.e. what if the subquery changes later on? Or should we somehow mark the subquery to signal, - // that changes to the select clause are invalid after a certain point? - return selectClause.getSelectionItems().toArray( SqmSelectableNode[]::new ); } - private static JavaType[] getTypeDescriptors(SqmSelectableNode[] components) { - final JavaType[] typeDescriptors = new JavaType[components.length]; - for ( int i = 0; i < components.length; i++ ) { - typeDescriptors[i] = components[i].getExpressible().getExpressibleJavaType(); + private static JavaType[] getTypeDescriptors(List> components) { + final JavaType[] typeDescriptors = new JavaType[components.size()]; + for ( int i = 0; i < components.size(); i++ ) { + typeDescriptors[i] = components.get( i ).getExpressible().getExpressibleJavaType(); } return typeDescriptors; } @@ -141,7 +159,7 @@ public int componentCount() { @Override public String getComponentName(int index) { - return components[index].getAlias(); + return componentNames[index]; } @Override @@ -177,20 +195,21 @@ public SqmPathSource findSubPathSource(String name) { final SqmSelectableNode component = components[index]; if ( component instanceof SqmPath ) { final SqmPath sqmPath = (SqmPath) component; - if ( sqmPath.getNodeType() instanceof SingularPersistentAttribute ) { + final Bindable model = sqmPath.getModel(); + if ( model instanceof SingularPersistentAttribute ) { //noinspection unchecked,rawtypes return new AnonymousTupleSqmAssociationPathSource( name, sqmPath, - ( (SingularPersistentAttribute) sqmPath.getNodeType() ).getType() + ( (SingularPersistentAttribute) model ).getType() ); } - else if ( sqmPath.getNodeType() instanceof PluralPersistentAttribute ) { + else if ( model instanceof PluralPersistentAttribute ) { //noinspection unchecked,rawtypes return new AnonymousTupleSqmAssociationPathSource( name, sqmPath, - ( (PluralPersistentAttribute) sqmPath.getNodeType() ).getElementType() + ( (PluralPersistentAttribute) model ).getElementType() ); } else if ( sqmPath.getNodeType() instanceof EntityDomainType ) { diff --git a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/NamedHqlQueryMementoImpl.java b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/NamedHqlQueryMementoImpl.java index 2cc32c0eb3ff..272c4a7cd6bb 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/NamedHqlQueryMementoImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/NamedHqlQueryMementoImpl.java @@ -22,6 +22,8 @@ import org.hibernate.query.sqm.spi.NamedSqmQueryMemento; import org.hibernate.query.sqm.tree.SqmStatement; +import org.checkerframework.checker.nullness.qual.Nullable; + /** * Definition of a named query, defined in the mapping metadata. * Additionally, as of JPA 2.1, named query definition can also come @@ -41,6 +43,7 @@ public class NamedHqlQueryMementoImpl extends AbstractNamedQueryMemento implemen public NamedHqlQueryMementoImpl( String name, + @Nullable Class resultType, String hqlString, Integer firstResult, Integer maxResults, @@ -57,6 +60,7 @@ public NamedHqlQueryMementoImpl( Map hints) { super( name, + resultType, cacheable, cacheRegion, cacheMode, @@ -103,6 +107,7 @@ public Map getParameterTypes() { public NamedSqmQueryMemento makeCopy(String name) { return new NamedHqlQueryMementoImpl( name, + getResultType(), hqlString, firstResult, maxResults, diff --git a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/QualifiedJoinPathConsumer.java b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/QualifiedJoinPathConsumer.java index 40cf38c349f1..365fb9af266d 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 @@ -27,6 +27,7 @@ import org.hibernate.query.sqm.tree.from.SqmEntityJoin; import org.hibernate.query.sqm.tree.from.SqmFrom; import org.hibernate.query.sqm.tree.from.SqmJoin; +import org.hibernate.query.sqm.tree.from.SqmQualifiedJoin; import org.hibernate.query.sqm.tree.from.SqmRoot; import org.jboss.logging.Logger; @@ -47,6 +48,7 @@ public class QualifiedJoinPathConsumer implements DotIdentifierConsumer { private final SqmJoinType joinType; private final boolean fetch; private final String alias; + private final boolean allowReuse; private ConsumerDelegate delegate; private boolean nested; @@ -56,11 +58,13 @@ public QualifiedJoinPathConsumer( SqmJoinType joinType, boolean fetch, String alias, + boolean allowReuse, SqmCreationState creationState) { this.sqmRoot = sqmRoot; this.joinType = joinType; this.fetch = fetch; this.alias = alias; + this.allowReuse = allowReuse; this.creationState = creationState; } @@ -74,6 +78,8 @@ public QualifiedJoinPathConsumer( this.joinType = joinType; this.fetch = fetch; this.alias = alias; + // This constructor is only used for entity names, so no need for join reuse + this.allowReuse = false; this.creationState = creationState; this.delegate = new AttributeJoinDelegate( sqmFrom, @@ -104,7 +110,13 @@ public void consumeIdentifier(String identifier, boolean isBase, boolean isTermi } else { assert delegate != null; - delegate.consumeIdentifier( identifier, !nested && isTerminal, !( nested && isTerminal ) ); + delegate.consumeIdentifier( + identifier, + !nested && isTerminal, + // Non-nested joins shall allow reuse, but nested ones (i.e. in treat) + // only allow join reuse for non-terminal parts + allowReuse && (!nested || !isTerminal) + ); } } @@ -197,7 +209,11 @@ private AttributeJoinDelegate resolveAlias(String identifier, boolean isTerminal if ( allowReuse ) { if ( !isTerminal ) { for ( SqmJoin sqmJoin : lhs.getSqmJoins() ) { - if ( sqmJoin.getAlias() == null && sqmJoin.getReferencedPathSource() == subPathSource ) { + // In order for an HQL join to be reusable, it must have the same path source, + if ( sqmJoin.getModel() == subPathSource + // and must not have a join condition. + && ( (SqmQualifiedJoin) sqmJoin ).getJoinPredicate() == null ) { + // We explicitly allow reusing implicit joins of any type return sqmJoin; } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SemanticQueryBuilder.java index f6ba427a3f69..eb486c3fd095 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 @@ -33,6 +33,7 @@ import java.util.Map; import java.util.Set; +import org.antlr.v4.runtime.Token; import org.hibernate.boot.registry.classloading.spi.ClassLoaderService; import org.hibernate.boot.registry.classloading.spi.ClassLoadingException; import org.hibernate.dialect.function.SqlColumn; @@ -807,87 +808,14 @@ public Object visitCte(HqlParser.CteContext ctx) { final JpaCteCriteria oldCte = currentPotentialRecursiveCte; try { currentPotentialRecursiveCte = null; - if ( queryExpressionContext instanceof HqlParser.SetQueryGroupContext ) { - final HqlParser.SetQueryGroupContext setContext = (HqlParser.SetQueryGroupContext) queryExpressionContext; - // A recursive query is only possible if the child count is lower than 5 e.g. `withClause? q1 op q2` - if ( setContext.getChildCount() < 5 ) { - final SetOperator setOperator = (SetOperator) setContext.getChild( setContext.getChildCount() - 2 ) - .accept( this ); - switch ( setOperator ) { - case UNION: - case UNION_ALL: - final HqlParser.OrderedQueryContext nonRecursiveQueryContext; - final HqlParser.OrderedQueryContext recursiveQueryContext; - // On count == 4, we have a withClause at index 0 - if ( setContext.getChildCount() == 4 ) { - nonRecursiveQueryContext = (HqlParser.OrderedQueryContext) setContext.getChild( 1 ); - recursiveQueryContext = (HqlParser.OrderedQueryContext) setContext.getChild( 3 ); - } - else { - nonRecursiveQueryContext = (HqlParser.OrderedQueryContext) setContext.getChild( 0 ); - recursiveQueryContext = (HqlParser.OrderedQueryContext) setContext.getChild( 2 ); - } - // First visit the non-recursive part - nonRecursiveQueryContext.accept( this ); - - // Visiting the possibly recursive part must happen within the call to SqmCteContainer.with, - // because in there, the SqmCteStatement/JpaCteCriteria is available for use in the recursive part. - // The structure (SqmCteTable) for the SqmCteStatement is based on the non-recursive part, - // which is necessary to have, so that the SqmCteRoot/SqmCteJoin can resolve sub-paths. - final SqmSelectStatement recursivePart = new SqmSelectStatement<>( creationContext.getNodeBuilder() ); - - processingStateStack.pop(); - processingStateStack.push( - new SqmQueryPartCreationProcessingStateStandardImpl( - processingStateStack.getCurrent(), - recursivePart, - this - ) - ); - final JpaCteCriteria cteDefinition; - if ( setOperator == SetOperator.UNION ) { - cteDefinition = cteContainer.withRecursiveUnionDistinct( - name, - cte, - cteCriteria -> { - currentPotentialRecursiveCte = cteCriteria; - recursiveQueryContext.accept( this ); - return recursivePart; - } - ); - } - else { - cteDefinition = cteContainer.withRecursiveUnionAll( - name, - cte, - cteCriteria -> { - currentPotentialRecursiveCte = cteCriteria; - recursiveQueryContext.accept( this ); - return recursivePart; - } - ); - } - if ( materialization != null ) { - cteDefinition.setMaterialization( materialization ); - } - final ParseTree lastChild = ctx.getChild( ctx.getChildCount() - 1 ); - final ParseTree potentialSearchClause; - if ( lastChild instanceof HqlParser.CycleClauseContext ) { - applyCycleClause( cteDefinition, (HqlParser.CycleClauseContext) lastChild ); - potentialSearchClause = ctx.getChild( ctx.getChildCount() - 2 ); - } - else { - potentialSearchClause = lastChild; - } - if ( potentialSearchClause instanceof HqlParser.SearchClauseContext ) { - applySearchClause( cteDefinition, (HqlParser.SearchClauseContext) potentialSearchClause ); - } - return null; - } + // A recursive query is only possible if there are 2 ordered queries e.g. `q1 op q2` + if ( queryExpressionContext.orderedQuery().size() == 2 ) { + if ( handleRecursive( ctx, queryExpressionContext, cteContainer, name, cte, materialization ) ) { + return null; } } queryExpressionContext.accept( this ); - final JpaCteCriteria cteDefinition = cteContainer.with( name, cte ); + final JpaCteCriteria cteDefinition = cteContainer.with( name, cte ); if ( materialization != null ) { cteDefinition.setMaterialization( materialization ); } @@ -899,6 +827,76 @@ public Object visitCte(HqlParser.CteContext ctx) { return null; } + private boolean handleRecursive( + HqlParser.CteContext cteContext, + HqlParser.QueryExpressionContext setContext, + SqmCteContainer cteContainer, + String name, + SqmSelectQuery cte, + CteMaterialization materialization) { + final SetOperator setOperator = (SetOperator) setContext.setOperator(0).accept( this ); + switch ( setOperator ) { + case UNION: + case UNION_ALL: + final var nonRecursiveQueryContext = setContext.orderedQuery(0); + final var recursiveQueryContext = setContext.orderedQuery(1); + // First visit the non-recursive part + nonRecursiveQueryContext.accept( this ); + + // Visiting the possibly recursive part must happen within the call to SqmCteContainer.with, + // because in there, the SqmCteStatement/JpaCteCriteria is available for use in the recursive part. + // The structure (SqmCteTable) for the SqmCteStatement is based on the non-recursive part, + // which is necessary to have, so that the SqmCteRoot/SqmCteJoin can resolve sub-paths. + final SqmSelectStatement recursivePart = + new SqmSelectStatement<>( creationContext.getNodeBuilder() ); + + processingStateStack.pop(); + processingStateStack.push( + new SqmQueryPartCreationProcessingStateStandardImpl( + processingStateStack.getCurrent(), + recursivePart, + this + ) + ); + final JpaCteCriteria cteDefinition; + if ( setOperator == SetOperator.UNION ) { + cteDefinition = cteContainer.withRecursiveUnionDistinct( + name, + cte, + cteCriteria -> { + currentPotentialRecursiveCte = cteCriteria; + recursiveQueryContext.accept( this ); + return recursivePart; + } + ); + } + else { + cteDefinition = cteContainer.withRecursiveUnionAll( + name, + cte, + cteCriteria -> { + currentPotentialRecursiveCte = cteCriteria; + recursiveQueryContext.accept( this ); + return recursivePart; + } + ); + } + if ( materialization != null ) { + cteDefinition.setMaterialization( materialization ); + } + final var cycleClauseContext = cteContext.cycleClause(); + if ( cycleClauseContext != null ) { + applyCycleClause( cteDefinition, cycleClauseContext ); + } + final var searchClauseContext = cteContext.searchClause(); + if ( searchClauseContext != null ) { + applySearchClause( cteDefinition, searchClauseContext ); + } + return true; + } + return false; + } + private void applyCycleClause(JpaCteCriteria cteDefinition, HqlParser.CycleClauseContext ctx) { final HqlParser.CteAttributesContext attributesContext = ctx.cteAttributes(); final String cycleMarkAttributeName = visitIdentifier( (HqlParser.IdentifierContext) ctx.getChild( 3 ) ); @@ -1016,15 +1014,6 @@ private void applySearchClause(JpaCteCriteria cteDefinition, HqlParser.Search cteDefinition.search( kind, searchAttributeName, searchOrders ); } - @Override - public SqmQueryPart visitSimpleQueryGroup(HqlParser.SimpleQueryGroupContext ctx) { - final int lastChild = ctx.getChildCount() - 1; - if ( lastChild != 0 ) { - ctx.getChild( 0 ).accept( this ); - } - return (SqmQueryPart) ctx.getChild( lastChild ).accept( this ); - } - @Override public SqmQueryPart visitQueryOrderExpression(HqlParser.QueryOrderExpressionContext ctx) { final SqmQuerySpec sqmQuerySpec = currentQuerySpec(); @@ -1064,37 +1053,41 @@ public SqmQueryPart visitNestedQueryExpression(HqlParser.NestedQueryExpressio } @Override - public SqmQueryGroup visitSetQueryGroup(HqlParser.SetQueryGroupContext ctx) { - final List children = ctx.children; - final int firstIndex; - if ( children.get( 0 ) instanceof HqlParser.WithClauseContext ) { - children.get( 0 ).accept( this ); - firstIndex = 1; + public SqmQueryPart visitQueryExpression(HqlParser.QueryExpressionContext ctx) { + var withClauseContext = ctx.withClause(); + if ( withClauseContext != null ) { + withClauseContext.accept( this ); } - else { - firstIndex = 0; + final var orderedQueryContexts = ctx.orderedQuery(); + final SqmQueryPart firstQueryPart = + (SqmQueryPart) orderedQueryContexts.get( 0 ).accept( this ); + if ( orderedQueryContexts.size() == 1 ) { + return firstQueryPart; } if ( creationOptions.useStrictJpaCompliance() ) { throw new StrictJpaComplianceViolation( StrictJpaComplianceViolation.Type.SET_OPERATIONS ); } - final SqmQueryPart firstQueryPart = (SqmQueryPart) children.get( firstIndex ).accept( this ); SqmQueryGroup queryGroup; - if ( firstQueryPart instanceof SqmQueryGroup) { + if ( firstQueryPart instanceof SqmQueryGroup ) { queryGroup = (SqmQueryGroup) firstQueryPart; } else { queryGroup = new SqmQueryGroup<>( firstQueryPart ); } setCurrentQueryPart( queryGroup ); - final int size = children.size(); + final var setOperatorContexts = ctx.setOperator(); final SqmCreationProcessingState firstProcessingState = processingStateStack.pop(); - for ( int i = firstIndex + 1; i < size; i += 2 ) { - final SetOperator operator = visitSetOperator( (HqlParser.SetOperatorContext) children.get(i) ); - final HqlParser.OrderedQueryContext simpleQueryCtx = - (HqlParser.OrderedQueryContext) children.get( i + 1 ); - queryGroup = getSqmQueryGroup( operator, simpleQueryCtx, queryGroup, size, firstProcessingState, i ); + for ( int i = 0; i < setOperatorContexts.size(); i++ ) { + queryGroup = getSqmQueryGroup( + visitSetOperator( setOperatorContexts.get(i) ), + orderedQueryContexts.get( i + 1 ), + queryGroup, + setOperatorContexts.size(), + firstProcessingState, + i + ); } processingStateStack.push( firstProcessingState ); @@ -1108,8 +1101,6 @@ private SqmQueryGroup getSqmQueryGroup( int size, SqmCreationProcessingState firstProcessingState, int i) { - - final List> queryParts; processingStateStack.push( new SqmQueryPartCreationProcessingStateStandardImpl( processingStateStack.getCurrent(), @@ -1117,7 +1108,9 @@ private SqmQueryGroup getSqmQueryGroup( this ) ); - if ( queryGroup.getSetOperator() == null || queryGroup.getSetOperator() == operator ) { + final List> queryParts; + final SetOperator setOperator = queryGroup.getSetOperator(); + if ( setOperator == null || setOperator == operator ) { queryGroup.setSetOperator( operator ); queryParts = queryGroup.queryParts(); } @@ -1129,15 +1122,14 @@ private SqmQueryGroup getSqmQueryGroup( } try { - final List subChildren = simpleQueryCtx.children; - if ( subChildren.get( 0 ) instanceof HqlParser.QueryContext ) { + if ( simpleQueryCtx instanceof HqlParser.QuerySpecExpressionContext ) { final SqmQuerySpec querySpec = new SqmQuerySpec<>( creationContext.getNodeBuilder() ); queryParts.add( querySpec ); visitQuerySpecExpression( (HqlParser.QuerySpecExpressionContext) simpleQueryCtx ); } - else { + else if ( simpleQueryCtx instanceof HqlParser.NestedQueryExpressionContext ) { try { - final SqmSelectStatement selectStatement = + final SqmSelectStatement selectStatement = new SqmSelectStatement<>( creationContext.getNodeBuilder() ); processingStateStack.push( new SqmQueryPartCreationProcessingStateStandardImpl( @@ -1155,6 +1147,7 @@ private SqmQueryGroup getSqmQueryGroup( processingStateStack.pop(); } } + // else if QueryOrderExpressionContext, nothing to do } finally { processingStateStack.pop(); @@ -1898,8 +1891,34 @@ public Object visitGeneralPathExpression(HqlParser.GeneralPathExpressionContext } @Override - public SqmExpression visitFunctionExpression(HqlParser.FunctionExpressionContext ctx) { - return (SqmExpression) ctx.function().accept( this ); + public Object visitFunctionExpression(HqlParser.FunctionExpressionContext ctx) { + final var slicedFragmentsCtx = ctx.slicedPathAccessFragment(); + if ( slicedFragmentsCtx != null ) { + final List slicedFragments = slicedFragmentsCtx.expression(); + return getFunctionDescriptor( "array_slice" ).generateSqmExpression( + List.of( + (SqmTypedNode) visitFunction( ctx.function() ), + (SqmTypedNode) slicedFragments.get( 0 ).accept( this ), + (SqmTypedNode) slicedFragments.get( 1 ).accept( this ) + ), + null, + creationContext.getQueryEngine() + ); + } + else { + final var function = (SqmExpression) visitFunction( ctx.function() ); + final var indexedPathAccessFragment = ctx.indexedPathAccessFragment(); + final var pathContinuation = ctx.pathContinuation(); + if ( indexedPathAccessFragment == null && pathContinuation == null ) { + return function; + } + else { + return visitPathContinuation( + visitIndexedPathAccessFragment( (SemanticPathPart) function, indexedPathAccessFragment ), + pathContinuation + ); + } + } } @Override @@ -2219,10 +2238,12 @@ protected void consumeJoin(HqlParser.JoinContext parserJoin, SqmRoot sqmR throw new SemanticException( "The 'from' clause of a subquery has a 'fetch'", query ); } - dotIdentifierConsumerStack.push( new QualifiedJoinPathConsumer( sqmRoot, joinType, fetch, alias, this ) ); + final HqlParser.JoinRestrictionContext joinRestrictionContext = parserJoin.joinRestriction(); + // Joins are allowed to be reused if they don't have a join condition + final boolean allowReuse = joinRestrictionContext == null; + dotIdentifierConsumerStack.push( new QualifiedJoinPathConsumer( sqmRoot, joinType, fetch, alias, allowReuse, this ) ); try { final SqmQualifiedJoin join = getJoin( sqmRoot, joinType, qualifiedJoinTargetContext, alias, fetch ); - final HqlParser.JoinRestrictionContext joinRestrictionContext = parserJoin.joinRestriction(); if ( join instanceof SqmEntityJoin || join instanceof SqmDerivedJoin || join instanceof SqmCteJoin ) { sqmRoot.addSqmJoin( join ); } @@ -2338,7 +2359,7 @@ protected void consumeJpaCollectionJoin(HqlParser.JpaCollectionJoinContext ctx, final String alias = extractAlias( ctx.variable() ); dotIdentifierConsumerStack.push( // According to JPA spec 4.4.6 this is an inner join - new QualifiedJoinPathConsumer( sqmRoot, SqmJoinType.INNER, false, alias, this ) + new QualifiedJoinPathConsumer( sqmRoot, SqmJoinType.INNER, false, alias, true, this ) ); try { @@ -2435,90 +2456,135 @@ public SqmBetweenPredicate visitBetweenPredicate(HqlParser.BetweenPredicateConte ); } - @Override - public SqmNullnessPredicate visitIsNullPredicate(HqlParser.IsNullPredicateContext ctx) { - return new SqmNullnessPredicate( - (SqmExpression) ctx.expression().accept( this ), - ctx.NOT() != null, - creationContext.getNodeBuilder() - ); + public SqmPredicate visitUnaryIsPredicate(HqlParser.UnaryIsPredicateContext ctx) { + final var expression = (SqmExpression) ctx.expression().accept( this ); + final var negated = ctx.NOT() != null; + final var nodeBuilder = creationContext.getNodeBuilder(); + switch ( ((TerminalNode) ctx.getChild( ctx.getChildCount() - 1 )).getSymbol().getType() ) { + case HqlParser.NULL: + return new SqmNullnessPredicate( expression, negated, nodeBuilder ); + case HqlParser.EMPTY: + if ( expression instanceof SqmPluralValuedSimplePath ) { + return new SqmEmptinessPredicate( (SqmPluralValuedSimplePath) expression, negated, nodeBuilder ); + } + else { + throw new SemanticException( "Operand of 'is empty' operator must be a plural path", query ); + } + case HqlParser.TRUE: + return new SqmTruthnessPredicate( expression, true, negated, nodeBuilder ); + case HqlParser.FALSE: + return new SqmTruthnessPredicate( expression, false, negated, nodeBuilder ); + default: + throw new AssertionError( "Unknown unary is predicate: " + ctx.getChild( ctx.getChildCount() - 1 ) ); + } } @Override - public SqmEmptinessPredicate visitIsEmptyPredicate(HqlParser.IsEmptyPredicateContext ctx) { - SqmExpression expression = (SqmExpression) ctx.expression().accept(this); - if ( expression instanceof SqmPluralValuedSimplePath ) { - return new SqmEmptinessPredicate( - (SqmPluralValuedSimplePath) expression, - ctx.NOT() != null, - creationContext.getNodeBuilder() - ); + public SqmPredicate visitBinaryExpressionPredicate(HqlParser.BinaryExpressionPredicateContext ctx) { + final var firstSymbol = ((TerminalNode) ctx.getChild( 1 )).getSymbol(); + final boolean negated; + final Token operationSymbol; + if ( firstSymbol.getType() == HqlParser.NOT ) { + negated = true; + operationSymbol = ((TerminalNode) ctx.getChild( 2 )).getSymbol(); } else { - throw new SemanticException( "Operand of 'is empty' operator must be a plural path", query ); - } - } - - @Override - public Object visitIsTruePredicate(HqlParser.IsTruePredicateContext ctx) { - return new SqmTruthnessPredicate( - (SqmExpression) ctx.expression().accept( this ), - true, - ctx.NOT() != null, - creationContext.getNodeBuilder() - ); - } - - @Override - public Object visitIsFalsePredicate(HqlParser.IsFalsePredicateContext ctx) { - return new SqmTruthnessPredicate( - (SqmExpression) ctx.expression().accept( this ), - false, - ctx.NOT() != null, - creationContext.getNodeBuilder() - ); - } - - @Override - public Object visitComparisonOperator(HqlParser.ComparisonOperatorContext ctx) { - final TerminalNode firstToken = (TerminalNode) ctx.getChild( 0 ); - switch ( firstToken.getSymbol().getType() ) { - case HqlLexer.EQUAL: - return ComparisonOperator.EQUAL; - case HqlLexer.NOT_EQUAL: - return ComparisonOperator.NOT_EQUAL; - case HqlLexer.LESS: - return ComparisonOperator.LESS_THAN; - case HqlLexer.LESS_EQUAL: - return ComparisonOperator.LESS_THAN_OR_EQUAL; - case HqlLexer.GREATER: - return ComparisonOperator.GREATER_THAN; - case HqlLexer.GREATER_EQUAL: - return ComparisonOperator.GREATER_THAN_OR_EQUAL; + negated = firstSymbol.getType() == HqlParser.IS + && ((TerminalNode) ctx.getChild( 2 )).getSymbol().getType() == HqlParser.NOT; + operationSymbol = firstSymbol; + } + final var expressions = ctx.expression(); + final var lhsCtx = expressions.get( 0 ); + final var rhsCtx = expressions.get( 1 ); + switch ( operationSymbol.getType() ) { + case HqlParser.CONTAINS: { + final var lhs = (SqmExpression) lhsCtx.accept( this ); + final var rhs = (SqmExpression) rhsCtx.accept( this ); + final var lhsExpressible = lhs.getExpressible(); + if ( lhsExpressible != null && !(lhsExpressible.getSqmType() instanceof BasicPluralType) ) { + throw new SemanticException( + "First operand for contains predicate must be a basic plural type expression, but found: " + lhsExpressible.getSqmType(), + query + ); + } + final SelfRenderingSqmFunction contains = getFunctionDescriptor( + "array_contains" ).generateSqmExpression( + asList( lhs, rhs ), + null, + creationContext.getQueryEngine() + ); + return new SqmBooleanExpressionPredicate( contains, negated, creationContext.getNodeBuilder() ); + } + case HqlParser.INCLUDES: { + final var lhs = (SqmExpression) lhsCtx.accept( this ); + final var rhs = (SqmExpression) rhsCtx.accept( this ); + final var lhsExpressible = lhs.getExpressible(); + final var rhsExpressible = rhs.getExpressible(); + if ( lhsExpressible != null && !( lhsExpressible.getSqmType() instanceof BasicPluralType) ) { + throw new SemanticException( + "First operand for includes predicate must be a basic plural type expression, but found: " + + lhsExpressible.getSqmType(), + query + ); + } + if ( rhsExpressible != null && !( rhsExpressible.getSqmType() instanceof BasicPluralType) ) { + throw new SemanticException( + "Second operand for includes predicate must be a basic plural type expression, but found: " + + rhsExpressible.getSqmType(), + query + ); + } + final SelfRenderingSqmFunction contains = getFunctionDescriptor( "array_includes" ).generateSqmExpression( + asList( lhs, rhs ), + null, + creationContext.getQueryEngine() + ); + return new SqmBooleanExpressionPredicate( contains, negated, creationContext.getNodeBuilder() ); + } + case HqlParser.INTERSECTS: { + final var lhs = (SqmExpression) lhsCtx.accept( this ); + final var rhs = (SqmExpression) rhsCtx.accept( this ); + final var lhsExpressible = lhs.getExpressible(); + if ( lhsExpressible != null && !( lhsExpressible.getSqmType() instanceof BasicPluralType ) ) { + throw new SemanticException( + "First operand for intersects predicate must be a basic plural type expression, but found: " + + lhsExpressible.getSqmType(), + query + ); + } + final SelfRenderingSqmFunction contains = + getFunctionDescriptor( "array_intersects" ) + .generateSqmExpression( + asList( lhs, rhs ), + null, + creationContext.getQueryEngine() + ); + return new SqmBooleanExpressionPredicate( contains, negated, creationContext.getNodeBuilder() ); + } + case HqlParser.EQUAL: + return createComparisonPredicate( ComparisonOperator.EQUAL, lhsCtx, rhsCtx ); + case HqlParser.NOT_EQUAL: + return createComparisonPredicate( ComparisonOperator.NOT_EQUAL, lhsCtx, rhsCtx ); + case HqlParser.LESS: + return createComparisonPredicate( ComparisonOperator.LESS_THAN, lhsCtx, rhsCtx ); + case HqlParser.LESS_EQUAL: + return createComparisonPredicate( ComparisonOperator.LESS_THAN_OR_EQUAL, lhsCtx, rhsCtx ); + case HqlParser.GREATER: + return createComparisonPredicate( ComparisonOperator.GREATER_THAN, lhsCtx, rhsCtx ); + case HqlParser.GREATER_EQUAL: + return createComparisonPredicate( ComparisonOperator.GREATER_THAN_OR_EQUAL, lhsCtx, rhsCtx ); + case HqlParser.IS: { + final ComparisonOperator comparisonOperator = !negated + ? ComparisonOperator.DISTINCT_FROM + : ComparisonOperator.NOT_DISTINCT_FROM; + return createComparisonPredicate( comparisonOperator, lhsCtx, rhsCtx ); + } default: - throw new ParsingException("Unrecognized comparison operator"); + throw new AssertionError( "Unknown binary expression predicate: " + operationSymbol ); } } - @Override - public SqmPredicate visitComparisonPredicate(HqlParser.ComparisonPredicateContext ctx) { - final ComparisonOperator comparisonOperator = (ComparisonOperator) ctx.comparisonOperator().accept( this ); - final HqlParser.ExpressionContext leftExpressionContext = ctx.expression( 0 ); - final HqlParser.ExpressionContext rightExpressionContext = ctx.expression( 1 ); - return createComparisonPredicate( comparisonOperator, leftExpressionContext, rightExpressionContext ); - } - - @Override - public SqmPredicate visitIsDistinctFromPredicate(HqlParser.IsDistinctFromPredicateContext ctx) { - final HqlParser.ExpressionContext leftExpressionContext = ctx.expression( 0 ); - final HqlParser.ExpressionContext rightExpressionContext = ctx.expression( 1 ); - final ComparisonOperator comparisonOperator = ctx.NOT() == null - ? ComparisonOperator.DISTINCT_FROM - : ComparisonOperator.NOT_DISTINCT_FROM; - return createComparisonPredicate( comparisonOperator, leftExpressionContext, rightExpressionContext ); - } - private SqmComparisonPredicate createComparisonPredicate( ComparisonOperator comparisonOperator, HqlParser.ExpressionContext leftExpressionContext, @@ -2645,73 +2711,6 @@ private String getPossibleEnumValue(HqlParser.ExpressionContext expressionContex return null; } - @Override - public SqmPredicate visitContainsPredicate(HqlParser.ContainsPredicateContext ctx) { - final boolean negated = ctx.NOT() != null; - final SqmExpression lhs = (SqmExpression) ctx.expression( 0 ).accept( this ); - final SqmExpression rhs = (SqmExpression) ctx.expression( 1 ).accept( this ); - final SqmExpressible lhsExpressible = lhs.getExpressible(); - if ( lhsExpressible != null && !( lhsExpressible.getSqmType() instanceof BasicPluralType) ) { - throw new SemanticException( - "First operand for contains predicate must be a basic plural type expression, but found: " + lhsExpressible.getSqmType(), - query - ); - } - final SelfRenderingSqmFunction contains = getFunctionDescriptor( "array_contains" ).generateSqmExpression( - asList( lhs, rhs ), - null, - creationContext.getQueryEngine() - ); - return new SqmBooleanExpressionPredicate( contains, negated, creationContext.getNodeBuilder() ); - } - - @Override - public SqmPredicate visitIncludesPredicate(HqlParser.IncludesPredicateContext ctx) { - final boolean negated = ctx.NOT() != null; - final SqmExpression lhs = (SqmExpression) ctx.expression( 0 ).accept( this ); - final SqmExpression rhs = (SqmExpression) ctx.expression( 1 ).accept( this ); - final SqmExpressible lhsExpressible = lhs.getExpressible(); - final SqmExpressible rhsExpressible = rhs.getExpressible(); - if ( lhsExpressible != null && !( lhsExpressible.getSqmType() instanceof BasicPluralType) ) { - throw new SemanticException( - "First operand for includes predicate must be a basic plural type expression, but found: " + lhsExpressible.getSqmType(), - query - ); - } - if ( rhsExpressible != null && !( rhsExpressible.getSqmType() instanceof BasicPluralType) ) { - throw new SemanticException( - "Second operand for includes predicate must be a basic plural type expression, but found: " + rhsExpressible.getSqmType(), - query - ); - } - final SelfRenderingSqmFunction contains = getFunctionDescriptor( "array_includes" ).generateSqmExpression( - asList( lhs, rhs ), - null, - creationContext.getQueryEngine() - ); - return new SqmBooleanExpressionPredicate( contains, negated, creationContext.getNodeBuilder() ); - } - - @Override - public SqmPredicate visitIntersectsPredicate(HqlParser.IntersectsPredicateContext ctx) { - final boolean negated = ctx.NOT() != null; - final SqmExpression lhs = (SqmExpression) ctx.expression( 0 ).accept( this ); - final SqmExpression rhs = (SqmExpression) ctx.expression( 1 ).accept( this ); - final SqmExpressible lhsExpressible = lhs.getExpressible(); - if ( lhsExpressible != null && !( lhsExpressible.getSqmType() instanceof BasicPluralType) ) { - throw new SemanticException( - "First operand for intersects predicate must be a basic plural type expression, but found: " + lhsExpressible.getSqmType(), - query - ); - } - final SelfRenderingSqmFunction contains = getFunctionDescriptor( "array_intersects" ).generateSqmExpression( - asList( lhs, rhs ), - null, - creationContext.getQueryEngine() - ); - return new SqmBooleanExpressionPredicate( contains, negated, creationContext.getNodeBuilder() ); - } - @Override public SqmPredicate visitLikePredicate(HqlParser.LikePredicateContext ctx) { final boolean negated = ctx.NOT() != null; @@ -2886,11 +2885,14 @@ else if ( inListContext instanceof HqlParser.ArrayInListContext ) { final SqmExpression arrayExpr = (SqmExpression) arrayInListContext.expression().accept( this ); final SqmExpressible arrayExpressible = arrayExpr.getExpressible(); - if ( arrayExpressible != null && !( arrayExpressible.getSqmType() instanceof BasicPluralType) ) { - throw new SemanticException( - "Right operand for in-array predicate must be a basic plural type expression, but found: " + arrayExpressible.getSqmType(), - query - ); + if ( arrayExpressible != null ) { + if ( !(arrayExpressible.getSqmType() instanceof BasicPluralType) ) { + throw new SemanticException( + "Right operand for in-array predicate must be a basic plural type expression, but found: " + arrayExpressible.getSqmType(), + query + ); + } + testExpression.applyInferableType( ( (BasicPluralType) arrayExpressible.getSqmType() ).getElementType() ); } final SelfRenderingSqmFunction contains = getFunctionDescriptor( "array_contains" ).generateSqmExpression( asList( arrayExpr, testExpression ), @@ -2971,9 +2973,15 @@ public SqmPath visitEntityIdReference(HqlParser.EntityIdReferenceContext ctx) final SqmPath sqmPath = consumeDomainPath( ctx.path() ); final DomainType sqmPathType = sqmPath.getReferencedPathSource().getSqmPathType(); - if ( sqmPathType instanceof IdentifiableDomainType ) { - final SqmPathSource identifierDescriptor = ( (IdentifiableDomainType) sqmPathType ).getIdentifierDescriptor(); + final IdentifiableDomainType identifiableType = (IdentifiableDomainType) sqmPathType; + final SqmPathSource identifierDescriptor = identifiableType.getIdentifierDescriptor(); + if ( identifierDescriptor == null ) { + // mainly for benefit of Hibernate Processor + throw new FunctionArgumentException( "Argument '" + sqmPath.getNavigablePath() + + "' of 'id()' is a '" + identifiableType.getTypeName() + + "' and does not have a well-defined '@Id' attribute" ); + } return sqmPath.get( identifierDescriptor.getPathName() ); } else if ( sqmPath instanceof SqmAnyValuedSimplePath ) { @@ -2981,7 +2989,7 @@ else if ( sqmPath instanceof SqmAnyValuedSimplePath ) { } else { throw new FunctionArgumentException( "Argument '" + sqmPath.getNavigablePath() - + "' of 'id()' function does not resolve to an entity type" ); + + "' of 'id()' does not resolve to an entity type" ); } } @@ -2994,26 +3002,22 @@ public SqmExpression visitEntityVersionExpression(HqlParser.EntityVersionExpr public SqmPath visitEntityVersionReference(HqlParser.EntityVersionReferenceContext ctx) { final SqmPath sqmPath = consumeDomainPath( ctx.path() ); final DomainType sqmPathType = sqmPath.getReferencedPathSource().getSqmPathType(); - if ( sqmPathType instanceof IdentifiableDomainType ) { - @SuppressWarnings("unchecked") - final IdentifiableDomainType identifiableType = (IdentifiableDomainType) sqmPathType; - final SingularPersistentAttribute versionAttribute = identifiableType.findVersionAttribute(); - if ( versionAttribute == null ) { - throw new FunctionArgumentException( - String.format( - "Argument '%s' of 'version()' function resolved to entity type '%s' which does not have a '@Version' attribute", - sqmPath.getNavigablePath(), - identifiableType.getTypeName() - ) - ); + final IdentifiableDomainType identifiableType = (IdentifiableDomainType) sqmPathType; + if ( !identifiableType.hasVersionAttribute() ) { + throw new FunctionArgumentException( "Argument '" + sqmPath.getNavigablePath() + + "' of 'version()' is a '" + identifiableType.getTypeName() + + "' and does not have a '@Version' attribute" ); } - + @SuppressWarnings("unchecked") + final SingularPersistentAttribute versionAttribute = + (SingularPersistentAttribute) identifiableType.findVersionAttribute(); return sqmPath.get( versionAttribute ); } - - throw new FunctionArgumentException( "Argument '" + sqmPath.getNavigablePath() - + "' of 'version()' function does not resolve to an entity type" ); + else { + throw new FunctionArgumentException( "Argument '" + sqmPath.getNavigablePath() + + "' of 'version()' does not resolve to an entity type" ); + } } @Override @@ -3032,41 +3036,30 @@ public SqmPath visitEntityNaturalIdReference(HqlParser.EntityNaturalIdReferen if ( sqmPathType instanceof IdentifiableDomainType ) { @SuppressWarnings("unchecked") - final IdentifiableDomainType identifiableType = (IdentifiableDomainType) sqmPathType; + final IdentifiableDomainType identifiableType = (IdentifiableDomainType) sqmPathType; final List> attributes = identifiableType.findNaturalIdAttributes(); if ( attributes == null ) { - throw new FunctionArgumentException( - String.format( - "Argument '%s' of 'naturalid()' function resolved to entity type '%s' which does not have a natural id", - sqmPath.getNavigablePath(), - identifiableType.getTypeName() - ) + throw new FunctionArgumentException( "Argument '" + sqmPath.getNavigablePath() + + "' of 'naturalid()' is a '" + identifiableType.getTypeName() + + "' and does not have a natural id" ); } else if ( attributes.size() >1 ) { - throw new FunctionArgumentException( - String.format( - "Argument '%s' of 'naturalid()' function resolved to entity type '%s' which has a composite natural id", - sqmPath.getNavigablePath(), - identifiableType.getTypeName() - ) + throw new FunctionArgumentException( "Argument '" + sqmPath.getNavigablePath() + + "' of 'naturalid()' is a '" + identifiableType.getTypeName() + + "' and has a composite natural id" ); } @SuppressWarnings("unchecked") - SingularAttribute naturalIdAttribute + final SingularAttribute naturalIdAttribute = (SingularAttribute) attributes.get(0); return sqmPath.get( naturalIdAttribute ); } throw new FunctionArgumentException( "Argument '" + sqmPath.getNavigablePath() - + "' of 'naturalid()' function does not resolve to an entity type" ); + + "' of 'naturalid()' does not resolve to an entity type" ); } -// -// @Override -// public Object visitToOneFkExpression(HqlParser.ToOneFkExpressionContext ctx) { -// return visitToOneFkReference( (HqlParser.ToOneFkReferenceContext) ctx.getChild( 0 ) ); -// } @Override public SqmFkExpression visitToOneFkReference(HqlParser.ToOneFkReferenceContext ctx) { @@ -5263,9 +5256,11 @@ public SqmSubQuery visitSubquery(HqlParser.SubqueryContext ctx) { final List> selections = subQuery.getQuerySpec().getSelectClause().getSelections(); if ( selections.size() == 1 ) { - subQuery.applyInferableType( selections.get( 0 ).getExpressible().getSqmType() ); + final SqmExpressible expressible = selections.get( 0 ).getExpressible(); + if ( expressible != null ) { + subQuery.applyInferableType( expressible.getSqmType() ); + } } - return subQuery; } finally { @@ -5308,33 +5303,6 @@ else if ( ctx.collectionValueNavigablePath() != null ) { else if ( ctx.mapKeyNavigablePath() != null ) { return visitMapKeyNavigablePath( ctx.mapKeyNavigablePath() ); } - else if ( ctx.toOneFkReference() != null ) { - return visitToOneFkReference( ctx.toOneFkReference() ); - } - else if ( ctx.function() != null ) { - final HqlParser.SlicedPathAccessFragmentContext slicedFragmentsCtx = ctx.slicedPathAccessFragment(); - if ( slicedFragmentsCtx != null ) { - final List slicedFragments = slicedFragmentsCtx.expression(); - return getFunctionDescriptor( "array_slice" ).generateSqmExpression( - List.of( - (SqmTypedNode) visitFunction( ctx.function() ), - (SqmTypedNode) slicedFragments.get( 0 ).accept( this ), - (SqmTypedNode) slicedFragments.get( 1 ).accept( this ) - ), - null, - creationContext.getQueryEngine() - ); - } - else { - return visitPathContinuation( - visitIndexedPathAccessFragment( - (SemanticPathPart) visitFunction( ctx.function() ), - ctx.indexedPathAccessFragment() - ), - ctx.pathContinuation() - ); - } - } else if ( ctx.simplePath() != null && ctx.indexedPathAccessFragment() != null ) { return visitIndexedPathAccessFragment( visitSimplePath( ctx.simplePath() ), ctx.indexedPathAccessFragment() ); } diff --git a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SqmPathRegistryImpl.java b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SqmPathRegistryImpl.java index 66df7acc85ae..5f2d34097ae4 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SqmPathRegistryImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/hql/internal/SqmPathRegistryImpl.java @@ -17,6 +17,7 @@ import org.hibernate.metamodel.model.domain.BasicDomainType; import org.hibernate.metamodel.model.domain.spi.JpaMetamodelImplementor; import org.hibernate.query.SemanticException; +import org.hibernate.query.criteria.JpaJoinedFrom; import org.hibernate.query.hql.HqlLogging; import org.hibernate.query.hql.spi.SqmCreationProcessingState; import org.hibernate.query.hql.spi.SqmPathRegistry; @@ -24,12 +25,10 @@ import org.hibernate.query.sqm.ParsingException; import org.hibernate.query.sqm.SqmPathSource; import org.hibernate.query.sqm.SqmTreeCreationLogger; +import org.hibernate.query.sqm.tree.domain.AbstractSqmFrom; +import org.hibernate.query.sqm.tree.domain.SqmCorrelation; import org.hibernate.query.sqm.tree.domain.SqmPath; -import org.hibernate.query.sqm.tree.from.SqmCrossJoin; -import org.hibernate.query.sqm.tree.from.SqmEntityJoin; -import org.hibernate.query.sqm.tree.from.SqmFrom; -import org.hibernate.query.sqm.tree.from.SqmJoin; -import org.hibernate.query.sqm.tree.from.SqmRoot; +import org.hibernate.query.sqm.tree.from.*; import org.hibernate.query.sqm.tree.select.SqmAliasedNode; import org.hibernate.query.sqm.tree.select.SqmSubQuery; import org.hibernate.spi.NavigablePath; @@ -244,6 +243,12 @@ else if ( parentRegistered instanceof SqmCrossJoin ) { else if ( parentRegistered instanceof SqmEntityJoin ) { correlated = selectQuery.correlate( (SqmEntityJoin) parentRegistered ); } + else if ( parentRegistered instanceof AbstractSqmFrom) { + final SqmCorrelation correlation = + ((AbstractSqmFrom) parentRegistered).createCorrelation(); + selectQuery.getQuerySpec().addRoot( correlation.getCorrelatedRoot() ); + correlated = correlation; + } else { throw new UnsupportedOperationException( "Can't correlate from node: " + parentRegistered ); } 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 191a6a05940d..7a831a0e1b08 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 @@ -132,9 +132,14 @@ public void reportContextSensitivity(Parser recognizer, DFA dfa, int startIndex, 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 + // CommonTokenStream and cause a wrong parse + // hqlLexer.reset(); + // reset the input token stream and parser state - hqlLexer.reset(); hqlParser.reset(); // fall back to LL(k)-based parsing diff --git a/hibernate-core/src/main/java/org/hibernate/query/internal/BindingTypeHelper.java b/hibernate-core/src/main/java/org/hibernate/query/internal/BindingTypeHelper.java index 2fe6aa697876..12d34e14e184 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/internal/BindingTypeHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/query/internal/BindingTypeHelper.java @@ -41,21 +41,27 @@ public BindableType resolveTemporalPrecision( BindableType declaredParameterType, SessionFactoryImplementor sessionFactory) { if ( precision != null ) { - final SqmExpressible sqmExpressible = declaredParameterType.resolveExpressible( sessionFactory ); - if ( !( JavaTypeHelper.isTemporal( sqmExpressible.getExpressibleJavaType() ) ) ) { - throw new UnsupportedOperationException( - "Cannot treat non-temporal parameter type with temporal precision" - ); + final TemporalJavaType temporalJtd; + if ( declaredParameterType != null ) { + final SqmExpressible sqmExpressible = declaredParameterType.resolveExpressible( sessionFactory ); + if ( !( JavaTypeHelper.isTemporal( sqmExpressible.getExpressibleJavaType() ) ) ) { + throw new UnsupportedOperationException( + "Cannot treat non-temporal parameter type with temporal precision" + ); + } + temporalJtd = (TemporalJavaType) sqmExpressible.getExpressibleJavaType(); + } + else { + temporalJtd = null; } - final TemporalJavaType temporalJtd = (TemporalJavaType) sqmExpressible.getExpressibleJavaType(); - if ( temporalJtd.getPrecision() != precision ) { + if ( temporalJtd == null || temporalJtd.getPrecision() != precision ) { final TypeConfiguration typeConfiguration = sessionFactory.getTypeConfiguration(); final TemporalJavaType temporalTypeForPrecision; // Special case java.util.Date, because TemporalJavaType#resolveTypeForPrecision doesn't support widening, // since the main purpose of that method is to determine the final java type based on the reflective type // + the explicit @Temporal(TemporalType...) configuration - if ( java.util.Date.class.isAssignableFrom( temporalJtd.getJavaTypeClass() ) ) { + if ( temporalJtd == null || java.util.Date.class.isAssignableFrom( temporalJtd.getJavaTypeClass() ) ) { //noinspection unchecked temporalTypeForPrecision = (TemporalJavaType) typeConfiguration.getJavaTypeRegistry().getDescriptor( TemporalJavaType.resolveJavaTypeClass( precision ) diff --git a/hibernate-core/src/main/java/org/hibernate/query/internal/QueryParameterBindingImpl.java b/hibernate-core/src/main/java/org/hibernate/query/internal/QueryParameterBindingImpl.java index e5ea76cb30c8..5f102201ae5b 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/internal/QueryParameterBindingImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/internal/QueryParameterBindingImpl.java @@ -20,6 +20,7 @@ import org.hibernate.query.spi.QueryParameterBinding; import org.hibernate.query.spi.QueryParameterBindingValidator; import org.hibernate.query.sqm.SqmExpressible; +import org.hibernate.query.sqm.tree.expression.NullSqmExpressible; import org.hibernate.type.descriptor.java.JavaType; import org.hibernate.type.descriptor.java.JavaTypeHelper; import org.hibernate.type.spi.TypeConfiguration; @@ -113,10 +114,10 @@ public void setBindValue(T value, boolean resolveJdbcTypeIfNecessary) { final Object coerced; if ( ! sessionFactory.getSessionFactoryOptions().getJpaCompliance().isLoadByIdComplianceEnabled() ) { try { - if ( bindType != null ) { + if ( canValueBeCoerced( bindType) ) { coerced = coerce( value, bindType ); } - else if ( queryParameter.getHibernateType() != null ) { + else if ( canValueBeCoerced( queryParameter.getHibernateType() ) ) { coerced = coerce( value, queryParameter.getHibernateType() ); } else { @@ -190,7 +191,7 @@ private boolean isRegisteredAsBasicType(Class valueClass) { private void bindValue(Object value) { isBound = true; bindValue = value; - if ( bindType == null && value != null ) { + if ( canBindValueBeSet( value, bindType ) ) { bindType = sessionFactory.getMappingMetamodel().resolveParameterBindType( value ); } } @@ -206,10 +207,10 @@ public void setBindValue(T value, BindableType clarifiedType) { } final Object coerced; - if ( bindType != null ) { + if ( canValueBeCoerced( bindType ) ) { coerced = coerce( value, bindType ); } - else if ( queryParameter.getHibernateType() != null ) { + else if ( canValueBeCoerced( queryParameter.getHibernateType() ) ) { coerced = coerce( value, queryParameter.getHibernateType() ); } else { @@ -233,7 +234,7 @@ public void setBindValue(T value, TemporalType temporalTypePrecision) { final Object coerced; if ( ! sessionFactory.getSessionFactoryOptions().getJpaCompliance().isLoadByIdComplianceEnabled() ) { - if ( bindType != null ) { + if (canValueBeCoerced( bindType ) ) { try { coerced = coerce( value, bindType ); } @@ -249,7 +250,7 @@ public void setBindValue(T value, TemporalType temporalTypePrecision) { ); } } - else if ( queryParameter.getHibernateType() != null ) { + else if ( canValueBeCoerced( queryParameter.getHibernateType() ) ) { coerced = coerce( value, queryParameter.getHibernateType() ); } else { @@ -299,10 +300,9 @@ public void setBindValues(Collection values) { value = iterator.next(); } - if ( bindType == null && value != null ) { - this.bindType = sessionFactory.getMappingMetamodel().resolveParameterBindType( value ); + if ( canBindValueBeSet( value, bindType ) ) { + bindType = sessionFactory.getMappingMetamodel().resolveParameterBindType( value ); } - } @Override @@ -378,4 +378,12 @@ private void validate(Object value, TemporalType clarifiedTemporalType) { public TypeConfiguration getTypeConfiguration() { return sessionFactory.getTypeConfiguration(); } + + private static boolean canValueBeCoerced(BindableType bindType) { + return bindType != null && !( bindType instanceof NullSqmExpressible ); + } + + private static boolean canBindValueBeSet(Object value, BindableType bindType) { + return value != null && ( bindType == null || bindType instanceof NullSqmExpressible ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/internal/QueryParameterIdentifiedImpl.java b/hibernate-core/src/main/java/org/hibernate/query/internal/QueryParameterIdentifiedImpl.java new file mode 100644 index 000000000000..bbee649d102b --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/internal/QueryParameterIdentifiedImpl.java @@ -0,0 +1,65 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.query.internal; + +import org.hibernate.query.BindableType; +import org.hibernate.query.named.NamedQueryMemento; +import org.hibernate.query.spi.AbstractQueryParameter; +import org.hibernate.query.sqm.tree.expression.SqmJpaCriteriaParameterWrapper; + +import org.checkerframework.checker.nullness.qual.Nullable; + + +/** + * QueryParameter impl for unnamed JPA Criteria-parameters. + */ +public class QueryParameterIdentifiedImpl extends AbstractQueryParameter { + /** + * Create an identified parameter descriptor from the SQM parameter + * + * @param parameter The source parameter info + * + * @return The parameter descriptor + */ + public static QueryParameterIdentifiedImpl fromSqm(SqmJpaCriteriaParameterWrapper parameter) { + assert parameter.getName() == null; + assert parameter.getPosition() == null; + return new QueryParameterIdentifiedImpl<>( + parameter.getUnnamedParameterId(), + parameter.allowMultiValuedBinding(), + parameter.getAnticipatedType() + ); + } + + private final int unnamedParameterId; + + private QueryParameterIdentifiedImpl(int unnamedParameterId, boolean allowMultiValuedBinding, @Nullable BindableType anticipatedType) { + super( allowMultiValuedBinding, anticipatedType ); + this.unnamedParameterId = unnamedParameterId; + } + + public int getUnnamedParameterId() { + return unnamedParameterId; + } + + @Override + public NamedQueryMemento.ParameterMemento toMemento() { + return session -> new QueryParameterIdentifiedImpl<>( unnamedParameterId, allowsMultiValuedBinding(), getHibernateType() ); + } + + @Override + public boolean equals(Object o) { + return this == o + || o instanceof QueryParameterIdentifiedImpl + && unnamedParameterId == ( (QueryParameterIdentifiedImpl) o ).unnamedParameterId; + } + + @Override + public int hashCode() { + return unnamedParameterId; + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/named/AbstractNamedQueryMemento.java b/hibernate-core/src/main/java/org/hibernate/query/named/AbstractNamedQueryMemento.java index 960cdeb2d68f..14e1bc408fa2 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/named/AbstractNamedQueryMemento.java +++ b/hibernate-core/src/main/java/org/hibernate/query/named/AbstractNamedQueryMemento.java @@ -14,12 +14,15 @@ import org.hibernate.CacheMode; import org.hibernate.FlushMode; +import org.checkerframework.checker.nullness.qual.Nullable; + /** * @author Steve Ebersole * @author Gavin King */ public abstract class AbstractNamedQueryMemento implements NamedQueryMemento { private final String name; + private final @Nullable Class resultType; private final Boolean cacheable; private final String cacheRegion; @@ -37,6 +40,7 @@ public abstract class AbstractNamedQueryMemento implements NamedQueryMemento { protected AbstractNamedQueryMemento( String name, + @Nullable Class resultType, Boolean cacheable, String cacheRegion, CacheMode cacheMode, @@ -47,6 +51,7 @@ protected AbstractNamedQueryMemento( String comment, Map hints) { this.name = name; + this.resultType = resultType; this.cacheable = cacheable; this.cacheRegion = cacheRegion; this.cacheMode = cacheMode; @@ -63,6 +68,10 @@ public String getRegistrationName() { return name; } + public @Nullable Class getResultType() { + return resultType; + } + @Override public Boolean getCacheable() { return cacheable; diff --git a/hibernate-core/src/main/java/org/hibernate/query/results/DomainResultCreationStateImpl.java b/hibernate-core/src/main/java/org/hibernate/query/results/DomainResultCreationStateImpl.java index 7744bedfcd63..235e3e2643b4 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/results/DomainResultCreationStateImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/results/DomainResultCreationStateImpl.java @@ -87,6 +87,7 @@ public class DomainResultCreationStateImpl private boolean processingKeyFetches = false; private boolean resolvingCircularFetch; private ForeignKeyDescriptor.Nature currentlyResolvingForeignKeySide; + private boolean isProcedureOrNativeQuery; public DomainResultCreationStateImpl( String stateIdentifier, @@ -94,6 +95,7 @@ public DomainResultCreationStateImpl( Map> legacyFetchBuilders, Consumer sqlSelectionConsumer, LoadQueryInfluencers loadQueryInfluencers, + boolean isProcedureOrNativeQuery, SessionFactoryImplementor sessionFactory) { this.stateIdentifier = stateIdentifier; this.jdbcResultsMetadata = jdbcResultsMetadata; @@ -105,6 +107,8 @@ public DomainResultCreationStateImpl( this.legacyFetchResolver = new LegacyFetchResolverImpl( legacyFetchBuilders ); this.sessionFactory = sessionFactory; + + this.isProcedureOrNativeQuery = isProcedureOrNativeQuery; } public LegacyFetchResolver getLegacyFetchResolver() { @@ -588,4 +592,8 @@ public void setCurrentlyResolvingForeignKeyPart(ForeignKeyDescriptor.Nature curr this.currentlyResolvingForeignKeySide = currentlyResolvingForeignKeySide; } + @Override + public boolean isProcedureOrNativeQuery() { + return isProcedureOrNativeQuery; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/results/ResultSetMappingImpl.java b/hibernate-core/src/main/java/org/hibernate/query/results/ResultSetMappingImpl.java index bed3143a199c..407adccc1e2c 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/results/ResultSetMappingImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/results/ResultSetMappingImpl.java @@ -209,6 +209,7 @@ public JdbcValuesMapping resolve( legacyFetchBuilders, sqlSelections::add, loadQueryInfluencers, + true, sessionFactory ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/results/dynamic/DynamicResultBuilderEntityStandard.java b/hibernate-core/src/main/java/org/hibernate/query/results/dynamic/DynamicResultBuilderEntityStandard.java index 17aab3d40547..4bd6141fb5b1 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/results/dynamic/DynamicResultBuilderEntityStandard.java +++ b/hibernate-core/src/main/java/org/hibernate/query/results/dynamic/DynamicResultBuilderEntityStandard.java @@ -28,7 +28,6 @@ import org.hibernate.query.results.DomainResultCreationStateImpl; import org.hibernate.query.results.FetchBuilder; import org.hibernate.query.results.ResultsHelper; -import org.hibernate.query.results.TableGroupImpl; import org.hibernate.query.results.complete.CompleteFetchBuilder; import org.hibernate.spi.NavigablePath; import org.hibernate.sql.ast.spi.FromClauseAccess; @@ -196,15 +195,17 @@ private T buildResultOrFetch( final TableGroup tableGroup = fromClauseAccess.resolveTableGroup( elementNavigablePath, np -> { - final TableReference tableReference = entityMapping.createPrimaryTableReference( - new SqlAliasBaseConstant( tableAlias ), - creationState - ); - if ( lockMode != null ) { domainResultCreationState.getSqlAstCreationState().registerLockMode( tableAlias, lockMode ); } - return new TableGroupImpl( elementNavigablePath, tableAlias, tableReference, entityMapping ); + return entityMapping.createRootTableGroup( + true, + navigablePath, + tableAlias, + new SqlAliasBaseConstant( tableAlias ), + null, + creationState + ); } ); final TableReference tableReference = tableGroup.getPrimaryTableReference(); diff --git a/hibernate-core/src/main/java/org/hibernate/query/spi/AbstractCommonQueryContract.java b/hibernate-core/src/main/java/org/hibernate/query/spi/AbstractCommonQueryContract.java index 32f8739f9515..b89ae0641071 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/spi/AbstractCommonQueryContract.java +++ b/hibernate-core/src/main/java/org/hibernate/query/spi/AbstractCommonQueryContract.java @@ -45,6 +45,7 @@ import org.hibernate.query.criteria.JpaExpression; import org.hibernate.query.internal.QueryOptionsImpl; import org.hibernate.query.sqm.SqmExpressible; +import org.hibernate.query.sqm.tree.expression.NullSqmExpressible; import org.hibernate.query.sqm.tree.expression.SqmLiteral; import org.hibernate.query.sqm.tree.expression.SqmParameter; import org.hibernate.query.sqm.tree.select.SqmSelectStatement; @@ -98,10 +99,16 @@ */ public abstract class AbstractCommonQueryContract implements CommonQueryContract { private final SharedSessionContractImplementor session; - private final QueryOptionsImpl queryOptions = new QueryOptionsImpl(); + private final QueryOptionsImpl queryOptions; public AbstractCommonQueryContract(SharedSessionContractImplementor session) { this.session = session; + this.queryOptions = new QueryOptionsImpl(); + } + + protected AbstractCommonQueryContract(AbstractCommonQueryContract original) { + this.session = original.session; + this.queryOptions = original.queryOptions; } public SharedSessionContractImplementor getSession() { @@ -723,7 +730,7 @@ else if ( parameter.getPosition() != null ) { protected

    QueryParameterBinding

    locateBinding(QueryParameterImplementor

    parameter) { getSession().checkOpen(); - return getQueryParameterBindings().getBinding( parameter ); + return getQueryParameterBindings().getBinding( getQueryParameter( parameter ) ); } protected

    QueryParameterBinding

    locateBinding(String name) { @@ -738,8 +745,12 @@ protected

    QueryParameterBinding

    locateBinding(int position) { public boolean isBound(Parameter param) { getSession().checkOpen(); - final QueryParameterImplementor qp = getParameterMetadata().resolve( param ); - return qp != null && getQueryParameterBindings().isBound( qp ); + final QueryParameterImplementor parameter = getParameterMetadata().resolve( param ); + return parameter != null && getQueryParameterBindings().isBound( getQueryParameter( parameter ) ); + } + + protected

    QueryParameterImplementor

    getQueryParameter(QueryParameterImplementor

    parameter) { + return parameter; } public T getParameterValue(Parameter param) { @@ -752,7 +763,7 @@ public T getParameterValue(Parameter param) { throw new IllegalArgumentException( "The parameter [" + param + "] is not part of this Query" ); } - final QueryParameterBinding binding = getQueryParameterBindings().getBinding( qp ); + final QueryParameterBinding binding = getQueryParameterBindings().getBinding( getQueryParameter( qp ) ); if ( binding == null || !binding.isBound() ) { throw new IllegalStateException( "Parameter value not yet bound : " + param.toString() ); } @@ -817,7 +828,7 @@ public CommonQueryContract setParameter(String name, Object value) { final QueryParameter param = binding.getQueryParameter(); if ( param.allowsMultiValuedBinding() ) { final BindableType hibernateType = param.getHibernateType(); - if ( hibernateType == null || isInstance( hibernateType, value ) ) { + if ( hibernateType == null || value == null || isInstance( hibernateType, value ) ) { if ( value instanceof Collection && !isRegisteredAsBasicType( value.getClass() ) ) { //noinspection rawtypes return setParameterList( name, (Collection) value ); @@ -906,7 +917,7 @@ public CommonQueryContract setParameter(int position, Object value) { final QueryParameter param = binding.getQueryParameter(); if ( param.allowsMultiValuedBinding() ) { final BindableType hibernateType = param.getHibernateType(); - if ( hibernateType == null || isInstance( hibernateType, value ) ) { + if ( hibernateType == null || hibernateType instanceof NullSqmExpressible || isInstance( hibernateType, value ) ) { if ( value instanceof Collection && !isRegisteredAsBasicType( value.getClass() ) ) { //noinspection rawtypes return setParameterList( position, (Collection) value ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/spi/AbstractQueryParameter.java b/hibernate-core/src/main/java/org/hibernate/query/spi/AbstractQueryParameter.java index 5719bc5a2f26..4d7048176abc 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/spi/AbstractQueryParameter.java +++ b/hibernate-core/src/main/java/org/hibernate/query/spi/AbstractQueryParameter.java @@ -27,7 +27,7 @@ public AbstractQueryParameter( @Override public void disallowMultiValuedBinding() { QueryLogging.QUERY_MESSAGE_LOGGER.debugf( "QueryParameter#disallowMultiValuedBinding() called : %s", this ); - this.allowMultiValuedBinding = true; + this.allowMultiValuedBinding = false; } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/query/spi/AbstractSelectionQuery.java b/hibernate-core/src/main/java/org/hibernate/query/spi/AbstractSelectionQuery.java index df42432cff0e..687302e2902b 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/spi/AbstractSelectionQuery.java +++ b/hibernate-core/src/main/java/org/hibernate/query/spi/AbstractSelectionQuery.java @@ -36,6 +36,7 @@ import org.hibernate.query.IllegalQueryOperationException; import org.hibernate.query.QueryParameter; import org.hibernate.query.SelectionQuery; +import org.hibernate.query.internal.QueryOptionsImpl; import org.hibernate.query.internal.ScrollableResultsIterator; import org.hibernate.query.named.NamedQueryMemento; import org.hibernate.sql.exec.internal.CallbackImpl; @@ -81,6 +82,12 @@ public AbstractSelectionQuery(SharedSessionContractImplementor session) { super( session ); } + protected AbstractSelectionQuery(AbstractSelectionQuery original) { + super( original ); + this.sessionFlushMode = original.sessionFlushMode; + this.sessionCacheMode = original.sessionCacheMode; + } + protected void applyOptions(NamedQueryMemento memento) { if ( memento.getHints() != null ) { memento.getHints().forEach( this::applyHint ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sql/internal/NamedNativeQueryMementoImpl.java b/hibernate-core/src/main/java/org/hibernate/query/sql/internal/NamedNativeQueryMementoImpl.java index 8ba29214b482..5d9f383aae08 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sql/internal/NamedNativeQueryMementoImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sql/internal/NamedNativeQueryMementoImpl.java @@ -38,6 +38,7 @@ public class NamedNativeQueryMementoImpl extends AbstractNamedQueryMemento imple public NamedNativeQueryMementoImpl( String name, + Class resultClass, String sqlString, String originalSqlString, String resultSetMappingName, @@ -56,6 +57,7 @@ public NamedNativeQueryMementoImpl( Map hints) { super( name, + resultClass, cacheable, cacheRegion, cacheMode, @@ -123,6 +125,7 @@ public Integer getMaxResults() { public NamedNativeQueryMemento makeCopy(String name) { return new NamedNativeQueryMementoImpl( name, + getResultType(), sqlString, originalSqlString, resultSetMappingName, @@ -149,7 +152,8 @@ public void validate(QueryEngine queryEngine) { @Override public NativeQueryImplementor toQuery(SharedSessionContractImplementor session) { - return new NativeQueryImpl<>( this, session ); + //noinspection unchecked + return new NativeQueryImpl<>( this, (Class) getResultType(), session ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/query/sql/internal/NativeQueryImpl.java b/hibernate-core/src/main/java/org/hibernate/query/sql/internal/NativeQueryImpl.java index 6179773fc609..b9cf797343b7 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sql/internal/NativeQueryImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sql/internal/NativeQueryImpl.java @@ -20,8 +20,13 @@ import java.util.function.Consumer; import java.util.function.Supplier; +import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.CacheMode; import org.hibernate.FlushMode; +import org.hibernate.jpa.spi.NativeQueryArrayTransformer; +import org.hibernate.jpa.spi.NativeQueryConstructorTransformer; +import org.hibernate.jpa.spi.NativeQueryListTransformer; +import org.hibernate.jpa.spi.NativeQueryMapTransformer; import org.hibernate.HibernateException; import org.hibernate.LockMode; import org.hibernate.LockOptions; @@ -55,7 +60,6 @@ import org.hibernate.query.internal.DelegatingDomainQueryExecutionContext; import org.hibernate.query.internal.ParameterMetadataImpl; import org.hibernate.query.internal.QueryOptionsImpl; -import org.hibernate.query.internal.QueryParameterBindingsImpl; import org.hibernate.query.internal.ResultSetMappingResolutionContext; import org.hibernate.query.named.NamedResultSetMappingMemento; import org.hibernate.query.results.Builders; @@ -92,7 +96,6 @@ import org.hibernate.query.sql.spi.SelectInterpretationsKey; import org.hibernate.sql.exec.internal.CallbackImpl; import org.hibernate.sql.exec.spi.Callback; -import org.hibernate.sql.results.jdbc.spi.JdbcValuesMappingProducer; import org.hibernate.sql.results.spi.SingleResultConsumer; import org.hibernate.transform.ResultTransformer; import org.hibernate.type.BasicType; @@ -110,8 +113,13 @@ import jakarta.persistence.TemporalType; import jakarta.persistence.Tuple; import jakarta.persistence.metamodel.SingularAttribute; +import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.descriptor.java.spi.UnknownBasicJavaType; +import static org.hibernate.internal.util.ReflectHelper.isClass; +import static org.hibernate.internal.util.StringHelper.unqualify; import static org.hibernate.jpa.HibernateHints.HINT_NATIVE_LOCK_MODE; +import static org.hibernate.query.sqm.internal.SqmUtil.isResultTypeAlwaysAllowed; import static org.hibernate.query.results.Builders.resultClassBuilder; /** @@ -126,6 +134,7 @@ public class NativeQueryImpl private final List parameterOccurrences; private final QueryParameterBindings parameterBindings; + private final Class resultType; private final ResultSetMapping resultSetMapping; private final boolean resultMappingSuppliedToCtor; @@ -177,6 +186,7 @@ else if ( memento.getResultMappingClass() != null ) { return false; }, + null, session ); } @@ -206,39 +216,16 @@ public NativeQueryImpl( } } - if ( memento.getResultMappingClass() != null ) { - resultSetMapping.addResultBuilder( resultClassBuilder( - memento.getResultMappingClass(), - context - ) ); + if ( memento.getResultType() != null ) { + resultSetMapping.addResultBuilder( resultClassBuilder( memento.getResultType(), context ) ); return true; } return false; }, + resultJavaType, session ); - - if ( resultJavaType == Tuple.class ) { - setTupleTransformer( new NativeQueryTupleTransformer() ); - } - else if ( resultJavaType != null && !resultJavaType.isArray() ) { - switch ( resultSetMapping.getNumberOfResultBuilders() ) { - case 0: { - throw new IllegalArgumentException( "Named query exists, but did not specify a resultClass" ); - } - case 1: { - final Class actualResultJavaType = resultSetMapping.getResultBuilders().get( 0 ).getJavaType(); - if ( actualResultJavaType != null && !resultJavaType.isAssignableFrom( actualResultJavaType ) ) { - throw buildIncompatibleException( resultJavaType, actualResultJavaType ); - } - break; - } - default: { - throw new IllegalArgumentException( "Cannot create TypedQuery for query with more than one return" ); - } - } - } } /** @@ -260,6 +247,7 @@ public NativeQueryImpl( mappingMemento.resolve( resultSetMapping, querySpaceConsumer, context ); return true; }, + null, session ); @@ -270,6 +258,15 @@ public NativeQueryImpl( Supplier resultSetMappingCreator, ResultSetMappingHandler resultSetMappingHandler, SharedSessionContractImplementor session) { + this( memento, resultSetMappingCreator, resultSetMappingHandler, null, session ); + } + + public NativeQueryImpl( + NamedNativeQueryMemento memento, + Supplier resultSetMappingCreator, + ResultSetMappingHandler resultSetMappingHandler, + @Nullable Class resultType, + SharedSessionContractImplementor session) { super( session ); this.originalSqlString = memento.getOriginalSqlString(); @@ -283,6 +280,7 @@ public NativeQueryImpl( this.parameterMetadata = parameterInterpretation.toParameterMetadata( session ); this.parameterOccurrences = parameterInterpretation.getOrderedParameterOccurrences(); this.parameterBindings = parameterMetadata.createBindings( session.getFactory() ); + this.resultType = resultType; this.querySpaces = new HashSet<>(); this.resultSetMapping = resultSetMappingCreator.get(); @@ -296,6 +294,27 @@ public NativeQueryImpl( this.resultMappingSuppliedToCtor = appliedAnyResults; + if ( resultType != null ) { + if ( !isResultTypeAlwaysAllowed( resultType ) ) { + switch ( resultSetMapping.getNumberOfResultBuilders() ) { + case 0: + throw new IllegalArgumentException( "Named query exists, but did not specify a resultClass" ); + case 1: + final Class actualResultJavaType = resultSetMapping.getResultBuilders().get( 0 ) + .getJavaType(); + if ( actualResultJavaType != null && !resultType.isAssignableFrom( actualResultJavaType ) ) { + throw buildIncompatibleException( resultType, actualResultJavaType ); + } + break; + default: + throw new IllegalArgumentException( + "Cannot create TypedQuery for query with more than one return" ); + } + } + else { + setTupleTransformerForResultType( resultType ); + } + } applyOptions( memento ); } @@ -312,6 +331,7 @@ public NativeQueryImpl( this.parameterMetadata = parameterInterpretation.toParameterMetadata( session ); this.parameterOccurrences = parameterInterpretation.getOrderedParameterOccurrences(); this.parameterBindings = parameterMetadata.createBindings( session.getFactory() ); + this.resultType = null; this.querySpaces = new HashSet<>(); this.resultSetMapping = buildResultSetMapping( resultSetMappingMemento.getName(), false, session ); @@ -325,6 +345,10 @@ public NativeQueryImpl( } public NativeQueryImpl(String sqlString, SharedSessionContractImplementor session) { + this( sqlString, null, session ); + } + + public NativeQueryImpl(String sqlString, @Nullable Class resultType, SharedSessionContractImplementor session) { super( session ); this.querySpaces = new HashSet<>(); @@ -335,11 +359,50 @@ public NativeQueryImpl(String sqlString, SharedSessionContractImplementor sessio this.parameterMetadata = parameterInterpretation.toParameterMetadata( session ); this.parameterOccurrences = parameterInterpretation.getOrderedParameterOccurrences(); this.parameterBindings = parameterMetadata.createBindings( session.getFactory() ); + this.resultType = resultType; + if ( resultType != null ) { + setTupleTransformerForResultType( resultType ); + } this.resultSetMapping = ResultSetMapping.resolveResultSetMapping( sqlString, true, session.getFactory() ); this.resultMappingSuppliedToCtor = false; } + protected void setTupleTransformerForResultType(Class resultClass) { + final TupleTransformer tupleTransformer = determineTupleTransformerForResultType( resultClass ); + if ( tupleTransformer != null ) { + setTupleTransformer( tupleTransformer ); + } + } + + protected @Nullable TupleTransformer determineTupleTransformerForResultType(Class resultClass) { + if ( Tuple.class.equals( resultClass ) ) { + return NativeQueryTupleTransformer.INSTANCE; + } + else if ( Map.class.equals( resultClass ) ) { + return NativeQueryMapTransformer.INSTANCE; + } + else if ( List.class.equals( resultClass ) ) { + return NativeQueryListTransformer.INSTANCE; + } + else if ( Object[].class.equals( resultClass )) { + return NativeQueryArrayTransformer.INSTANCE; + } + else if ( resultClass != Object.class ) { + if ( isClass( resultClass ) && !hasJavaTypeDescriptor( resultClass ) ) { + // not a basic type + return new NativeQueryConstructorTransformer<>( resultClass ); + } + } + return null; + } + + private boolean hasJavaTypeDescriptor(Class resultClass) { + final JavaType descriptor = getSessionFactory().getTypeConfiguration().getJavaTypeRegistry() + .findDescriptor( resultClass ); + return descriptor != null && descriptor.getClass() != UnknownBasicJavaType.class; + } + @FunctionalInterface private interface ResultSetMappingHandler { boolean resolveResultSetMapping( @@ -456,10 +519,16 @@ public QueryParameterBindings getParameterBindings() { return getQueryParameterBindings(); } + @Override + public Class getResultType() { + return resultType; + } + @Override public NamedNativeQueryMemento toMemento(String name) { return new NamedNativeQueryMementoImpl( name, + resultType != null ? resultType : extractResultClass( resultSetMapping ), sqlString, originalSqlString, resultSetMapping.getMappingIdentifier(), @@ -479,14 +548,14 @@ public NamedNativeQueryMemento toMemento(String name) { ); } - private Class extractResultClass(ResultSetMapping resultSetMapping) { + private Class extractResultClass(ResultSetMapping resultSetMapping) { final List resultBuilders = resultSetMapping.getResultBuilders(); if ( resultBuilders.size() == 1 ) { final ResultBuilder resultBuilder = resultBuilders.get( 0 ); if ( resultBuilder instanceof ImplicitResultClassBuilder || resultBuilder instanceof ImplicitModelPartResultBuilderEntity || resultBuilder instanceof DynamicResultBuilderEntityCalculated ) { - return resultBuilder.getJavaType(); + return (Class) resultBuilder.getJavaType(); } } return null; @@ -645,19 +714,32 @@ public KeyedResultList getKeyedResultList(KeyedPage page) { } protected SelectQueryPlan resolveSelectQueryPlan() { - if ( isCacheableQuery() ) { - final QueryInterpretationCache.Key cacheKey = generateSelectInterpretationsKey( resultSetMapping ); - return getSession().getFactory().getQueryEngine().getInterpretationCache() - .resolveSelectQueryPlan( cacheKey, () -> createQueryPlan( resultSetMapping ) ); + final ResultSetMapping mapping; + if ( resultType != null && resultSetMapping.isDynamic() && resultSetMapping.getNumberOfResultBuilders() == 0 ) { + mapping = ResultSetMapping.resolveResultSetMapping( originalSqlString, true, getSessionFactory() ); + + if ( getSessionFactory().getMappingMetamodel().isEntityClass( resultType ) ) { + mapping.addResultBuilder( + Builders.entityCalculated( unqualify( resultType.getName() ), resultType.getName(), + LockMode.READ, getSessionFactory() ) ); + } + else if ( !isResultTypeAlwaysAllowed( resultType ) + && (!isClass( resultType ) || hasJavaTypeDescriptor( resultType )) ) { + mapping.addResultBuilder( Builders.resultClassBuilder( resultType, getSessionFactory() ) ); + } } else { - return createQueryPlan( resultSetMapping ); + mapping = resultSetMapping; } + return isCacheableQuery() + ? getSession().getFactory().getQueryEngine().getInterpretationCache().resolveSelectQueryPlan( selectInterpretationsKey( mapping ), () -> createQueryPlan( mapping ) ) + : createQueryPlan( mapping ); } private NativeSelectQueryPlan createQueryPlan(ResultSetMapping resultSetMapping) { - final String sqlString = expandParameterLists(); final NativeSelectQueryDefinition queryDefinition = new NativeSelectQueryDefinition<>() { + final String sqlString = expandParameterLists(); + @Override public String getSqlString() { return sqlString; @@ -864,13 +946,11 @@ public static int determineBindValueMaxCount(boolean paddingEnabled, int inExprL return bindValueMaxCount; } - private SelectInterpretationsKey generateSelectInterpretationsKey(JdbcValuesMappingProducer resultSetMapping) { + private SelectInterpretationsKey selectInterpretationsKey(ResultSetMapping resultSetMapping) { return new SelectInterpretationsKey( getQueryString(), resultSetMapping, - getSynchronizedQuerySpaces(), - getQueryOptions().getTupleTransformer(), - getQueryOptions().getResultListTransformer() + getSynchronizedQuerySpaces() ); } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sql/internal/ResultSetMappingProcessor.java b/hibernate-core/src/main/java/org/hibernate/query/sql/internal/ResultSetMappingProcessor.java index c051eb564647..8dc388088673 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sql/internal/ResultSetMappingProcessor.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sql/internal/ResultSetMappingProcessor.java @@ -15,6 +15,7 @@ import java.util.Map; import java.util.Set; +import org.hibernate.AssertionFailure; import org.hibernate.HibernateException; import org.hibernate.LockMode; import org.hibernate.MappingException; @@ -25,6 +26,7 @@ import org.hibernate.loader.internal.AliasConstantsHelper; import org.hibernate.metamodel.mapping.CollectionPart; import org.hibernate.metamodel.mapping.EntityMappingType; +import org.hibernate.metamodel.mapping.internal.ToOneAttributeMapping; import org.hibernate.persister.collection.CollectionPersister; import org.hibernate.persister.collection.QueryableCollection; import org.hibernate.persister.collection.SQLLoadableCollection; @@ -279,15 +281,21 @@ private void applyFetchBuilder( } private NavigablePath determineNavigablePath(DynamicFetchBuilderLegacy fetchBuilder) { - final NativeQuery.ResultNode ownerResult = alias2Return.get( fetchBuilder.getOwnerAlias() ); + final var ownerResult = alias2Return.get( fetchBuilder.getOwnerAlias() ); + final NavigablePath basePath; if ( ownerResult instanceof NativeQuery.RootReturn ) { - return ( (NativeQuery.RootReturn) ownerResult ).getNavigablePath() - .append( fetchBuilder.getFetchableName() ); + basePath = ((NativeQuery.RootReturn) ownerResult).getNavigablePath(); + } + else if ( ownerResult instanceof DynamicFetchBuilderLegacy ) { + basePath = determineNavigablePath( ((DynamicFetchBuilderLegacy) ownerResult) ); } else { - return determineNavigablePath( ( DynamicFetchBuilderLegacy) ownerResult ) - .append( fetchBuilder.getFetchableName() ); + throw new AssertionFailure( "Unexpected fetch builder" ); } + final NavigablePath path = alias2CollectionPersister.containsKey( fetchBuilder.getOwnerAlias() ) + ? basePath.append( CollectionPart.Nature.ELEMENT.getName() ) + : basePath; + return path.append( fetchBuilder.getFetchableName() ); } private DynamicResultBuilderEntityStandard createSuffixedResultBuilder( @@ -399,6 +407,17 @@ else if ( propertyType instanceof ComponentType ) { resultBuilderEntity.addFetchBuilder( propertyName, fetchBuilder ); } else if ( columnAliases.length != 0 ) { + if ( propertyType instanceof EntityType ) { + final ToOneAttributeMapping toOne = (ToOneAttributeMapping) loadable.findAttributeMapping( propertyName ); + if ( !toOne.getIdentifyingColumnsTableExpression().equals( loadable.getMappedTableDetails().getTableName() ) ) { + // The to-one has a join-table, use the plain join column name instead of the alias + assert columnAliases.length == 1; + final String[] targetAliases = new String[1]; + targetAliases[0] = toOne.getTargetKeyPropertyName(); + resultBuilderEntity.addProperty( propertyName, targetAliases ); + return; + } + } resultBuilderEntity.addProperty( propertyName, columnAliases ); } } @@ -409,7 +428,7 @@ private CompleteResultBuilderCollectionStandard createSuffixedResultBuilder( String entitySuffix) { final CollectionPersister collectionPersister = collectionReturn.getPluralAttribute().getCollectionDescriptor(); final String[] elementColumnAliases; - if ( collectionPersister.getElementType().isEntityType() ) { + if ( collectionPersister.getElementType() instanceof EntityType ) { final Loadable elementPersister = (Loadable) ( ( QueryableCollection ) collectionPersister).getElementPersister(); final String[] propertyNames = elementPersister.getPropertyNames(); final String[] identifierAliases = elementPersister.getIdentifierAliases( entitySuffix ); @@ -568,13 +587,13 @@ private void processFetchReturn(NativeQuery.FetchReturn fetchReturn) { SQLLoadable ownerPersister = ( SQLLoadable ) alias2Persister.get( ownerAlias ); Type returnType = ownerPersister.getPropertyType( fetchReturn.getFetchableName() ); - if ( returnType.isCollectionType() ) { + if ( returnType instanceof CollectionType ) { String role = ownerPersister.getEntityName() + '.' + fetchReturn.getFetchableName(); Map propertyResultsMap = Collections.emptyMap();//fetchReturn.getPropertyResultsMap() addCollection( role, alias, propertyResultsMap ); // collectionOwnerAliases.add( ownerAlias ); } - else if ( returnType.isEntityType() ) { + else if ( returnType instanceof EntityType ) { EntityType eType = ( EntityType ) returnType; String returnEntityName = eType.getAssociatedEntityName(); SQLLoadable persister = getSQLLoadable( returnEntityName ); @@ -634,7 +653,7 @@ public Map getPropertyResultsMap(String alias) { // } // for ( CollectionPersister persister : alias2CollectionPersister.values() ) { // final Type elementType = persister.getElementType(); -// if ( elementType.isEntityType() && ! elementType.isAnyType() ) { +// if ( elementType instanceof EntityType && ! elementType instanceof AnyType ) { // final Joinable joinable = ( (EntityType) elementType ).getAssociatedJoinable( factory ); // Collections.addAll( spaces, (String[]) ( (EntityPersister) joinable ).getQuerySpaces() ); // } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sql/internal/SQLQueryParser.java b/hibernate-core/src/main/java/org/hibernate/query/sql/internal/SQLQueryParser.java index 801d04af673c..045d24df06ce 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sql/internal/SQLQueryParser.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sql/internal/SQLQueryParser.java @@ -81,16 +81,26 @@ protected String substituteBrackets(String sqlQuery) throws QueryException { final char ch = sqlQuery.charAt( index ); switch (ch) { case '\'': - if (!doubleQuoted && !escaped) { - singleQuoted = !singleQuoted; + if (escaped) { + token.append(ch); + } + else { + if (!doubleQuoted) { + singleQuoted = !singleQuoted; + } + result.append(ch); } - result.append(ch); break; case '"': - if (!singleQuoted && !escaped) { - doubleQuoted = !doubleQuoted; + if (escaped) { + token.append(ch); + } + else { + if ( !singleQuoted ) { + doubleQuoted = !doubleQuoted; + } + result.append( ch ); } - result.append(ch); break; case '{': if (!singleQuoted && !doubleQuoted) { @@ -216,7 +226,7 @@ private String resolveCollectionProperties( } aliasesFound++; return collectionPersister.selectFragment( aliasName, collectionSuffix ) - + ", " + resolveProperties( aliasName, propertyName ); + + ", " + resolveProperties( aliasName, propertyName ); case "element.*": return resolveProperties( aliasName, "*" ); default: @@ -266,7 +276,7 @@ private void validate(String aliasName, String propertyName, String[] columnAlia // TODO: better error message since we actually support composites if names are explicitly listed throw new QueryException( "SQL queries only support properties mapped to a single column - property [" + - propertyName + "] is mapped to " + columnAliases.length + " columns.", + propertyName + "] is mapped to " + columnAliases.length + " columns.", originalQueryString ); } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sql/spi/NamedNativeQueryMemento.java b/hibernate-core/src/main/java/org/hibernate/query/sql/spi/NamedNativeQueryMemento.java index 54aad16b4cb4..8d4659b15f2f 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sql/spi/NamedNativeQueryMemento.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sql/spi/NamedNativeQueryMemento.java @@ -9,6 +9,7 @@ import java.util.Set; import jakarta.persistence.SqlResultSetMapping; +import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.boot.query.NamedNativeQueryDefinition; import org.hibernate.boot.registry.classloading.spi.ClassLoaderService; @@ -50,6 +51,8 @@ default String getOriginalSqlString(){ */ Class getResultMappingClass(); + @Nullable Class getResultType(); + Integer getFirstResult(); Integer getMaxResults(); @@ -135,6 +138,7 @@ public void setResultSetMappingClassName(String resultSetMappingClassName) { public NamedNativeQueryMemento build(SessionFactoryImplementor sessionFactory) { return new NamedNativeQueryMementoImpl( name, + null, queryString, queryString, resultSetMappingName, diff --git a/hibernate-core/src/main/java/org/hibernate/query/sql/spi/SelectInterpretationsKey.java b/hibernate-core/src/main/java/org/hibernate/query/sql/spi/SelectInterpretationsKey.java index 439c7187be49..ab0cbce636f2 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sql/spi/SelectInterpretationsKey.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sql/spi/SelectInterpretationsKey.java @@ -22,21 +22,25 @@ public class SelectInterpretationsKey implements QueryInterpretationCache.Key { private final String sql; private final JdbcValuesMappingProducer jdbcValuesMappingProducer; private final Collection querySpaces; - private final TupleTransformer tupleTransformer; - private final ResultListTransformer resultListTransformer; private final int hash; + @Deprecated(forRemoval = true) public SelectInterpretationsKey( String sql, JdbcValuesMappingProducer jdbcValuesMappingProducer, Collection querySpaces, TupleTransformer tupleTransformer, ResultListTransformer resultListTransformer) { + this( sql, jdbcValuesMappingProducer, querySpaces ); + } + + public SelectInterpretationsKey( + String sql, + JdbcValuesMappingProducer jdbcValuesMappingProducer, + Collection querySpaces) { this.sql = sql; this.jdbcValuesMappingProducer = jdbcValuesMappingProducer; this.querySpaces = querySpaces; - this.tupleTransformer = tupleTransformer; - this.resultListTransformer = resultListTransformer; this.hash = generateHashCode(); } @@ -44,14 +48,10 @@ private SelectInterpretationsKey( String sql, JdbcValuesMappingProducer jdbcValuesMappingProducer, Collection querySpaces, - TupleTransformer tupleTransformer, - ResultListTransformer resultListTransformer, int hash) { this.sql = sql; this.jdbcValuesMappingProducer = jdbcValuesMappingProducer; this.querySpaces = querySpaces; - this.tupleTransformer = tupleTransformer; - this.resultListTransformer = resultListTransformer; this.hash = hash; } @@ -66,8 +66,6 @@ public QueryInterpretationCache.Key prepareForStore() { sql, jdbcValuesMappingProducer.cacheKeyInstance(), new HashSet<>( querySpaces ), - tupleTransformer, - resultListTransformer, hash ); } @@ -94,7 +92,6 @@ public boolean equals(Object o) { return sql.equals( that.sql ) && Objects.equals( jdbcValuesMappingProducer, that.jdbcValuesMappingProducer ) && Objects.equals( querySpaces, that.querySpaces ) - && Objects.equals( tupleTransformer, that.tupleTransformer ) - && Objects.equals( resultListTransformer, that.resultListTransformer ); + ; } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/NodeBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/NodeBuilder.java index a550f9d4d7a0..9fa7899fdedb 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/NodeBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/NodeBuilder.java @@ -32,7 +32,6 @@ import org.hibernate.query.criteria.JpaSelection; import org.hibernate.query.criteria.JpaSimpleCase; import org.hibernate.query.criteria.JpaWindow; -import org.hibernate.query.criteria.ValueHandlingMode; import org.hibernate.query.spi.QueryEngine; import org.hibernate.query.sqm.tree.delete.SqmDeleteStatement; import org.hibernate.query.sqm.tree.domain.SqmBagJoin; @@ -86,8 +85,6 @@ public interface NodeBuilder extends HibernateCriteriaBuilder { QueryEngine getQueryEngine(); - void setCriteriaValueHandlingMode(ValueHandlingMode criteriaValueHandlingMode); - SqmTuple tuple( Class tupleType, SqmExpression... expressions); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/SemanticQueryWalker.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/SemanticQueryWalker.java index 60fdb465ae59..785abb1ccad8 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/SemanticQueryWalker.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/SemanticQueryWalker.java @@ -19,6 +19,8 @@ import org.hibernate.query.sqm.tree.domain.SqmBasicValuedSimplePath; import org.hibernate.query.sqm.tree.domain.SqmCorrelatedBagJoin; import org.hibernate.query.sqm.tree.domain.SqmCorrelatedCrossJoin; +import org.hibernate.query.sqm.tree.domain.SqmCorrelatedCteJoin; +import org.hibernate.query.sqm.tree.domain.SqmCorrelatedDerivedJoin; import org.hibernate.query.sqm.tree.domain.SqmCorrelatedEntityJoin; import org.hibernate.query.sqm.tree.domain.SqmCorrelatedListJoin; import org.hibernate.query.sqm.tree.domain.SqmCorrelatedMapJoin; @@ -170,6 +172,14 @@ public interface SemanticQueryWalker { T visitQualifiedAttributeJoin(SqmAttributeJoin joinedFromElement); + default T visitCorrelatedCteJoin(SqmCorrelatedCteJoin join){ + return visitQualifiedCteJoin( join ); + } + + default T visitCorrelatedDerivedJoin(SqmCorrelatedDerivedJoin join){ + return visitQualifiedDerivedJoin( join ); + } + default T visitCorrelatedCrossJoin(SqmCorrelatedCrossJoin join) { return visitCrossJoin( join ); } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/function/SelfRenderingSqmWindowFunction.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/function/SelfRenderingSqmWindowFunction.java index fd0287bdae7d..dd25861e9d58 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/function/SelfRenderingSqmWindowFunction.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/function/SelfRenderingSqmWindowFunction.java @@ -144,7 +144,7 @@ public void appendHqlString(StringBuilder sb) { sb.append( getFunctionName() ); sb.append( '(' ); int i = 1; - if ( arguments.get( 0 ) instanceof SqmDistinct ) { + if ( !arguments.isEmpty() && arguments.get( 0 ) instanceof SqmDistinct ) { arguments.get( 0 ).appendHqlString( sb ); if ( arguments.size() > 1 ) { sb.append( ' ' ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/AbstractSqmSelectionQuery.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/AbstractSqmSelectionQuery.java index 7d7783cc140c..3778821f3d43 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/AbstractSqmSelectionQuery.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/AbstractSqmSelectionQuery.java @@ -17,7 +17,6 @@ import org.hibernate.query.QueryLogging; import org.hibernate.query.SelectionQuery; import org.hibernate.query.criteria.JpaSelection; -import org.hibernate.query.criteria.ValueHandlingMode; import org.hibernate.query.hql.internal.NamedHqlQueryMementoImpl; import org.hibernate.query.hql.internal.QuerySplitter; import org.hibernate.query.named.NamedQueryMemento; @@ -27,10 +26,13 @@ import org.hibernate.query.spi.QueryEngine; import org.hibernate.query.spi.QueryInterpretationCache; import org.hibernate.query.spi.QueryOptions; +import org.hibernate.query.spi.QueryParameterBindings; +import org.hibernate.query.spi.QueryParameterImplementor; import org.hibernate.query.spi.SelectQueryPlan; -import org.hibernate.query.sqm.NodeBuilder; import org.hibernate.query.sqm.spi.NamedSqmQueryMemento; import org.hibernate.query.sqm.tree.SqmStatement; +import org.hibernate.query.sqm.tree.expression.JpaCriteriaParameter; +import org.hibernate.query.sqm.tree.expression.SqmJpaCriteriaParameterWrapper; import org.hibernate.query.sqm.tree.from.SqmRoot; import org.hibernate.query.sqm.tree.select.SqmQueryGroup; import org.hibernate.query.sqm.tree.select.SqmQueryPart; @@ -52,7 +54,6 @@ import static java.util.stream.Collectors.toList; import static org.hibernate.cfg.QuerySettings.FAIL_ON_PAGINATION_OVER_COLLECTION_FETCH; import static org.hibernate.query.KeyedPage.KeyInterpretation.KEY_OF_FIRST_ON_NEXT_PAGE; -import static org.hibernate.query.sqm.internal.KeyBasedPagination.paginate; import static org.hibernate.query.sqm.internal.KeyedResult.collectKeys; import static org.hibernate.query.sqm.internal.KeyedResult.collectResults; import static org.hibernate.query.sqm.internal.SqmUtil.isHqlTuple; @@ -69,6 +70,10 @@ abstract class AbstractSqmSelectionQuery extends AbstractSelectionQuery { super(session); } + AbstractSqmSelectionQuery(AbstractSqmSelectionQuery original) { + super( original ); + } + protected int max(boolean hasLimit, SqmSelectStatement sqmStatement, List list) { return !hasLimit || getQueryOptions().getLimit().getMaxRows() == null ? getMaxRows( sqmStatement, list.size() ) @@ -124,6 +129,44 @@ private SqmSelectStatement getSqmSelectStatement() { } } + protected static void bindValueBindCriteriaParameters( + DomainParameterXref domainParameterXref, + QueryParameterBindings bindings) { + for ( var entry : domainParameterXref.getQueryParameters().entrySet() ) { + final var sqmParameter = entry.getValue().get( 0 ); + if ( sqmParameter instanceof SqmJpaCriteriaParameterWrapper ) { + final SqmJpaCriteriaParameterWrapper wrapper = (SqmJpaCriteriaParameterWrapper) sqmParameter; + @SuppressWarnings("unchecked") + final var criteriaParameter = (JpaCriteriaParameter) wrapper.getJpaCriteriaParameter(); + final var value = criteriaParameter.getValue(); + // We don't set a null value, unless the type is also null which + // is the case when using HibernateCriteriaBuilder.value + if ( value != null || criteriaParameter.getNodeType() == null ) { + // Use the anticipated type for binding the value if possible + //noinspection unchecked + final var parameter = (QueryParameterImplementor) entry.getKey(); + bindings.getBinding( parameter ) + .setBindValue( value, criteriaParameter.getAnticipatedType() ); + } + } + } + } + + @Override + protected

    QueryParameterImplementor

    getQueryParameter(QueryParameterImplementor

    parameter) { + if ( parameter instanceof JpaCriteriaParameter ) { + final JpaCriteriaParameter criteriaParameter = (JpaCriteriaParameter) parameter; + final var parameterWrapper = getDomainParameterXref().getParameterResolutions() + .getJpaCriteriaParamResolutions() + .get( criteriaParameter ); + //noinspection unchecked + return (QueryParameterImplementor

    ) getDomainParameterXref().getQueryParameter( parameterWrapper ); + } + else { + return parameter; + } + } + @Override public SelectionQuery setOrder(List> orderList) { SqmSelectStatement sqm = getSqmSelectStatement(); @@ -158,41 +201,14 @@ public SelectionQuery setPage(Page page) { return this; } - private SqmSelectStatement> paginateQuery( - List> keyDefinition, List> keyValues) { - @SuppressWarnings("unchecked") - final SqmSelectStatement> sqm = - (SqmSelectStatement>) - getSqmSelectStatement().copy( noParamCopyContext() ); - final NodeBuilder builder = sqm.nodeBuilder(); - //TODO: find a better way handle parameters - builder.setCriteriaValueHandlingMode(ValueHandlingMode.INLINE); - return paginate( keyDefinition, keyValues, sqm, builder ); - } - - @Override public KeyedResultList getKeyedResultList(KeyedPage keyedPage) { if ( keyedPage == null ) { throw new IllegalArgumentException( "KeyedPage was null" ); } + final List> results = new SqmSelectionQueryImpl>( this, keyedPage ) + .getResultList(); final Page page = keyedPage.getPage(); - final List> key = keyedPage.getKey(); - final List> keyDefinition = keyedPage.getKeyDefinition(); - final List> appliedKeyDefinition = - keyedPage.getKeyInterpretation() == KEY_OF_FIRST_ON_NEXT_PAGE - ? Order.reverse(keyDefinition) : keyDefinition; - - setMaxResults( page.getMaxResults() + 1 ); - if ( key == null ) { - setFirstResult( page.getFirstResult() ); - } - -// getQueryOptions().setQueryPlanCachingEnabled( false ); - final List> results = - buildConcreteQueryPlan( paginateQuery( appliedKeyDefinition, key ), getQueryOptions() ) - .performList(this); - return new KeyedResultList<>( collectResults( results, page.getSize(), keyedPage.getKeyInterpretation() ), collectKeys( results, page.getSize() ), @@ -276,10 +292,6 @@ protected ConcreteSqmSelectQueryPlan buildConcreteQueryPlan( ); } - private SelectQueryPlan buildConcreteQueryPlan(SqmSelectStatement sqmStatement, QueryOptions options) { - return buildConcreteQueryPlan( sqmStatement, null, null, options ); - } - protected void applyOptions(NamedSqmQueryMemento memento) { applyOptions( (NamedQueryMemento) memento ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/ConcreteSqmSelectQueryPlan.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/ConcreteSqmSelectQueryPlan.java index eedb762ccd95..eff9b319d1fe 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/ConcreteSqmSelectQueryPlan.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/ConcreteSqmSelectQueryPlan.java @@ -76,7 +76,6 @@ public class ConcreteSqmSelectQueryPlan implements SelectQueryPlan { private final SqmSelectStatement sqm; private final DomainParameterXref domainParameterXref; - private final RowTransformer rowTransformer; private final SqmInterpreter> executeQueryInterpreter; private final SqmInterpreter, Void> listInterpreter; private final SqmInterpreter, ScrollMode> scrollInterpreter; @@ -93,8 +92,6 @@ public ConcreteSqmSelectQueryPlan( this.sqm = sqm; this.domainParameterXref = domainParameterXref; - this.rowTransformer = determineRowTransformer( sqm, resultType, tupleMetadata, queryOptions ); - final ListResultsConsumer.UniqueSemantic uniqueSemantic; if ( sqm.producesUniqueResults() && !AppliedGraphs.containsCollectionFetches( queryOptions ) ) { uniqueSemantic = ListResultsConsumer.UniqueSemantic.NONE; @@ -122,7 +119,7 @@ public ConcreteSqmSelectQueryPlan( jdbcSelect, jdbcParameterBindings, listInterpreterExecutionContext( hql, executionContext, jdbcSelect, subSelectFetchKeyHandler ), - rowTransformer, + determineRowTransformer( sqm, resultType, tupleMetadata, executionContext.getQueryOptions() ), null, resultCountEstimate, resultsConsumer @@ -153,7 +150,7 @@ public ConcreteSqmSelectQueryPlan( jdbcSelect, jdbcParameterBindings, listInterpreterExecutionContext( hql, executionContext, jdbcSelect, subSelectFetchKeyHandler ), - rowTransformer, + determineRowTransformer( sqm, resultType, tupleMetadata, executionContext.getQueryOptions() ), (Class) executionContext.getResultType(), uniqueSemantic, resultCountEstimate @@ -189,7 +186,7 @@ public ConcreteSqmSelectQueryPlan( scrollMode, jdbcParameterBindings, new SqmJdbcExecutionContextAdapter( executionContext, jdbcSelect ), - rowTransformer, + determineRowTransformer( sqm, resultType, tupleMetadata, executionContext.getQueryOptions() ), resultCountEstimate ); } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/DomainParameterXref.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/DomainParameterXref.java index 8368de1c20c0..6d6c15b24a1b 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/DomainParameterXref.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/DomainParameterXref.java @@ -14,6 +14,7 @@ import java.util.Map; import java.util.TreeMap; +import org.hibernate.query.internal.QueryParameterIdentifiedImpl; import org.hibernate.query.internal.QueryParameterNamedImpl; import org.hibernate.query.internal.QueryParameterPositionalImpl; import org.hibernate.query.spi.QueryParameterImplementor; @@ -41,14 +42,6 @@ public class DomainParameterXref { * Create a DomainParameterXref for the parameters defined in the SQM statement */ public static DomainParameterXref from(SqmStatement sqmStatement) { - // `xrefMap` is used to help maintain the proper cardinality between an - // SqmParameter and a QueryParameter. Multiple SqmParameter references - // can map to the same QueryParameter. Consider, e.g., - // `.. where a.b = :param or a.c = :param`. Here we have 2 SqmParameter - // references (one for each occurrence of `:param`) both of which map to - // the same QueryParameter. - final Map, QueryParameterImplementor> xrefMap = new TreeMap<>(); - final SqmStatement.ParameterResolutions parameterResolutions = sqmStatement.resolveParameters(); if ( parameterResolutions.getSqmParameters().isEmpty() ) { return EMPTY; @@ -67,41 +60,32 @@ public static DomainParameterXref from(SqmStatement sqmStatement) { ); } - final QueryParameterImplementor queryParameter = xrefMap.computeIfAbsent( - sqmParameter, - p -> { - if ( sqmParameter instanceof SqmJpaCriteriaParameterWrapper ) { - return ( (SqmJpaCriteriaParameterWrapper) sqmParameter ).getJpaCriteriaParameter(); - } - else if ( sqmParameter.getName() != null ) { - return QueryParameterNamedImpl.fromSqm( sqmParameter ); - } - else if ( sqmParameter.getPosition() != null ) { - return QueryParameterPositionalImpl.fromSqm( sqmParameter ); - } - else { - throw new UnsupportedOperationException( "Unexpected SqmParameter type : " + sqmParameter ); - } - } - ); - - if ( ! sqmParameter.allowMultiValuedBinding() ) { - if ( queryParameter.allowsMultiValuedBinding() ) { - SqmTreeTransformationLogger.LOGGER.debugf( - "SqmParameter [%s] does not allow multi-valued binding, " + - "but mapped to existing QueryParameter [%s] that does - " + - "disallowing multi-valued binding" , - sqmParameter, - queryParameter - ); - queryParameter.disallowMultiValuedBinding(); + + + final QueryParameterImplementor queryParameter; + if ( sqmParameter.getName() != null ) { + queryParameter = QueryParameterNamedImpl.fromSqm( sqmParameter ); + } + else if ( sqmParameter.getPosition() != null ) { + queryParameter = QueryParameterPositionalImpl.fromSqm( sqmParameter ); + } + else if ( sqmParameter instanceof SqmJpaCriteriaParameterWrapper ) { + final SqmJpaCriteriaParameterWrapper criteriaParameter = (SqmJpaCriteriaParameterWrapper) sqmParameter; + if ( sqmParameter.allowMultiValuedBinding() + && sqmParameter.getExpressible() != null + && sqmParameter.getExpressible().getSqmType() instanceof BasicCollectionType ) { + // The wrapper parameter was inferred to be of a basic collection type, + // so we disallow multivalued bindings, because binding a list of collections isn't useful + criteriaParameter.getJpaCriteriaParameter().disallowMultiValuedBinding(); } + queryParameter = QueryParameterIdentifiedImpl.fromSqm( criteriaParameter ); } - else if ( sqmParameter.getExpressible() != null && sqmParameter.getExpressible().getSqmType() instanceof BasicCollectionType ) { - queryParameter.disallowMultiValuedBinding(); + else { + throw new UnsupportedOperationException( + "Unexpected SqmParameter type : " + sqmParameter ); } - sqmParamsByQueryParam.computeIfAbsent( queryParameter, qp -> new ArrayList<>() ).add( sqmParameter ); + sqmParamsByQueryParam.computeIfAbsent( queryParameter, impl -> new ArrayList<>() ).add( sqmParameter ); queryParamBySqmParam.put( sqmParameter, queryParameter ); } @@ -177,10 +161,7 @@ public List> getSqmParameters(QueryParameterImplementor query } public QueryParameterImplementor getQueryParameter(SqmParameter sqmParameter) { - if ( sqmParameter instanceof SqmJpaCriteriaParameterWrapper ) { - return ( (SqmJpaCriteriaParameterWrapper) sqmParameter ).getJpaCriteriaParameter(); - } - else if ( sqmParameter instanceof QueryParameterImplementor ) { + if ( sqmParameter instanceof QueryParameterImplementor ) { return (QueryParameterImplementor) sqmParameter; } return queryParamBySqmParam.get( sqmParameter ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/KeyBasedPagination.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/KeyBasedPagination.java index f1eb6eabab1d..098f3ec442f5 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/KeyBasedPagination.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/KeyBasedPagination.java @@ -118,11 +118,6 @@ private static > SqmPredicate keyPredicate( Expression key, C keyValue, SortDirection direction, List> previousKeys, List> keyValues, NodeBuilder builder) { - // TODO: use a parameter here and create a binding for it -// @SuppressWarnings("unchecked") -// final Class valueClass = (Class) keyValue.getClass(); -// final JpaParameterExpression parameter = builder.parameter(valueClass); -// setParameter( parameter, keyValue ); SqmPredicate predicate; switch ( direction ) { case ASCENDING: diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/NoParamSqmCopyContext.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/NoParamSqmCopyContext.java index 8a3d960a2a05..ae8f09f48b39 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/NoParamSqmCopyContext.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/NoParamSqmCopyContext.java @@ -6,6 +6,7 @@ */ package org.hibernate.query.sqm.internal; +import org.hibernate.query.sqm.SqmQuerySource; import org.hibernate.query.sqm.tree.expression.SqmParameter; import org.checkerframework.checker.nullness.qual.Nullable; @@ -14,6 +15,13 @@ * @author Marco Belladelli */ public class NoParamSqmCopyContext extends SimpleSqmCopyContext { + public NoParamSqmCopyContext() { + } + + public NoParamSqmCopyContext(@Nullable SqmQuerySource querySource) { + super( querySource ); + } + @Override public @Nullable T getCopy(T original) { if ( original instanceof SqmParameter ) { diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/QuerySqmImpl.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/QuerySqmImpl.java index 785f0ccaf0ee..87e43b838e12 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/QuerySqmImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/QuerySqmImpl.java @@ -226,11 +226,7 @@ public QuerySqmImpl( this.parameterBindings = parameterMetadata.createBindings( producer.getFactory() ); // Parameters might be created through HibernateCriteriaBuilder.value which we need to bind here - for ( SqmParameter sqmParameter : domainParameterXref.getParameterResolutions().getSqmParameters() ) { - if ( sqmParameter instanceof SqmJpaCriteriaParameterWrapper ) { - bindCriteriaParameter((SqmJpaCriteriaParameterWrapper) sqmParameter); - } - } + bindValueBindCriteriaParameters( domainParameterXref, parameterBindings ); if ( sqm instanceof SqmSelectStatement ) { final SqmSelectStatement selectStatement = (SqmSelectStatement) sqm; final SqmQueryPart queryPart = selectStatement.getQueryPart(); @@ -250,18 +246,6 @@ public QuerySqmImpl( tupleMetadata = buildTupleMetadata( criteria, expectedResultType ); } - private void bindCriteriaParameter(SqmJpaCriteriaParameterWrapper sqmParameter) { - final JpaCriteriaParameter jpaCriteriaParameter = sqmParameter.getJpaCriteriaParameter(); - final T value = jpaCriteriaParameter.getValue(); - // We don't set a null value, unless the type is also null which - // is the case when using HibernateCriteriaBuilder.value - if ( value != null || jpaCriteriaParameter.getNodeType() == null ) { - // Use the anticipated type for binding the value if possible - getQueryParameterBindings().getBinding( jpaCriteriaParameter ) - .setBindValue( value, jpaCriteriaParameter.getAnticipatedType() ); - } - } - @Override public TupleMetadata getTupleMetadata() { return tupleMetadata; @@ -904,6 +888,7 @@ public NamedQueryMemento toMemento(String name) { } return new NamedCriteriaQueryMementoImpl( name, + getResultType(), sqmStatement, getQueryOptions().getLimit().getFirstRow(), getQueryOptions().getLimit().getMaxRows(), @@ -923,6 +908,7 @@ public NamedQueryMemento toMemento(String name) { return new NamedHqlQueryMementoImpl( name, + getResultType(), getQueryString(), getQueryOptions().getLimit().getFirstRow(), getQueryOptions().getLimit().getMaxRows(), diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SimpleSqmCopyContext.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SimpleSqmCopyContext.java index 37e61b8d4145..0a3c20cbb051 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SimpleSqmCopyContext.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SimpleSqmCopyContext.java @@ -8,6 +8,7 @@ import java.util.IdentityHashMap; +import org.hibernate.query.sqm.SqmQuerySource; import org.hibernate.query.sqm.tree.SqmCopyContext; import org.checkerframework.checker.nullness.qual.Nullable; @@ -17,6 +18,15 @@ */ public class SimpleSqmCopyContext implements SqmCopyContext { private final IdentityHashMap map = new IdentityHashMap<>(); + private final @Nullable SqmQuerySource querySource; + + public SimpleSqmCopyContext() { + this( null ); + } + + public SimpleSqmCopyContext(@Nullable SqmQuerySource querySource) { + this.querySource = querySource; + } @Override @SuppressWarnings( "unchecked" ) @@ -32,4 +42,9 @@ public T registerCopy(T original, T copy) { } return copy; } + + @Override + public @Nullable SqmQuerySource getQuerySource() { + return querySource; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java index 65d8569f8fb4..632953b7e9b6 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmCriteriaNodeBuilder.java @@ -46,7 +46,6 @@ import org.hibernate.metamodel.model.domain.DomainType; import org.hibernate.metamodel.model.domain.JpaMetamodel; import org.hibernate.metamodel.model.domain.SingularPersistentAttribute; -import org.hibernate.metamodel.model.domain.internal.BasicTypeImpl; import org.hibernate.metamodel.model.domain.spi.JpaMetamodelImplementor; import org.hibernate.metamodel.spi.MappingMetamodelImplementor; import org.hibernate.query.BindableType; @@ -199,7 +198,7 @@ public class SqmCriteriaNodeBuilder implements NodeBuilder, SqmCreationContext, private final transient boolean jpaComplianceEnabled; private final transient QueryEngine queryEngine; private final transient Supplier sessionFactory; - private transient ValueHandlingMode criteriaValueHandlingMode; + private final transient ValueHandlingMode criteriaValueHandlingMode; private transient BasicType booleanType; private transient BasicType integerType; private transient BasicType longType; @@ -228,10 +227,6 @@ public SqmCriteriaNodeBuilder( } } - public void setCriteriaValueHandlingMode(ValueHandlingMode criteriaValueHandlingMode) { - this.criteriaValueHandlingMode = criteriaValueHandlingMode; - } - @Override public JpaMetamodel getDomainModel() { return getSessionFactory().getJpaMetamodel(); @@ -1331,7 +1326,7 @@ else if ( value == null ) { final EnumJavaType javaType = new EnumJavaType<>( type ); final JdbcType jdbcType = javaType.getRecommendedJdbcType( typeConfiguration.getCurrentBaseSqlTypeIndicators() ); - return new BasicTypeImpl<>( javaType, jdbcType ); + return typeConfiguration.getBasicTypeRegistry().resolve( javaType, jdbcType ); } else { return result; diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmInterpretationsKey.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmInterpretationsKey.java index ea7d5859c949..aa5da96ab5e1 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmInterpretationsKey.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmInterpretationsKey.java @@ -14,8 +14,6 @@ import org.hibernate.LockOptions; import org.hibernate.engine.spi.LoadQueryInfluencers; -import org.hibernate.query.ResultListTransformer; -import org.hibernate.query.TupleTransformer; import org.hibernate.query.spi.QueryInterpretationCache; import org.hibernate.query.spi.QueryOptions; import org.hibernate.query.sqm.tree.SqmStatement; @@ -50,8 +48,6 @@ public static SqmInterpretationsKey createInterpretationsKey(InterpretationsKeyS query.hashCode(), keySource.getResultType(), keySource.getQueryOptions().getLockOptions(), - keySource.getQueryOptions().getTupleTransformer(), - keySource.getQueryOptions().getResultListTransformer(), memoryEfficientDefensiveSetCopy( keySource.getLoadQueryInfluencers().getEnabledFetchProfileNames() ) ); } @@ -110,8 +106,6 @@ public static QueryInterpretationCache.Key generateNonSelectKey(InterpretationsK private final Object query; private final Class resultType; private final LockOptions lockOptions; - private final TupleTransformer tupleTransformer; - private final ResultListTransformer resultListTransformer; private final Collection enabledFetchProfiles; private final int hashcode; @@ -120,15 +114,11 @@ private SqmInterpretationsKey( int hash, Class resultType, LockOptions lockOptions, - TupleTransformer tupleTransformer, - ResultListTransformer resultListTransformer, Collection enabledFetchProfiles) { this.query = query; this.hashcode = hash; this.resultType = resultType; this.lockOptions = lockOptions; - this.tupleTransformer = tupleTransformer; - this.resultListTransformer = resultListTransformer; this.enabledFetchProfiles = enabledFetchProfiles; } @@ -140,8 +130,6 @@ public QueryInterpretationCache.Key prepareForStore() { resultType, // Since lock options might be mutable, we need a copy for the cache key lockOptions.makeDefensiveCopy(), - tupleTransformer, - resultListTransformer, enabledFetchProfiles ); } @@ -165,8 +153,6 @@ public boolean equals(Object o) { && query.equals( that.query ) && Objects.equals( resultType, that.resultType ) && Objects.equals( lockOptions, that.lockOptions ) - && Objects.equals( tupleTransformer, that.tupleTransformer ) - && Objects.equals( resultListTransformer, that.resultListTransformer ) && Objects.equals( enabledFetchProfiles, that.enabledFetchProfiles ); } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmSelectionQueryImpl.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmSelectionQueryImpl.java index 47b1ac97cb59..88b7c7412895 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmSelectionQueryImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmSelectionQueryImpl.java @@ -30,7 +30,11 @@ import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.graph.spi.AppliedGraph; import org.hibernate.internal.util.collections.IdentitySet; +import org.hibernate.metamodel.mapping.MappingModelExpressible; import org.hibernate.query.BindableType; +import org.hibernate.query.KeyedPage; +import org.hibernate.query.Order; +import org.hibernate.query.Page; import org.hibernate.query.QueryParameter; import org.hibernate.query.criteria.internal.NamedCriteriaQueryMementoImpl; import org.hibernate.query.hql.internal.NamedHqlQueryMementoImpl; @@ -42,9 +46,11 @@ import org.hibernate.query.spi.ParameterMetadataImplementor; import org.hibernate.query.spi.QueryInterpretationCache; import org.hibernate.query.spi.QueryOptions; +import org.hibernate.query.spi.QueryParameterBinding; import org.hibernate.query.spi.QueryParameterBindings; import org.hibernate.query.spi.ScrollableResultsImplementor; import org.hibernate.query.spi.SelectQueryPlan; +import org.hibernate.query.sqm.SqmQuerySource; import org.hibernate.query.sqm.SqmSelectionQuery; import org.hibernate.query.sqm.internal.SqmInterpretationsKey.InterpretationsKeySource; import org.hibernate.query.sqm.spi.SqmSelectionQueryImplementor; @@ -58,6 +64,7 @@ import org.hibernate.sql.results.internal.TupleMetadata; import org.hibernate.sql.results.spi.ResultsConsumer; import org.hibernate.sql.results.spi.SingleResultConsumer; +import org.hibernate.type.descriptor.java.JavaType; import static org.hibernate.jpa.HibernateHints.HINT_CACHEABLE; import static org.hibernate.jpa.HibernateHints.HINT_CACHE_MODE; @@ -69,9 +76,12 @@ import static org.hibernate.jpa.LegacySpecHints.HINT_JAVAEE_CACHE_STORE_MODE; import static org.hibernate.jpa.SpecHints.HINT_SPEC_CACHE_RETRIEVE_MODE; import static org.hibernate.jpa.SpecHints.HINT_SPEC_CACHE_STORE_MODE; +import static org.hibernate.query.KeyedPage.KeyInterpretation.KEY_OF_FIRST_ON_NEXT_PAGE; import static org.hibernate.query.spi.SqlOmittingQueryOptions.omitSqlQueryOptions; +import static org.hibernate.query.sqm.internal.KeyBasedPagination.paginate; import static org.hibernate.query.sqm.internal.SqmInterpretationsKey.createInterpretationsKey; import static org.hibernate.query.sqm.internal.SqmUtil.isSelectionAssignableToResultType; +import static org.hibernate.query.sqm.tree.SqmCopyContext.noParamCopyContext; /** * @author Steve Ebersole @@ -159,11 +169,7 @@ public SqmSelectionQueryImpl( this.parameterBindings = parameterMetadata.createBindings( session.getFactory() ); // Parameters might be created through HibernateCriteriaBuilder.value which we need to bind here - for ( SqmParameter sqmParameter : domainParameterXref.getParameterResolutions().getSqmParameters() ) { - if ( sqmParameter instanceof SqmJpaCriteriaParameterWrapper ) { - bindCriteriaParameter( (SqmJpaCriteriaParameterWrapper) sqmParameter ); - } - } + bindValueBindCriteriaParameters( domainParameterXref, parameterBindings ); this.expectedResultType = expectedResultType; this.resultType = determineResultType( sqm, expectedResultType ); @@ -179,6 +185,83 @@ public SqmSelectionQueryImpl( this.tupleMetadata = buildTupleMetadata( sqm, expectedResultType ); } + SqmSelectionQueryImpl(AbstractSqmSelectionQuery original, KeyedPage keyedPage) { + super( original ); + + final Page page = keyedPage.getPage(); + final List> key = keyedPage.getKey(); + final List> keyDefinition = keyedPage.getKeyDefinition(); + final List> appliedKeyDefinition = + keyedPage.getKeyInterpretation() == KEY_OF_FIRST_ON_NEXT_PAGE + ? Order.reverse( keyDefinition ) : keyDefinition; + + //noinspection unchecked + this.sqm = (SqmSelectStatement) paginate( + appliedKeyDefinition, + key, + // Change the query source to CRITERIA, because we will change the query and introduce parameters + (SqmSelectStatement>) original.getSqmStatement() + .copy( noParamCopyContext( SqmQuerySource.CRITERIA ) ), + original.getSqmStatement().nodeBuilder() + ); + this.hql = CRITERIA_HQL_STRING; + + this.domainParameterXref = DomainParameterXref.from( sqm ); + this.parameterMetadata = domainParameterXref.hasParameters() + ? new ParameterMetadataImpl( domainParameterXref.getQueryParameters() ) + : ParameterMetadataImpl.EMPTY; + + // Just use the original parameter bindings since this object is never going to be mutated + this.parameterBindings = parameterMetadata.createBindings( original.getSession().getSessionFactory() ); + // Don't remove this cast. This is here to work around this bug: https://bugs.openjdk.org/browse/JDK-8340443 + (( DomainQueryExecutionContext) original ).getQueryParameterBindings().visitBindings( + (parameter, binding) -> { + //noinspection unchecked + final QueryParameterBinding parameterBinding = + (QueryParameterBinding) this.parameterBindings.getBinding( parameter ); + //noinspection unchecked + final BindableType bindType = (BindableType) binding.getBindType(); + final TemporalType explicitTemporalPrecision = binding.getExplicitTemporalPrecision(); + if ( explicitTemporalPrecision != null ) { + if ( binding.isMultiValued() ) { + parameterBinding.setBindValues( + binding.getBindValues(), + explicitTemporalPrecision, + getSessionFactory().getTypeConfiguration() + ); + } + else { + parameterBinding.setBindValue( binding.getBindValue(), explicitTemporalPrecision ); + } + } + else { + if ( binding.isMultiValued() ) { + parameterBinding.setBindValues( binding.getBindValues(), bindType ); + } + else { + parameterBinding.setBindValue( binding.getBindValue(), bindType ); + } + } + //noinspection unchecked + parameterBinding.setType( (MappingModelExpressible) binding.getType() ); + } + ); + + // Parameters might be created through HibernateCriteriaBuilder.value which we need to bind here + bindValueBindCriteriaParameters( domainParameterXref, parameterBindings ); + + //noinspection unchecked + this.expectedResultType = (Class) KeyedResult.class; + this.resultType = determineResultType( sqm, expectedResultType ); + this.tupleMetadata = null; + + setMaxResults( page.getMaxResults() + 1 ); + if ( key == null ) { + setFirstResult( page.getFirstResult() ); + } + } + + private static Class determineResultType(SqmSelectStatement sqm, Class expectedResultType) { final List> selections = sqm.getQuerySpec().getSelectClause().getSelections(); if ( selections.size() == 1 ) { @@ -187,15 +270,15 @@ private static Class determineResultType(SqmSelectStatement sqm, Class return Object[].class; } else { - final SqmSelection selection = selections.get(0); + final SqmSelection selection = selections.get( 0 ); if ( isSelectionAssignableToResultType( selection, expectedResultType ) ) { - return selection.getNodeJavaType().getJavaTypeClass(); - } - else { - // let's assume there's some - // way to instantiate it - return expectedResultType; + final JavaType nodeJavaType = selection.getNodeJavaType(); + if ( nodeJavaType != null ) { + return nodeJavaType.getJavaTypeClass(); + } } + // let's assume there's some way to instantiate it + return expectedResultType; } } else if ( expectedResultType != null ) { @@ -209,19 +292,6 @@ else if ( expectedResultType != null ) { } } - private void bindCriteriaParameter(SqmJpaCriteriaParameterWrapper sqmParameter) { - final JpaCriteriaParameter jpaCriteriaParameter = sqmParameter.getJpaCriteriaParameter(); - final T value = jpaCriteriaParameter.getValue(); - // We don't set a null value, unless the type is also null which - // is the case when using HibernateCriteriaBuilder.value - if ( value != null || jpaCriteriaParameter.getNodeType() == null ) { - // Use the anticipated type for binding the value if possible - getQueryParameterBindings() - .getBinding( jpaCriteriaParameter ) - .setBindValue( value, jpaCriteriaParameter.getAnticipatedType() ); - } - } - @Override public TupleMetadata getTupleMetadata() { return tupleMetadata; diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmUtil.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmUtil.java index d6a63c5cb3a9..bf1cb5eb8319 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmUtil.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/SqmUtil.java @@ -24,6 +24,7 @@ import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.internal.util.collections.CollectionHelper; +import org.hibernate.internal.util.collections.Stack; import org.hibernate.metamodel.MappingMetamodel; import org.hibernate.metamodel.mapping.BasicValuedMapping; import org.hibernate.metamodel.mapping.Bindable; @@ -37,12 +38,17 @@ import org.hibernate.metamodel.mapping.ModelPart; import org.hibernate.metamodel.mapping.ModelPartContainer; import org.hibernate.metamodel.mapping.PluralAttributeMapping; +import org.hibernate.metamodel.mapping.internal.ToOneAttributeMapping; import org.hibernate.metamodel.model.domain.BasicDomainType; import org.hibernate.metamodel.model.domain.DomainType; import org.hibernate.metamodel.model.domain.EntityDomainType; import org.hibernate.metamodel.model.domain.IdentifiableDomainType; +import org.hibernate.metamodel.model.domain.ManagedDomainType; import org.hibernate.metamodel.model.domain.SimpleDomainType; +import org.hibernate.metamodel.model.domain.SingularPersistentAttribute; import org.hibernate.metamodel.model.domain.internal.EntitySqmPathSource; +import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.query.BindableType; import org.hibernate.query.IllegalQueryOperationException; import org.hibernate.query.IllegalSelectQueryException; import org.hibernate.query.Order; @@ -63,6 +69,7 @@ import org.hibernate.query.sqm.tree.SqmJoinType; import org.hibernate.query.sqm.tree.SqmStatement; import org.hibernate.query.sqm.tree.domain.SqmPath; +import org.hibernate.query.sqm.tree.domain.SqmSingularJoin; import org.hibernate.query.sqm.tree.expression.JpaCriteriaParameter; import org.hibernate.query.sqm.tree.expression.SqmAliasedNodeRef; import org.hibernate.query.sqm.tree.expression.SqmExpression; @@ -102,6 +109,7 @@ import org.hibernate.type.spi.TypeConfiguration; import jakarta.persistence.Tuple; +import jakarta.persistence.metamodel.Type; import org.checkerframework.checker.nullness.qual.Nullable; import static java.util.stream.Collectors.toList; @@ -168,38 +176,66 @@ public static ModelPartContainer getTargetMappingIfNeeded( SqmPath sqmPath, ModelPartContainer modelPartContainer, SqmToSqlAstConverter sqlAstCreationState) { - final SqmQueryPart queryPart = sqlAstCreationState.getCurrentSqmQueryPart(); - if ( queryPart != null ) { - // We only need to do this for queries - final Clause clause = sqlAstCreationState.getCurrentClauseStack().getCurrent(); - if ( clause != Clause.FROM && modelPartContainer.getPartMappingType() != modelPartContainer && sqmPath.getLhs() instanceof SqmFrom ) { - final ModelPart modelPart; - if ( modelPartContainer instanceof PluralAttributeMapping ) { - modelPart = getCollectionPart( - (PluralAttributeMapping) modelPartContainer, - castNonNull( sqmPath.getNavigablePath().getParent() ) - ); - } - else { - modelPart = modelPartContainer; - } - if ( modelPart instanceof EntityAssociationMapping ) { - final EntityAssociationMapping association = (EntityAssociationMapping) modelPart; - // If the path is one of the association's target key properties, - // we need to render the target side if in group/order by - if ( association.getTargetKeyPropertyNames().contains( sqmPath.getReferencedPathSource().getPathName() ) - && ( clause == Clause.GROUP || clause == Clause.ORDER - || !isFkOptimizationAllowed( sqmPath.getLhs() ) - || queryPart.getFirstQuerySpec().groupByClauseContains( sqmPath.getNavigablePath(), sqlAstCreationState ) - || queryPart.getFirstQuerySpec().orderByClauseContains( sqmPath.getNavigablePath(), sqlAstCreationState ) ) ) { - return association.getAssociatedEntityMappingType(); - } + // We only need to do this for queries + if ( sqlAstCreationState.getCurrentClauseStack().getCurrent() != Clause.FROM + && modelPartContainer.getPartMappingType() != modelPartContainer && sqmPath.getLhs() instanceof SqmFrom ) { + final ModelPart modelPart; + if ( modelPartContainer instanceof PluralAttributeMapping ) { + modelPart = getCollectionPart( + (PluralAttributeMapping) modelPartContainer, + castNonNull( sqmPath.getNavigablePath().getParent() ) + ); + } + else { + modelPart = modelPartContainer; + } + if ( modelPart instanceof EntityAssociationMapping ) { + final EntityAssociationMapping association = (EntityAssociationMapping) modelPart; + if ( shouldRenderTargetSide( sqmPath, association, sqlAstCreationState ) ) { + return association.getAssociatedEntityMappingType(); } } } return modelPartContainer; } + private static boolean shouldRenderTargetSide( + SqmPath sqmPath, + EntityAssociationMapping association, + SqmToSqlAstConverter sqlAstCreationState) { + if ( !association.getTargetKeyPropertyNames().contains( sqmPath.getReferencedPathSource().getPathName() ) ) { + return false; + } + // If the path is one of the association's target key properties, + // we need to render the target side if in group/order by + final Clause clause = sqlAstCreationState.getCurrentClauseStack().getCurrent(); + return clause == Clause.GROUP || clause == Clause.ORDER + || !isFkOptimizationAllowed( sqmPath.getLhs(), association ) + || clauseContainsPath( Clause.GROUP, sqmPath, sqlAstCreationState ) + || clauseContainsPath( Clause.ORDER, sqmPath, sqlAstCreationState ); + } + + private static boolean clauseContainsPath( + Clause clauseToCheck, + SqmPath sqmPath, + SqmToSqlAstConverter sqlAstCreationState) { + final Stack queryPartStack = sqlAstCreationState.getSqmQueryPartStack(); + final NavigablePath navigablePath = sqmPath.getNavigablePath(); + final Boolean found = queryPartStack.findCurrentFirst( queryPart -> { + final SqmQuerySpec spec = queryPart.getFirstQuerySpec(); + if ( clauseToCheck == Clause.GROUP && spec.groupByClauseContains( navigablePath, sqlAstCreationState ) ) { + return true; + } + else if ( clauseToCheck == Clause.ORDER && spec.orderByClauseContains( navigablePath, sqlAstCreationState ) ) { + return true; + } + else { + return null; + } + } ); + return Boolean.TRUE.equals( found ); + } + private static CollectionPart getCollectionPart(PluralAttributeMapping attribute, NavigablePath path) { final CollectionPart.Nature nature = CollectionPart.Nature.fromNameExact( path.getLocalName() ); if ( nature != null ) { @@ -208,6 +244,8 @@ private static CollectionPart getCollectionPart(PluralAttributeMapping attribute return attribute.getElementDescriptor(); case INDEX: return attribute.getIndexDescriptor(); + case ID: + return attribute.getIdentifierDescriptor(); } } return null; @@ -218,13 +256,52 @@ private static CollectionPart getCollectionPart(PluralAttributeMapping attribute * a join that cannot be dereferenced through the foreign key on the associated table, * i.e. a join that's neither {@linkplain SqmJoinType#INNER} nor {@linkplain SqmJoinType#LEFT} * or one that has an explicit on clause predicate. + * + * @deprecated Use {@link #isFkOptimizationAllowed(SqmPath, EntityAssociationMapping)} instead */ + @Deprecated(forRemoval = true, since = "6.6.1") public static boolean isFkOptimizationAllowed(SqmPath sqmPath) { if ( sqmPath instanceof SqmJoin ) { final SqmJoin sqmJoin = (SqmJoin) sqmPath; switch ( sqmJoin.getSqmJoinType() ) { + case LEFT: + final EntityAssociationMapping associationMapping = resolveAssociationMapping( sqmJoin ); + if ( associationMapping != null && isFiltered( associationMapping ) ) { + return false; + } + // FallThrough intended case INNER: + return !( sqmJoin instanceof SqmQualifiedJoin) + || ( (SqmQualifiedJoin) sqmJoin ).getJoinPredicate() == null; + default: + return false; + } + } + return false; + } + + /** + * Utility that returns {@code false} when the provided {@link SqmPath sqmPath} is + * a join that cannot be dereferenced through the foreign key on the associated table, + * i.e. a join that's neither {@linkplain SqmJoinType#INNER} nor {@linkplain SqmJoinType#LEFT} + * or one that has an explicit on clause predicate. + */ + public static boolean isFkOptimizationAllowed(SqmPath sqmPath, EntityAssociationMapping associationMapping) { + // By default, never allow the FK optimization if the path is a join, unless the association has a join table + // Hibernate ORM has no way for users to refer to collection/join table rows, + // so referring the columns of these rows by default when requesting FK column attributes is sensible. + // Users that need to refer to the actual target table columns will have to add an explicit entity join. + if ( associationMapping.isFkOptimizationAllowed() + && sqmPath instanceof SqmJoin + && hasJoinTable( associationMapping ) ) { + final SqmJoin sqmJoin = (SqmJoin) sqmPath; + switch ( sqmJoin.getSqmJoinType() ) { case LEFT: + if ( isFiltered( associationMapping ) ) { + return false; + } + // FallThrough intended + case INNER: return !( sqmJoin instanceof SqmQualifiedJoin) || ( (SqmQualifiedJoin) sqmJoin ).getJoinPredicate() == null; default: @@ -234,6 +311,69 @@ public static boolean isFkOptimizationAllowed(SqmPath sqmPath) { return false; } + private static boolean hasJoinTable(EntityAssociationMapping associationMapping) { + if ( associationMapping instanceof CollectionPart ) { + return !( (CollectionPart) associationMapping ).getCollectionAttribute().getCollectionDescriptor().isOneToMany(); + } + else if ( associationMapping instanceof ToOneAttributeMapping ) { + return ( (ToOneAttributeMapping) associationMapping ).hasJoinTable(); + } + return false; + } + + private static boolean isFiltered(EntityAssociationMapping associationMapping) { + final EntityMappingType entityMappingType = associationMapping.getAssociatedEntityMappingType(); + return !associationMapping.isFkOptimizationAllowed() + // When the identifier mappings are different we have a joined subclass entity + // which will filter rows based on a discriminator predicate + || entityMappingType.getIdentifierMappingForJoin() != entityMappingType.getIdentifierMapping(); + } + + private static @Nullable EntityAssociationMapping resolveAssociationMapping(SqmJoin sqmJoin) { + if ( sqmJoin instanceof SqmSingularJoin ) { + final SqmSingularJoin singularJoin = (SqmSingularJoin) sqmJoin; + if ( singularJoin.getAttribute().getSqmPathType() instanceof EntityDomainType ) { + return resolveAssociationMapping( singularJoin ); + } + } + return null; + } + + private static @Nullable EntityAssociationMapping resolveAssociationMapping(SqmSingularJoin sqmJoin) { + SingularPersistentAttribute attribute = sqmJoin.getAttribute(); + ManagedDomainType declaringType = attribute.getDeclaringType(); + if ( declaringType.getPersistenceType() != Type.PersistenceType.ENTITY ) { + final StringBuilder pathBuilder = new StringBuilder(); + do { + if ( pathBuilder.length() > 0 ) { + pathBuilder.insert(0, '.'); + } + pathBuilder.insert( 0, attribute.getName() ); + final SqmFrom lhs = sqmJoin.getLhs(); + if ( !(lhs instanceof SqmSingularJoin ) ) { + return null; + } + sqmJoin = (SqmSingularJoin) lhs; + attribute = sqmJoin.getAttribute(); + declaringType = attribute.getDeclaringType(); + } while (declaringType.getPersistenceType() != Type.PersistenceType.ENTITY ); + pathBuilder.insert(0, '.'); + pathBuilder.insert( 0, attribute.getName() ); + final EntityPersister entityDescriptor = sqmJoin.nodeBuilder() + .getSessionFactory() + .getMappingMetamodel() + .getEntityDescriptor( ( (EntityDomainType) declaringType ).getHibernateEntityName() ); + return (EntityAssociationMapping) entityDescriptor.findByPath( pathBuilder.toString() ); + } + else { + final EntityPersister entityDescriptor = sqmJoin.nodeBuilder() + .getSessionFactory() + .getMappingMetamodel() + .getEntityDescriptor( ( (EntityDomainType) declaringType ).getHibernateEntityName() ); + return (EntityAssociationMapping) entityDescriptor.findAttributeMapping( attribute.getName() ); + } + } + public static List getWhereClauseNavigablePaths(SqmQuerySpec querySpec) { final SqmWhereClause where = querySpec.getWhereClause(); if ( where == null || where.getPredicate() == null ) { @@ -287,7 +427,7 @@ public static SqmAttributeJoin findCompatibleFetchJoin( SqmPathSource pathSource, SqmJoinType requestedJoinType) { for ( final SqmJoin join : sqmFrom.getSqmJoins() ) { - if ( join.getReferencedPathSource() == pathSource ) { + if ( join.getModel() == pathSource ) { final SqmAttributeJoin attributeJoin = (SqmAttributeJoin) join; if ( attributeJoin.isFetched() ) { final SqmJoinType joinType = join.getSqmJoinType(); @@ -505,7 +645,7 @@ else if ( domainParamBinding.isMultiValued() ) { jdbcMapping = (JdbcMapping) domainParamBinding.getType(); } else if ( domainParamBinding.getBindType() instanceof BasicValuedMapping ) { - jdbcMapping = ( (BasicValuedMapping) domainParamBinding.getType() ).getJdbcMapping(); + jdbcMapping = ( (BasicValuedMapping) domainParamBinding.getBindType() ).getJdbcMapping(); } else { jdbcMapping = null; @@ -762,10 +902,16 @@ static JpaOrder sortSpecification(SqmSelectStatement sqm, Order order) { } public static boolean isSelectionAssignableToResultType(SqmSelection selection, Class expectedResultType) { - if ( expectedResultType == null - || selection != null && selection.getSelectableNode() instanceof SqmParameter ) { + if ( expectedResultType == null ) { return true; } + else if ( selection != null && selection.getSelectableNode() instanceof SqmParameter ) { + final SqmParameter sqmParameter = (SqmParameter) selection.getSelectableNode(); + final Class anticipatedClass = sqmParameter.getAnticipatedType() != null ? + sqmParameter.getAnticipatedType().getBindableJavaType() : + null; + return anticipatedClass != null && expectedResultType.isAssignableFrom( anticipatedClass ); + } else if ( selection == null || !isHqlTuple( selection ) && selection.getSelectableNode().isCompoundSelection() ) { return false; @@ -1030,7 +1176,10 @@ protected static void verifyResultType(Class resultClass, @Nullable SqmExpres } final JavaType selectionExpressibleJavaType = selectionExpressible.getExpressibleJavaType(); - assert selectionExpressibleJavaType != null; + if ( selectionExpressibleJavaType == null ) { + // nothing we can validate + return; + } final Class selectionExpressibleJavaTypeClass = selectionExpressibleJavaType.getJavaTypeClass(); if ( selectionExpressibleJavaTypeClass == Object.class ) { diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/TypecheckUtil.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/TypecheckUtil.java index d44a614cf9a1..1fab71f008b2 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/TypecheckUtil.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/internal/TypecheckUtil.java @@ -443,8 +443,8 @@ public static void assertAssignable( } public static void assertOperable(SqmExpression left, SqmExpression right, BinaryArithmeticOperator op) { - final SqmExpressible leftNodeType = left.getNodeType(); - final SqmExpressible rightNodeType = right.getNodeType(); + final SqmExpressible leftNodeType = left.getExpressible(); + final SqmExpressible rightNodeType = right.getExpressible(); if ( leftNodeType != null && rightNodeType != null ) { final Class leftJavaType = leftNodeType.getRelationalJavaType().getJavaTypeClass(); final Class rightJavaType = rightNodeType.getRelationalJavaType().getJavaTypeClass(); @@ -579,7 +579,7 @@ public static void assertDuration(SqmExpression expression) { } public static void assertNumeric(SqmExpression expression, UnaryArithmeticOperator op) { - final SqmExpressible nodeType = expression.getNodeType(); + final SqmExpressible nodeType = expression.getExpressible(); if ( nodeType != null ) { final DomainType domainType = nodeType.getSqmType(); if ( !( domainType instanceof JdbcMapping ) || !( (JdbcMapping) domainType ).getJdbcType().isNumber() ) { diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/CteInsertStrategy.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/CteInsertStrategy.java index 4aac6f499231..dadbc9764fab 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/CteInsertStrategy.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/CteInsertStrategy.java @@ -121,13 +121,6 @@ public CteInsertStrategy( ); } - if ( !dialect.supportsValuesList() ) { - throw new UnsupportedOperationException( - getClass().getSimpleName() + - " can only be used with Dialects that support VALUES lists" - ); - } - // The table name might be a sub-query, which is inappropriate for a temporary table name final String originalTableName = rootDescriptor.getEntityPersister().getSynchronizedQuerySpaces()[0]; final String name; diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/CteMutationStrategy.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/CteMutationStrategy.java index 9f2a73270d87..2bc95e42297e 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/CteMutationStrategy.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/CteMutationStrategy.java @@ -78,13 +78,6 @@ public CteMutationStrategy( ); } - if ( !dialect.supportsValuesList() ) { - throw new UnsupportedOperationException( - getClass().getSimpleName() + - " can only be used with Dialects that support VALUES lists" - ); - } - this.idCteTable = CteTable.createIdTable( ID_TABLE_NAME, rootDescriptor ); } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/CteUpdateHandler.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/CteUpdateHandler.java index 1e4a3f598277..e2428b65283b 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/CteUpdateHandler.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/CteUpdateHandler.java @@ -157,8 +157,8 @@ protected void addDmlCtes( tableExpression, true ); - final List assignmentList = assignmentsByTable.get( updatingTableReference ); - if ( assignmentList == null ) { + final List assignmentsForInsert = assignmentsByTable.get( updatingTableReference ); + if ( assignmentsForInsert == null ) { continue; } final String insertCteTableName = getInsertCteTableName( tableExpression ); @@ -233,7 +233,7 @@ protected void addDmlCtes( // Collect the target column references from the key expressions final List targetColumnReferences = new ArrayList<>( existsKeyColumns ); // And transform assignments to target column references and selections - for ( Assignment assignment : assignments ) { + for ( Assignment assignment : assignmentsForInsert ) { targetColumnReferences.addAll( assignment.getAssignable().getColumnReferences() ); querySpec.getSelectClause().addSqlSelection( new SqlSelectionImpl( diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/InsertExecutionDelegate.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/InsertExecutionDelegate.java index 67ba56a0d2d8..62b101d2dc4f 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/InsertExecutionDelegate.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/InsertExecutionDelegate.java @@ -72,13 +72,12 @@ import org.hibernate.sql.ast.tree.update.UpdateStatement; import org.hibernate.sql.exec.internal.JdbcParameterBindingImpl; import org.hibernate.sql.exec.internal.JdbcParameterBindingsImpl; -import org.hibernate.sql.exec.internal.JdbcParameterImpl; +import org.hibernate.sql.exec.internal.SqlTypedMappingJdbcParameter; import org.hibernate.sql.exec.spi.ExecutionContext; import org.hibernate.sql.exec.spi.JdbcOperationQueryMutation; import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; import org.hibernate.sql.exec.spi.JdbcParameterBindings; import org.hibernate.sql.results.graph.basic.BasicFetch; -import org.hibernate.sql.results.graph.basic.BasicResultAssembler; import org.hibernate.sql.results.internal.SqlSelectionImpl; import org.hibernate.sql.results.spi.ListResultsConsumer; import org.hibernate.type.descriptor.ValueBinder; @@ -430,7 +429,7 @@ private int insertRootTable( new ComparisonPredicate( columnReference, ComparisonOperator.EQUAL, - new JdbcParameterImpl( identifierMapping.getJdbcMapping() ) + new SqlTypedMappingJdbcParameter( identifierMapping ) ) ); } @@ -444,8 +443,8 @@ private int insertRootTable( .noneMatch( c -> keyColumns[0].equals( c.getColumnExpression() ) ) ) { final BasicEntityIdentifierMapping identifierMapping = (BasicEntityIdentifierMapping) entityDescriptor.getIdentifierMapping(); - final JdbcParameter rowNumber = new JdbcParameterImpl( identifierMapping.getJdbcMapping() ); - final JdbcParameter rootIdentity = new JdbcParameterImpl( identifierMapping.getJdbcMapping() ); + final JdbcParameter rowNumber = new SqlTypedMappingJdbcParameter( identifierMapping ); + final JdbcParameter rootIdentity = new SqlTypedMappingJdbcParameter( identifierMapping ); final List temporaryTableAssignments = new ArrayList<>( 1 ); final ColumnReference idColumnReference = new ColumnReference( (String) null, identifierMapping ); temporaryTableAssignments.add( new Assignment( idColumnReference, rootIdentity ) ); @@ -594,8 +593,8 @@ public Object getEntity() { entry.setValue( rootIdentity ); } - final JdbcParameter entityIdentity = new JdbcParameterImpl( identifierMapping.getJdbcMapping() ); - final JdbcParameter rootIdentity = new JdbcParameterImpl( identifierMapping.getJdbcMapping() ); + final JdbcParameter entityIdentity = new SqlTypedMappingJdbcParameter( identifierMapping ); + final JdbcParameter rootIdentity = new SqlTypedMappingJdbcParameter( identifierMapping ); final List temporaryTableAssignments = new ArrayList<>( 1 ); temporaryTableAssignments.add( new Assignment( diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/TableBasedInsertHandler.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/TableBasedInsertHandler.java index 1be8d2c3d1dd..bb3ff53cf6d9 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/TableBasedInsertHandler.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/temptable/TableBasedInsertHandler.java @@ -7,9 +7,6 @@ package org.hibernate.query.sqm.mutation.internal.temptable; import java.util.ArrayList; -import java.util.Collections; -import java.util.IdentityHashMap; -import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.function.BiConsumer; @@ -24,7 +21,6 @@ import org.hibernate.id.OptimizableGenerator; import org.hibernate.id.enhanced.Optimizer; import org.hibernate.internal.util.collections.CollectionHelper; -import org.hibernate.metamodel.mapping.MappingModelExpressible; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.query.spi.DomainQueryExecutionContext; import org.hibernate.query.sqm.internal.DomainParameterXref; @@ -33,7 +29,6 @@ import org.hibernate.query.sqm.mutation.internal.MultiTableSqmMutationConverter; import org.hibernate.query.sqm.mutation.internal.SqmInsertStrategyHelper; import org.hibernate.query.sqm.sql.BaseSqmToSqlAstConverter; -import org.hibernate.query.sqm.tree.expression.SqmParameter; import org.hibernate.query.sqm.tree.insert.SqmInsertSelectStatement; import org.hibernate.query.sqm.tree.insert.SqmInsertStatement; import org.hibernate.query.sqm.tree.insert.SqmInsertValuesStatement; @@ -51,7 +46,7 @@ import org.hibernate.sql.ast.tree.insert.Values; import org.hibernate.sql.ast.tree.select.QueryPart; import org.hibernate.sql.ast.tree.update.Assignment; -import org.hibernate.sql.exec.internal.JdbcParameterImpl; +import org.hibernate.sql.exec.internal.SqlTypedMappingJdbcParameter; import org.hibernate.sql.exec.spi.ExecutionContext; import org.hibernate.sql.results.internal.SqlSelectionImpl; import org.hibernate.type.BasicType; @@ -96,7 +91,7 @@ public TableBasedInsertHandler( this.sessionUidParameter = null; } else { - this.sessionUidParameter = new JdbcParameterImpl( sessionUidColumn.getJdbcMapping() ); + this.sessionUidParameter = new SqlTypedMappingJdbcParameter( sessionUidColumn ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/produce/function/FunctionReturnTypeResolver.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/produce/function/FunctionReturnTypeResolver.java index 8fb1fa934e04..cce579ac9205 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/produce/function/FunctionReturnTypeResolver.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/produce/function/FunctionReturnTypeResolver.java @@ -43,7 +43,7 @@ public interface FunctionReturnTypeResolver { ReturnableType impliedType, List> arguments, TypeConfiguration typeConfiguration) { - throw new UnsupportedOperationException( "Not implemented for " + getClass().getName() ); + return resolveFunctionReturnType( impliedType, new FakeSqmToSqlAstConverter( null ), arguments, typeConfiguration ); } /** diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/produce/function/StandardFunctionReturnTypeResolvers.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/produce/function/StandardFunctionReturnTypeResolvers.java index ec941199417f..eca56d466d02 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/produce/function/StandardFunctionReturnTypeResolvers.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/produce/function/StandardFunctionReturnTypeResolvers.java @@ -241,13 +241,8 @@ else if ( !(specifiedArgType instanceof ReturnableType) ) { } private static SqmExpressible getArgumentExpressible(SqmTypedNode specifiedArgument) { - final SqmExpressible expressible = specifiedArgument.getNodeType(); - final SqmExpressible specifiedArgType = expressible instanceof SqmTypedNode - ? ( (SqmTypedNode) expressible ).getNodeType() - : expressible; - return specifiedArgType instanceof SqmPathSource - ? ( (SqmPathSource) specifiedArgType ).getSqmPathType() - : specifiedArgType; + final SqmExpressible expressible = specifiedArgument.getExpressible(); + return expressible != null ? expressible.getSqmType() : null; } public static JdbcMapping extractArgumentJdbcMapping( diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/spi/SqmCreationHelper.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/spi/SqmCreationHelper.java index 1c43c1ae9b34..858ae81d2fce 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/spi/SqmCreationHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/spi/SqmCreationHelper.java @@ -8,11 +8,19 @@ import org.hibernate.metamodel.mapping.CollectionPart; import org.hibernate.metamodel.model.domain.PluralPersistentAttribute; +import org.hibernate.query.criteria.JpaPredicate; +import org.hibernate.query.sqm.tree.predicate.SqmJunctionPredicate; +import org.hibernate.query.sqm.tree.predicate.SqmPredicate; import org.hibernate.spi.NavigablePath; import org.hibernate.query.sqm.tree.domain.SqmPath; +import java.util.List; import java.util.concurrent.atomic.AtomicLong; +import jakarta.persistence.criteria.Predicate; + +import static org.hibernate.internal.util.collections.CollectionHelper.isEmpty; + /** * @author Steve Ebersole */ @@ -68,6 +76,87 @@ public static NavigablePath buildSubNavigablePath(SqmPath lhs, String subNavi return buildSubNavigablePath( navigablePath, subNavigable, alias ); } + public static SqmPredicate combinePredicates(SqmPredicate baseRestriction, List incomingRestrictions) { + if ( isEmpty( incomingRestrictions ) ) { + return baseRestriction; + } + + SqmPredicate combined = combinePredicates( null, baseRestriction ); + for ( int i = 0; i < incomingRestrictions.size(); i++ ) { + combined = combinePredicates( combined, (SqmPredicate) incomingRestrictions.get(i) ); + } + return combined; + } + + public static SqmPredicate combinePredicates(SqmPredicate baseRestriction, JpaPredicate... incomingRestrictions) { + if ( isEmpty( incomingRestrictions ) ) { + return baseRestriction; + } + + SqmPredicate combined = combinePredicates( null, baseRestriction ); + for ( int i = 0; i < incomingRestrictions.length; i++ ) { + combined = combinePredicates( combined, incomingRestrictions[i] ); + } + return combined; + } + + public static SqmPredicate combinePredicates(SqmPredicate baseRestriction, Predicate... incomingRestrictions) { + if ( isEmpty( incomingRestrictions ) ) { + return baseRestriction; + } + + SqmPredicate combined = combinePredicates( null, baseRestriction ); + for ( int i = 0; i < incomingRestrictions.length; i++ ) { + combined = combinePredicates( combined, incomingRestrictions[i] ); + } + return combined; + } + + + public static SqmPredicate combinePredicates(SqmPredicate baseRestriction, SqmPredicate incomingRestriction) { + if ( baseRestriction == null ) { + return incomingRestriction; + } + + if ( incomingRestriction == null ) { + return baseRestriction; + } + + final SqmJunctionPredicate combinedPredicate; + + if ( baseRestriction instanceof SqmJunctionPredicate ) { + final SqmJunctionPredicate junction = (SqmJunctionPredicate) baseRestriction; + // we already had multiple before + if ( junction.getPredicates().isEmpty() ) { + return incomingRestriction; + } + + if ( junction.getOperator() == Predicate.BooleanOperator.AND ) { + combinedPredicate = junction; + } + else { + combinedPredicate = new SqmJunctionPredicate( + Predicate.BooleanOperator.AND, + baseRestriction.getExpressible(), + baseRestriction.nodeBuilder() + ); + combinedPredicate.getPredicates().add( baseRestriction ); + } + } + else { + combinedPredicate = new SqmJunctionPredicate( + Predicate.BooleanOperator.AND, + baseRestriction.getExpressible(), + baseRestriction.nodeBuilder() + ); + combinedPredicate.getPredicates().add( baseRestriction ); + } + + combinedPredicate.getPredicates().add( incomingRestriction ); + + return combinedPredicate; + } + private SqmCreationHelper() { } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java index d52065febb34..a4b9740bbb51 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/BaseSqmToSqlAstConverter.java @@ -6,6 +6,7 @@ */ package org.hibernate.query.sqm.sql; +import jakarta.annotation.Nullable; import jakarta.persistence.TemporalType; import jakarta.persistence.metamodel.SingularAttribute; import jakarta.persistence.metamodel.Type; @@ -127,6 +128,7 @@ import org.hibernate.query.sqm.mutation.internal.SqmInsertStrategyHelper; import org.hibernate.query.sqm.produce.function.internal.PatternRenderer; import org.hibernate.query.sqm.spi.BaseSemanticQueryWalker; +import org.hibernate.query.sqm.spi.SqmCreationHelper; import org.hibernate.query.sqm.sql.internal.AnyDiscriminatorPathInterpretation; import org.hibernate.query.sqm.sql.internal.AsWrappedExpression; import org.hibernate.query.sqm.sql.internal.BasicValuedPathInterpretation; @@ -226,6 +228,7 @@ import org.hibernate.query.sqm.tree.from.SqmFrom; import org.hibernate.query.sqm.tree.from.SqmFromClause; import org.hibernate.query.sqm.tree.from.SqmJoin; +import org.hibernate.query.sqm.tree.from.SqmQualifiedJoin; import org.hibernate.query.sqm.tree.from.SqmRoot; import org.hibernate.query.sqm.tree.insert.SqmConflictClause; import org.hibernate.query.sqm.tree.insert.SqmConflictUpdateAction; @@ -384,7 +387,6 @@ 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.collection.internal.EagerCollectionFetch; import org.hibernate.sql.results.graph.entity.EntityResultGraphNode; import org.hibernate.sql.results.graph.instantiation.internal.DynamicInstantiation; import org.hibernate.sql.results.graph.internal.ImmutableFetchList; @@ -435,6 +437,7 @@ import static org.hibernate.boot.model.internal.SoftDeleteHelper.createNonSoftDeletedRestriction; import static org.hibernate.generator.EventType.INSERT; import static org.hibernate.internal.util.NullnessHelper.coalesceSuppliedValues; +import static org.hibernate.metamodel.mapping.EntityDiscriminatorMapping.DISCRIMINATOR_ROLE_NAME; import static org.hibernate.query.sqm.BinaryArithmeticOperator.ADD; import static org.hibernate.query.sqm.BinaryArithmeticOperator.MULTIPLY; import static org.hibernate.query.sqm.BinaryArithmeticOperator.SUBTRACT; @@ -446,6 +449,7 @@ import static org.hibernate.query.sqm.internal.SqmUtil.isFkOptimizationAllowed; import static org.hibernate.sql.ast.spi.SqlAstTreeHelper.combinePredicates; import static org.hibernate.type.spi.TypeConfiguration.isDuration; +import static org.hibernate.usertype.internal.AbstractTimeZoneStorageCompositeUserType.ZONE_OFFSET_NAME; /** * @author Steve Ebersole @@ -477,7 +481,7 @@ public abstract class BaseSqmToSqlAstConverter extends Base private boolean deduplicateSelectionItems; private ForeignKeyDescriptor.Nature currentlyResolvingForeignKeySide; private SqmStatement currentSqmStatement; - private SqmQueryPart currentSqmQueryPart; + private Stack sqmQueryPartStack = new StandardStack<>( SqmQueryPart.class ); private CteContainer cteContainer; /** * A map from {@link SqmCteTable#getCteName()} to the final SQL name. @@ -783,9 +787,10 @@ public Stack getCurrentClauseStack() { return currentClauseStack; } + @SuppressWarnings("rawtypes") @Override - public SqmQueryPart getCurrentSqmQueryPart() { - return currentSqmQueryPart; + public Stack getSqmQueryPartStack() { + return sqmQueryPartStack; } // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -891,9 +896,8 @@ public void addVersionedAssignment(Consumer assignmentConsumer, SqmU currentClauseStack.push( Clause.SET ); final EntityVersionMapping versionMapping = persister.getVersionMapping(); final List targetColumnReferences = BasicValuedPathInterpretation.from( - (SqmBasicValuedSimplePath) sqmStatement - .getRoot() - .get( versionMapping.getPartName() ), + (SqmBasicValuedSimplePath) SqmExpressionHelper.get( sqmStatement + .getRoot(), versionMapping.getPartName() ), this, jpaQueryComplianceEnabled ).getColumnReferences(); @@ -1269,11 +1273,6 @@ public InsertStatement visitInsertValuesStatement(SqmInsertValuesStatement sq this ); - if ( !rootTableGroup.getTableReferenceJoins().isEmpty() - || !rootTableGroup.getTableGroupJoins().isEmpty() ) { - throw new HibernateException( "Not expecting multiple table references for an SQM INSERT-SELECT" ); - } - getFromClauseAccess().registerTableGroup( rootPath, rootTableGroup ); final InsertSelectStatement insertStatement = new InsertSelectStatement( @@ -1289,9 +1288,8 @@ public InsertStatement visitInsertValuesStatement(SqmInsertValuesStatement sq rootTableGroup ); - if ( !rootTableGroup.getTableReferenceJoins().isEmpty() - || !rootTableGroup.getTableGroupJoins().isEmpty() ) { - throw new SemanticException( "Not expecting multiple table references for an SQM INSERT-SELECT" ); + if ( hasJoins( rootTableGroup ) ) { + throw new HibernateException( "Not expecting multiple table references for an SQM INSERT-SELECT" ); } for ( SqmValues sqmValues : sqmStatement.getValuesList() ) { @@ -1389,8 +1387,7 @@ else if ( localName.equals( versionAttributeName ) ) { } if ( needsVersionInsert ) { final BasicValuedPathInterpretation versionPath = BasicValuedPathInterpretation.from( - (SqmBasicValuedSimplePath) sqmStatement.getTarget() - .get( versionAttributeName ), + (SqmBasicValuedSimplePath) SqmExpressionHelper.get( sqmStatement.getTarget(), versionAttributeName ), this, jpaQueryComplianceEnabled ); @@ -1734,8 +1731,7 @@ public CteStatement visitCteStatement(SqmCteStatement sqmCteStatement) { ); final DelegatingSqmAliasedNodeCollector collector = (DelegatingSqmAliasedNodeCollector) processingState .getSqlExpressionResolver(); - final SqmQueryPart oldSqmQueryPart = currentSqmQueryPart; - currentSqmQueryPart = queryGroup; + sqmQueryPartStack.push( queryGroup ); pushProcessingState( processingState ); try { @@ -1779,7 +1775,7 @@ public CteStatement visitCteStatement(SqmCteStatement sqmCteStatement) { } finally { popProcessingStateStack(); - currentSqmQueryPart = oldSqmQueryPart; + sqmQueryPartStack.pop(); } } finally { @@ -1931,11 +1927,14 @@ public CteContainer visitCteContainer(SqmCteContainer consumer) { final Collection> sqmCteStatements = consumer.getCteStatements(); cteContainer = new CteContainerImpl( cteContainer ); if ( !sqmCteStatements.isEmpty() ) { + final boolean originalDeduplicateSelectionItems = deduplicateSelectionItems; + deduplicateSelectionItems = false; currentClauseStack.push( Clause.WITH ); for ( SqmCteStatement sqmCteStatement : sqmCteStatements ) { visitCteStatement( sqmCteStatement ); } currentClauseStack.pop(); + deduplicateSelectionItems = originalDeduplicateSelectionItems; // Avoid leaking the processing state from CTEs to upper levels lastPoppedFromClauseIndex = null; lastPoppedProcessingState = null; @@ -1992,13 +1991,16 @@ public QueryGroup visitQueryGroup(SqmQueryGroup queryGroup) { ); final DelegatingSqmAliasedNodeCollector collector = (DelegatingSqmAliasedNodeCollector) processingState .getSqlExpressionResolver(); - final SqmQueryPart sqmQueryPart = currentSqmQueryPart; - currentSqmQueryPart = queryGroup; + sqmQueryPartStack.push( queryGroup ); pushProcessingState( processingState ); + FromClauseIndex firstQueryPartIndex = null; + SqlAstProcessingState firstPoppedProcessingState = null; try { newQueryParts.add( visitQueryPart( queryParts.get( 0 ) ) ); + firstQueryPartIndex = lastPoppedFromClauseIndex; + firstPoppedProcessingState = lastPoppedProcessingState; collector.setSqmAliasedNodeCollector( (SqmAliasedNodeCollector) lastPoppedProcessingState.getSqlExpressionResolver() ); @@ -2015,7 +2017,9 @@ public QueryGroup visitQueryGroup(SqmQueryGroup queryGroup) { } finally { popProcessingStateStack(); - currentSqmQueryPart = sqmQueryPart; + sqmQueryPartStack.pop(); + lastPoppedFromClauseIndex = firstQueryPartIndex; + lastPoppedProcessingState = firstPoppedProcessingState; } } @@ -2075,9 +2079,8 @@ else if ( sqmQuerySpec.hasPositionalGroupItem() ) { ); } - final SqmQueryPart sqmQueryPart = currentSqmQueryPart; final boolean originalDeduplicateSelectionItems = deduplicateSelectionItems; - currentSqmQueryPart = sqmQuerySpec; + sqmQueryPartStack.push( sqmQuerySpec ); // In sub-queries, we can never deduplicate the selection items as that might change semantics deduplicateSelectionItems = false; pushProcessingState( processingState ); @@ -2144,7 +2147,7 @@ else if ( sqmQuerySpec.hasPositionalGroupItem() ) { inNestedContext = oldInNestedContext; popProcessingStateStack(); queryTransformers.pop(); - currentSqmQueryPart = sqmQueryPart; + sqmQueryPartStack.pop(); deduplicateSelectionItems = originalDeduplicateSelectionItems; } } @@ -2210,7 +2213,7 @@ public SelectClause visitSelectClause(SqmSelectClause selectClause) { try { final SelectClause sqlSelectClause = currentQuerySpec().getSelectClause(); if ( selectClause == null ) { - final SqmFrom implicitSelection = determineImplicitSelection( (SqmQuerySpec) currentSqmQueryPart ); + final SqmFrom implicitSelection = determineImplicitSelection( (SqmQuerySpec) getCurrentSqmQueryPart() ); visitSelection( 0, new SqmSelection<>( implicitSelection, implicitSelection.nodeBuilder() ) ); } else { @@ -2235,7 +2238,7 @@ public SelectClause visitSelectClause(SqmSelectClause selectClause) { @Override public Void visitSelection(SqmSelection sqmSelection) { return visitSelection( - currentSqmQueryPart.getFirstQuerySpec().getSelectClause().getSelections().indexOf( sqmSelection ), + getCurrentSqmQueryPart().getFirstQuerySpec().getSelectClause().getSelections().indexOf( sqmSelection ), sqmSelection ); } @@ -2354,7 +2357,7 @@ else if ( statement.getQuerySource() == SqmQuerySource.CRITERIA && currentClause // To avoid this issue, we determine the position and let the SqlAstTranslator handle the rest. // Usually it will render `select ?, count(*) from dual group by 1` if supported // or force rendering the parameter as literal instead so that the database can see the grouping is fine - final SqmQuerySpec querySpec = currentSqmQueryPart.getFirstQuerySpec(); + final SqmQuerySpec querySpec = getCurrentSqmQueryPart().getFirstQuerySpec(); sqmPosition = indexOfExpression( querySpec.getSelectClause().getSelections(), groupByClauseExpression ); path = null; } @@ -2382,7 +2385,7 @@ else if ( statement.getQuerySource() == SqmQuerySource.CRITERIA && currentClause continue OUTER; } } - if ( currentSqmQueryPart instanceof SqmQueryGroup ) { + if ( getCurrentSqmQueryPart() instanceof SqmQueryGroup ) { // Reusing the SqlSelection for query groups would be wrong because the aliases do no exist // So we have to use a literal expression in a new SqlSelection instance to refer to the position expressions.add( @@ -2682,7 +2685,12 @@ protected void consumeFromClauseCorrelatedRoot(SqmRoot sqmRoot) { // as roots anyway, so nothing to worry about log.tracef( "Resolved SqmRoot [%s] to correlated TableGroup [%s]", sqmRoot, tableGroup ); - consumeExplicitJoins( from, tableGroup ); + if ( from instanceof SqmRoot ) { + consumeJoins( (SqmRoot) from, fromClauseIndex, tableGroup ); + } + else { + consumeExplicitJoins( from, tableGroup ); + } return; } else { @@ -3021,10 +3029,20 @@ private void registerPathAttributeEntityNameUsage(SqmPath sqmPath, TableGroup parentType.getRootEntityDescriptor().getEntityName() ); } + final EntityDiscriminatorMapping discriminator = parentType.getDiscriminatorMapping(); + final String entityName; + if ( discriminator != null && discriminator.hasPhysicalColumn() && !parentType.getSubMappingTypes().isEmpty() ) { + // This is needed to preserve optimization for joined + discriminator inheritance + // see JoinedSubclassEntityPersister#getIdentifierMappingForJoin + entityName = parentType.getRootEntityDescriptor().getEntityName(); + } + else { + entityName = parentType.getEntityName(); + } registerEntityNameUsage( tableGroup, EntityNameUse.EXPRESSION, - parentType.getEntityName() + entityName ); } else { @@ -3161,10 +3179,6 @@ else if ( useKind == EntityNameUse.UseKind.PROJECTION ) { subType.getEntityName(), (s, existingUse) -> finalEntityNameUse.stronger( existingUse ) ); - actualTableGroup.resolveTableReference( - null, - subType.getEntityPersister().getMappedTableDetails().getTableName() - ); } } } @@ -3339,6 +3353,39 @@ private TableGroup consumeAttributeJoin( SqmMappingModelHelper.resolveExplicitTreatTarget( sqmJoin, this ) ); + final List> sqmTreats = sqmJoin.getSqmTreats(); + final SqmPredicate joinPredicate; + final SqmPredicate[] treatPredicates; + final boolean hasPredicate; + if ( !sqmTreats.isEmpty() ) { + if ( sqmTreats.size() == 1 ) { + // If there is only a single treat, combine the predicates just as they are + joinPredicate = SqmCreationHelper.combinePredicates( + sqmJoin.getJoinPredicate(), + ( (SqmQualifiedJoin) sqmTreats.get( 0 ) ).getJoinPredicate() + ); + treatPredicates = null; + hasPredicate = joinPredicate != null; + } + else { + // When there are multiple predicates, we have to apply type filters + joinPredicate = sqmJoin.getJoinPredicate(); + treatPredicates = new SqmPredicate[sqmTreats.size()]; + boolean hasTreatPredicate = false; + for ( int i = 0; i < sqmTreats.size(); i++ ) { + final var p = ( (SqmQualifiedJoin) sqmTreats.get( i ) ).getJoinPredicate(); + treatPredicates[i] = p; + hasTreatPredicate = hasTreatPredicate || p != null; + } + hasPredicate = joinPredicate != null || hasTreatPredicate; + } + } + else { + joinPredicate = sqmJoin.getJoinPredicate(); + treatPredicates = null; + hasPredicate = joinPredicate != null; + } + if ( pathSource instanceof PluralPersistentAttribute ) { assert modelPart instanceof PluralAttributeMapping; @@ -3355,7 +3402,7 @@ private TableGroup consumeAttributeJoin( null, sqmJoinType.getCorrespondingSqlJoinType(), sqmJoin.isFetched(), - sqmJoin.getJoinPredicate() != null, + hasPredicate, this ); @@ -3371,7 +3418,7 @@ private TableGroup consumeAttributeJoin( null, sqmJoinType.getCorrespondingSqlJoinType(), sqmJoin.isFetched(), - sqmJoin.getJoinPredicate() != null, + hasPredicate, this ); @@ -3380,7 +3427,7 @@ private TableGroup consumeAttributeJoin( // Since this is an explicit join, we force the initialization of a possible lazy table group // to retain the cardinality, but only if this is a non-trivial attribute join. // Left or inner singular attribute joins without a predicate can be safely optimized away - if ( sqmJoin.getJoinPredicate() != null || sqmJoinType != SqmJoinType.INNER && sqmJoinType != SqmJoinType.LEFT ) { + if ( hasPredicate || sqmJoinType != SqmJoinType.INNER && sqmJoinType != SqmJoinType.LEFT ) { joinedTableGroup.getPrimaryTableReference(); } } @@ -3417,14 +3464,26 @@ private TableGroup consumeAttributeJoin( final TableGroupJoin joinForPredicate; // add any additional join restrictions - if ( sqmJoin.getJoinPredicate() != null ) { + if ( hasPredicate ) { if ( sqmJoin.isFetched() ) { QueryLogging.QUERY_MESSAGE_LOGGER.debugf( "Join fetch [%s] is restricted", sqmJoinNavigablePath ); } final SqmJoin oldJoin = currentlyProcessingJoin; currentlyProcessingJoin = sqmJoin; - final Predicate predicate = visitNestedTopLevelPredicate( sqmJoin.getJoinPredicate() ); + Predicate predicate = joinPredicate == null ? null : visitNestedTopLevelPredicate( joinPredicate ); + if ( treatPredicates != null ) { + final Junction orPredicate = new Junction( Junction.Nature.DISJUNCTION ); + for ( int i = 0; i < treatPredicates.length; i++ ) { + final EntityDomainType treatType = + (EntityDomainType) ( (SqmTreatedPath) sqmTreats.get( i ) ).getTreatTarget(); + orPredicate.add( combinePredicates( + createTreatTypeRestriction( sqmJoin, treatType ), + treatPredicates[i] == null ? null : visitNestedTopLevelPredicate( treatPredicates[i] ) + ) ); + } + predicate = predicate != null ? combinePredicates( predicate, orPredicate ) : orPredicate; + } joinForPredicate = TableGroupJoinHelper.determineJoinForPredicateApply( joinedTableGroupJoin ); // If translating the join predicate didn't initialize the table group, // we can safely apply it on the collection table group instead @@ -3925,7 +3984,7 @@ private TableGroup createTableGroup(TableGroup parentTableGroup, SqmPath join this ); // Implicit joins in the ON clause need to be added as nested table group joins - final boolean nested = currentClauseStack.getCurrent() == Clause.FROM; + final boolean nested = currentlyProcessingJoin != null; if ( nested ) { parentTableGroup.addNestedTableGroupJoin( tableGroupJoin ); } @@ -3999,6 +4058,13 @@ private void registerPluralTableGroupParts(NavigablePath navigablePath, TableGro } } + @Override + public @Nullable SqlAstJoinType getCurrentlyProcessingJoinType() { + return currentlyProcessingJoin == null + ? null + : currentlyProcessingJoin.getSqmJoinType().getCorrespondingSqlJoinType(); + } + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // SqmPath handling // - Note that SqmFrom references defined in the FROM-clause are already @@ -4128,8 +4194,7 @@ private Expression visitTableGroup(TableGroup tableGroup, SqmPath path) { // When the inferred mapping is null, we try to resolve to the FK by default, which is fine because // expansion to all target columns for select and group by clauses is handled in EntityValuedPathInterpretation if ( entityValuedModelPart instanceof EntityAssociationMapping - && ( (EntityAssociationMapping) entityValuedModelPart ).isFkOptimizationAllowed() - && isFkOptimizationAllowed( path ) ) { + && isFkOptimizationAllowed( path, (EntityAssociationMapping) entityValuedModelPart ) ) { // If the table group uses an association mapping that is not a one-to-many, // we make use of the FK model part - unless the path is a non-optimizable join, // for which we should always use the target's identifier to preserve semantics @@ -5443,7 +5508,8 @@ private Predicate createTreatTypeRestriction(SqmPath lhs, EntityDomainType private Predicate createTreatTypeRestriction(SqmPath lhs, Set subclassEntityNames) { // Do what visitSelfInterpretingSqmPath does, except for calling preparingReusablePath // as that would register a type usage for the table group that we don't want here - final EntityDiscriminatorSqmPath discriminatorSqmPath = (EntityDiscriminatorSqmPath) lhs.type(); + final EntityDiscriminatorSqmPath discriminatorSqmPath = + (EntityDiscriminatorSqmPath) SqmExpressionHelper.get( lhs, DISCRIMINATOR_ROLE_NAME ); registerTypeUsage( discriminatorSqmPath ); return createTreatTypeRestriction( DiscriminatorPathInterpretation.from( discriminatorSqmPath, this ), @@ -5612,40 +5678,7 @@ else if ( inferableExpressible instanceof BasicValuedMapping ) { final BasicValuedMapping basicValuedMapping = (BasicValuedMapping) inferableExpressible; final BasicValueConverter valueConverter = basicValuedMapping.getJdbcMapping().getValueConverter(); if ( valueConverter != null ) { - final Object value = literal.getLiteralValue(); - final Object sqlLiteralValue; - // For converted query literals, we support both, the domain and relational java type - if ( value == null || valueConverter.getDomainJavaType().isInstance( value ) ) { - sqlLiteralValue = valueConverter.toRelationalValue( value ); - } - else if ( valueConverter.getRelationalJavaType().isInstance( value ) ) { - sqlLiteralValue = value; - } - else if ( Character.class.isAssignableFrom( valueConverter.getRelationalJavaType().getJavaTypeClass() ) - && value instanceof CharSequence && ( (CharSequence) value ).length() == 1 ) { - sqlLiteralValue = ( (CharSequence) value ).charAt( 0 ); - } - // In HQL, number literals might not match the relational java type exactly, - // so we allow coercion between the number types - else if ( Number.class.isAssignableFrom( valueConverter.getRelationalJavaType().getJavaTypeClass() ) - && value instanceof Number ) { - sqlLiteralValue = valueConverter.getRelationalJavaType().coerce( - value, - creationContext.getSessionFactory()::getTypeConfiguration - ); - } - else { - throw new SemanticException( - String.format( - Locale.ROOT, - "Literal type '%s' did not match domain type '%s' nor converted type '%s'", - value.getClass(), - valueConverter.getDomainJavaType().getJavaTypeClass().getName(), - valueConverter.getRelationalJavaType().getJavaTypeClass().getName() - ) - ); - } - return new QueryLiteral<>( sqlLiteralValue, basicValuedMapping ); + return new QueryLiteral<>( sqlLiteralValue( valueConverter, literal.getLiteralValue() ), basicValuedMapping ); } } @@ -5762,6 +5795,40 @@ else if ( expressible instanceof EntityValuedModelPart ) { } } + private Object sqlLiteralValue(BasicValueConverter valueConverter, D value) { + // For converted query literals, we support both, the domain and relational java type + if ( value == null || valueConverter.getDomainJavaType().isInstance( value ) ) { + return valueConverter.toRelationalValue( value ); + } + else if ( valueConverter.getRelationalJavaType().isInstance( value ) ) { + return value; + } + else if ( Character.class.isAssignableFrom( valueConverter.getRelationalJavaType().getJavaTypeClass() ) + && value instanceof CharSequence && ( (CharSequence) value ).length() == 1 ) { + return ( (CharSequence) value ).charAt( 0 ); + } + // In HQL, number literals might not match the relational java type exactly, + // so we allow coercion between the number types + else if ( Number.class.isAssignableFrom( valueConverter.getRelationalJavaType().getJavaTypeClass() ) + && value instanceof Number ) { + return valueConverter.getRelationalJavaType().coerce( + value, + creationContext.getSessionFactory()::getTypeConfiguration + ); + } + else { + throw new SemanticException( + String.format( + Locale.ROOT, + "Literal type '%s' did not match domain type '%s' nor converted type '%s'", + value.getClass(), + valueConverter.getDomainJavaType().getJavaTypeClass().getName(), + valueConverter.getRelationalJavaType().getJavaTypeClass().getName() + ) + ); + } + } + @Override public Expression visitHqlNumericLiteral(SqmHqlNumericLiteral numericLiteral) { final BasicValuedMapping inferredExpressible = (BasicValuedMapping) getInferredValueMapping(); @@ -5874,45 +5941,13 @@ protected Expression consumeSqmParameter( final QueryParameterImplementor queryParameter = domainParameterXref.getQueryParameter( sqmParameter ); final QueryParameterBinding binding = domainParameterBindings.getBinding( queryParameter ); - if ( binding.setType( valueMapping ) ) { - replaceJdbcParametersType( - sqmParameter, - domainParameterXref.getSqmParameters( queryParameter ), - valueMapping - ); - } + binding.setType( valueMapping ); return new SqmParameterInterpretation( jdbcParametersForSqm, valueMapping ); } - private void replaceJdbcParametersType( - SqmParameter sourceSqmParameter, - List> sqmParameters, - MappingModelExpressible valueMapping) { - final JdbcMapping jdbcMapping = valueMapping.getSingleJdbcMapping(); - for ( SqmParameter sqmParameter : sqmParameters ) { - if ( sqmParameter == sourceSqmParameter ) { - continue; - } - sqmParameterMappingModelTypes.put( sqmParameter, valueMapping ); - final List> jdbcParamsForSqmParameter = jdbcParamsBySqmParam.get( sqmParameter ); - if ( jdbcParamsForSqmParameter != null ) { - for ( List parameters : jdbcParamsForSqmParameter ) { - assert parameters.size() == 1; - final JdbcParameter jdbcParameter = parameters.get( 0 ); - if ( ( (SqlExpressible) jdbcParameter ).getJdbcMapping() != jdbcMapping ) { - final JdbcParameter newJdbcParameter = new JdbcParameterImpl( jdbcMapping ); - parameters.set( 0, newJdbcParameter ); - jdbcParameters.getJdbcParameters().remove( jdbcParameter ); - jdbcParameters.getJdbcParameters().add( newJdbcParameter ); - } - } - } - } - } - protected Expression consumeSqmParameter(SqmParameter sqmParameter) { if ( sqmParameter.allowMultiValuedBinding() ) { final QueryParameterImplementor domainParam = domainParameterXref.getQueryParameter( sqmParameter ); @@ -5957,7 +5992,7 @@ public MappingModelExpressible determineValueMapping(SqmExpression sqmExpr private MappingModelExpressible determineValueMapping(SqmExpression sqmExpression, FromClauseIndex fromClauseIndex) { if ( sqmExpression instanceof SqmParameter ) { - return determineValueMapping( (SqmParameter) sqmExpression ); + return determineValueMapping( getSqmParameter( sqmExpression ) ); } if ( sqmExpression instanceof SqmPath ) { @@ -6280,7 +6315,14 @@ else if ( bindable instanceof SelectableMapping ) { final QueryParameterBinding binding = domainParameterBindings.getBinding( domainParameterXref.getQueryParameter( expression ) ); - final Object bindValue = binding.getBindValue(); + final Object bindValue; + if ( binding.isMultiValued() ) { + final Collection bindValues = binding.getBindValues(); + bindValue = !bindValues.isEmpty() ? bindValues.iterator().next() : null; + } + else { + bindValue = binding.getBindValue(); + } if ( bindValue != null ) { if ( bindValue instanceof BigInteger ) { int precision = bindValue.toString().length() - ( ( (BigInteger) bindValue ).signum() < 0 ? 1 : 0 ); @@ -6709,16 +6751,12 @@ else if (type instanceof ValueMapping ) { if ( lhs != expression.getLeftHandOperand() ) { final SqmPath temporalPath = (SqmPath) expression.getLeftHandOperand(); baseNavigablePath = temporalPath.getNavigablePath().getParent(); - offset = (temporalPath).get( - AbstractTimeZoneStorageCompositeUserType.ZONE_OFFSET_NAME - ).accept( this ); + offset = SqmExpressionHelper.get( temporalPath, ZONE_OFFSET_NAME ).accept( this ); } else if ( rhs != expression.getRightHandOperand() ) { final SqmPath temporalPath = (SqmPath) expression.getRightHandOperand(); baseNavigablePath = temporalPath.getNavigablePath().getParent(); - offset = ( temporalPath ).get( - AbstractTimeZoneStorageCompositeUserType.ZONE_OFFSET_NAME - ).accept( this ); + offset = SqmExpressionHelper.get( temporalPath, ZONE_OFFSET_NAME ).accept( this ); } else { return result; @@ -7118,6 +7156,8 @@ public CaseSimpleExpression visitSimpleCaseExpression(SqmCaseSimple expres inferrableTypeAccessStack.push( () -> fixtureType ); final Expression checkValue = (Expression) whenFragment.getCheckValue().accept( this ); inferrableTypeAccessStack.pop(); + handleTypeInCaseExpression( fixture, checkValue ); + final MappingModelExpressible alreadyKnown = resolved; inferrableTypeAccessStack.push( () -> alreadyKnown == null && inferenceSupplier != null ? inferenceSupplier.get() : alreadyKnown @@ -7153,6 +7193,42 @@ public CaseSimpleExpression visitSimpleCaseExpression(SqmCaseSimple expres ); } + private void handleTypeInCaseExpression(Expression fixture, Expression checkValue) { + if ( fixture instanceof DiscriminatorPathInterpretation ) { + final DiscriminatorPathInterpretation typeExpression = (DiscriminatorPathInterpretation) fixture; + final TableGroup tableGroup = getFromClauseIndex().getTableGroup( typeExpression.getNavigablePath().getParent() ); + final MappingType partMappingType = tableGroup.getModelPart().getPartMappingType(); + if ( !(partMappingType instanceof EntityMappingType) ) { + return; + } + final EntityMappingType entityMappingType = (EntityMappingType) partMappingType; + if ( entityMappingType.getDiscriminatorMapping().hasPhysicalColumn() ) { + // If the entity type has a physical type column we only need to register an expression + // usage for the root type to prevent pruning the table where the discriminator is found + registerEntityNameUsage( + tableGroup, + EntityNameUse.EXPRESSION, + entityMappingType.getRootEntityDescriptor().getEntityName() + ); + } + else if ( checkValue instanceof EntityTypeLiteral ) { + // Register an expression type usage for the literal subtype to prevent pruning its table group + registerEntityNameUsage( + tableGroup, + EntityNameUse.EXPRESSION, + ( (EntityTypeLiteral) checkValue ).getEntityTypeDescriptor().getEntityName() + ); + } + else { + // We have to assume all types are possible and can't do optimizations + registerEntityNameUsage( tableGroup, EntityNameUse.EXPRESSION, entityMappingType.getEntityName() ); + for ( EntityMappingType subMappingType : entityMappingType.getSubMappingTypes() ) { + registerEntityNameUsage( tableGroup, EntityNameUse.EXPRESSION, subMappingType.getEntityName() ); + } + } + } + } + @Override public CaseSearchedExpression visitSearchedCaseExpression(SqmCaseSearched expression) { final List whenFragments = new ArrayList<>( expression.getWhenFragments().size() ); @@ -7338,12 +7414,13 @@ private static > QueryLiteral queryLiteral( ); } + @SuppressWarnings({"unchecked", "rawtypes"}) @Override public Object visitFieldLiteral(SqmFieldLiteral sqmFieldLiteral) { - return new QueryLiteral<>( - sqmFieldLiteral.getValue(), - (BasicValuedMapping) determineValueMapping( sqmFieldLiteral ) - ); + final BasicValuedMapping valueMapping = (BasicValuedMapping) determineValueMapping( sqmFieldLiteral ); + final Object value = sqmFieldLiteral.getValue(); + final BasicValueConverter converter = valueMapping.getJdbcMapping().getValueConverter(); + return new QueryLiteral<>( converter != null ? sqlLiteralValue( converter, value ) : value, valueMapping ); } // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -7407,13 +7484,7 @@ public Junction visitJunctionPredicate(SqmJunctionPredicate predicate) { new ArrayList<>( predicate.getPredicates().size() ), getBooleanType() ); - final Map> previousTableGroupEntityNameUses; - if ( tableGroupEntityNameUses.isEmpty() ) { - previousTableGroupEntityNameUses = null; - } - else { - previousTableGroupEntityNameUses = new IdentityHashMap<>( tableGroupEntityNameUses ); - } + final Map> previousTableGroupEntityNameUses = new IdentityHashMap<>( tableGroupEntityNameUses ); Map>[] disjunctEntityNameUsesArray = null; Map> entityNameUsesToPropagate = null; List treatedTableGroups = null; @@ -7425,9 +7496,7 @@ public Junction visitJunctionPredicate(SqmJunctionPredicate predicate) { if ( !tableGroupEntityNameUses.isEmpty() ) { if ( disjunctEntityNameUsesArray == null ) { disjunctEntityNameUsesArray = new Map[predicate.getPredicates().size()]; - entityNameUsesToPropagate = previousTableGroupEntityNameUses == null - ? new IdentityHashMap<>() - : new IdentityHashMap<>( previousTableGroupEntityNameUses ); + entityNameUsesToPropagate = new IdentityHashMap<>( previousTableGroupEntityNameUses ); } if ( i == 0 ) { // Collect the table groups for which filters are registered @@ -7465,7 +7534,9 @@ public Junction visitJunctionPredicate(SqmJunctionPredicate predicate) { // If every disjunct contains a FILTER, we can merge the filters // If every disjunct contains a TREAT, we can merge the treats // Otherwise, we downgrade the entity name uses to expression uses - for ( Map.Entry> entry : tableGroupEntityNameUses.entrySet() ) { + final Iterator>> iterator = tableGroupEntityNameUses.entrySet().iterator(); + while ( iterator.hasNext() ) { + final Map.Entry> entry = iterator.next(); final TableGroup tableGroup = entry.getKey(); final Map entityNameUses = entityNameUsesToPropagate.computeIfAbsent( tableGroup, @@ -7473,10 +7544,22 @@ public Junction visitJunctionPredicate(SqmJunctionPredicate predicate) { ); final boolean downgradeTreatUses; final boolean downgradeFilterUses; - if ( i == 0 ) { - // Never downgrade the treat uses of the first disjunct + if ( getFromClauseAccess().findTableGroup( tableGroup.getNavigablePath() ) == null ) { + // Always preserver name uses for table groups not found in the current from clause index + previousTableGroupEntityNameUses.put( tableGroup, entry.getValue() ); + // Remove from the current junction context since no more processing is required + if ( treatedTableGroups != null ) { + treatedTableGroups.remove( tableGroup ); + } + if ( filteredTableGroups != null ) { + filteredTableGroups.remove( tableGroup ); + } + iterator.remove(); + continue; + } + else if ( i == 0 ) { + // Never downgrade treat or filter uses of the first disjunct downgradeTreatUses = false; - // Never downgrade the filter uses of the first disjunct downgradeFilterUses = false; } else { @@ -7563,9 +7646,7 @@ else if ( useKind == EntityNameUse.UseKind.FILTER ) { } } if ( disjunctEntityNameUsesArray == null ) { - if ( previousTableGroupEntityNameUses != null ) { - tableGroupEntityNameUses.putAll( previousTableGroupEntityNameUses ); - } + tableGroupEntityNameUses.putAll( previousTableGroupEntityNameUses ); return disjunction; } @@ -7636,9 +7717,7 @@ else if ( useKind == EntityNameUse.UseKind.FILTER ) { // Restore the parent context entity name uses state tableGroupEntityNameUses.clear(); - if ( previousTableGroupEntityNameUses != null ) { - tableGroupEntityNameUses.putAll( previousTableGroupEntityNameUses ); - } + tableGroupEntityNameUses.putAll( previousTableGroupEntityNameUses ); // Propagate the union of the entity name uses upwards for ( Map.Entry> entry : entityNameUsesToPropagate.entrySet() ) { final Map entityNameUses = tableGroupEntityNameUses.putIfAbsent( @@ -7726,19 +7805,44 @@ public Predicate visitMemberOfPredicate(SqmMemberOfPredicate predicate) { this ) ); + subQuerySpec.applyPredicate( + new ComparisonPredicate( + toSingleExpression( subQuerySpec.getSelectClause().getSqlSelections(), lhs ), + ComparisonOperator.EQUAL, + lhs + ) + ); + subQuerySpec.getSelectClause().getSqlSelections().clear(); + subQuerySpec.getSelectClause().addSqlSelection( + new SqlSelectionImpl( new QueryLiteral<>( 1, basicType( Integer.class ) ) ) + ); } finally { popProcessingStateStack(); } - return new InSubQueryPredicate( - lhs, + return new ExistsPredicate( new SelectStatement( subQuerySpec ), predicate.isNegated(), getBooleanType() ); } + private Expression toSingleExpression(List sqlSelections, Expression inferenceSource) { + assert !sqlSelections.isEmpty(); + + if ( sqlSelections.size() == 1 ) { + return sqlSelections.get( 0 ).getExpression(); + } + else { + final var expressions = new ArrayList( sqlSelections.size() ); + for ( SqlSelection sqlSelection : sqlSelections ) { + expressions.add( sqlSelection.getExpression() ); + } + return new SqlTuple( expressions, (MappingModelExpressible) inferenceSource.getExpressionType() ); + } + } + @Override public NegatedPredicate visitNegatedPredicate(SqmNegatedPredicate predicate) { return new NegatedPredicate( @@ -8140,13 +8244,14 @@ private InListPredicate processInSingleCriteriaParameter( JpaCriteriaParameter jpaCriteriaParameter) { assert jpaCriteriaParameter.allowsMultiValuedBinding(); - final QueryParameterBinding domainParamBinding = domainParameterBindings.getBinding( jpaCriteriaParameter ); + final SqmJpaCriteriaParameterWrapper sqmWrapper = jpaCriteriaParamResolutions.get( jpaCriteriaParameter ); + final QueryParameterImplementor domainParam = domainParameterXref.getQueryParameter( sqmWrapper ); + final QueryParameterBinding domainParamBinding = domainParameterBindings.getBinding( domainParam ); if ( !domainParamBinding.isMultiValued() ) { return null; } - final SqmJpaCriteriaParameterWrapper sqmWrapper = jpaCriteriaParamResolutions.get( jpaCriteriaParameter ); - return processInSingleParameter( sqmPredicate, sqmWrapper, jpaCriteriaParameter, domainParamBinding ); + return processInSingleParameter( sqmPredicate, sqmWrapper, domainParam, domainParamBinding ); } @SuppressWarnings( "rawtypes" ) @@ -8326,6 +8431,15 @@ private Fetch createFetch(FetchParent fetchParent, Fetchable fetchable, Boolean joined = true; alias = fetchedJoin.getExplicitAlias(); explicitFetch = true; + + if ( entityGraphTraversalState != null ) { + // Still do traverse the entity graph even if we encounter a fetch join + traversalResult = entityGraphTraversalState.traverse( + fetchParent, + fetchable, + isKeyFetchable + ); + } } else { fetchablePath = resolvedNavigablePath; @@ -8404,7 +8518,7 @@ else if ( getLoadQueryInfluencers().hasEnabledFetchProfiles() ) { final TableGroupJoinProducer joinProducer = (TableGroupJoinProducer) fetchable; final TableGroup compatibleTableGroup = lhs.findCompatibleJoinedGroup( joinProducer, - joinProducer.determineSqlJoinType( lhs, null, true ) + joinProducer.getDefaultSqlAstJoinType( lhs ) ); final SqmQueryPart queryPart = getCurrentSqmQueryPart(); if ( compatibleTableGroup == null @@ -8425,6 +8539,16 @@ else if ( getLoadQueryInfluencers().hasEnabledFetchProfiles() ) { } else { tableGroup = compatibleTableGroup; + + if ( joinProducer instanceof PluralAttributeMapping ) { + final PluralAttributeMapping attributeMapping = (PluralAttributeMapping) joinProducer; + if ( attributeMapping.getOrderByFragment() != null ) { + applyOrdering( tableGroup, attributeMapping.getOrderByFragment() ); + } + if ( attributeMapping.getManyToManyOrderByFragment() != null ) { + applyOrdering( tableGroup, attributeMapping.getManyToManyOrderByFragment() ); + } + } } // and return the joined group @@ -8535,14 +8659,6 @@ public R withNestedFetchParent(FetchParent fetchParent, Function(); + @Override + public void applyOrdering(TableGroup tableGroup, OrderByFragment orderByFragment) { + if ( currentQuerySpec().isRoot() ) { + if ( orderByFragments == null ) { + orderByFragments = new ArrayList<>(); + } + orderByFragments.add( new AbstractMap.SimpleEntry<>( orderByFragment, tableGroup ) ); } - orderByFragments.add( new AbstractMap.SimpleEntry<>( orderByFragment, tableGroup ) ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/FakeSqmToSqlAstConverter.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/FakeSqmToSqlAstConverter.java index 20065e26dc77..d997c0f7c3f9 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/FakeSqmToSqlAstConverter.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/FakeSqmToSqlAstConverter.java @@ -21,6 +21,7 @@ import org.hibernate.query.sqm.tree.predicate.SqmPredicate; import org.hibernate.query.sqm.tree.select.SqmQueryPart; import org.hibernate.sql.ast.Clause; +import org.hibernate.sql.ast.SqlAstJoinType; import org.hibernate.sql.ast.spi.FromClauseAccess; import org.hibernate.sql.ast.spi.SqlAliasBaseGenerator; import org.hibernate.sql.ast.spi.SqlAstCreationContext; @@ -31,6 +32,8 @@ import org.hibernate.sql.ast.tree.expression.QueryTransformer; import org.hibernate.sql.ast.tree.predicate.Predicate; +import jakarta.annotation.Nullable; + /** * */ @@ -92,6 +95,11 @@ public Stack getCurrentClauseStack() { return null; } + @Override + public Stack getSqmQueryPartStack() { + return null; + } + @Override public SqmQueryPart getCurrentSqmQueryPart() { return null; @@ -101,6 +109,11 @@ public SqmQueryPart getCurrentSqmQueryPart() { public void registerQueryTransformer(QueryTransformer transformer) { } + @Override + public @Nullable SqlAstJoinType getCurrentlyProcessingJoinType() { + return null; + } + @Override public boolean isInTypeInference() { return false; diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/SqmToSqlAstConverter.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/SqmToSqlAstConverter.java index f5fc0caea370..3442b0958398 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/SqmToSqlAstConverter.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/SqmToSqlAstConverter.java @@ -18,6 +18,7 @@ import org.hibernate.query.sqm.tree.expression.SqmParameter; import org.hibernate.query.sqm.tree.predicate.SqmPredicate; import org.hibernate.query.sqm.tree.select.SqmQueryPart; +import org.hibernate.sql.ast.SqlAstJoinType; import org.hibernate.sql.ast.spi.SqlAstCreationState; import org.hibernate.sql.ast.Clause; import org.hibernate.sql.ast.tree.expression.Expression; @@ -34,10 +35,20 @@ public interface SqmToSqlAstConverter extends SemanticQueryWalker, SqlAstCreationState { Stack getCurrentClauseStack(); - SqmQueryPart getCurrentSqmQueryPart(); + Stack getSqmQueryPartStack(); + + default SqmQueryPart getCurrentSqmQueryPart() { + return getSqmQueryPartStack().getCurrent(); + } void registerQueryTransformer(QueryTransformer transformer); + /** + * Returns the {@link SqlAstJoinType} of the currently processing join if there is one, or {@code null}. + * This is used to determine the join type for implicit joins happening in the {@code ON} clause. + */ + @Nullable SqlAstJoinType getCurrentlyProcessingJoinType(); + /** * Returns whether the state of the translation is currently in type inference mode. * This is useful to avoid type inference based on other incomplete inference information. diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/internal/EntityValuedPathInterpretation.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/internal/EntityValuedPathInterpretation.java index 0eb0826f87fb..9ffdfe6dac06 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/internal/EntityValuedPathInterpretation.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/sql/internal/EntityValuedPathInterpretation.java @@ -407,7 +407,9 @@ private static boolean supportsFunctionalDependency(Dialect dialect, EntityMappi final FunctionalDependencyAnalysisSupport analysisSupport = dialect.getFunctionalDependencyAnalysisSupport(); if ( analysisSupport.supportsAnalysis() ) { if ( entityMappingType.getSqmMultiTableMutationStrategy() == null ) { - return true; + // A subquery may be used to render a single-table inheritance subtype, in which case + // we cannot use functional dependency analysis unless the dialect supports table groups + return analysisSupport.supportsTableGroups() || entityMappingType.getSuperMappingType() == null; } else { return analysisSupport.supportsTableGroups() && ( analysisSupport.supportsConstants() || diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/AbstractSqmStatement.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/AbstractSqmStatement.java index 11a97f6e9162..3b98f810d083 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/AbstractSqmStatement.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/AbstractSqmStatement.java @@ -7,7 +7,7 @@ package org.hibernate.query.sqm.tree; import java.util.Collections; -import java.util.HashSet; +import java.util.LinkedHashSet; import java.util.Set; import org.hibernate.query.sqm.NodeBuilder; @@ -46,7 +46,7 @@ protected Set> copyParameters(SqmCopyContext context) { return null; } else { - final Set> parameters = new HashSet<>( this.parameters.size() ); + final Set> parameters = new LinkedHashSet<>( this.parameters.size() ); for ( SqmParameter parameter : this.parameters ) { parameters.add( parameter.copy( context ) ); } @@ -62,7 +62,7 @@ public SqmQuerySource getQuerySource() { @Override public void addParameter(SqmParameter parameter) { if ( parameters == null ) { - parameters = new HashSet<>(); + parameters = new LinkedHashSet<>(); } parameters.add( parameter ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/SqmCopyContext.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/SqmCopyContext.java index b57278264f01..fa1af21bbe23 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/SqmCopyContext.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/SqmCopyContext.java @@ -7,6 +7,7 @@ package org.hibernate.query.sqm.tree; import org.hibernate.Incubating; +import org.hibernate.query.sqm.SqmQuerySource; import org.hibernate.query.sqm.internal.NoParamSqmCopyContext; import org.hibernate.query.sqm.internal.SimpleSqmCopyContext; @@ -31,11 +32,30 @@ default boolean copyFetchedFlag() { return true; } + /** + * Returns the query source to use for copied queries. + * {@code null} means, that the original query source should be retained. + * + * @since 7.0 + */ + @Incubating + default @Nullable SqmQuerySource getQuerySource() { + return null; + } + static SqmCopyContext simpleContext() { return new SimpleSqmCopyContext(); } + static SqmCopyContext simpleContext(SqmQuerySource querySource) { + return new SimpleSqmCopyContext( querySource ); + } + static SqmCopyContext noParamCopyContext() { return new NoParamSqmCopyContext(); } + + static SqmCopyContext noParamCopyContext(SqmQuerySource querySource) { + return new NoParamSqmCopyContext( querySource ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/cte/SqmCteTable.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/cte/SqmCteTable.java index cbaafd48b326..a0125a9a64a1 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/cte/SqmCteTable.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/cte/SqmCteTable.java @@ -18,8 +18,6 @@ import org.hibernate.query.sqm.SqmExpressible; import org.hibernate.query.sqm.SqmPathSource; import org.hibernate.query.sqm.tree.select.SqmSelectQuery; -import org.hibernate.query.sqm.tree.select.SqmSelectStatement; -import org.hibernate.query.sqm.tree.select.SqmSelectableNode; import org.hibernate.sql.ast.spi.FromClauseAccess; import org.hibernate.sql.ast.spi.SqlSelection; import org.hibernate.type.BasicType; @@ -36,8 +34,8 @@ public class SqmCteTable extends AnonymousTupleType implements JpaCteCrite private SqmCteTable( String name, SqmCteStatement cteStatement, - SqmSelectableNode[] sqmSelectableNodes) { - super( sqmSelectableNodes ); + SqmSelectQuery selectStatement) { + super(selectStatement); this.name = name; this.cteStatement = cteStatement; final List columns = new ArrayList<>( componentCount() ); @@ -57,12 +55,7 @@ public static SqmCteTable createStatementTable( String name, SqmCteStatement cteStatement, SqmSelectQuery selectStatement) { - final SqmSelectableNode[] sqmSelectableNodes = selectStatement.getQueryPart() - .getFirstQuerySpec() - .getSelectClause() - .getSelectionItems() - .toArray( SqmSelectableNode[]::new ); - return new SqmCteTable<>( name, cteStatement, sqmSelectableNodes ); + return new SqmCteTable<>( name, cteStatement, selectStatement ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/delete/SqmDeleteStatement.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/delete/SqmDeleteStatement.java index 0acd3cee0075..c8cb931a1f48 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/delete/SqmDeleteStatement.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/delete/SqmDeleteStatement.java @@ -93,7 +93,7 @@ public SqmDeleteStatement copy(SqmCopyContext context) { this, new SqmDeleteStatement<>( nodeBuilder(), - getQuerySource(), + context.getQuerySource() == null ? getQuerySource() : context.getQuerySource(), copyParameters( context ), copyCteStatements( context ), getTarget().copy( context ) diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/AbstractSqmAttributeJoin.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/AbstractSqmAttributeJoin.java index 2f7c6989fcd9..d333b2f965cc 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/AbstractSqmAttributeJoin.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/AbstractSqmAttributeJoin.java @@ -122,7 +122,7 @@ public X accept(SemanticQueryWalker walker) { @Override public PersistentAttribute getAttribute() { //noinspection unchecked - return (PersistentAttribute) getReferencedPathSource(); + return (PersistentAttribute) getModel(); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/AbstractSqmFrom.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/AbstractSqmFrom.java index 7a4b03a5cb91..3db35b70fef7 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/AbstractSqmFrom.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/AbstractSqmFrom.java @@ -166,7 +166,7 @@ public SqmPath resolvePathPart( if ( sqmJoin instanceof SqmSingularJoin && name.equals( sqmJoin.getReferencedPathSource().getPathName() ) ) { final SqmAttributeJoin attributeJoin = (SqmAttributeJoin) sqmJoin; - if ( attributeJoin.getOn() == null ) { + if ( attributeJoin.getJoinPredicate() == null ) { // todo (6.0): to match the expectation of the JPA spec I think we also have to check // that the join type is INNER or the default join type for the attribute, // but as far as I understand, in 5.x we expect to ignore this behavior @@ -720,9 +720,11 @@ private SqmAttributeJoin buildJoin( SqmPathSource joinedPathSource, SqmJoinType joinType, boolean fetched) { - final SqmAttributeJoin compatibleFetchJoin = findCompatibleFetchJoin( this, joinedPathSource, joinType ); - if ( compatibleFetchJoin != null ) { - return compatibleFetchJoin; + if ( fetched ) { + final SqmAttributeJoin compatibleFetchJoin = findCompatibleFetchJoin( this, joinedPathSource, joinType ); + if ( compatibleFetchJoin != null ) { + return compatibleFetchJoin; + } } final SqmAttributeJoin sqmJoin; diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/AbstractSqmPath.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/AbstractSqmPath.java index e283e5d5a960..b212cc8092ad 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/AbstractSqmPath.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/AbstractSqmPath.java @@ -158,6 +158,7 @@ public SqmPathSource getModel() { public SqmPathSource getResolvedModel() { final DomainType lhsType; final SqmPathSource pathSource = getReferencedPathSource(); + if ( pathSource.isGeneric() && ( lhsType = getLhs().getResolvedModel().getSqmPathType() ) instanceof ManagedDomainType ) { final PersistentAttribute concreteAttribute = ( (ManagedDomainType) lhsType ).findConcreteGenericAttribute( pathSource.getPathName() diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmCorrelatedCteJoin.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmCorrelatedCteJoin.java new file mode 100644 index 000000000000..4c409e92625d --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmCorrelatedCteJoin.java @@ -0,0 +1,100 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.query.sqm.tree.domain; + +import org.hibernate.query.sqm.SemanticQueryWalker; +import org.hibernate.query.sqm.SqmPathSource; +import org.hibernate.query.sqm.tree.SqmCopyContext; +import org.hibernate.query.sqm.tree.SqmJoinType; +import org.hibernate.query.sqm.tree.cte.SqmCteStatement; +import org.hibernate.query.sqm.tree.from.SqmCteJoin; +import org.hibernate.query.sqm.tree.from.SqmRoot; +import org.hibernate.spi.NavigablePath; + +/** + * @author Christian Beikov + */ +public class SqmCorrelatedCteJoin extends SqmCteJoin implements SqmCorrelation { + + private final SqmCorrelatedRootJoin correlatedRootJoin; + private final SqmCteJoin correlationParent; + + public SqmCorrelatedCteJoin(SqmCteJoin correlationParent) { + //noinspection unchecked + super( + correlationParent.getCte(), + correlationParent.getExplicitAlias(), + correlationParent.getSqmJoinType(), + (SqmRoot) correlationParent.getRoot() + ); + this.correlatedRootJoin = SqmCorrelatedDerivedRootJoin.create( correlationParent, this ); + this.correlationParent = correlationParent; + } + + private SqmCorrelatedCteJoin( + NavigablePath navigablePath, + SqmCteStatement cte, + SqmPathSource pathSource, + String alias, + SqmJoinType joinType, + SqmRoot sqmRoot, + SqmCorrelatedRootJoin correlatedRootJoin, + SqmCteJoin correlationParent) { + super( navigablePath, cte, pathSource, alias, joinType, sqmRoot ); + this.correlatedRootJoin = correlatedRootJoin; + this.correlationParent = correlationParent; + } + + @Override + public SqmCorrelatedCteJoin copy(SqmCopyContext context) { + final SqmCorrelatedCteJoin existing = context.getCopy( this ); + if ( existing != null ) { + return existing; + } + final SqmCorrelatedCteJoin path = context.registerCopy( + this, + new SqmCorrelatedCteJoin<>( + getNavigablePath(), + getCte().copy( context ), + getReferencedPathSource(), + getExplicitAlias(), + getSqmJoinType(), + (SqmRoot) findRoot().copy( context ), + correlatedRootJoin.copy( context ), + correlationParent.copy( context ) + ) + ); + copyTo( path, context ); + return path; + } + + @Override + public SqmCteJoin getCorrelationParent() { + return correlationParent; + } + + @Override + public SqmPath getWrappedPath() { + return correlationParent; + } + + @Override + public boolean isCorrelated() { + return true; + } + + @Override + public SqmRoot getCorrelatedRoot() { + return correlatedRootJoin; + } + + @Override + public X accept(SemanticQueryWalker walker) { + return walker.visitCorrelatedCteJoin( this ); + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmCorrelatedDerivedJoin.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmCorrelatedDerivedJoin.java new file mode 100644 index 000000000000..ecf42b319956 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmCorrelatedDerivedJoin.java @@ -0,0 +1,105 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.query.sqm.tree.domain; + +import org.hibernate.query.sqm.SemanticQueryWalker; +import org.hibernate.query.sqm.SqmPathSource; +import org.hibernate.query.sqm.tree.SqmCopyContext; +import org.hibernate.query.sqm.tree.SqmJoinType; +import org.hibernate.query.sqm.tree.from.SqmDerivedJoin; +import org.hibernate.query.sqm.tree.from.SqmRoot; +import org.hibernate.query.sqm.tree.select.SqmSubQuery; +import org.hibernate.spi.NavigablePath; + +/** + * @author Christian Beikov + */ +public class SqmCorrelatedDerivedJoin extends SqmDerivedJoin implements SqmCorrelation { + + private final SqmCorrelatedRootJoin correlatedRootJoin; + private final SqmDerivedJoin correlationParent; + + public SqmCorrelatedDerivedJoin(SqmDerivedJoin correlationParent) { + //noinspection unchecked + super( + correlationParent.getNavigablePath(), + correlationParent.getQueryPart(), + correlationParent.isLateral(), + correlationParent.getReferencedPathSource(), + correlationParent.getExplicitAlias(), + correlationParent.getSqmJoinType(), + (SqmRoot) correlationParent.getRoot() + ); + this.correlatedRootJoin = SqmCorrelatedDerivedRootJoin.create( correlationParent, this ); + this.correlationParent = correlationParent; + } + + private SqmCorrelatedDerivedJoin( + NavigablePath navigablePath, + SqmSubQuery subQuery, + boolean lateral, + SqmPathSource pathSource, + String alias, + SqmJoinType joinType, + SqmRoot sqmRoot, + SqmCorrelatedRootJoin correlatedRootJoin, + SqmDerivedJoin correlationParent) { + super( navigablePath, subQuery, lateral, pathSource, alias, joinType, sqmRoot ); + this.correlatedRootJoin = correlatedRootJoin; + this.correlationParent = correlationParent; + } + + @Override + public SqmCorrelatedDerivedJoin copy(SqmCopyContext context) { + final SqmCorrelatedDerivedJoin existing = context.getCopy( this ); + if ( existing != null ) { + return existing; + } + final SqmCorrelatedDerivedJoin path = context.registerCopy( + this, + new SqmCorrelatedDerivedJoin<>( + getNavigablePath(), + getQueryPart(), + isLateral(), + getReferencedPathSource(), + getExplicitAlias(), + getSqmJoinType(), + (SqmRoot) findRoot().copy( context ), + correlatedRootJoin.copy( context ), + correlationParent.copy( context ) + ) + ); + copyTo( path, context ); + return path; + } + + @Override + public SqmDerivedJoin getCorrelationParent() { + return correlationParent; + } + + @Override + public SqmPath getWrappedPath() { + return correlationParent; + } + + @Override + public boolean isCorrelated() { + return true; + } + + @Override + public SqmRoot getCorrelatedRoot() { + return correlatedRootJoin; + } + + @Override + public X accept(SemanticQueryWalker walker) { + return walker.visitCorrelatedDerivedJoin( this ); + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmCorrelatedDerivedRoot.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmCorrelatedDerivedRoot.java new file mode 100644 index 000000000000..43e125f5d907 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmCorrelatedDerivedRoot.java @@ -0,0 +1,69 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.query.sqm.tree.domain; + +import org.hibernate.metamodel.model.domain.EntityDomainType; +import org.hibernate.query.sqm.SqmPathSource; +import org.hibernate.query.sqm.tree.SqmCopyContext; +import org.hibernate.query.sqm.tree.from.SqmRoot; + +/** + * @author Steve Ebersole + */ +public class SqmCorrelatedDerivedRoot extends SqmCorrelatedRoot implements SqmPathWrapper, SqmCorrelation { + + public SqmCorrelatedDerivedRoot(SqmDerivedRoot correlationParent) { + this( (SqmRoot) correlationParent ); + } + + public SqmCorrelatedDerivedRoot(SqmCteRoot correlationParent) { + this( (SqmRoot) correlationParent ); + } + + private SqmCorrelatedDerivedRoot(SqmRoot correlationParent) { + super( + correlationParent.getNavigablePath(), + correlationParent.getReferencedPathSource(), + correlationParent.nodeBuilder(), + correlationParent + ); + } + + @Override + public SqmCorrelatedDerivedRoot copy(SqmCopyContext context) { + final SqmCorrelatedDerivedRoot existing = context.getCopy( this ); + if ( existing != null ) { + return existing; + } + final SqmCorrelatedDerivedRoot path = context.registerCopy( + this, + new SqmCorrelatedDerivedRoot<>( getCorrelationParent().copy( context ) ) + ); + copyTo( path, context ); + return path; + } + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // JPA + + @Override + public EntityDomainType getModel() { + // Or should we throw an exception instead? + return null; + } + + @Override + public String getEntityName() { + return null; + } + + @Override + public SqmPathSource getResolvedModel() { + return getReferencedPathSource(); + } + +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmCorrelatedDerivedRootJoin.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmCorrelatedDerivedRootJoin.java new file mode 100644 index 000000000000..89d627eb8577 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmCorrelatedDerivedRootJoin.java @@ -0,0 +1,93 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.query.sqm.tree.domain; + +import org.hibernate.metamodel.model.domain.EntityDomainType; +import org.hibernate.query.sqm.NodeBuilder; +import org.hibernate.query.sqm.SqmPathSource; +import org.hibernate.query.sqm.tree.SqmCopyContext; +import org.hibernate.query.sqm.tree.from.SqmFrom; +import org.hibernate.query.sqm.tree.from.SqmJoin; +import org.hibernate.spi.NavigablePath; + +/** + * @author Steve Ebersole + */ +public class SqmCorrelatedDerivedRootJoin extends SqmCorrelatedRootJoin { + + public SqmCorrelatedDerivedRootJoin( + NavigablePath navigablePath, + SqmPathSource referencedNavigable, + NodeBuilder nodeBuilder) { + super( navigablePath, referencedNavigable, nodeBuilder ); + } + + @Override + public SqmCorrelatedDerivedRootJoin copy(SqmCopyContext context) { + final SqmCorrelatedDerivedRootJoin existing = context.getCopy( this ); + if ( existing != null ) { + return existing; + } + final SqmCorrelatedDerivedRootJoin path = context.registerCopy( + this, + new SqmCorrelatedDerivedRootJoin<>( + getNavigablePath(), + getReferencedPathSource(), + nodeBuilder() + ) + ); + copyTo( path, context ); + return path; + } + + @SuppressWarnings("unchecked") + public static > SqmCorrelatedDerivedRootJoin create(J correlationParent, J correlatedJoin) { + final SqmFrom parentPath = (SqmFrom) correlationParent.getParentPath(); + final SqmCorrelatedDerivedRootJoin rootJoin; + if ( parentPath == null ) { + rootJoin = new SqmCorrelatedDerivedRootJoin<>( + correlationParent.getNavigablePath(), + (SqmPathSource) correlationParent.getReferencedPathSource(), + correlationParent.nodeBuilder() + ); + } + else { + rootJoin = new SqmCorrelatedDerivedRootJoin<>( + parentPath.getNavigablePath(), + parentPath.getReferencedPathSource(), + correlationParent.nodeBuilder() + ); + } + rootJoin.addSqmJoin( correlatedJoin ); + return rootJoin; + } + + @Override + public boolean containsOnlyInnerJoins() { + // The derived join is just referenced, no need to create any table groups + return true; + } + + // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + // JPA + + @Override + public EntityDomainType getModel() { + // Or should we throw an exception instead? + return null; + } + + @Override + public String getEntityName() { + return null; + } + + @Override + public SqmPathSource getResolvedModel() { + return getReferencedPathSource(); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmCorrelatedRoot.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmCorrelatedRoot.java index 46b6a2e6cccc..07421314dea6 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmCorrelatedRoot.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmCorrelatedRoot.java @@ -7,9 +7,14 @@ package org.hibernate.query.sqm.tree.domain; import org.hibernate.query.criteria.JpaSelection; +import org.hibernate.query.sqm.NodeBuilder; import org.hibernate.query.sqm.SemanticQueryWalker; +import org.hibernate.query.sqm.SqmPathSource; import org.hibernate.query.sqm.tree.SqmCopyContext; import org.hibernate.query.sqm.tree.from.SqmRoot; +import org.hibernate.spi.NavigablePath; + +import java.util.Objects; /** * @author Steve Ebersole @@ -28,6 +33,11 @@ public SqmCorrelatedRoot(SqmRoot correlationParent) { this.correlationParent = correlationParent; } + protected SqmCorrelatedRoot(NavigablePath navigablePath, SqmPathSource referencedNavigable, NodeBuilder nodeBuilder, SqmRoot correlationParent) { + super( navigablePath, referencedNavigable, nodeBuilder ); + this.correlationParent = correlationParent; + } + @Override public SqmCorrelatedRoot copy(SqmCopyContext context) { final SqmCorrelatedRoot existing = context.getCopy( this ); @@ -82,4 +92,16 @@ public SqmRoot getCorrelatedRoot() { public X accept(SemanticQueryWalker walker) { return walker.visitCorrelatedRoot( this ); } + + @Override + public boolean equals(Object object) { + return object instanceof SqmCorrelatedRoot + && super.equals( object ) + && Objects.equals( this.correlationParent, ((SqmCorrelatedRoot) object).correlationParent ); + } + + @Override + public int hashCode() { + return Objects.hash( super.hashCode(), correlationParent ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmCorrelation.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmCorrelation.java index cc498164130e..43f5f2adceb8 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmCorrelation.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmCorrelation.java @@ -18,4 +18,8 @@ public interface SqmCorrelation extends SqmFrom, SqmPathWrapper { SqmRoot getCorrelatedRoot(); + @Override + default SqmRoot findRoot() { + return getCorrelatedRoot(); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmCteRoot.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmCteRoot.java index b5ad796921ed..db11cdaf805b 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmCteRoot.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmCteRoot.java @@ -102,7 +102,7 @@ public SqmPathSource getResolvedModel() { @Override public SqmCorrelatedRoot createCorrelation() { - throw new UnsupportedOperationException(); + return new SqmCorrelatedDerivedRoot<>( this ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmDerivedRoot.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmDerivedRoot.java index 11088294e3f9..180d6e75ba02 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmDerivedRoot.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmDerivedRoot.java @@ -104,7 +104,7 @@ public SqmPathSource getResolvedModel() { @Override public SqmCorrelatedRoot createCorrelation() { - throw new UnsupportedOperationException(); + return new SqmCorrelatedDerivedRoot<>( this ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmPluralValuedSimplePath.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmPluralValuedSimplePath.java index 3ce9b34384db..f370925e78cb 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmPluralValuedSimplePath.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmPluralValuedSimplePath.java @@ -18,23 +18,25 @@ import org.hibernate.query.hql.spi.SqmPathRegistry; import org.hibernate.query.sqm.NodeBuilder; import org.hibernate.query.sqm.SemanticQueryWalker; +import org.hibernate.query.sqm.SqmPathSource; import org.hibernate.query.sqm.tree.SqmCopyContext; import org.hibernate.query.sqm.tree.SqmJoinType; import org.hibernate.query.sqm.tree.expression.SqmExpression; import org.hibernate.query.sqm.tree.from.SqmFrom; +import org.hibernate.type.descriptor.java.JavaType; import org.hibernate.query.sqm.tree.from.SqmQualifiedJoin; /** * An SqmPath for plural attribute paths * - * @param The collection element type, which is the "bindable" type in the SQM tree + * @param The collection type * * @author Steve Ebersole */ -public class SqmPluralValuedSimplePath extends AbstractSqmSimplePath { +public class SqmPluralValuedSimplePath extends AbstractSqmSimplePath { public SqmPluralValuedSimplePath( NavigablePath navigablePath, - PluralPersistentAttribute referencedNavigable, + PluralPersistentAttribute referencedNavigable, SqmPath lhs, NodeBuilder nodeBuilder) { this( navigablePath, referencedNavigable, lhs, null, nodeBuilder ); @@ -42,26 +44,29 @@ public SqmPluralValuedSimplePath( public SqmPluralValuedSimplePath( NavigablePath navigablePath, - PluralPersistentAttribute referencedNavigable, + PluralPersistentAttribute referencedNavigable, SqmPath lhs, String explicitAlias, NodeBuilder nodeBuilder) { - super( navigablePath, referencedNavigable, lhs, explicitAlias, nodeBuilder ); + // We need to do an unchecked cast here: PluralPersistentAttribute implements path source with + // the element type, but paths generated from it must be collection-typed. + //noinspection unchecked + super( navigablePath, (SqmPathSource) referencedNavigable, lhs, explicitAlias, nodeBuilder ); } @Override - public SqmPluralValuedSimplePath copy(SqmCopyContext context) { - final SqmPluralValuedSimplePath existing = context.getCopy( this ); + public SqmPluralValuedSimplePath copy(SqmCopyContext context) { + final SqmPluralValuedSimplePath existing = context.getCopy( this ); if ( existing != null ) { return existing; } final SqmPath lhsCopy = getLhs().copy( context ); - final SqmPluralValuedSimplePath path = context.registerCopy( + final SqmPluralValuedSimplePath path = context.registerCopy( this, new SqmPluralValuedSimplePath<>( getNavigablePathCopy( lhsCopy ), - getModel(), + (PluralPersistentAttribute) getModel(), lhsCopy, getExplicitAlias(), nodeBuilder() @@ -71,19 +76,13 @@ public SqmPluralValuedSimplePath copy(SqmCopyContext context) { return path; } - @Override - public PluralPersistentAttribute getReferencedPathSource() { - return (PluralPersistentAttribute) super.getReferencedPathSource(); - } - - @Override - public PluralPersistentAttribute getModel() { - return (PluralPersistentAttribute) super.getModel(); + public PluralPersistentAttribute getPluralAttribute() { + return (PluralPersistentAttribute) getModel(); } @Override - public PluralPersistentAttribute getNodeType() { - return getReferencedPathSource(); + public JavaType getJavaTypeDescriptor() { + return getPluralAttribute().getAttributeJavaType(); } @Override @@ -125,12 +124,11 @@ public SqmPath resolveIndexedAccess( } SqmFrom path = pathRegistry.findFromByPath( navigablePath.getParent() ); if ( path == null ) { - final PluralPersistentAttribute referencedPathSource = getReferencedPathSource(); + final SqmPathSource referencedPathSource = getReferencedPathSource(); final SqmFrom parent = pathRegistry.resolveFrom( getLhs() ); final SqmQualifiedJoin join; final SqmExpression index; if ( referencedPathSource instanceof ListPersistentAttribute ) { - //noinspection unchecked join = new SqmListJoin<>( parent, (ListPersistentAttribute) referencedPathSource, @@ -142,7 +140,6 @@ public SqmPath resolveIndexedAccess( index = ( (SqmListJoin) join ).index(); } else if ( referencedPathSource instanceof MapPersistentAttribute ) { - //noinspection unchecked join = new SqmMapJoin<>( parent, (MapPersistentAttribute) referencedPathSource, @@ -171,38 +168,17 @@ else if ( referencedPathSource instanceof MapPersistentAttribute ) { } @Override - public SqmExpression> type() { + public SqmExpression> type() { throw new UnsupportedOperationException( "Cannot access the type of plural valued simple paths" ); } @Override - public SqmTreatedPath treatAs(Class treatJavaType) throws PathException { + public SqmTreatedPath treatAs(Class treatJavaType) throws PathException { throw new UnsupportedOperationException( "Cannot treat plural valued simple paths" ); } @Override - public SqmTreatedEntityValuedSimplePath treatAs(EntityDomainType treatTarget) throws PathException { + public SqmTreatedEntityValuedSimplePath treatAs(EntityDomainType treatTarget) throws PathException { throw new UnsupportedOperationException( "Cannot treat plural valued simple paths" ); } - -// @Override -// public DomainResult createDomainResult( -// String resultVariable, -// DomainResultCreationState creationState, -// DomainResultCreationContext creationContext) { -// return new CollectionResultImpl( -// getReferencedNavigable().getPluralAttribute().getDescribedAttribute(), -// getNavigablePath(), -// resultVariable, -// LockMode.NONE, -// getReferencedNavigable().getPluralAttribute().getCollectionKeyDescriptor().createDomainResult( -// getNavigablePath().append( "{id}" ), -// null, -// creationState, -// creationContext -// ), -// initializerProducerCreator.createProducer( resultVariable, creationState, creationContext ) -// ); -// } - } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmSingularJoin.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmSingularJoin.java index a5b2a78f8f12..307dde42d7e2 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmSingularJoin.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/domain/SqmSingularJoin.java @@ -14,6 +14,7 @@ import org.hibernate.metamodel.model.domain.SingularPersistentAttribute; import org.hibernate.metamodel.model.domain.TreatableDomainType; import org.hibernate.query.sqm.SemanticQueryWalker; +import org.hibernate.query.sqm.SqmPathSource; import org.hibernate.spi.NavigablePath; import org.hibernate.query.hql.spi.SqmCreationProcessingState; import org.hibernate.query.sqm.NodeBuilder; @@ -86,6 +87,16 @@ public SqmSingularJoin copy(SqmCopyContext context) { return path; } + @Override + public SqmPathSource getNodeType() { + return getReferencedPathSource(); + } + + @Override + public SqmPathSource getReferencedPathSource() { + return getModel().getPathSource(); + } + @Override public SingularPersistentAttribute getModel() { return (SingularPersistentAttribute) super.getNodeType(); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmBinaryArithmetic.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmBinaryArithmetic.java index dc80903ec5f2..aca3096867f2 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmBinaryArithmetic.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmBinaryArithmetic.java @@ -14,8 +14,6 @@ import org.hibernate.query.sqm.tree.SqmCopyContext; import org.hibernate.query.sqm.tree.select.SqmSelectableNode; -import org.checkerframework.checker.nullness.qual.Nullable; - import static org.hibernate.query.sqm.BinaryArithmeticOperator.ADD; import static org.hibernate.query.sqm.BinaryArithmeticOperator.SUBTRACT; import static org.hibernate.type.spi.TypeConfiguration.isDuration; @@ -37,8 +35,8 @@ public SqmBinaryArithmetic( //noinspection unchecked super( (SqmExpressible) domainModel.getTypeConfiguration().resolveArithmeticType( - lhsOperand.getNodeType(), - rhsOperand.getNodeType(), + lhsOperand.getExpressible(), + rhsOperand.getExpressible(), operator ), nodeBuilder @@ -52,8 +50,8 @@ public SqmBinaryArithmetic( ( operator == ADD || operator == SUBTRACT ) ) { return; } - this.lhsOperand.applyInferableType( rhsOperand.getNodeType() ); - this.rhsOperand.applyInferableType( lhsOperand.getNodeType() ); + this.lhsOperand.applyInferableType( rhsOperand.getExpressible() ); + this.rhsOperand.applyInferableType( lhsOperand.getExpressible() ); } public SqmBinaryArithmetic( diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmExpressionHelper.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmExpressionHelper.java index 6940e41d7ef1..e9ce3c0ec16a 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmExpressionHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmExpressionHelper.java @@ -17,6 +17,7 @@ import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.metamodel.model.domain.internal.EmbeddedSqmPathSource; +import org.hibernate.query.sqm.SqmPathSource; import org.hibernate.query.BindableType; import org.hibernate.query.hql.spi.SqmCreationState; import org.hibernate.query.spi.QueryEngine; @@ -117,12 +118,10 @@ public static boolean isCompositeTemporal(SqmExpression expression) { public static SqmExpression getActualExpression(SqmExpression expression) { if ( isCompositeTemporal( expression ) ) { - if ( expression.getJavaTypeDescriptor().getJavaTypeClass() == OffsetTime.class ) { - return ( (SqmPath) expression ).get( OffsetTimeCompositeUserType.LOCAL_TIME_NAME ); - } - else { - return ( (SqmPath) expression ).get( AbstractTimeZoneStorageCompositeUserType.INSTANT_NAME ); - } + final SqmPath path = (SqmPath) expression; + return expression.getJavaTypeDescriptor().getJavaTypeClass() == OffsetTime.class + ? get( path, OffsetTimeCompositeUserType.LOCAL_TIME_NAME ) + : get( path, AbstractTimeZoneStorageCompositeUserType.INSTANT_NAME ); } else { return expression; @@ -132,13 +131,10 @@ public static SqmExpression getActualExpression(SqmExpression expression) public static SqmExpression getOffsetAdjustedExpression(SqmExpression expression) { if ( isCompositeTemporal( expression ) ) { final SqmPath compositePath = (SqmPath) expression; - final SqmPath temporalPath; - if ( expression.getJavaTypeDescriptor().getJavaTypeClass() == OffsetTime.class ) { - temporalPath = compositePath.get( OffsetTimeCompositeUserType.LOCAL_TIME_NAME ); - } - else { - temporalPath = compositePath.get( AbstractTimeZoneStorageCompositeUserType.INSTANT_NAME ); - } + final SqmPath temporalPath = + expression.getJavaTypeDescriptor().getJavaTypeClass() == OffsetTime.class + ? get( compositePath, OffsetTimeCompositeUserType.LOCAL_TIME_NAME ) + : get( compositePath, AbstractTimeZoneStorageCompositeUserType.INSTANT_NAME ); final NodeBuilder nodeBuilder = temporalPath.nodeBuilder(); return new SqmBinaryArithmetic<>( BinaryArithmeticOperator.ADD, @@ -158,6 +154,22 @@ public static SqmExpression getOffsetAdjustedExpression(SqmExpression expr } } + public static SqmPath get(SqmPath lhs, String attributeName) { + // Don't use SqmPath.get() to avoid mutating the SQM tree here, since the SQM tree is shared + final SqmPath existingSqmPath = lhs.getReusablePath( attributeName ); + if ( existingSqmPath != null ) { + return existingSqmPath; + } + else { + final SqmPathSource referencedPathSource = lhs.getReferencedPathSource() + .getSubPathSource( attributeName ); + return referencedPathSource.createSqmPath( + lhs, + lhs.getResolvedModel().getIntermediatePathSource( referencedPathSource ) + ); + } + } + public static SqmPath findPath(SqmExpression expression, SqmExpressible nodeType) { if ( nodeType != expression.getNodeType() ) { return null; diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmFieldLiteral.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmFieldLiteral.java index 1fb04fc91833..caa38969a5f0 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmFieldLiteral.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmFieldLiteral.java @@ -180,7 +180,8 @@ public SqmPredicate in(Expression... values) { @Override public SqmPredicate in(Collection values) { - return nodeBuilder().in( this, values ); + //noinspection unchecked + return nodeBuilder().in( this, (Collection) values ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmJpaCriteriaParameterWrapper.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmJpaCriteriaParameterWrapper.java index 0dbdc0154e7c..310422866897 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmJpaCriteriaParameterWrapper.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmJpaCriteriaParameterWrapper.java @@ -27,13 +27,19 @@ public class SqmJpaCriteriaParameterWrapper extends AbstractSqmExpression implements SqmParameter { private final JpaCriteriaParameter jpaCriteriaParameter; + private final int criteriaParameterId; + private final int unnamedParameterId; public SqmJpaCriteriaParameterWrapper( BindableType type, JpaCriteriaParameter jpaCriteriaParameter, + int criteriaParameterId, + int unnamedParameterId, NodeBuilder criteriaBuilder) { super( toSqmType( type, criteriaBuilder ), criteriaBuilder ); this.jpaCriteriaParameter = jpaCriteriaParameter; + this.criteriaParameterId = criteriaParameterId; + this.unnamedParameterId = unnamedParameterId; } @Override @@ -47,6 +53,8 @@ public SqmJpaCriteriaParameterWrapper copy(SqmCopyContext context) { new SqmJpaCriteriaParameterWrapper<>( getNodeType(), jpaCriteriaParameter.copy( context ), + criteriaParameterId, + unnamedParameterId, nodeBuilder() ) ); @@ -67,6 +75,27 @@ public JpaCriteriaParameter getJpaCriteriaParameter() { return jpaCriteriaParameter; } + /** + * The 0-based encounter of a {@link JpaCriteriaParameter} instance in a + * {@link org.hibernate.query.sqm.SqmQuerySource#CRITERIA} query. + * + * @see org.hibernate.query.sqm.tree.jpa.ParameterCollector + */ + public int getCriteriaParameterId() { + return criteriaParameterId; + } + + /** + * The 0-based encounter of an unnamed {@link JpaCriteriaParameter} instance in a + * {@link org.hibernate.query.sqm.SqmQuerySource#CRITERIA} query. + * If the {@link #getJpaCriteriaParameter()} has a name, returns -1. + * + * @see org.hibernate.query.sqm.tree.jpa.ParameterCollector + */ + public int getUnnamedParameterId() { + return unnamedParameterId; + } + @Override public Class getParameterType() { return jpaCriteriaParameter.getParameterType(); @@ -87,6 +116,8 @@ public SqmParameter copy() { return new SqmJpaCriteriaParameterWrapper<>( getNodeType(), jpaCriteriaParameter, + criteriaParameterId, + unnamedParameterId, nodeBuilder() ); } @@ -123,10 +154,21 @@ public void appendHqlString(StringBuilder sb) { jpaCriteriaParameter.appendHqlString( sb ); } + @Override + public final boolean equals(Object o) { + return o instanceof SqmJpaCriteriaParameterWrapper + && criteriaParameterId == ( (SqmJpaCriteriaParameterWrapper) o ).criteriaParameterId; + } + + @Override + public int hashCode() { + return criteriaParameterId; + } + @Override public int compareTo(SqmParameter anotherParameter) { return anotherParameter instanceof SqmJpaCriteriaParameterWrapper ? - getJpaCriteriaParameter().compareTo( ( (SqmJpaCriteriaParameterWrapper) anotherParameter ).getJpaCriteriaParameter() ) + Integer.compare( criteriaParameterId, ( (SqmJpaCriteriaParameterWrapper) anotherParameter ).getCriteriaParameterId() ) : 1; } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmParameter.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmParameter.java index d91e03ba58b1..8fe0dd6832f6 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmParameter.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmParameter.java @@ -6,6 +6,8 @@ */ package org.hibernate.query.sqm.tree.expression; +import java.util.Comparator; + import org.hibernate.HibernateException; import org.hibernate.query.BindableType; import org.hibernate.query.criteria.JpaParameterExpression; @@ -23,6 +25,22 @@ * @author Steve Ebersole */ public interface SqmParameter extends SqmExpression, JpaParameterExpression, Comparable> { + Comparator> COMPARATOR = new Comparator<>() { + @Override + public int compare(SqmParameter o1, SqmParameter o2) { + if ( o1 instanceof SqmNamedParameter ) { + return o2 instanceof SqmNamedParameter + ? o1.getName().compareTo( o2.getName() ) + : -1; + } + else if ( o1 instanceof SqmPositionalParameter ) { + return o2 instanceof SqmPositionalParameter + ? o1.getPosition().compareTo( o2.getPosition() ) + : 1; + } + throw new HibernateException( "Unexpected SqmParameter type for comparison : " + this + " & " + o2 ); + } + }; /** * If this represents a named parameter, return that parameter name; * otherwise return {@code null}. diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmUnaryOperation.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmUnaryOperation.java index ba626e6d0a7f..6d4133262011 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmUnaryOperation.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/expression/SqmUnaryOperation.java @@ -25,7 +25,7 @@ public SqmUnaryOperation(UnaryArithmeticOperator operation, SqmExpression ope operation, operand, operand.nodeBuilder().getTypeConfiguration().getBasicTypeForJavaType( - operand.getNodeType().getRelationalJavaType().getJavaType() + operand.getExpressible().getRelationalJavaType().getJavaType() ) ); } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmCteJoin.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmCteJoin.java index 8f392bc896cf..d6c1fa758020 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmCteJoin.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmCteJoin.java @@ -17,7 +17,7 @@ import org.hibernate.query.sqm.tree.SqmJoinType; import org.hibernate.query.sqm.tree.cte.SqmCteStatement; import org.hibernate.query.sqm.tree.domain.AbstractSqmQualifiedJoin; -import org.hibernate.query.sqm.tree.domain.SqmCorrelatedEntityJoin; +import org.hibernate.query.sqm.tree.domain.SqmCorrelatedCteJoin; import org.hibernate.query.sqm.tree.domain.SqmPath; import org.hibernate.spi.NavigablePath; @@ -115,8 +115,8 @@ public X accept(SemanticQueryWalker walker) { // JPA @Override - public SqmCorrelatedEntityJoin createCorrelation() { - throw new UnsupportedOperationException(); + public SqmCorrelatedCteJoin createCorrelation() { + return new SqmCorrelatedCteJoin<>( this ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmDerivedJoin.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmDerivedJoin.java index beb87f5c6534..16e696bdf735 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmDerivedJoin.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmDerivedJoin.java @@ -12,6 +12,7 @@ import org.hibernate.query.criteria.JpaDerivedJoin; import org.hibernate.query.criteria.JpaExpression; import org.hibernate.query.criteria.JpaPredicate; +import org.hibernate.query.sqm.tree.domain.SqmCorrelatedDerivedJoin; import org.hibernate.query.derived.AnonymousTupleType; import org.hibernate.query.sqm.SemanticQueryWalker; import org.hibernate.query.sqm.SqmPathSource; @@ -19,7 +20,6 @@ import org.hibernate.query.sqm.tree.SqmCopyContext; import org.hibernate.query.sqm.tree.SqmJoinType; import org.hibernate.query.sqm.tree.domain.AbstractSqmQualifiedJoin; -import org.hibernate.query.sqm.tree.domain.SqmCorrelatedEntityJoin; import org.hibernate.query.sqm.tree.domain.SqmPath; import org.hibernate.query.sqm.tree.select.SqmSubQuery; import org.hibernate.spi.NavigablePath; @@ -166,8 +166,8 @@ public X accept(SemanticQueryWalker walker) { // JPA @Override - public SqmCorrelatedEntityJoin createCorrelation() { - throw new UnsupportedOperationException(); + public SqmCorrelatedDerivedJoin createCorrelation() { + return new SqmCorrelatedDerivedJoin<>( this ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmFromClause.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmFromClause.java index 24ffbed1143f..049e646136f3 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmFromClause.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/from/SqmFromClause.java @@ -14,6 +14,7 @@ import org.hibernate.internal.util.collections.CollectionHelper; import org.hibernate.query.sqm.tree.SqmCopyContext; +import org.hibernate.query.sqm.tree.SqmJoinType; import org.hibernate.query.sqm.tree.domain.SqmTreatedPath; /** @@ -117,57 +118,62 @@ public void appendHqlString(StringBuilder sb) { } public static void appendJoins(SqmFrom sqmFrom, StringBuilder sb) { - for ( SqmJoin sqmJoin : sqmFrom.getSqmJoins() ) { - switch ( sqmJoin.getSqmJoinType() ) { - case LEFT: - sb.append( " left join " ); - break; - case RIGHT: - sb.append( " right join " ); - break; - case INNER: - sb.append( " join " ); - break; - case FULL: - sb.append( " full join " ); - break; - case CROSS: - sb.append( " cross join " ); - break; - } + if ( sqmFrom instanceof SqmRoot && ( (SqmRoot) sqmFrom ).getOrderedJoins() != null ) { + appendJoins( sqmFrom, ( (SqmRoot) sqmFrom ).getOrderedJoins(), sb, false ); + } + else { + appendJoins( sqmFrom, sqmFrom.getSqmJoins(), sb, true ); + } + } + + private static void appendJoins(SqmFrom sqmFrom, List> joins, StringBuilder sb, boolean transitive) { + for ( SqmJoin sqmJoin : joins ) { + appendJoinType( sb, sqmJoin.getSqmJoinType() ); if ( sqmJoin instanceof SqmAttributeJoin ) { final SqmAttributeJoin attributeJoin = (SqmAttributeJoin) sqmJoin; - if ( sqmFrom instanceof SqmTreatedPath ) { - final SqmTreatedPath treatedPath = (SqmTreatedPath) sqmFrom; - sb.append( "treat(" ); - sb.append( treatedPath.getWrappedPath().resolveAlias() ); - sb.append( " as " ).append( treatedPath.getTreatTarget().getTypeName() ).append( ')' ); + final List> sqmTreats = attributeJoin.getSqmTreats(); + if ( attributeJoin.getExplicitAlias() != null && !sqmTreats.isEmpty() ) { + for ( int i = 0; i < sqmTreats.size(); i++ ) { + final var treatJoin = (SqmAttributeJoin) sqmTreats.get( i ); + if ( i != 0 ) { + appendJoinType( sb, sqmJoin.getSqmJoinType() ); + } + sb.append( "treat(" ); + appendAttributeJoin( sqmFrom, sb, attributeJoin ); + sb.append( " as " ); + sb.append( ((SqmTreatedPath) treatJoin).getTreatTarget().getTypeName() ); + sb.append( ')' ); + appendJoinAliasAndOnClause( sb, treatJoin ); + if ( transitive ) { + appendJoins( treatJoin, sb ); + } + } } else { - sb.append( sqmFrom.resolveAlias() ); - } - sb.append( '.' ).append( ( attributeJoin ).getAttribute().getName() ); - sb.append( ' ' ).append( sqmJoin.resolveAlias() ); - if ( attributeJoin.getJoinPredicate() != null ) { - sb.append( " on " ); - attributeJoin.getJoinPredicate().appendHqlString( sb ); + appendAttributeJoin( sqmFrom, sb, attributeJoin ); + appendJoinAliasAndOnClause( sb, attributeJoin ); + if ( transitive ) { + appendJoins( attributeJoin, sb ); + appendTreatJoins( sqmJoin, sb ); + } } - appendJoins( sqmJoin, sb ); } else if ( sqmJoin instanceof SqmCrossJoin ) { sb.append( ( (SqmCrossJoin) sqmJoin ).getEntityName() ); sb.append( ' ' ).append( sqmJoin.resolveAlias() ); - appendJoins( sqmJoin, sb ); + if ( transitive ) { + appendJoins( sqmJoin, sb ); + appendTreatJoins( sqmJoin, sb ); + } } else if ( sqmJoin instanceof SqmEntityJoin ) { final SqmEntityJoin sqmEntityJoin = (SqmEntityJoin) sqmJoin; - sb.append( ( sqmEntityJoin ).getEntityName() ); - sb.append( ' ' ).append( sqmJoin.resolveAlias() ); - if ( sqmEntityJoin.getJoinPredicate() != null ) { - sb.append( " on " ); - sqmEntityJoin.getJoinPredicate().appendHqlString( sb ); + sb.append( sqmEntityJoin.getEntityName() ); + appendJoinAliasAndOnClause( sb, sqmEntityJoin ); + if ( transitive ) { + appendJoins( sqmJoin, sb ); + appendTreatJoins( sqmJoin, sb ); } - appendJoins( sqmJoin, sb ); } else { throw new UnsupportedOperationException( "Unsupported join: " + sqmJoin ); @@ -175,6 +181,52 @@ else if ( sqmJoin instanceof SqmEntityJoin ) { } } + private static void appendJoinAliasAndOnClause(StringBuilder sb, SqmQualifiedJoin join) { + sb.append( ' ' ).append( join.resolveAlias() ); + if ( join.getJoinPredicate() != null ) { + sb.append( " on " ); + join.getJoinPredicate().appendHqlString( sb ); + } + } + + private static void appendAttributeJoin(SqmFrom sqmFrom, StringBuilder sb, SqmAttributeJoin attributeJoin) { + if ( sqmFrom instanceof SqmTreatedPath ) { + final SqmTreatedPath treatedPath = (SqmTreatedPath) sqmFrom; + sb.append( "treat(" ); + treatedPath.getWrappedPath().appendHqlString( sb ); +// sb.append( treatedPath.getWrappedPath().resolveAlias( context ) ); + sb.append( " as " ).append( treatedPath.getTreatTarget().getTypeName() ).append( ')' ); + } + else { + sb.append( sqmFrom.resolveAlias() ); + } + sb.append( '.' ).append( attributeJoin.getAttribute().getName() ); + } + + private static void appendJoinType(StringBuilder sb, SqmJoinType sqmJoinType) { + final String joinText; + switch ( sqmJoinType ) { + case LEFT: + joinText = " left join "; + break; + case RIGHT: + joinText = " right join "; + break; + case INNER: + joinText = " join "; + break; + case FULL: + joinText = " full join "; + break; + case CROSS: + joinText = " cross join "; + break; + default: + throw new UnsupportedOperationException( "Unsupported join type: " + sqmJoinType ); + } + sb.append( joinText ); + } + private void appendJoins(SqmFrom sqmFrom, String correlationPrefix, StringBuilder sb) { String separator = ""; for ( SqmJoin sqmJoin : sqmFrom.getSqmJoins() ) { diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/AbstractSqmInsertStatement.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/AbstractSqmInsertStatement.java index 889e7d6add97..125e90f8d8e8 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/AbstractSqmInsertStatement.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/AbstractSqmInsertStatement.java @@ -89,6 +89,10 @@ protected List> copyInsertionTargetPaths(SqmCopyContext context) { } } + void setConflictClause(SqmConflictClause conflictClause) { + this.conflictClause = conflictClause; + } + protected void verifyInsertTypesMatch( List> insertionTargetPaths, List> expressions) { diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/SqmConflictClause.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/SqmConflictClause.java index 547e03d94d6c..6c3f23056223 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/SqmConflictClause.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/SqmConflictClause.java @@ -54,7 +54,7 @@ private SqmConflictClause( this.insertStatement = insertStatement; this.excludedRoot = excludedRoot; this.constraintName = constraintName; - this.constraintPaths = Collections.unmodifiableList( constraintPaths ); + this.constraintPaths = constraintPaths == null ? null : Collections.unmodifiableList( constraintPaths ); this.updateAction = updateAction; } @@ -157,7 +157,7 @@ public SqmConflictClause copy(SqmCopyContext context) { insertStatement.copy( context ), excludedRoot.copy( context ), constraintName, - copyOf( constraintPaths, context ), + constraintPaths == null ? null : copyOf( constraintPaths, context ), updateAction == null ? null : updateAction.copy( context ) ) ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/SqmInsertSelectStatement.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/SqmInsertSelectStatement.java index 90d0396bf2a8..ba65f8c2ca6c 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/SqmInsertSelectStatement.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/SqmInsertSelectStatement.java @@ -77,19 +77,26 @@ public SqmInsertSelectStatement copy(SqmCopyContext context) { if ( existing != null ) { return existing; } - return context.registerCopy( - this, - new SqmInsertSelectStatement<>( - nodeBuilder(), - getQuerySource(), - copyParameters( context ), - copyCteStatements( context ), - getTarget().copy( context ), - copyInsertionTargetPaths( context ), - getConflictClause() == null ? null : getConflictClause().copy( context ), - selectQueryPart.copy( context ) - ) + final SqmInsertSelectStatement sqmInsertSelectStatementCopy = new SqmInsertSelectStatement<>( + nodeBuilder(), + context.getQuerySource() == null ? getQuerySource() : context.getQuerySource(), + copyParameters( context ), + copyCteStatements( context ), + getTarget().copy( context ), + null, + null, + selectQueryPart.copy( context ) ); + + context.registerCopy( this, sqmInsertSelectStatementCopy ); + + sqmInsertSelectStatementCopy.setInsertionTargetPaths( copyInsertionTargetPaths( context ) ); + + if ( getConflictClause() != null ) { + sqmInsertSelectStatementCopy.setConflictClause( getConflictClause().copy( context ) ); + } + + return sqmInsertSelectStatementCopy; } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/SqmInsertValuesStatement.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/SqmInsertValuesStatement.java index d51a1b097790..8fe1511a7e02 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/SqmInsertValuesStatement.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/insert/SqmInsertValuesStatement.java @@ -82,19 +82,26 @@ public SqmInsertValuesStatement copy(SqmCopyContext context) { valuesList.add( sqmValues.copy( context ) ); } } - return context.registerCopy( + + final SqmInsertValuesStatement sqmInsertValuesStatementCopy = context.registerCopy( this, new SqmInsertValuesStatement<>( nodeBuilder(), - getQuerySource(), + context.getQuerySource() == null ? getQuerySource() : context.getQuerySource(), copyParameters( context ), copyCteStatements( context ), getTarget().copy( context ), copyInsertionTargetPaths( context ), - getConflictClause() == null ? null : getConflictClause().copy( context ), + null, valuesList ) ); + + if ( getConflictClause() != null ) { + sqmInsertValuesStatementCopy.setConflictClause( getConflictClause().copy( context ) ); + } + + return sqmInsertValuesStatementCopy; } public SqmInsertValuesStatement copyWithoutValues(SqmCopyContext context) { @@ -102,7 +109,7 @@ public SqmInsertValuesStatement copyWithoutValues(SqmCopyContext context) { this, new SqmInsertValuesStatement<>( nodeBuilder(), - getQuerySource(), + context.getQuerySource() == null ? getQuerySource() : context.getQuerySource(), copyParameters( context ), copyCteStatements( context ), getTarget().copy( context ), diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/jpa/ParameterCollector.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/jpa/ParameterCollector.java index f2d04eaca227..a7c1aa96f980 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/jpa/ParameterCollector.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/jpa/ParameterCollector.java @@ -8,6 +8,7 @@ import java.util.Collections; import java.util.HashSet; +import java.util.IdentityHashMap; import java.util.Set; import java.util.function.Consumer; @@ -64,6 +65,8 @@ private ParameterCollector(Consumer> consumer) { private Set> parameterExpressions; private final Consumer> consumer; + private int criteriaParameterId; + private IdentityHashMap, Integer> unnamedParameterIdMap; @Override public Object visitPositionalParameterExpression(SqmPositionalParameter expression) { @@ -86,10 +89,28 @@ public Object visitNamedParameterExpression(SqmNamedParameter expression) { */ @Override public SqmJpaCriteriaParameterWrapper visitJpaCriteriaParameter(JpaCriteriaParameter expression) { + final int unnamedParameterId; + if ( expression.getName() == null ) { + if ( unnamedParameterIdMap == null ) { + unnamedParameterIdMap = new IdentityHashMap<>(); + } + final var index = unnamedParameterIdMap.get( expression ); + if ( index == null ) { + unnamedParameterIdMap.put( expression, unnamedParameterId = unnamedParameterIdMap.size() ); + } + else { + unnamedParameterId = index; + } + } + else { + unnamedParameterId = -1; + } return visitParameter( new SqmJpaCriteriaParameterWrapper<>( getInferredParameterType( expression ), expression, + criteriaParameterId++, + unnamedParameterId, expression.nodeBuilder() ) ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/predicate/SqmJunctionPredicate.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/predicate/SqmJunctionPredicate.java index 343f07a65c75..b36d4fcd2313 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/predicate/SqmJunctionPredicate.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/predicate/SqmJunctionPredicate.java @@ -11,6 +11,7 @@ import org.hibernate.query.sqm.NodeBuilder; import org.hibernate.query.sqm.SemanticQueryWalker; +import org.hibernate.query.sqm.SqmExpressible; import org.hibernate.query.sqm.tree.SqmCopyContext; import jakarta.persistence.criteria.Expression; @@ -22,6 +23,15 @@ public class SqmJunctionPredicate extends AbstractSqmPredicate { private final BooleanOperator booleanOperator; private final List predicates; + public SqmJunctionPredicate( + BooleanOperator booleanOperator, + SqmExpressible expressible, + NodeBuilder nodeBuilder) { + super( expressible, nodeBuilder ); + this.booleanOperator = booleanOperator; + this.predicates = new ArrayList<>(); + } + public SqmJunctionPredicate( BooleanOperator booleanOperator, SqmPredicate leftHandPredicate, diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/predicate/SqmMemberOfPredicate.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/predicate/SqmMemberOfPredicate.java index 82769080643e..2e75e1176903 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/predicate/SqmMemberOfPredicate.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/predicate/SqmMemberOfPredicate.java @@ -37,7 +37,7 @@ public SqmMemberOfPredicate( this.pluralPath = pluralPath; this.leftHandExpression = leftHandExpression; - final SimpleDomainType simpleDomainType = pluralPath.getReferencedPathSource().getElementType(); + final SimpleDomainType simpleDomainType = pluralPath.getPluralAttribute().getElementType(); if ( !areTypesComparable(leftHandExpression.getNodeType(), simpleDomainType, nodeBuilder.getSessionFactory()) ) { throw new SemanticException( diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmOrderByClause.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmOrderByClause.java index adb5301a63d5..a8b4bc88f798 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmOrderByClause.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmOrderByClause.java @@ -8,13 +8,15 @@ import java.io.Serializable; import java.util.ArrayList; -import java.util.Collections; import java.util.List; import org.hibernate.query.sqm.tree.SqmCopyContext; import org.hibernate.query.sqm.tree.expression.SqmAliasedNodeRef; import org.hibernate.query.sqm.tree.expression.SqmExpression; +import static java.util.Collections.emptyList; +import static java.util.Collections.unmodifiableList; + /** * @author Steve Ebersole */ @@ -71,12 +73,7 @@ public SqmOrderByClause addSortSpecification(SqmExpression expression) { } public List getSortSpecifications() { - if ( sortSpecifications == null ) { - return Collections.emptyList(); - } - else { - return Collections.unmodifiableList( sortSpecifications ); - } + return sortSpecifications == null ? emptyList() : unmodifiableList( sortSpecifications ); } public void setSortSpecifications(List sortSpecifications) { diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmQueryGroup.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmQueryGroup.java index f90c532e59b7..9f2fdd738d98 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmQueryGroup.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmQueryGroup.java @@ -170,7 +170,8 @@ private void validateQueryGroupFetchStructure(List> ty for ( int j = 0; j < firstSelectionSize; j++ ) { final SqmTypedNode firstSqmSelection = typedNodes.get( j ); final JavaType firstJavaType = firstSqmSelection.getNodeJavaType(); - if ( firstJavaType != selections.get( j ).getNodeJavaType() ) { + final JavaType nodeJavaType = selections.get( j ).getNodeJavaType(); + if ( nodeJavaType != null && firstJavaType != null && firstJavaType != nodeJavaType ) { throw new SemanticException( "Select items of the same index must have the same java type across all query parts" ); diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmQueryPart.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmQueryPart.java index 230ca8cf1eb6..22f494796130 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmQueryPart.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmQueryPart.java @@ -6,7 +6,6 @@ */ package org.hibernate.query.sqm.tree.select; -import java.util.Collections; import java.util.List; import org.hibernate.query.sqm.FetchClauseType; @@ -18,6 +17,8 @@ import org.hibernate.query.sqm.tree.SqmVisitableNode; import org.hibernate.query.sqm.tree.expression.SqmExpression; +import static java.util.Collections.emptyList; + /** * Defines the ordering and fetch/offset part of a query which is shared with query groups. * @@ -129,11 +130,7 @@ public FetchClauseType getFetchClauseType() { @Override public List getSortSpecifications() { - if ( getOrderByClause() == null ) { - return Collections.emptyList(); - } - - return getOrderByClause().getSortSpecifications(); + return getOrderByClause() == null ? emptyList() : getOrderByClause().getSortSpecifications(); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmSelectStatement.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmSelectStatement.java index 631d3eeff92c..6d23adb216ad 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmSelectStatement.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmSelectStatement.java @@ -140,7 +140,7 @@ private SqmSelectStatement createCopy(SqmCopyContext context, Class re nodeBuilder(), copyCteStatements( context ), resultType, - getQuerySource(), + context.getQuerySource() == null ? getQuerySource() : context.getQuerySource(), parameters ) ); @@ -492,6 +492,9 @@ public SqmSelectStatement createCountQuery() { final SqmSelectStatement query = nodeBuilder().createQuery( Long.class ); query.from( subquery ); query.select( nodeBuilder().count() ); + if ( subquery.getFetch() == null && subquery.getOffset() == null ) { + subquery.getQueryPart().setOrderByClause( null ); + } return query; } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmSelectableNode.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmSelectableNode.java index a8da9a186005..5434bbfc7d8d 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmSelectableNode.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmSelectableNode.java @@ -11,6 +11,7 @@ import org.hibernate.metamodel.model.domain.DomainType; import org.hibernate.query.criteria.JpaSelection; +import org.hibernate.query.sqm.SqmExpressible; import org.hibernate.query.sqm.tree.SqmCopyContext; import org.hibernate.query.sqm.tree.SqmTypedNode; @@ -35,7 +36,8 @@ public interface SqmSelectableNode extends JpaSelection, SqmTypedNode { SqmSelectableNode copy(SqmCopyContext context); default Integer getTupleLength() { - final DomainType sqmType = getNodeType() == null ? null : getNodeType().getSqmType(); + final SqmExpressible nodeType = getExpressible(); + final DomainType sqmType = nodeType == null ? null : nodeType.getSqmType(); return sqmType == null ? 1 : sqmType.getTupleLength(); } } diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmSubQuery.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmSubQuery.java index 87766ca7feb0..493e8aa216d1 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmSubQuery.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/select/SqmSubQuery.java @@ -592,7 +592,8 @@ public SqmInPredicate in(Expression... values) { @Override public SqmInPredicate in(Collection values) { - return nodeBuilder().in( this, values ); + //noinspection unchecked + return nodeBuilder().in( this, (Collection) values ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/update/SqmUpdateStatement.java b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/update/SqmUpdateStatement.java index f3accf2906c7..050b292c56d7 100644 --- a/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/update/SqmUpdateStatement.java +++ b/hibernate-core/src/main/java/org/hibernate/query/sqm/tree/update/SqmUpdateStatement.java @@ -107,7 +107,7 @@ public SqmUpdateStatement copy(SqmCopyContext context) { this, new SqmUpdateStatement<>( nodeBuilder(), - getQuerySource(), + context.getQuerySource() == null ? getQuerySource() : context.getQuerySource(), copyParameters( context ), copyCteStatements( context ), getTarget().copy( context ) @@ -183,7 +183,9 @@ public SqmUpdateStatement set(SingularAttribute attribute, @Override public SqmUpdateStatement set(Path attribute, X value) { - applyAssignment( (SqmPath) attribute, (SqmExpression) nodeBuilder().value( value ) ); + final SqmCriteriaNodeBuilder nodeBuilder = (SqmCriteriaNodeBuilder) nodeBuilder(); + final SqmPath sqmAttribute = (SqmPath) attribute; + applyAssignment( sqmAttribute, nodeBuilder.value( value, sqmAttribute ) ); return this; } @@ -195,15 +197,15 @@ public SqmUpdateStatement set(Path attribute, Expression @Override @SuppressWarnings({"rawtypes", "unchecked"}) public SqmUpdateStatement set(String attributeName, Object value) { - final SqmPath sqmPath = getTarget().get(attributeName); + final SqmPath sqmPath = getTarget().get( attributeName ); final SqmExpression expression; if ( value instanceof SqmExpression ) { expression = (SqmExpression) value; } else { - expression = (SqmExpression) nodeBuilder().value( value ); + final SqmCriteriaNodeBuilder nodeBuilder = (SqmCriteriaNodeBuilder) nodeBuilder(); + expression = nodeBuilder.value( value, sqmPath ); } - assertAssignable( null, sqmPath, expression, nodeBuilder().getSessionFactory() ); applyAssignment( sqmPath, expression ); return this; } diff --git a/hibernate-core/src/main/java/org/hibernate/resource/transaction/backend/jta/internal/JtaIsolationDelegate.java b/hibernate-core/src/main/java/org/hibernate/resource/transaction/backend/jta/internal/JtaIsolationDelegate.java index 51c1e3f80cd5..53cf8921b35b 100644 --- a/hibernate-core/src/main/java/org/hibernate/resource/transaction/backend/jta/internal/JtaIsolationDelegate.java +++ b/hibernate-core/src/main/java/org/hibernate/resource/transaction/backend/jta/internal/JtaIsolationDelegate.java @@ -117,7 +117,9 @@ private T doInSuspendedTransaction(HibernateCallable callable) { try { // First we suspend any current JTA transaction final Transaction surroundingTransaction = transactionManager.suspend(); - LOG.debugf( "Surrounding JTA transaction suspended [%s]", surroundingTransaction ); + if ( surroundingTransaction != null ) { + LOG.debugf( "Surrounding JTA transaction suspended [%s]", surroundingTransaction ); + } try { return callable.call(); @@ -127,8 +129,10 @@ private T doInSuspendedTransaction(HibernateCallable callable) { } finally { try { - transactionManager.resume( surroundingTransaction ); - LOG.debugf( "Surrounding JTA transaction resumed [%s]", surroundingTransaction ); + if ( surroundingTransaction != null ) { + transactionManager.resume( surroundingTransaction ); + LOG.debugf( "Surrounding JTA transaction resumed [%s]", surroundingTransaction ); + } } catch ( Throwable t2 ) { // if the actually work had an error use that, otherwise error based on t @@ -199,7 +203,11 @@ private T doTheWork(WorkExecutorVisitable work) { } } catch (SQLException e) { - throw sqlExceptionConverter().apply( e, "unable to obtain isolated JDBC connection" ); + final JDBCException jdbcException = sqlExceptionConverter().apply( e, "unable to obtain isolated JDBC connection" ); + if ( jdbcException == null ) { + throw new HibernateException( "Unable to obtain isolated JDBC connection", e ); + } + throw jdbcException; } } diff --git a/hibernate-core/src/main/java/org/hibernate/result/internal/OutputsImpl.java b/hibernate-core/src/main/java/org/hibernate/result/internal/OutputsImpl.java index 9728d5a4e38a..0d9a6bdb0dec 100644 --- a/hibernate-core/src/main/java/org/hibernate/result/internal/OutputsImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/result/internal/OutputsImpl.java @@ -16,6 +16,7 @@ import java.util.function.Supplier; import org.hibernate.JDBCException; +import org.hibernate.engine.jdbc.spi.JdbcCoordinator; import org.hibernate.engine.jdbc.spi.SqlStatementLogger; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.event.spi.EventManager; @@ -147,7 +148,9 @@ public boolean goToNext() { @Override public void release() { - context.getSession().getJdbcCoordinator().getLogicalConnection().getResourceRegistry().release( jdbcStatement ); + final JdbcCoordinator jdbcCoordinator = context.getSession().getJdbcCoordinator(); + jdbcCoordinator.getLogicalConnection().getResourceRegistry().release( jdbcStatement ); + jdbcCoordinator.afterStatementExecution(); } private List extractCurrentResults() { diff --git a/hibernate-core/src/main/java/org/hibernate/sql/Template.java b/hibernate-core/src/main/java/org/hibernate/sql/Template.java index 05c5a78d028c..7e8253d36a00 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/Template.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/Template.java @@ -19,6 +19,8 @@ import org.hibernate.query.sqm.function.SqmFunctionRegistry; import org.hibernate.type.spi.TypeConfiguration; +import static java.lang.Boolean.parseBoolean; +import static java.lang.Character.isLetter; import static org.hibernate.internal.util.StringHelper.WHITESPACE; /** @@ -92,6 +94,7 @@ public final class Template { LITERAL_PREFIXES.add("date"); LITERAL_PREFIXES.add("time"); LITERAL_PREFIXES.add("timestamp"); + LITERAL_PREFIXES.add("zone"); } public static final String TEMPLATE = "$PlaceHolder$"; @@ -139,12 +142,9 @@ public static String renderWhereStringTemplate( // which the tokens occur. Depending on the state of those flags we decide whether we need to qualify // identifier references. - String symbols = PUNCTUATION + - WHITESPACE + - dialect.openQuote() + - dialect.closeQuote(); - StringTokenizer tokens = new StringTokenizer( sqlWhereString, symbols, true ); - StringBuilder result = new StringBuilder(); + final String symbols = PUNCTUATION + WHITESPACE + dialect.openQuote() + dialect.closeQuote(); + final StringTokenizer tokens = new StringTokenizer( sqlWhereString, symbols, true ); + final StringBuilder result = new StringBuilder(); boolean quoted = false; boolean quotedIdentifier = false; @@ -168,7 +168,7 @@ public static String renderWhereStringTemplate( } if ( !quoted ) { - boolean isOpenQuote; + final boolean isOpenQuote; if ( "`".equals(token) ) { isOpenQuote = !quotedIdentifier; token = lcToken = isOpenQuote @@ -177,182 +177,27 @@ public static String renderWhereStringTemplate( quotedIdentifier = isOpenQuote; isQuoteCharacter = true; } - else if ( !quotedIdentifier && ( dialect.openQuote()==token.charAt(0) ) ) { + else if ( !quotedIdentifier && dialect.openQuote()==token.charAt(0) ) { isOpenQuote = true; quotedIdentifier = true; isQuoteCharacter = true; } - else if ( quotedIdentifier && ( dialect.closeQuote()==token.charAt(0) ) ) { + else if ( quotedIdentifier && dialect.closeQuote()==token.charAt(0) ) { quotedIdentifier = false; isQuoteCharacter = true; isOpenQuote = false; } - else if ( LITERAL_PREFIXES.contains( lcToken ) ) { - if ( "'".equals( nextToken ) ) { - // Don't prefix a literal - result.append( token ); - continue; - } - else if ( nextToken != null && Character.isWhitespace( nextToken.charAt( 0 ) ) ) { - final StringBuilder additionalTokens = new StringBuilder(); - TimeZoneTokens possibleNextToken = null; - do { - possibleNextToken = possibleNextToken == null - ? TimeZoneTokens.getPossibleNextTokens( lcToken ) - : possibleNextToken.nextToken(); - do { - additionalTokens.append( nextToken ); - hasMore = tokens.hasMoreTokens(); - nextToken = tokens.nextToken(); - } while ( nextToken != null && Character.isWhitespace( nextToken.charAt( 0 ) ) ); - } while ( nextToken != null && possibleNextToken.isToken( nextToken ) ); - if ( "'".equals( nextToken ) ) { - // Don't prefix a literal - result.append( token ); - result.append( additionalTokens ); - continue; - } - else { - isOpenQuote = false; - } - } - else { - isOpenQuote = false; - } - } else { isOpenQuote = false; } - if ( isOpenQuote ) { result.append( placeholder ).append( '.' ); } } - // Special processing for ANSI SQL EXTRACT function - if ( "extract".equals( lcToken ) && "(".equals( nextToken ) ) { - final String field = extractUntil( tokens, "from" ); - final String source = renderWhereStringTemplate( - extractUntil( tokens, ")" ), - placeholder, - dialect, - typeConfiguration, - functionRegistry - ); - result.append( "extract(" ).append( field ).append( " from " ).append( source ).append( ')' ); - - hasMore = tokens.hasMoreTokens(); - nextToken = hasMore ? tokens.nextToken() : null; - - continue; - } - - // Special processing for ANSI SQL TRIM function - if ( "trim".equals( lcToken ) && "(".equals( nextToken ) ) { - List operands = new ArrayList<>(); - StringBuilder builder = new StringBuilder(); - - boolean hasMoreOperands = true; - String operandToken = tokens.nextToken(); - switch ( operandToken.toLowerCase( Locale.ROOT ) ) { - case "leading": - case "trailing": - case "both": - operands.add( operandToken ); - if ( hasMoreOperands = tokens.hasMoreTokens() ) { - operandToken = tokens.nextToken(); - } - break; - } - boolean quotedOperand = false; - int parenthesis = 0; - while ( hasMoreOperands ) { - final boolean isQuote = "'".equals( operandToken ); - if ( isQuote ) { - quotedOperand = !quotedOperand; - if ( !quotedOperand ) { - operands.add( builder.append( '\'' ).toString() ); - builder.setLength( 0 ); - } - else { - builder.append( '\'' ); - } - } - else if ( quotedOperand ) { - builder.append( operandToken ); - } - else if ( parenthesis != 0 ) { - builder.append( operandToken ); - switch ( operandToken ) { - case "(": - parenthesis++; - break; - case ")": - parenthesis--; - break; - } - } - else { - builder.append( operandToken ); - switch ( operandToken.toLowerCase( Locale.ROOT ) ) { - case "(": - parenthesis++; - break; - case ")": - parenthesis--; - break; - case "from": - if ( builder.length() != 0 ) { - operands.add( builder.substring( 0, builder.length() - 4 ) ); - builder.setLength( 0 ); - operands.add( operandToken ); - } - break; - } - } - operandToken = tokens.nextToken(); - hasMoreOperands = tokens.hasMoreTokens() && ( parenthesis != 0 || ! ")".equals( operandToken ) ); - } - if ( builder.length() != 0 ) { - operands.add( builder.toString() ); - } - - TrimOperands trimOperands = new TrimOperands( operands ); - result.append( "trim(" ); - if ( trimOperands.trimSpec != null ) { - result.append( trimOperands.trimSpec ).append( ' ' ); - } - if ( trimOperands.trimChar != null ) { - if ( trimOperands.trimChar.startsWith( "'" ) && trimOperands.trimChar.endsWith( "'" ) ) { - result.append( trimOperands.trimChar ); - } - else { - result.append( - renderWhereStringTemplate( trimOperands.trimSpec, placeholder, dialect, typeConfiguration, functionRegistry ) - ); - } - result.append( ' ' ); - } - if ( trimOperands.from != null ) { - result.append( trimOperands.from ).append( ' ' ); - } - else if ( trimOperands.trimSpec != null || trimOperands.trimChar != null ) { - // I think ANSI SQL says that the 'from' is not optional if either trim-spec or trim-char is specified - result.append( "from " ); - } - - result.append( renderWhereStringTemplate( trimOperands.trimSource, placeholder, dialect, typeConfiguration, functionRegistry ) ) - .append( ')' ); - - hasMore = tokens.hasMoreTokens(); - nextToken = hasMore ? tokens.nextToken() : null; - - continue; - } - - boolean quotedOrWhitespace = quoted || quotedIdentifier || isQuoteCharacter - || Character.isWhitespace( token.charAt(0) ); - + final boolean quotedOrWhitespace = + quoted || quotedIdentifier || isQuoteCharacter + || token.isBlank(); if ( quotedOrWhitespace ) { result.append( token ); } @@ -370,8 +215,21 @@ else if ( afterFromTable ) { else if ( isNamedParameter(token) ) { result.append(token); } + else if ( isExtractFunction( lcToken, nextToken ) ) { + // Special processing for ANSI SQL EXTRACT function + handleExtractFunction( placeholder, dialect, typeConfiguration, functionRegistry, tokens, result ); + hasMore = tokens.hasMoreTokens(); + nextToken = hasMore ? tokens.nextToken() : null; + } + else if ( isTrimFunction( lcToken, nextToken ) ) { + // Special processing for ANSI SQL TRIM function + handleTrimFunction( placeholder, dialect, typeConfiguration, functionRegistry, tokens, result ); + hasMore = tokens.hasMoreTokens(); + nextToken = hasMore ? tokens.nextToken() : null; + } else if ( isIdentifier(token) - && !isFunctionOrKeyword(lcToken, nextToken, dialect, typeConfiguration, functionRegistry) ) { + && !isFunctionOrKeyword( lcToken, nextToken, dialect, typeConfiguration, functionRegistry ) + && !isLiteral( lcToken, nextToken, sqlWhereString, symbols, tokens ) ) { result.append(placeholder) .append('.') .append( dialect.quote(token) ); @@ -385,7 +243,7 @@ else if ( inFromClause && ",".equals(lcToken) ) { beforeTable = true; } if ( isBoolean( token ) ) { - token = dialect.toBooleanValueString( Boolean.parseBoolean( token ) ); + token = dialect.toBooleanValueString( parseBoolean( token ) ); } result.append(token); } @@ -401,37 +259,173 @@ else if ( inFromClause && ",".equals(lcToken) ) { return result.toString(); } - private enum TimeZoneTokens { - NONE, - WITH, - TIME, - ZONE; - - static TimeZoneTokens getPossibleNextTokens(String lctoken) { - switch ( lctoken ) { - case "time": - case "timestamp": - return WITH; - default: - return NONE; + private static boolean isTrimFunction(String lcToken, String nextToken) { + return "trim".equals(lcToken) && "(".equals(nextToken); + } + + private static boolean isExtractFunction(String lcToken, String nextToken) { + return "extract".equals(lcToken) && "(".equals(nextToken); + } + + private static boolean isLiteral( + String lcToken, String next, + String sqlWhereString, String symbols, StringTokenizer tokens) { + if ( LITERAL_PREFIXES.contains( lcToken ) && next != null ) { + // easy cases first + if ( "'".equals(next) ) { + return true; + } + else if ( !next.isBlank() ) { + return false; + } + else { + // we need to look ahead in the token stream + // to find the first non-blank token + final StringTokenizer lookahead = + new StringTokenizer( sqlWhereString, symbols, true ); + while ( lookahead.countTokens() > tokens.countTokens()+1 ) { + lookahead.nextToken(); + } + if ( lookahead.hasMoreTokens() ) { + String nextToken; + do { + nextToken = lookahead.nextToken().toLowerCase(Locale.ROOT); + } + while ( nextToken.isBlank() && lookahead.hasMoreTokens() ); + return "'".equals( nextToken ) + || lcToken.equals( "time" ) && "with".equals( nextToken ) + || lcToken.equals( "timestamp" ) && "with".equals( nextToken ) + || lcToken.equals( "time" ) && "zone".equals( nextToken ); + } + else { + return false; + } } } + else { + return false; + } + } - public TimeZoneTokens nextToken() { - if ( this == WITH ) { - return TIME; + private static void handleTrimFunction( + String placeholder, Dialect dialect, + TypeConfiguration typeConfiguration, + SqmFunctionRegistry functionRegistry, + StringTokenizer tokens, + StringBuilder result) { + final List operands = new ArrayList<>(); + final StringBuilder builder = new StringBuilder(); + + boolean hasMoreOperands = true; + String operandToken = tokens.nextToken(); + switch ( operandToken.toLowerCase( Locale.ROOT ) ) { + case "leading": + case "trailing": + case "both": + operands.add( operandToken ); + if ( hasMoreOperands = tokens.hasMoreTokens() ) { + operandToken = tokens.nextToken(); + } + break; + } + boolean quotedOperand = false; + int parenthesis = 0; + while ( hasMoreOperands ) { + final boolean isQuote = "'".equals( operandToken ); + if ( isQuote ) { + quotedOperand = !quotedOperand; + if ( !quotedOperand ) { + operands.add( builder.append( '\'' ).toString() ); + builder.setLength( 0 ); + } + else { + builder.append( '\'' ); + } } - else if ( this == TIME ) { - return ZONE; + else if ( quotedOperand ) { + builder.append( operandToken ); + } + else if ( parenthesis != 0 ) { + builder.append( operandToken ); + switch ( operandToken ) { + case "(": + parenthesis++; + break; + case ")": + parenthesis--; + break; + } } else { - return NONE; + builder.append( operandToken ); + switch ( operandToken.toLowerCase( Locale.ROOT ) ) { + case "(": + parenthesis++; + break; + case ")": + parenthesis--; + break; + case "from": + if ( builder.length() != 0 ) { + operands.add( builder.substring( 0, builder.length() - 4 ) ); + builder.setLength( 0 ); + operands.add( operandToken ); + } + break; + } } + operandToken = tokens.nextToken(); + hasMoreOperands = tokens.hasMoreTokens() + && ( parenthesis != 0 || ! ")".equals( operandToken ) ); + } + if ( builder.length() != 0 ) { + operands.add( builder.toString() ); } - public boolean isToken(String token) { - return this != NONE && name().equalsIgnoreCase( token ); + final TrimOperands trimOperands = new TrimOperands( operands ); + result.append( "trim(" ); + if ( trimOperands.trimSpec != null ) { + result.append( trimOperands.trimSpec ).append( ' ' ); + } + if ( trimOperands.trimChar != null ) { + if ( trimOperands.trimChar.startsWith( "'" ) && trimOperands.trimChar.endsWith( "'" ) ) { + result.append( trimOperands.trimChar ); + } + else { + result.append( + renderWhereStringTemplate( trimOperands.trimSpec, placeholder, dialect, typeConfiguration, functionRegistry ) + ); + } + result.append( ' ' ); } + if ( trimOperands.from != null ) { + result.append( trimOperands.from ).append( ' ' ); + } + else if ( trimOperands.trimSpec != null || trimOperands.trimChar != null ) { + // I think ANSI SQL says that the 'from' is not optional if either trim-spec or trim-char is specified + result.append( "from " ); + } + + result.append( renderWhereStringTemplate( trimOperands.trimSource, placeholder, dialect, typeConfiguration, functionRegistry ) ) + .append( ')' ); + } + + private static void handleExtractFunction( + String placeholder, + Dialect dialect, + TypeConfiguration typeConfiguration, + SqmFunctionRegistry functionRegistry, + StringTokenizer tokens, + StringBuilder result) { + final String field = extractUntil( tokens, "from" ); + final String source = renderWhereStringTemplate( + extractUntil( tokens, ")" ), + placeholder, + dialect, + typeConfiguration, + functionRegistry + ); + result.append( "extract(" ).append( field ).append( " from " ).append( source ).append( ')' ); } public static List collectColumnNames( @@ -754,7 +748,7 @@ else if ( size == 4 ) { } private static String extractUntil(StringTokenizer tokens, String delimiter) { - StringBuilder valueBuilder = new StringBuilder(); + final StringBuilder valueBuilder = new StringBuilder(); String token = tokens.nextToken(); while ( ! delimiter.equalsIgnoreCase( token ) ) { valueBuilder.append( token ); @@ -773,12 +767,21 @@ private static boolean isFunctionOrKeyword( Dialect dialect, TypeConfiguration typeConfiguration, SqmFunctionRegistry functionRegistry) { - return "(".equals( nextToken ) || - KEYWORDS.contains( lcToken ) || - isType( lcToken, typeConfiguration ) || - isFunction( lcToken, nextToken, functionRegistry ) || - dialect.getKeywords().contains( lcToken ) || - FUNCTION_KEYWORDS.contains( lcToken ); + if ( "(".equals( nextToken ) ) { + return true; + } + else if ( "date".equals( lcToken ) || "time".equals( lcToken ) ) { + // these can be column names on some databases + // TODO: treat 'current date' as a function + return false; + } + else { + return KEYWORDS.contains( lcToken ) + || isType( lcToken, typeConfiguration ) + || isFunction( lcToken, nextToken, functionRegistry ) + || dialect.getKeywords().contains( lcToken ) + || FUNCTION_KEYWORDS.contains( lcToken ); + } } private static boolean isType(String lcToken, TypeConfiguration typeConfiguration) { @@ -800,10 +803,11 @@ private static boolean isIdentifier(String token) { if ( isBoolean( token ) ) { return false; } - return token.charAt( 0 ) == '`' || ( //allow any identifier quoted with backtick - Character.isLetter( token.charAt( 0 ) ) && //only recognizes identifiers beginning with a letter + return token.charAt( 0 ) == '`' + || ( //allow any identifier quoted with backtick + isLetter( token.charAt( 0 ) ) && //only recognizes identifiers beginning with a letter token.indexOf( '.' ) < 0 - ); + ); } private static boolean isBoolean(String token) { diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/internal/TableGroupHelper.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/internal/TableGroupHelper.java new file mode 100644 index 000000000000..06567a2c02b8 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/internal/TableGroupHelper.java @@ -0,0 +1,282 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ + +package org.hibernate.sql.ast.internal; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.hibernate.internal.util.collections.CollectionHelper; +import org.hibernate.metamodel.mapping.internal.CaseStatementDiscriminatorMappingImpl; +import org.hibernate.persister.internal.SqlFragmentPredicate; +import org.hibernate.sql.ast.spi.AbstractSqlAstWalker; +import org.hibernate.sql.ast.tree.expression.AggregateColumnWriteExpression; +import org.hibernate.sql.ast.tree.expression.ColumnReference; +import org.hibernate.sql.ast.tree.expression.Expression; +import org.hibernate.sql.ast.tree.expression.SelfRenderingExpression; +import org.hibernate.sql.ast.tree.expression.SelfRenderingSqlFragmentExpression; +import org.hibernate.sql.ast.tree.expression.SqlTuple; +import org.hibernate.sql.ast.tree.expression.SqlTupleContainer; +import org.hibernate.sql.ast.tree.from.NamedTableReference; +import org.hibernate.sql.ast.tree.from.TableGroup; +import org.hibernate.sql.ast.tree.from.TableReference; +import org.hibernate.sql.ast.tree.from.TableReferenceJoin; +import org.hibernate.sql.ast.tree.predicate.ComparisonPredicate; +import org.hibernate.sql.ast.tree.predicate.FilterPredicate; +import org.hibernate.sql.ast.tree.predicate.Junction; +import org.hibernate.sql.ast.tree.predicate.Predicate; +import org.hibernate.sql.model.ast.ColumnWriteFragment; + +/** + * A simple walker that checks if a predicate contains qualifiers. + * + * @author Christian Beikov + */ +public class TableGroupHelper extends AbstractSqlAstWalker { + + public static final int REAL_TABLE_GROUP_REQUIRED = Integer.MAX_VALUE; + public static final int NO_TABLE_GROUP_REQUIRED = -1; + + private final String primaryQualifier; + private final Map qualifiers; + private final String[] qualifierFragments; + private Integer usedTableReferenceJoinIndex; + + private TableGroupHelper(String primaryQualifier, Map qualifiers) { + this.primaryQualifier = primaryQualifier; + this.qualifiers = qualifiers; + final String[] qualifierFragments = new String[qualifiers.size()]; + for ( Map.Entry entry : qualifiers.entrySet() ) { + qualifierFragments[entry.getValue()] = entry.getKey() + "."; + } + this.qualifierFragments = qualifierFragments; + } + + /** + * Returns the index of a table reference join which can be swapped with the primary table reference + * to avoid rendering a real nested table group. + * {@link #REAL_TABLE_GROUP_REQUIRED} is returned if swapping is not possible. + * {@code #NO_TABLE_GROUP_REQUIRED} is returned if no swapping is necessary. + */ + public static int findReferenceJoinForPredicateSwap(TableGroup tableGroup, Predicate predicate) { + if ( predicate != null && !tableGroup.getTableReferenceJoins().isEmpty() ) { + final TableReference primaryTableReference = tableGroup.getPrimaryTableReference(); + final HashMap qualifiers = CollectionHelper.mapOfSize( tableGroup.getTableReferenceJoins().size() ); + final List tableReferenceJoins = tableGroup.getTableReferenceJoins(); + for ( int i = 0; i < tableReferenceJoins.size(); i++ ) { + final TableReferenceJoin tableReferenceJoin = tableReferenceJoins.get( i ); + if ( !tableGroup.canUseInnerJoins() ) { + if ( !isSimplePredicate( tableGroup, i ) ){//|| isSimpleOrOuterJoin( tableReferenceJoin ) ) { + // Can't do avoid the real table group rendering in this case if it's not inner joined, + // because doing so might change the meaning of the SQL. Consider this example: + // `from tbl1 t1 left join (tbl2 t2 join tbl3 t3 on t2.id=t3.id and ...) on t1.fk=t2.id` + // + // To avoid the nested table group rendering, the join on `tbl3` has to switch to left join + // `from tbl1 t1 left join tbl2 t2 on t1.fk=t2.id left join tbl3 t3 on t2.id=t3.id and ...` + // The additional predicate in the `tbl3` join can make `t3` null even though `t2` is non-null + return REAL_TABLE_GROUP_REQUIRED; + } + } + qualifiers.put( tableReferenceJoin.getJoinedTableReference().getIdentificationVariable(), i ); + } + final TableGroupHelper qualifierCollector = new TableGroupHelper( + primaryTableReference.getIdentificationVariable(), + qualifiers + ); + try { + predicate.accept( qualifierCollector ); + if ( qualifierCollector.usedTableReferenceJoinIndex == null ) { + return NO_TABLE_GROUP_REQUIRED; + } + if ( qualifierCollector.usedTableReferenceJoinIndex != NO_TABLE_GROUP_REQUIRED + && !tableGroup.canUseInnerJoins() && !isSimpleTableReference( primaryTableReference ) ) { + // Can't reorder table reference join with primary table reference if the primary table reference + // might filter out elements, since that affects result count with outer joins + return REAL_TABLE_GROUP_REQUIRED; + } + return qualifierCollector.usedTableReferenceJoinIndex; + } + catch (MultipleUsesFoundException ex) { + return REAL_TABLE_GROUP_REQUIRED; + } + } + return NO_TABLE_GROUP_REQUIRED; + } + + private static boolean isSimpleTableReference(TableReference tableReference) { + return tableReference instanceof NamedTableReference && !tableReference.getTableId().startsWith( "(select" ); + } + + /** + * Checks if the table reference join at the given index uses a simple equality join predicate. + * Predicates that contain anything but comparisons of the primary table reference with table reference join columns + * are non-simple. + */ + private static boolean isSimplePredicate(TableGroup tableGroup, int index) { + final TableReference primaryTableReference = tableGroup.getPrimaryTableReference(); + final TableReferenceJoin tableReferenceJoin = tableGroup.getTableReferenceJoins().get( index ); + final NamedTableReference joinedTableReference = tableReferenceJoin.getJoinedTableReference(); + final Predicate predicate = tableReferenceJoin.getPredicate(); + if ( predicate instanceof Junction ) { + final Junction junction = (Junction) predicate; + if ( junction.getNature() == Junction.Nature.CONJUNCTION ) { + for ( Predicate subPredicate : junction.getPredicates() ) { + if ( !isComparison( subPredicate, primaryTableReference, joinedTableReference ) ) { + return false; + } + } + return true; + } + } + else { + return isComparison( predicate, primaryTableReference, joinedTableReference ); + } + return false; + } + + private static boolean isComparison(Predicate predicate, TableReference table1, TableReference table2) { + if ( predicate instanceof ComparisonPredicate ) { + final ComparisonPredicate comparisonPredicate = (ComparisonPredicate) predicate; + final Expression lhs = comparisonPredicate.getLeftHandExpression(); + final Expression rhs = comparisonPredicate.getRightHandExpression(); + final SqlTuple lhsTuple; + if ( lhs instanceof SqlTupleContainer && ( lhsTuple = ( (SqlTupleContainer) lhs ).getSqlTuple() ) != null ) { + final SqlTuple rhsTuple = ( (SqlTupleContainer) rhs ).getSqlTuple(); + final List lhsExpressions = lhsTuple.getExpressions(); + final List rhsExpressions = rhsTuple.getExpressions(); + for ( int i = 0; i < lhsExpressions.size(); i++ ) { + final ColumnReference lhsColumn = lhsExpressions.get( i ).getColumnReference(); + final ColumnReference rhsColumn = rhsExpressions.get( i ).getColumnReference(); + if ( !isComparison( table1, table2, lhsColumn, rhsColumn ) ) { + return false; + } + } + return true; + } + else { + return isComparison( table1, table2, lhs.getColumnReference(), rhs.getColumnReference() ); + } + } + return false; + } + + private static boolean isComparison( + TableReference table1, + TableReference table2, + ColumnReference column1, + ColumnReference column2) { + if ( column1 != null && column2 != null ) { + final String column1Qualifier = column1.getQualifier(); + final String column2Qualifier = column2.getQualifier(); + final String table1Qualifier = table1.getIdentificationVariable(); + final String table2Qualifier = table2.getIdentificationVariable(); + return column1Qualifier.equals( table1Qualifier ) && column2Qualifier.equals( table2Qualifier ) + || column1Qualifier.equals( table2Qualifier ) && column2Qualifier.equals( table1Qualifier ); + } + return false; + } + + private static class MultipleUsesFoundException extends RuntimeException { + public MultipleUsesFoundException() { + } + + @Override + public Throwable fillInStackTrace() { + return this; + } + } + + private void checkQualifier(String qualifier) { + if ( primaryQualifier.equals( qualifier ) ) { + if ( usedTableReferenceJoinIndex != null && usedTableReferenceJoinIndex != NO_TABLE_GROUP_REQUIRED ) { + throw new MultipleUsesFoundException(); + } + usedTableReferenceJoinIndex = NO_TABLE_GROUP_REQUIRED; + } + else { + final Integer index = qualifiers.get( qualifier ); + if ( index != null ) { + if ( usedTableReferenceJoinIndex != null && usedTableReferenceJoinIndex.intValue() != index ) { + throw new MultipleUsesFoundException(); + } + usedTableReferenceJoinIndex = index; + } + } + } + + private void checkSql(String sql) { + if ( sql.contains( primaryQualifier + "." ) ) { + if ( usedTableReferenceJoinIndex != null && usedTableReferenceJoinIndex != NO_TABLE_GROUP_REQUIRED ) { + throw new MultipleUsesFoundException(); + } + usedTableReferenceJoinIndex = NO_TABLE_GROUP_REQUIRED; + } + else { + for ( int i = 0; i < qualifierFragments.length; i++ ) { + if ( sql.contains( qualifierFragments[i] ) ) { + if ( usedTableReferenceJoinIndex != null && usedTableReferenceJoinIndex != i ) { + throw new MultipleUsesFoundException(); + } + usedTableReferenceJoinIndex = i; + } + } + } + } + + @Override + public void visitSelfRenderingExpression(SelfRenderingExpression expression) { + if ( expression instanceof SelfRenderingSqlFragmentExpression ) { + checkSql( ( (SelfRenderingSqlFragmentExpression) expression ).getExpression() ); + } + else if ( expression instanceof CaseStatementDiscriminatorMappingImpl.CaseStatementDiscriminatorExpression ) { + for ( TableReference usedTableReference : ( (CaseStatementDiscriminatorMappingImpl.CaseStatementDiscriminatorExpression) expression ).getUsedTableReferences() ) { + usedTableReference.accept( this ); + } + } + else { + super.visitSelfRenderingExpression( expression ); + } + } + + @Override + public void visitNamedTableReference(NamedTableReference tableReference) { + checkQualifier( tableReference.getIdentificationVariable() ); + } + + @Override + public void visitColumnReference(ColumnReference columnReference) { + checkQualifier( columnReference.getQualifier() ); + } + + @Override + public void visitAggregateColumnWriteExpression(AggregateColumnWriteExpression aggregateColumnWriteExpression) { + checkQualifier( aggregateColumnWriteExpression.getAggregateColumnReference().getQualifier() ); + } + + @Override + public void visitFilterPredicate(FilterPredicate filterPredicate) { + for ( FilterPredicate.FilterFragmentPredicate fragment : filterPredicate.getFragments() ) { + visitFilterFragmentPredicate( fragment ); + } + } + + @Override + public void visitFilterFragmentPredicate(FilterPredicate.FilterFragmentPredicate fragmentPredicate) { + checkSql( fragmentPredicate.getSqlFragment() ); + } + + @Override + public void visitSqlFragmentPredicate(SqlFragmentPredicate predicate) { + checkSql( predicate.getSqlFragment() ); + } + + @Override + public void visitColumnWriteFragment(ColumnWriteFragment columnWriteFragment) { + checkSql( columnWriteFragment.getFragment() ); + } +} diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java index 9a9c79266a0e..a517fa0b6980 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java @@ -89,6 +89,7 @@ import org.hibernate.sql.ast.SqlAstNodeRenderingMode; import org.hibernate.sql.ast.SqlAstTranslator; import org.hibernate.sql.ast.SqlTreeCreationException; +import org.hibernate.sql.ast.internal.TableGroupHelper; import org.hibernate.sql.ast.internal.ParameterMarkerStrategyStandard; import org.hibernate.sql.ast.tree.AbstractUpdateOrDeleteStatement; import org.hibernate.sql.ast.tree.MutationStatement; @@ -1204,7 +1205,7 @@ protected void renderSetClause(List assignments) { protected void visitSetAssignment(Assignment assignment) { final List columnReferences = assignment.getAssignable().getColumnReferences(); if ( columnReferences.size() == 1 ) { - columnReferences.get( 0 ).appendColumnForWrite( this, null ); + appendAssignmentColumn( columnReferences.get( 0 ) ); appendSql( '=' ); final Expression assignedValue = assignment.getAssignedValue(); final SqlTuple sqlTuple = SqlTupleContainer.getSqlTuple( assignedValue ); @@ -1220,7 +1221,7 @@ protected void visitSetAssignment(Assignment assignment) { char separator = OPEN_PARENTHESIS; for ( ColumnReference columnReference : columnReferences ) { appendSql( separator ); - columnReference.appendColumnForWrite( this, null ); + appendAssignmentColumn( columnReference ); separator = COMMA_SEPARATOR_CHAR; } appendSql( ")=" ); @@ -1228,6 +1229,10 @@ protected void visitSetAssignment(Assignment assignment) { } } + protected void appendAssignmentColumn(ColumnReference column) { + column.appendColumnForWrite( this, null ); + } + protected void visitSetAssignmentEmulateJoin(Assignment assignment, UpdateStatement statement) { final List columnReferences = assignment.getAssignable().getColumnReferences(); final Expression valueExpression; @@ -4680,43 +4685,6 @@ protected void renderTopStartAtClause( } } - protected void renderRowsToClause(QuerySpec querySpec) { - if ( querySpec.isRoot() && hasLimit() ) { - prepareLimitOffsetParameters(); - renderRowsToClause( getOffsetParameter(), getLimitParameter() ); - } - else { - assertRowsOnlyFetchClauseType( querySpec ); - renderRowsToClause( querySpec.getOffsetClauseExpression(), querySpec.getFetchClauseExpression() ); - } - } - - protected void renderRowsToClause(Expression offsetClauseExpression, Expression fetchClauseExpression) { - if ( fetchClauseExpression != null ) { - appendSql( "rows " ); - final Stack clauseStack = getClauseStack(); - clauseStack.push( Clause.FETCH ); - try { - renderFetchExpression( fetchClauseExpression ); - } - finally { - clauseStack.pop(); - } - if ( offsetClauseExpression != null ) { - clauseStack.push( Clause.OFFSET ); - try { - appendSql( " to " ); - // According to RowsLimitHandler this is 1 based so we need to add 1 to the offset - renderFetchPlusOffsetExpression( fetchClauseExpression, offsetClauseExpression, 1 ); - } - finally { - clauseStack.pop(); - } - } - appendSql( WHITESPACE ); - } - } - protected void renderFetchPlusOffsetExpression( Expression fetchClauseExpression, Expression offsetClauseExpression, @@ -4776,38 +4744,23 @@ protected void renderFetchPlusOffsetExpressionAsSingleParameter( appendSql( PARAM_MARKER ); final JdbcParameter offsetParameter = (JdbcParameter) offsetClauseExpression; final JdbcParameter fetchParameter = (JdbcParameter) fetchClauseExpression; - final OffsetReceivingParameterBinder fetchBinder = new OffsetReceivingParameterBinder( + final FetchPlusOffsetParameterBinder fetchBinder = new FetchPlusOffsetParameterBinder( offsetParameter, fetchParameter, offset ); - // We don't register and bind the special OffsetJdbcParameter as that comes from the query options - // And in this case, we only want to bind a single JDBC parameter - if ( !( offsetParameter instanceof OffsetJdbcParameter ) ) { - jdbcParameters.addParameter( offsetParameter ); - parameterBinders.add( - (statement, startPosition, jdbcParameterBindings, executionContext) -> { - final JdbcParameterBinding binding = jdbcParameterBindings.getBinding( offsetParameter ); - if ( binding == null ) { - throw new ExecutionException( "JDBC parameter value not bound - " + offsetParameter ); - } - fetchBinder.dynamicOffset = (Number) binding.getBindValue(); - } - ); - } jdbcParameters.addParameter( fetchParameter ); parameterBinders.add( fetchBinder ); } } - private static class OffsetReceivingParameterBinder implements JdbcParameterBinder { + private static class FetchPlusOffsetParameterBinder implements JdbcParameterBinder { private final JdbcParameter offsetParameter; private final JdbcParameter fetchParameter; private final int staticOffset; - private Number dynamicOffset; - public OffsetReceivingParameterBinder( + public FetchPlusOffsetParameterBinder( JdbcParameter offsetParameter, JdbcParameter fetchParameter, int staticOffset) { @@ -4838,13 +4791,16 @@ public void bindParameterValue( offsetValue = executionContext.getQueryOptions().getEffectiveLimit().getFirstRow(); } else { - offsetValue = dynamicOffset.intValue() + staticOffset; - dynamicOffset = null; + final JdbcParameterBinding binding = jdbcParameterBindings.getBinding( offsetParameter ); + if ( binding == null ) { + throw new ExecutionException( "JDBC parameter value not bound - " + offsetParameter ); + } + offsetValue = ((Number) binding.getBindValue()).intValue(); } //noinspection unchecked fetchParameter.getExpressionType().getSingleJdbcMapping().getJdbcValueBinder().bind( statement, - bindValue.intValue() + offsetValue, + bindValue.intValue() + offsetValue + staticOffset, startPosition, executionContext.getSession() ); @@ -6035,10 +5991,60 @@ protected void renderRootTableGroup(TableGroup tableGroup, List } protected void renderTableGroup(TableGroup tableGroup, Predicate predicate, List tableGroupJoinCollector) { - // Without reference joins or nested join groups, even a real table group does not need parenthesis - final boolean realTableGroup = tableGroup.isRealTableGroup() - && ( CollectionHelper.isNotEmpty( tableGroup.getTableReferenceJoins() ) - || hasNestedTableGroupsToRender( tableGroup.getNestedTableGroupJoins() ) ); + final boolean realTableGroup; + int swappedJoinIndex = -1; + boolean forceLeftJoin = false; + if ( tableGroup.isRealTableGroup() ) { + if ( hasNestedTableGroupsToRender( tableGroup.getNestedTableGroupJoins() ) ) { + // If there are nested table groups, we need to render a real table group + realTableGroup = true; + } + else { + // Determine the reference join indexes of the table reference used in the predicate + final int referenceJoinIndexForPredicateSwap = TableGroupHelper.findReferenceJoinForPredicateSwap( + tableGroup, + predicate + ); + if ( referenceJoinIndexForPredicateSwap == TableGroupHelper.REAL_TABLE_GROUP_REQUIRED ) { + // Means that real table group rendering is necessary + realTableGroup = true; + } + else if ( referenceJoinIndexForPredicateSwap == TableGroupHelper.NO_TABLE_GROUP_REQUIRED ) { + // Means that no swap is necessary to avoid the table group rendering + realTableGroup = false; + forceLeftJoin = !tableGroup.canUseInnerJoins(); + } + else { + // Means that real table group rendering can be avoided if the primary table reference is swapped + // with the table reference join at the given index + realTableGroup = false; + forceLeftJoin = !tableGroup.canUseInnerJoins(); + swappedJoinIndex = referenceJoinIndexForPredicateSwap; + + // Render the table reference of the table reference join first + final TableReferenceJoin tableReferenceJoin = tableGroup.getTableReferenceJoins().get( swappedJoinIndex ); + renderNamedTableReference( tableReferenceJoin.getJoinedTableReference(), LockMode.NONE ); + // along with the predicate for the table group + if ( predicate != null ) { + appendSql( " on " ); + predicate.accept( this ); + } + + // Then render the join syntax and fall through to rendering the primary table reference + appendSql( WHITESPACE ); + if ( tableGroup.canUseInnerJoins() ) { + appendSql( tableReferenceJoin.getJoinType().getText() ); + } + else { + append( "left " ); + } + appendSql( "join " ); + } + } + } + else { + realTableGroup = false; + } if ( realTableGroup ) { appendSql( OPEN_PARENTHESIS ); } @@ -6066,7 +6072,8 @@ protected void renderTableGroup(TableGroup tableGroup, Predicate predicate, List tableGroupJoins = null; } - if ( predicate != null ) { + // Predicate was already rendered when swappedJoinIndex is not equal to -1 + if ( predicate != null && swappedJoinIndex == -1 ) { appendSql( " on " ); predicate.accept( this ); } @@ -6084,7 +6091,7 @@ protected void renderTableGroup(TableGroup tableGroup, Predicate predicate, List } if ( !realTableGroup ) { - renderTableReferenceJoins( tableGroup ); + renderTableReferenceJoins( tableGroup, swappedJoinIndex, forceLeftJoin ); processNestedTableGroupJoins( tableGroup, tableGroupJoinCollector ); } if ( tableGroupJoinCollector != null ) { @@ -6363,21 +6370,43 @@ protected void registerAffectedTable(String tableExpression) { } protected void renderTableReferenceJoins(TableGroup tableGroup) { + renderTableReferenceJoins( tableGroup, -1, false ); + } + + protected void renderTableReferenceJoins(TableGroup tableGroup, int swappedJoinIndex, boolean forceLeftJoin) { final List joins = tableGroup.getTableReferenceJoins(); if ( joins == null || joins.isEmpty() ) { return; } - for ( TableReferenceJoin tableJoin : joins ) { - appendSql( WHITESPACE ); - appendSql( tableJoin.getJoinType().getText() ); - appendSql( "join " ); + if ( swappedJoinIndex != -1 ) { + // Finish the join against the primary table reference after the swap + final TableReferenceJoin swappedJoin = joins.get( swappedJoinIndex ); + if ( swappedJoin.getPredicate() != null && !swappedJoin.getPredicate().isEmpty() ) { + appendSql( " on " ); + swappedJoin.getPredicate().accept( this ); + } + } - renderNamedTableReference( tableJoin.getJoinedTableReference(), LockMode.NONE ); + for ( int i = 0; i < joins.size(); i++ ) { + // Skip the swapped join since it was already rendered + if ( swappedJoinIndex != i ) { + final TableReferenceJoin tableJoin = joins.get( i ); + appendSql( WHITESPACE ); + if ( forceLeftJoin ) { + append( "left " ); + } + else { + appendSql( tableJoin.getJoinType().getText() ); + } + appendSql( "join " ); - if ( tableJoin.getPredicate() != null && !tableJoin.getPredicate().isEmpty() ) { - appendSql( " on " ); - tableJoin.getPredicate().accept( this ); + renderNamedTableReference( tableJoin.getJoinedTableReference(), LockMode.NONE ); + + if ( tableJoin.getPredicate() != null && !tableJoin.getPredicate().isEmpty() ) { + appendSql( " on " ); + tableJoin.getPredicate().accept( this ); + } } } } @@ -6779,9 +6808,12 @@ private int getSortSelectionIndex(QuerySpec querySpec, SortSpecification sortSpe private boolean isFetchFirstRowOnly(QueryPart queryPart) { return queryPart.getFetchClauseType() == FetchClauseType.ROWS_ONLY - && queryPart.getFetchClauseExpression() instanceof QueryLiteral - && Integer.valueOf( 1 ) - .equals( ( (QueryLiteral) queryPart.getFetchClauseExpression() ).getLiteralValue() ); + && queryPart.getFetchClauseExpression() != null + && Integer.valueOf( 1 ).equals( getLiteralValue( queryPart.getFetchClauseExpression() ) ); + } + + public X getLiteralValue(Expression expression) { + return interpretExpression( expression, jdbcParameterBindings ); } private SelectStatement stripToSelectClause(SelectStatement statement) { @@ -7666,14 +7698,23 @@ public void visitInListPredicate(InListPredicate inListPredicate) { if ( ( lhsTuple = SqlTupleContainer.getSqlTuple( inListPredicate.getTestExpression() ) ) != null ) { if ( lhsTuple.getExpressions().size() == 1 ) { // Special case for tuples with arity 1 as any DBMS supports scalar IN predicates - itemAccessor = listExpression -> SqlTupleContainer.getSqlTuple( listExpression ).getExpressions().get( 0 ); + if ( SqlTupleContainer.getSqlTuple( listExpressions.get( 0 ) ) != null ) { + itemAccessor = listExpression -> SqlTupleContainer.getSqlTuple( listExpression ).getExpressions().get( 0 ); + } } else if ( !supportsRowValueConstructorSyntaxInInList() ) { - final ComparisonOperator comparisonOperator = inListPredicate.isNegated() ? - ComparisonOperator.NOT_EQUAL : - ComparisonOperator.EQUAL; // Some DBs like Oracle support tuples only for the IN subquery predicate - if ( supportsRowValueConstructorSyntaxInInSubQuery() && dialect.supportsUnionAll() ) { + if ( supportsRowValueConstructorSyntaxInInSubQuery() && dialect.supportsValuesList() ) { + inListPredicate.getTestExpression().accept( this ); + if ( inListPredicate.isNegated() ) { + appendSql( " not" ); + } + appendSql( " in (" ); + renderExpressionsAsValuesSubquery( lhsTuple.getExpressionType().getJdbcTypeCount(), listExpressions ); + appendSql( CLOSE_PARENTHESIS ); + } + else if ( supportsRowValueConstructorSyntaxInInSubQuery() && dialect.supportsUnionAll() + && preferUnionQueryForTupleInListPredicate() ) { inListPredicate.getTestExpression().accept( this ); if ( inListPredicate.isNegated() ) { appendSql( " not" ); @@ -7690,17 +7731,21 @@ else if ( !supportsRowValueConstructorSyntaxInInList() ) { appendSql( CLOSE_PARENTHESIS ); } else { - String separator = NO_SEPARATOR; + final ComparisonOperator tupleComparisonOperator = inListPredicate.isNegated() ? + ComparisonOperator.NOT_EQUAL : + ComparisonOperator.EQUAL; + final String expressionJunction = inListPredicate.isNegated() ? " and " : " or "; appendSql( OPEN_PARENTHESIS ); - for ( Expression expression : listExpressions ) { - appendSql( separator ); + String separator = NO_SEPARATOR; + for (Expression expression : listExpressions) { + appendSql(separator); emulateTupleComparison( lhsTuple.getExpressions(), - SqlTupleContainer.getSqlTuple( expression ).getExpressions(), - comparisonOperator, + SqlTupleContainer.getSqlTuple(expression).getExpressions(), + tupleComparisonOperator, true ); - separator = " or "; + separator = expressionJunction; } appendSql( CLOSE_PARENTHESIS ); } @@ -7765,6 +7810,40 @@ else if ( !supportsRowValueConstructorSyntaxInInList() ) { } } + protected boolean preferUnionQueryForTupleInListPredicate() { + return true; + } + + protected void renderExpressionsAsValuesSubquery(int tupleSize, List listExpressions) { + appendSql( "select" ); + char separator = ' '; + for ( int i = 0; i < tupleSize; i++ ) { + appendSql( separator ); + appendSql( "v_.c" ); + appendSql( i ); + separator = ','; + } + appendSql( " from (values" ); + separator = ' '; + for ( Expression expression : listExpressions ) { + appendSql( separator ); + appendSql( OPEN_PARENTHESIS ); + renderCommaSeparated( SqlTupleContainer.getSqlTuple( expression ).getExpressions() ); + appendSql( CLOSE_PARENTHESIS ); + separator = ','; + } + appendSql( CLOSE_PARENTHESIS ); + appendSql( " v_" ); + separator = '('; + for ( int i = 0; i < tupleSize; i++ ) { + appendSql( separator ); + appendSql( "c" ); + appendSql( i ); + separator = ','; + } + appendSql( CLOSE_PARENTHESIS ); + } + private void appendInClauseSeparator(InListPredicate inListPredicate) { appendSql( CLOSE_PARENTHESIS ); appendSql( inListPredicate.isNegated() ? " and " : " or " ); @@ -8286,11 +8365,12 @@ else if ( rhsExpression instanceof Any ) { final ComparisonOperator operator = comparisonPredicate.getOperator(); if ( lhsTuple.getExpressions().size() == 1 ) { // Special case for tuples with arity 1 as any DBMS supports scalar IN predicates - if ( subquery == null ) { + if ( subquery == null && (rhsTuple = SqlTupleContainer.getSqlTuple( + comparisonPredicate.getRightHandExpression() )) != null ) { renderComparison( lhsTuple.getExpressions().get( 0 ), operator, - SqlTupleContainer.getSqlTuple( comparisonPredicate.getRightHandExpression() ).getExpressions().get( 0 ) + rhsTuple.getExpressions().get( 0 ) ); } else { @@ -8570,11 +8650,11 @@ protected String getFromDual() { * @return the SQL equivalent to Oracle's {@code dual}. */ protected String getDual() { - return "(values(0))"; + return dialect.getDual(); } protected String getFromDualForSelectOnly() { - return ""; + return dialect.getFromDualForSelectOnly(); } protected enum LockStrategy { diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/SqlAstCreationState.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/SqlAstCreationState.java index 8d3bfe0d23b6..661c9c968754 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/SqlAstCreationState.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/SqlAstCreationState.java @@ -9,6 +9,7 @@ import org.hibernate.Internal; import org.hibernate.LockMode; import org.hibernate.engine.spi.LoadQueryInfluencers; +import org.hibernate.metamodel.mapping.ordering.OrderByFragment; import org.hibernate.persister.entity.EntityNameUse; import org.hibernate.sql.ast.tree.from.TableGroup; @@ -49,4 +50,12 @@ default void registerEntityNameUsage( default boolean supportsEntityNameUsage() { return false; } + + @Internal + default void applyOrdering(TableGroup tableGroup, OrderByFragment orderByFragment) { + } + + default boolean isProcedureOrNativeQuery(){ + return false; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/MutatingTableReferenceGroupWrapper.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/MutatingTableReferenceGroupWrapper.java index efbaeeeda9af..a531d83b5b5c 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/MutatingTableReferenceGroupWrapper.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/MutatingTableReferenceGroupWrapper.java @@ -64,14 +64,12 @@ public TableReference getTableReference( NavigablePath navigablePath, String tableExpression, boolean resolve) { - return mutatingTableReference.getTableExpression().equals( tableExpression ) - ? mutatingTableReference - : null; + return mutatingTableReference.getTableReference( tableExpression ); } @Override public void applyAffectedTableNames(Consumer nameCollector) { - nameCollector.accept( mutatingTableReference.getTableExpression() ); + mutatingTableReference.applyAffectedTableNames( nameCollector); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/OneToManyTableGroup.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/OneToManyTableGroup.java index 45b250520c04..5d5b06ef8480 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/OneToManyTableGroup.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/OneToManyTableGroup.java @@ -63,9 +63,18 @@ public TableGroup getIndexTableGroup() { } public void registerIndexTableGroup(TableGroupJoin indexTableGroupJoin) { + registerIndexTableGroup( indexTableGroupJoin, true ); + } + + public void registerIndexTableGroup(TableGroupJoin indexTableGroupJoin, boolean nested) { assert this.indexTableGroup == null; this.indexTableGroup = indexTableGroupJoin.getJoinedGroup(); - addNestedTableGroupJoin( indexTableGroupJoin ); + if ( nested ) { + addNestedTableGroupJoin( indexTableGroupJoin ); + } + else { + addTableGroupJoin( indexTableGroupJoin ); + } } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/TableGroupJoinProducer.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/TableGroupJoinProducer.java index 80c5a392f01e..d2c795232d6b 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/TableGroupJoinProducer.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/tree/from/TableGroupJoinProducer.java @@ -12,10 +12,11 @@ import org.hibernate.sql.ast.SqlAstJoinType; import org.hibernate.sql.ast.spi.SqlAliasBase; import org.hibernate.sql.ast.spi.SqlAliasBaseGenerator; -import org.hibernate.sql.ast.spi.SqlAstCreationContext; import org.hibernate.sql.ast.spi.SqlAstCreationState; import org.hibernate.sql.ast.tree.predicate.Predicate; +import org.checkerframework.checker.nullness.qual.Nullable; + /** * @author Steve Ebersole */ @@ -41,9 +42,9 @@ public interface TableGroupJoinProducer extends TableGroupProducer { TableGroupJoin createTableGroupJoin( NavigablePath navigablePath, TableGroup lhs, - String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, - SqlAstJoinType sqlAstJoinType, + @Nullable String explicitSourceAlias, + @Nullable SqlAliasBase explicitSqlAliasBase, + @Nullable SqlAstJoinType sqlAstJoinType, boolean fetched, boolean addsPredicate, SqlAstCreationState creationState); @@ -70,14 +71,14 @@ TableGroupJoin createTableGroupJoin( TableGroup createRootTableGroupJoin( NavigablePath navigablePath, TableGroup lhs, - String explicitSourceAlias, - SqlAliasBase explicitSqlAliasBase, - SqlAstJoinType sqlAstJoinType, + @Nullable String explicitSourceAlias, + @Nullable SqlAliasBase explicitSqlAliasBase, + @Nullable SqlAstJoinType sqlAstJoinType, boolean fetched, - Consumer predicateConsumer, + @Nullable Consumer predicateConsumer, SqlAstCreationState creationState); - default SqlAstJoinType determineSqlJoinType(TableGroup lhs, SqlAstJoinType requestedJoinType, boolean fetched) { + default SqlAstJoinType determineSqlJoinType(TableGroup lhs, @Nullable SqlAstJoinType requestedJoinType, boolean fetched) { if ( requestedJoinType != null ) { return requestedJoinType; } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/AbstractJdbcParameter.java b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/AbstractJdbcParameter.java index aa8e1d668365..dea2c41ed899 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/AbstractJdbcParameter.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/exec/internal/AbstractJdbcParameter.java @@ -18,7 +18,6 @@ import org.hibernate.metamodel.mapping.MappingModelExpressible; import org.hibernate.metamodel.mapping.MappingType; import org.hibernate.metamodel.mapping.SqlExpressible; -import org.hibernate.metamodel.model.domain.internal.BasicTypeImpl; import org.hibernate.query.BindableType; import org.hibernate.sql.ast.SqlAstWalker; import org.hibernate.sql.ast.tree.expression.JdbcParameter; @@ -27,6 +26,7 @@ import org.hibernate.sql.exec.spi.JdbcParameterBinder; import org.hibernate.sql.exec.spi.JdbcParameterBinding; import org.hibernate.sql.exec.spi.JdbcParameterBindings; +import org.hibernate.type.BasicType; import org.hibernate.type.descriptor.java.EnumJavaType; import org.hibernate.type.descriptor.jdbc.JdbcType; import org.hibernate.type.descriptor.jdbc.JdbcTypeIndicators; @@ -135,7 +135,7 @@ private JdbcMapping guessBindType(ExecutionContext executionContext, Object bind } } - private static > BasicTypeImpl createEnumType(ExecutionContext executionContext, Class enumClass) { + private static > BasicType createEnumType(ExecutionContext executionContext, Class enumClass) { final EnumJavaType enumJavaType = new EnumJavaType<>( enumClass ); final JdbcTypeIndicators indicators = executionContext.getSession().getTypeConfiguration().getCurrentBaseSqlTypeIndicators(); @@ -144,7 +144,7 @@ private static > BasicTypeImpl createEnumType(ExecutionCont // so just accept the default from the TypeConfiguration, which // is usually ORDINAL (the default according to JPA) enumJavaType.getRecommendedJdbcType(indicators); - return new BasicTypeImpl<>( enumJavaType, jdbcType ); + return indicators.getTypeConfiguration().getBasicTypeRegistry().resolve( enumJavaType, jdbcType ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/sql/model/jdbc/DeleteOrUpsertOperation.java b/hibernate-core/src/main/java/org/hibernate/sql/model/jdbc/DeleteOrUpsertOperation.java index 2b629a707ff6..9c8ad687fcdf 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/model/jdbc/DeleteOrUpsertOperation.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/model/jdbc/DeleteOrUpsertOperation.java @@ -115,19 +115,25 @@ private void performDelete(JdbcValueBindings jdbcValueBindings, SharedSessionCon final PreparedStatementGroupSingleTable statementGroup = new PreparedStatementGroupSingleTable( upsertDelete, session ); final PreparedStatementDetails statementDetails = statementGroup.resolvePreparedStatementDetails( tableMapping.getTableName() ); - final PreparedStatement upsertDeleteStatement = statementDetails.resolveStatement(); - session.getJdbcServices().getSqlStatementLogger().logStatement( statementDetails.getSqlString() ); - - bindDeleteKeyValues( - jdbcValueBindings, - optionalTableUpdate.getParameters(), - statementDetails, - session - ); - final int rowCount = session.getJdbcCoordinator().getResultSetReturn() - .executeUpdate( upsertDeleteStatement, statementDetails.getSqlString() ); - MODEL_MUTATION_LOGGER.tracef( "`%s` rows upsert-deleted from `%s`", rowCount, tableMapping.getTableName() ); + try { + final PreparedStatement upsertDeleteStatement = statementDetails.resolveStatement(); + session.getJdbcServices().getSqlStatementLogger().logStatement( statementDetails.getSqlString() ); + + bindDeleteKeyValues( + jdbcValueBindings, + optionalTableUpdate.getParameters(), + statementDetails, + session + ); + + final int rowCount = session.getJdbcCoordinator().getResultSetReturn() + .executeUpdate( upsertDeleteStatement, statementDetails.getSqlString() ); + MODEL_MUTATION_LOGGER.tracef( "`%s` rows upsert-deleted from `%s`", rowCount, tableMapping.getTableName() ); + } + finally { + statementDetails.releaseStatement( session ); + } } private void bindDeleteKeyValues( @@ -189,14 +195,19 @@ private void performUpsert(JdbcValueBindings jdbcValueBindings, SharedSessionCon final PreparedStatementGroupSingleTable statementGroup = new PreparedStatementGroupSingleTable( upsertOperation, session ); final PreparedStatementDetails statementDetails = statementGroup.resolvePreparedStatementDetails( tableMapping.getTableName() ); - final PreparedStatement updateStatement = statementDetails.resolveStatement(); - session.getJdbcServices().getSqlStatementLogger().logStatement( statementDetails.getSqlString() ); + try { + final PreparedStatement updateStatement = statementDetails.resolveStatement(); + session.getJdbcServices().getSqlStatementLogger().logStatement( statementDetails.getSqlString() ); - jdbcValueBindings.beforeStatement( statementDetails ); + jdbcValueBindings.beforeStatement( statementDetails ); - final int rowCount = session.getJdbcCoordinator().getResultSetReturn() - .executeUpdate( updateStatement, statementDetails.getSqlString() ); + final int rowCount = session.getJdbcCoordinator().getResultSetReturn() + .executeUpdate( updateStatement, statementDetails.getSqlString() ); - MODEL_MUTATION_LOGGER.tracef( "`%s` rows upserted into `%s`", rowCount, tableMapping.getTableName() ); + MODEL_MUTATION_LOGGER.tracef( "`%s` rows upserted into `%s`", rowCount, tableMapping.getTableName() ); + } + finally { + statementDetails.releaseStatement( session ); + } } } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/model/jdbc/OptionalTableUpdateOperation.java b/hibernate-core/src/main/java/org/hibernate/sql/model/jdbc/OptionalTableUpdateOperation.java index 30d35748c942..d6f7f11b589d 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/model/jdbc/OptionalTableUpdateOperation.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/model/jdbc/OptionalTableUpdateOperation.java @@ -172,13 +172,20 @@ public void performMutation( private void performDelete(JdbcValueBindings jdbcValueBindings, SharedSessionContractImplementor session) { final JdbcDeleteMutation jdbcDelete = createJdbcDelete( session ); - final PreparedStatement deleteStatement = createStatementDetails( jdbcDelete, session ); + final JdbcCoordinator jdbcCoordinator = session.getJdbcCoordinator(); + final PreparedStatement deleteStatement = createStatementDetails( jdbcDelete, jdbcCoordinator ); session.getJdbcServices().getSqlStatementLogger().logStatement( jdbcDelete.getSqlString() ); bindKeyValues( jdbcValueBindings, deleteStatement, jdbcDelete, session ); - session.getJdbcCoordinator().getResultSetReturn() - .executeUpdate( deleteStatement, jdbcDelete.getSqlString() ); + try { + session.getJdbcCoordinator().getResultSetReturn() + .executeUpdate( deleteStatement, jdbcDelete.getSqlString() ); + } + finally { + jdbcCoordinator.getLogicalConnection().getResourceRegistry().release( deleteStatement ); + jdbcCoordinator.afterStatementExecution(); + } } private void bindKeyValues( @@ -374,12 +381,16 @@ private boolean performUpdate( statementDetails.getSqlString() ); } + finally { + statementDetails.releaseStatement( session ); + } } private void performInsert(JdbcValueBindings jdbcValueBindings, SharedSessionContractImplementor session) { final JdbcInsertMutation jdbcInsert = createJdbcInsert( session ); - final PreparedStatement insertStatement = createStatementDetails( jdbcInsert, session ); + final JdbcCoordinator jdbcCoordinator = session.getJdbcCoordinator(); + final PreparedStatement insertStatement = createStatementDetails( jdbcInsert, jdbcCoordinator ); try { session.getJdbcServices().getSqlStatementLogger().logStatement( jdbcInsert.getSqlString() ); @@ -408,11 +419,12 @@ private void performInsert(JdbcValueBindings jdbcValueBindings, SharedSessionCon } ); } - session.getJdbcCoordinator().getResultSetReturn() + jdbcCoordinator.getResultSetReturn() .executeUpdate( insertStatement, jdbcInsert.getSqlString() ); } finally { - session.getJdbcCoordinator().getLogicalConnection().getResourceRegistry().release( insertStatement ); + jdbcCoordinator.getLogicalConnection().getResourceRegistry().release( insertStatement ); + jdbcCoordinator.afterStatementExecution(); } } @@ -453,11 +465,10 @@ private JdbcInsertMutation createJdbcInsert(SharedSessionContractImplementor ses private static PreparedStatement createStatementDetails( PreparableMutationOperation operation, - SharedSessionContractImplementor session) { - final JdbcCoordinator jdbcCoordinator = session.getJdbcCoordinator(); + JdbcCoordinator jdbcCoordinator) { final MutationStatementPreparer statementPreparer = jdbcCoordinator.getMutationStatementPreparer(); final PreparedStatement statement = statementPreparer.prepareStatement( operation.getSqlString(), false ); - session.getJdbcCoordinator().getLogicalConnection().getResourceRegistry().register( null, statement ); + jdbcCoordinator.getLogicalConnection().getResourceRegistry().register( null, statement ); return statement; } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/InitializerParent.java b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/InitializerParent.java index b10f8741239b..df2fd0f42ebc 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/InitializerParent.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/InitializerParent.java @@ -6,6 +6,9 @@ */ package org.hibernate.sql.results.graph; + +import org.hibernate.Hibernate; + /** * Provides access to information about the owner/parent of a fetch * in relation to the current "row" being processed. @@ -13,5 +16,7 @@ * @author Steve Ebersole */ public interface InitializerParent extends Initializer { - + default Object getResolvedInstanceNoProxy(Data data){ + return Hibernate.unproxy( getResolvedInstance( data ) ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/collection/internal/ArrayInitializer.java b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/collection/internal/ArrayInitializer.java index 413221ae7c02..1af81b9fc668 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/collection/internal/ArrayInitializer.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/collection/internal/ArrayInitializer.java @@ -6,6 +6,7 @@ */ package org.hibernate.sql.results.graph.collection.internal; +import java.lang.reflect.Array; import java.util.Iterator; import java.util.List; import java.util.function.BiConsumer; @@ -131,9 +132,14 @@ protected void resolveInstanceSubInitializers(ImmediateCollectionInitializerData final Initializer initializer = elementAssembler.getInitializer(); if ( initializer != null ) { final RowProcessingState rowProcessingState = data.getRowProcessingState(); - final Iterator iter = getCollectionInstance( data ).elements(); - while ( iter.hasNext() ) { - initializer.resolveInstance( iter.next(), rowProcessingState ); + Integer index = listIndexAssembler.assemble( rowProcessingState ); + if ( index != null ) { + final PersistentArrayHolder arrayHolder = getCollectionInstance( data ); + assert arrayHolder != null; + if ( indexBase != 0 ) { + index -= indexBase; + } + initializer.resolveInstance( Array.get( arrayHolder.getArray(), index ), rowProcessingState ); } } } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/collection/internal/BagInitializer.java b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/collection/internal/BagInitializer.java index 00e207c4d4a9..b73643cd4711 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/collection/internal/BagInitializer.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/collection/internal/BagInitializer.java @@ -123,19 +123,7 @@ protected void initializeSubInstancesFromParent(ImmediateCollectionInitializerDa protected void resolveInstanceSubInitializers(ImmediateCollectionInitializerData data) { final Initializer initializer = elementAssembler.getInitializer(); if ( initializer != null ) { - final RowProcessingState rowProcessingState = data.getRowProcessingState(); - final PersistentCollection persistentCollection = getCollectionInstance( data ); - assert persistentCollection != null; - if ( persistentCollection instanceof PersistentBag ) { - for ( Object element : ( (PersistentBag) persistentCollection ) ) { - initializer.resolveInstance( element, rowProcessingState ); - } - } - else { - for ( Object element : ( (PersistentIdentifierBag) persistentCollection ) ) { - initializer.resolveInstance( element, rowProcessingState ); - } - } + initializer.resolveKey( data.getRowProcessingState() ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/collection/internal/ListInitializer.java b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/collection/internal/ListInitializer.java index 0850a0927df4..4ab0d800e2d5 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/collection/internal/ListInitializer.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/collection/internal/ListInitializer.java @@ -123,10 +123,14 @@ protected void resolveInstanceSubInitializers(ImmediateCollectionInitializerData final Initializer initializer = elementAssembler.getInitializer(); if ( initializer != null ) { final RowProcessingState rowProcessingState = data.getRowProcessingState(); - final PersistentList list = getCollectionInstance( data ); - assert list != null; - for ( Object element : list ) { - initializer.resolveInstance( element, rowProcessingState ); + Integer index = listIndexAssembler.assemble( rowProcessingState ); + if ( index != null ) { + final PersistentList list = getCollectionInstance( data ); + assert list != null; + if ( listIndexBase != 0 ) { + index -= listIndexBase; + } + initializer.resolveInstance( list.get( index ), rowProcessingState ); } } } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/collection/internal/MapInitializer.java b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/collection/internal/MapInitializer.java index 8c4f36185f31..49c21cc96579 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/collection/internal/MapInitializer.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/collection/internal/MapInitializer.java @@ -121,17 +121,27 @@ protected void initializeSubInstancesFromParent(ImmediateCollectionInitializerDa protected void resolveInstanceSubInitializers(ImmediateCollectionInitializerData data) { final Initializer keyInitializer = mapKeyAssembler.getInitializer(); final Initializer valueInitializer = mapValueAssembler.getInitializer(); - if ( keyInitializer != null || valueInitializer != null ) { - final RowProcessingState rowProcessingState = data.getRowProcessingState(); - final PersistentMap map = getCollectionInstance( data ); - assert map != null; - for ( Map.Entry entry : map.entrySet() ) { - if ( keyInitializer != null ) { - keyInitializer.resolveInstance( entry.getKey(), rowProcessingState ); - } - if ( valueInitializer != null ) { - valueInitializer.resolveInstance( entry.getValue(), rowProcessingState ); - } + final RowProcessingState rowProcessingState = data.getRowProcessingState(); + if ( keyInitializer == null && valueInitializer != null ) { + // For now, we only support resolving the value initializer instance when keys have no initializer, + // though we could also support map keys with an initializer given that the initialized java type: + // * is an entity that uses only the primary key in equals/hashCode. + // If the primary key type is an embeddable, the next condition must hold for that + // * or is an embeddable that has no initializers for fields being used in the equals/hashCode + // which violate this same requirement (recursion) + final Object key = mapKeyAssembler.assemble( rowProcessingState ); + if ( key != null ) { + final PersistentMap map = getCollectionInstance( data ); + assert map != null; + valueInitializer.resolveInstance( map.get( key ), rowProcessingState ); + } + } + else { + if ( keyInitializer != null ) { + keyInitializer.resolveKey( rowProcessingState ); + } + if ( valueInitializer != null ) { + valueInitializer.resolveKey( rowProcessingState ); } } } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/collection/internal/SetInitializer.java b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/collection/internal/SetInitializer.java index b2b86d88cc17..30909c6ce02d 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/collection/internal/SetInitializer.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/collection/internal/SetInitializer.java @@ -97,12 +97,7 @@ protected void initializeSubInstancesFromParent(ImmediateCollectionInitializerDa protected void resolveInstanceSubInitializers(ImmediateCollectionInitializerData data) { final Initializer initializer = elementAssembler.getInitializer(); if ( initializer != null ) { - final RowProcessingState rowProcessingState = data.getRowProcessingState(); - final PersistentSet set = getCollectionInstance( data ); - assert set != null; - for ( Object element : set ) { - initializer.resolveInstance( element, rowProcessingState ); - } + initializer.resolveKey( data.getRowProcessingState() ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/AggregateEmbeddableInitializerImpl.java b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/AggregateEmbeddableInitializerImpl.java index 194e9bc539c9..a9e45cdd5935 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/AggregateEmbeddableInitializerImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/AggregateEmbeddableInitializerImpl.java @@ -28,7 +28,7 @@ public AggregateEmbeddableInitializerImpl( InitializerParent parent, AssemblerCreationState creationState, boolean isResultInitializer) { - super( resultDescriptor, discriminatorFetch, parent, creationState, isResultInitializer ); + super( resultDescriptor, discriminatorFetch, null, parent, creationState, isResultInitializer ); this.aggregateValuesArrayPositions = resultDescriptor.getAggregateValuesArrayPositions(); } @@ -37,6 +37,17 @@ public void startLoading(RowProcessingState rowProcessingState) { super.startLoading( NestedRowProcessingState.wrap( this, rowProcessingState ) ); } + @Override + protected void extractRowState(EmbeddableInitializerData data) { + super.extractRowState( data ); + if ( data.getState() == State.MISSING + && !isPartOfKey() + && getJdbcValues( data.getRowProcessingState().unwrap() ) != null ) { + // When all values are null, the embeddable shall be non-null if the JDBC object is not null + data.setState( State.RESOLVED ); + } + } + public int[] getAggregateValuesArrayPositions() { return aggregateValuesArrayPositions; } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/EmbeddableAssembler.java b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/EmbeddableAssembler.java index fc4131f01f85..da4959b2405f 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/EmbeddableAssembler.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/EmbeddableAssembler.java @@ -34,6 +34,9 @@ public JavaType getAssembledJavaType() { public Object assemble(RowProcessingState rowProcessingState) { final InitializerData data = initializer.getData( rowProcessingState ); final Initializer.State state = data.getState(); + if ( state == Initializer.State.UNINITIALIZED ) { + initializer.resolveKey( data ); + } if ( state == Initializer.State.KEY_RESOLVED ) { initializer.resolveInstance( data ); } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/EmbeddableExpressionResultImpl.java b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/EmbeddableExpressionResultImpl.java index 41e19ecd85c7..e6fa8e1e5208 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/EmbeddableExpressionResultImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/EmbeddableExpressionResultImpl.java @@ -141,6 +141,6 @@ public Initializer createInitializer( @Override public Initializer createInitializer(InitializerParent parent, AssemblerCreationState creationState) { - return new EmbeddableInitializerImpl( this, null, parent, creationState, true ); + return new EmbeddableInitializerImpl( this, null, null, parent, creationState, true ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/EmbeddableFetchImpl.java b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/EmbeddableFetchImpl.java index fb358a198210..1b7c35034099 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/EmbeddableFetchImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/EmbeddableFetchImpl.java @@ -6,6 +6,7 @@ */ package org.hibernate.sql.results.graph.embeddable.internal; +import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.engine.FetchTiming; import org.hibernate.graph.spi.GraphHelper; import org.hibernate.graph.spi.GraphImplementor; @@ -14,11 +15,15 @@ import org.hibernate.metamodel.model.domain.JpaMetamodel; import org.hibernate.spi.NavigablePath; import org.hibernate.sql.ast.SqlAstJoinType; +import org.hibernate.sql.ast.tree.expression.Expression; 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.NullnessPredicate; import org.hibernate.sql.results.graph.AbstractFetchParent; import org.hibernate.sql.results.graph.AssemblerCreationState; +import org.hibernate.sql.results.graph.DomainResult; import org.hibernate.sql.results.graph.DomainResultAssembler; import org.hibernate.sql.results.graph.DomainResultCreationState; import org.hibernate.sql.results.graph.Fetch; @@ -31,6 +36,7 @@ import org.hibernate.sql.results.graph.embeddable.EmbeddableInitializer; import org.hibernate.sql.results.graph.embeddable.EmbeddableResultGraphNode; import org.hibernate.sql.results.graph.embeddable.EmbeddableValuedFetchable; +import org.hibernate.type.BasicType; import static org.hibernate.internal.util.NullnessUtil.castNonNull; @@ -46,6 +52,7 @@ public class EmbeddableFetchImpl extends AbstractFetchParent private final boolean hasTableGroup; private final EmbeddableMappingType fetchContainer; private final BasicFetch discriminatorFetch; + private final @Nullable DomainResult nullIndicatorResult; public EmbeddableFetchImpl( NavigablePath navigablePath, @@ -83,6 +90,19 @@ public EmbeddableFetchImpl( ); this.discriminatorFetch = creationState.visitEmbeddableDiscriminatorFetch( this, false ); + if ( fetchContainer.getAggregateMapping() != null ) { + final TableReference tableReference = tableGroup.resolveTableReference( + fetchContainer.getAggregateMapping().getContainingTableExpression() ); + final Expression aggregateExpression = creationState.getSqlAstCreationState().getSqlExpressionResolver() + .resolveSqlExpression( tableReference, fetchContainer.getAggregateMapping() ); + final BasicType booleanType = creationState.getSqlAstCreationState().getCreationContext() + .getSessionFactory().getTypeConfiguration().getBasicTypeForJavaType( Boolean.class ); + this.nullIndicatorResult = new NullnessPredicate( aggregateExpression, false, booleanType ) + .createDomainResult( null, creationState ); + } + else { + this.nullIndicatorResult = null; + } afterInitialize( this, creationState ); } @@ -98,6 +118,7 @@ protected EmbeddableFetchImpl(EmbeddableFetchImpl original) { tableGroup = original.tableGroup; hasTableGroup = original.hasTableGroup; discriminatorFetch = original.discriminatorFetch; + nullIndicatorResult = original.nullIndicatorResult; } @Override @@ -171,7 +192,7 @@ public Initializer createInitializer( @Override public EmbeddableInitializer createInitializer(InitializerParent parent, AssemblerCreationState creationState) { - return new EmbeddableInitializerImpl( this, discriminatorFetch, parent, creationState, true ); + return new EmbeddableInitializerImpl( this, discriminatorFetch, nullIndicatorResult, parent, creationState, true ); } @Override @@ -185,7 +206,13 @@ public FetchParent asFetchParent() { return this; } + // Used by Hibernate Reactive protected BasicFetch getDiscriminatorFetch() { return discriminatorFetch; } + + // Used by Hibernate Reactive + protected @Nullable DomainResult getNullIndicatorResult() { + return nullIndicatorResult; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/EmbeddableForeignKeyResultImpl.java b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/EmbeddableForeignKeyResultImpl.java index 50f43bc193e9..958c14dd4488 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/EmbeddableForeignKeyResultImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/EmbeddableForeignKeyResultImpl.java @@ -54,6 +54,9 @@ public EmbeddableForeignKeyResultImpl( resetFetches( creationState.visitFetches( this ) ); } + /* + * Used by Hibernate Reactive + */ protected EmbeddableForeignKeyResultImpl(EmbeddableForeignKeyResultImpl original) { super( original ); this.resultVariable = original.resultVariable; @@ -126,7 +129,7 @@ public Initializer createInitializer( public EmbeddableInitializer createInitializer(InitializerParent parent, AssemblerCreationState creationState) { return getReferencedModePart() instanceof NonAggregatedIdentifierMapping ? new NonAggregatedIdentifierMappingInitializer( this, null, creationState, true ) - : new EmbeddableInitializerImpl( this, null, null, creationState, true ); + : new EmbeddableInitializerImpl( this, null, null, null, creationState, true ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/EmbeddableInitializerImpl.java b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/EmbeddableInitializerImpl.java index 762af601e1a1..f8203750cc3e 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/EmbeddableInitializerImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/EmbeddableInitializerImpl.java @@ -25,6 +25,7 @@ import org.hibernate.proxy.LazyInitializer; import org.hibernate.spi.NavigablePath; import org.hibernate.sql.results.graph.AssemblerCreationState; +import org.hibernate.sql.results.graph.DomainResult; import org.hibernate.sql.results.graph.DomainResultAssembler; import org.hibernate.sql.results.graph.Fetch; import org.hibernate.sql.results.graph.FetchParent; @@ -63,6 +64,7 @@ public class EmbeddableInitializerImpl extends AbstractInitializer[][] assemblers; protected final BasicResultAssembler discriminatorAssembler; + protected final @Nullable DomainResultAssembler nullIndicatorAssembler; protected final @Nullable Initializer[][] subInitializers; protected final @Nullable Initializer[][] subInitializersForResolveFromInitialized; protected final @Nullable Initializer[][] collectionContainingSubInitializers; @@ -101,12 +103,24 @@ public int getSubclassId() { } } + // Used by Hibernate Reactive + @Deprecated(forRemoval = true) public EmbeddableInitializerImpl( EmbeddableResultGraphNode resultDescriptor, BasicFetch discriminatorFetch, InitializerParent parent, AssemblerCreationState creationState, boolean isResultInitializer) { + this( resultDescriptor, discriminatorFetch, null, parent, creationState, isResultInitializer ); + } + + public EmbeddableInitializerImpl( + EmbeddableResultGraphNode resultDescriptor, + BasicFetch discriminatorFetch, + @Nullable DomainResult nullIndicatorResult, + InitializerParent parent, + AssemblerCreationState creationState, + boolean isResultInitializer) { super( creationState ); this.navigablePath = resultDescriptor.getNavigablePath(); this.embedded = resultDescriptor.getReferencedMappingContainer(); @@ -191,6 +205,8 @@ public EmbeddableInitializerImpl( this.discriminatorAssembler = discriminatorFetch != null ? (BasicResultAssembler) discriminatorFetch.createAssembler( this, creationState ) : null; + this.nullIndicatorAssembler = + nullIndicatorResult == null ? null : nullIndicatorResult.createResultAssembler( this, creationState ); this.subInitializers = subInitializers; this.subInitializersForResolveFromInitialized = isEnhancedForLazyLoading( embeddableMappingType ) ? subInitializers @@ -465,7 +481,7 @@ private void prepareCompositeInstance(EmbeddableInitializerData data) { if ( parent != null && embedded instanceof VirtualModelPart && !isPartOfKey && data.getState() != State.MISSING ) { final InitializerData subData = parent.getData( data.getRowProcessingState() ); parent.resolveInstance( subData ); - data.setInstance( parent.getResolvedInstance( subData ) ); + data.setInstance( parent.getResolvedInstanceNoProxy( subData ) ); if ( data.getState() == State.INITIALIZED ) { return; } @@ -476,7 +492,7 @@ private void prepareCompositeInstance(EmbeddableInitializerData data) { } } - private void extractRowState(EmbeddableInitializerData data) { + protected void extractRowState(EmbeddableInitializerData data) { boolean stateAllNull = true; final DomainResultAssembler[] subAssemblers = assemblers[data.getSubclassId()]; final RowProcessingState rowProcessingState = data.getRowProcessingState(); @@ -501,10 +517,15 @@ else if ( isPartOfKey ) { } } if ( stateAllNull ) { - data.setState( State.MISSING ); + data.setState( isNull( data ) ? State.MISSING : State.RESOLVED ); } } + protected boolean isNull(EmbeddableInitializerData data) { + return nullIndicatorAssembler == null + || Boolean.TRUE == nullIndicatorAssembler.assemble( data.getRowProcessingState() ); + } + @Override public void resolveState(EmbeddableInitializerData data) { final RowProcessingState rowProcessingState = data.getRowProcessingState(); diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/EmbeddableResultImpl.java b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/EmbeddableResultImpl.java index ecc4918b0c02..f694cc98addb 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/EmbeddableResultImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/EmbeddableResultImpl.java @@ -6,14 +6,18 @@ */ package org.hibernate.sql.results.graph.embeddable.internal; +import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.internal.util.NullnessUtil; import org.hibernate.metamodel.mapping.EmbeddableMappingType; import org.hibernate.metamodel.mapping.EmbeddableValuedModelPart; import org.hibernate.spi.NavigablePath; import org.hibernate.sql.ast.SqlAstJoinType; import org.hibernate.sql.ast.spi.FromClauseAccess; +import org.hibernate.sql.ast.tree.expression.Expression; 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.NullnessPredicate; import org.hibernate.sql.results.graph.AbstractFetchParent; import org.hibernate.sql.results.graph.AssemblerCreationState; import org.hibernate.sql.results.graph.DomainResult; @@ -27,6 +31,7 @@ import org.hibernate.sql.results.graph.embeddable.EmbeddableResult; import org.hibernate.sql.results.graph.embeddable.EmbeddableResultGraphNode; import org.hibernate.sql.results.graph.internal.ImmutableFetchList; +import org.hibernate.type.BasicType; import org.hibernate.type.descriptor.java.JavaType; /** @@ -40,6 +45,7 @@ public class EmbeddableResultImpl extends AbstractFetchParent implements Embe private final boolean containsAnyNonScalars; private final EmbeddableMappingType fetchContainer; private final BasicFetch discriminatorFetch; + private final @Nullable DomainResult nullIndicatorResult; public EmbeddableResultImpl( NavigablePath navigablePath, @@ -57,7 +63,7 @@ public EmbeddableResultImpl( final FromClauseAccess fromClauseAccess = creationState.getSqlAstCreationState().getFromClauseAccess(); - fromClauseAccess.resolveTableGroup( + final TableGroup embeddableTableGroup = fromClauseAccess.resolveTableGroup( getNavigablePath(), np -> { final EmbeddableValuedModelPart embeddedValueMapping = modelPart.getEmbeddableTypeDescriptor().getEmbeddedValueMapping(); @@ -78,6 +84,19 @@ public EmbeddableResultImpl( ); this.discriminatorFetch = creationState.visitEmbeddableDiscriminatorFetch( this, false ); + if ( fetchContainer.getAggregateMapping() != null ) { + final TableReference tableReference = embeddableTableGroup.resolveTableReference( + fetchContainer.getAggregateMapping().getContainingTableExpression() ); + final Expression aggregateExpression = creationState.getSqlAstCreationState().getSqlExpressionResolver() + .resolveSqlExpression( tableReference, fetchContainer.getAggregateMapping() ); + final BasicType booleanType = creationState.getSqlAstCreationState().getCreationContext() + .getSessionFactory().getTypeConfiguration().getBasicTypeForJavaType( Boolean.class ); + this.nullIndicatorResult = new NullnessPredicate( aggregateExpression, false, booleanType ) + .createDomainResult( null, creationState ); + } + else { + this.nullIndicatorResult = null; + } afterInitialize( this, creationState ); @@ -143,6 +162,6 @@ public Initializer createInitializer( @Override public Initializer createInitializer(InitializerParent parent, AssemblerCreationState creationState) { - return new EmbeddableInitializerImpl( this, discriminatorFetch, parent, creationState, true ); + return new EmbeddableInitializerImpl( this, discriminatorFetch, nullIndicatorResult, parent, creationState, true ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/NonAggregatedIdentifierMappingInitializer.java b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/NonAggregatedIdentifierMappingInitializer.java index 0b4eeba77bed..a20f40fb8bf4 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/NonAggregatedIdentifierMappingInitializer.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/embeddable/internal/NonAggregatedIdentifierMappingInitializer.java @@ -307,6 +307,10 @@ public void resolveInstance(@Nullable Object instance, NonAggregatedIdentifierMa data.setState( State.MISSING ); data.setInstance( null ); } + else if ( hasIdClass ) { + resolveKey( data ); + resolveInstance( data ); + } else { data.setState( State.INITIALIZED ); data.setInstance( instance ); @@ -324,7 +328,7 @@ private void resolveInstanceSubInitializers(Object instance, RowProcessingState for ( int i = 0; i < subInitializersForResolveFromInitialized.length; i++ ) { final Initializer initializer = subInitializersForResolveFromInitialized[i]; if ( initializer != null ) { - final Object subInstance = virtualIdEmbeddable.getValue( instance, i ); + final Object subInstance = representationEmbeddable.getValue( instance, i ); if ( subInstance == LazyPropertyInitializer.UNFETCHED_PROPERTY ) { // Go through the normal initializer process initializer.resolveKey( rowProcessingState ); diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/AbstractBatchEntitySelectFetchInitializer.java b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/AbstractBatchEntitySelectFetchInitializer.java index 793a5451b15c..5e4e47cdd9f6 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/AbstractBatchEntitySelectFetchInitializer.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/AbstractBatchEntitySelectFetchInitializer.java @@ -10,10 +10,8 @@ import org.hibernate.FetchNotFoundException; import org.hibernate.Hibernate; import org.hibernate.annotations.NotFoundAction; -import org.hibernate.engine.spi.EntityHolder; -import org.hibernate.engine.spi.EntityKey; -import org.hibernate.engine.spi.PersistenceContext; -import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.bytecode.enhance.spi.interceptor.EnhancementAsProxyLazinessInterceptor; +import org.hibernate.engine.spi.*; import org.hibernate.metamodel.mapping.AttributeMapping; import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.mapping.internal.ToOneAttributeMapping; @@ -31,7 +29,9 @@ import org.checkerframework.checker.nullness.qual.Nullable; +import static org.hibernate.engine.internal.ManagedTypeHelper.isPersistentAttributeInterceptable; import static org.hibernate.proxy.HibernateProxy.extractLazyInitializer; +import static org.hibernate.sql.results.graph.entity.internal.EntityInitializerImpl.getAttributeInterceptor; public abstract class AbstractBatchEntitySelectFetchInitializer extends EntitySelectFetchInitializer implements EntityInitializer { @@ -111,6 +111,10 @@ public void resolveInstance(Data data) { return; } } + resolveInstanceFromIdentifier( data ); + } + + protected void resolveInstanceFromIdentifier(Data data) { if ( data.batchDisabled ) { initialize( data ); } @@ -133,34 +137,86 @@ public void resolveInstance(Object instance, Data data) { data.setInstance( null ); return; } - final RowProcessingState rowProcessingState = data.getRowProcessingState(); + final var rowProcessingState = data.getRowProcessingState(); + final var session = rowProcessingState.getSession(); + final var persistenceContext = session.getPersistenceContextInternal(); // Only need to extract the identifier if the identifier has a many to one final LazyInitializer lazyInitializer = extractLazyInitializer( instance ); - data.entityKey = null; + data.entityIdentifier = null; if ( lazyInitializer == null ) { - // Entity is initialized - data.setState( State.INITIALIZED ); - if ( keyIsEager ) { - data.entityIdentifier = concreteDescriptor.getIdentifier( instance, rowProcessingState.getSession() ); - } + // Entity is most probably initialized data.setInstance( instance ); + final PersistentAttributeInterceptor interceptor; + if ( concreteDescriptor.getBytecodeEnhancementMetadata().isEnhancedForLazyLoading() + && isPersistentAttributeInterceptable( instance ) + && ( interceptor = getAttributeInterceptor( instance ) ) instanceof EnhancementAsProxyLazinessInterceptor ) { + final EnhancementAsProxyLazinessInterceptor enhancementInterceptor = (EnhancementAsProxyLazinessInterceptor) interceptor; + if ( enhancementInterceptor.isInitialized() ) { + data.setState( State.INITIALIZED ); + } + else { + data.setState( State.RESOLVED ); + data.entityIdentifier = enhancementInterceptor.getIdentifier(); + } + } + else { + // If the entity initializer is null, we know the entity is fully initialized, + // otherwise it will be initialized by some other initializer + data.setState( State.RESOLVED ); + data.entityIdentifier = concreteDescriptor.getIdentifier( instance, session ); + } + if ( data.entityIdentifier == null ) { + data.entityIdentifier = concreteDescriptor.getIdentifier( instance, session ); + } } else if ( lazyInitializer.isUninitialized() ) { data.setState( State.RESOLVED ); - if ( keyIsEager ) { - data.entityIdentifier = lazyInitializer.getIdentifier(); - } - // Resolve and potentially create the entity instance - registerToBatchFetchQueue( data ); + data.entityIdentifier = lazyInitializer.getInternalIdentifier(); } else { // Entity is initialized data.setState( State.INITIALIZED ); - if ( keyIsEager ) { - data.entityIdentifier = lazyInitializer.getIdentifier(); - } + data.entityIdentifier = lazyInitializer.getInternalIdentifier(); data.setInstance( lazyInitializer.getImplementation() ); } + + data.entityKey = new EntityKey( data.entityIdentifier, concreteDescriptor ); + final var entityHolder = persistenceContext.getEntityHolder( + data.entityKey + ); + + if ( entityHolder == null || entityHolder.getEntity() != instance && entityHolder.getProxy() != instance ) { + // the existing entity instance is detached or transient + if ( entityHolder != null ) { + final var managed = entityHolder.getManagedObject(); + data.setInstance( managed ); + data.entityKey = entityHolder.getEntityKey(); + data.entityIdentifier = data.entityKey.getIdentifier(); + if ( entityHolder.isInitialized() ) { + data.setState( State.INITIALIZED ); + } + else { + data.setState( State.RESOLVED ); + } + } + else { + data.setState( State.RESOLVED ); + } + } + + if ( data.getState() == State.RESOLVED ) { + // similar to resolveInstanceFromIdentifier, but we already have the holder here + if ( data.batchDisabled ) { + initialize( data, entityHolder, session, persistenceContext ); + } + else if ( entityHolder == null || !entityHolder.isEventuallyInitialized() ) { + // need to add the key to the batch queue only when the entity has not been already loaded or + // there isn't another initializer that is loading it + registerResolutionListener( data ); + registerToBatchFetchQueue( data ); + } + } + if ( keyIsEager ) { final Initializer initializer = keyAssembler.getInitializer(); assert initializer != null; @@ -210,6 +266,7 @@ public void initializeInstanceFromParent(Object parentInstance, Data data) { : parentInstance; // No need to initialize these fields data.entityKey = null; + data.entityIdentifier = null; data.setInstance( null ); if ( instance == null ) { data.setState( State.MISSING ); @@ -217,7 +274,7 @@ public void initializeInstanceFromParent(Object parentInstance, Data data) { else { final LazyInitializer lazyInitializer = HibernateProxy.extractLazyInitializer( instance ); if ( lazyInitializer != null && lazyInitializer.isUninitialized() ) { - data.entityKey = new EntityKey( lazyInitializer.getIdentifier(), concreteDescriptor ); + data.entityKey = new EntityKey( lazyInitializer.getInternalIdentifier(), concreteDescriptor ); registerToBatchFetchQueue( data ); } data.setState( State.INITIALIZED ); diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/BatchEntitySelectFetchInitializer.java b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/BatchEntitySelectFetchInitializer.java index 3eb69b6cd0b6..a5b623fe5a52 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/BatchEntitySelectFetchInitializer.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/BatchEntitySelectFetchInitializer.java @@ -67,14 +67,17 @@ protected InitializerData createInitializerData(RowProcessingState rowProcessing protected void registerResolutionListener(BatchEntitySelectFetchInitializerData data) { final RowProcessingState rowProcessingState = data.getRowProcessingState(); final InitializerData owningData = owningEntityInitializer.getData( rowProcessingState ); + HashMap> toBatchLoad = data.toBatchLoad; + if ( toBatchLoad == null ) { + toBatchLoad = data.toBatchLoad = new HashMap<>(); + } + // Always register the entity key for resolution + final List parentInfos = toBatchLoad.computeIfAbsent( data.entityKey, key -> new ArrayList<>() ); final AttributeMapping parentAttribute; + // But only add the parent info if the parent entity is not already initialized if ( owningData.getState() != State.INITIALIZED && ( parentAttribute = parentAttributes[owningEntityInitializer.getConcreteDescriptor( owningData ).getSubclassId()] ) != null ) { - HashMap> toBatchLoad = data.toBatchLoad; - if ( toBatchLoad == null ) { - toBatchLoad = data.toBatchLoad = new HashMap<>(); - } - toBatchLoad.computeIfAbsent( data.entityKey, key -> new ArrayList<>() ).add( + parentInfos.add( new ParentInfo( owningEntityInitializer.getTargetInstance( owningData ), parentAttribute.getStateArrayPosition() diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/DiscriminatedEntityInitializer.java b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/DiscriminatedEntityInitializer.java index bf7a0cb41893..2baef932bd04 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/DiscriminatedEntityInitializer.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/DiscriminatedEntityInitializer.java @@ -210,33 +210,50 @@ public void resolveInstance(Object instance, DiscriminatedEntityInitializerData data.setInstance( null ); } else { - final RowProcessingState rowProcessingState = data.getRowProcessingState(); + final var rowProcessingState = data.getRowProcessingState(); + final var session = rowProcessingState.getSession(); final LazyInitializer lazyInitializer = extractLazyInitializer( instance ); if ( lazyInitializer == null ) { data.setState( State.INITIALIZED ); - if ( keyIsEager ) { - final SharedSessionContractImplementor session = rowProcessingState.getSession(); - data.concreteDescriptor = session.getEntityPersister( null, instance ); - data.entityIdentifier = data.concreteDescriptor.getIdentifier( instance, session ); - } + data.concreteDescriptor = session.getEntityPersister( null, instance ); + data.entityIdentifier = data.concreteDescriptor.getIdentifier( instance, session ); } else if ( lazyInitializer.isUninitialized() ) { data.setState( eager ? State.RESOLVED : State.INITIALIZED ); - if ( keyIsEager ) { - // Read the discriminator from the result set if necessary - final Object discriminatorValue = discriminatorValueAssembler.assemble( rowProcessingState ); - data.concreteDescriptor = fetchedPart.resolveDiscriminatorValue( discriminatorValue ).getEntityPersister(); - data.entityIdentifier = lazyInitializer.getIdentifier(); - } + // Read the discriminator from the result set if necessary + final Object discriminatorValue = discriminatorValueAssembler.assemble( rowProcessingState ); + data.concreteDescriptor = fetchedPart.resolveDiscriminatorValue( discriminatorValue ).getEntityPersister(); + data.entityIdentifier = lazyInitializer.getInternalIdentifier(); } else { data.setState( State.INITIALIZED ); - if ( keyIsEager ) { - data.concreteDescriptor = rowProcessingState.getSession().getEntityPersister( null, lazyInitializer.getImplementation() ); - data.entityIdentifier = lazyInitializer.getIdentifier(); + data.concreteDescriptor = session.getEntityPersister( null, lazyInitializer.getImplementation() ); + data.entityIdentifier = lazyInitializer.getInternalIdentifier(); + } + + + final var entityKey = new EntityKey( data.entityIdentifier, data.concreteDescriptor ); + final var entityHolder = session.getPersistenceContextInternal().getEntityHolder( + entityKey + ); + + if ( entityHolder == null || entityHolder.getEntity() != instance && entityHolder.getProxy() != instance ) { + // the existing entity instance is detached or transient + if ( entityHolder != null ) { + final var managed = entityHolder.getManagedObject(); + data.setInstance( managed ); + data.entityIdentifier = entityHolder.getEntityKey().getIdentifier(); + data.setState( !eager || entityHolder.isInitialized() ? State.INITIALIZED : State.RESOLVED ); + } + else { + data.setState( State.RESOLVED ); + initializeInstance( data ); } } - data.setInstance( instance ); + else { + data.setInstance( instance ); + } + if ( keyIsEager ) { final Initializer initializer = keyValueAssembler.getInitializer(); assert initializer != null; diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntityDelayedFetchInitializer.java b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntityDelayedFetchInitializer.java index bdcacb08ba82..3b0572ff6ed7 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntityDelayedFetchInitializer.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntityDelayedFetchInitializer.java @@ -9,12 +9,17 @@ import java.util.function.BiConsumer; import org.hibernate.FetchNotFoundException; -import org.hibernate.bytecode.enhance.spi.LazyPropertyInitializer; +import org.hibernate.bytecode.enhance.spi.interceptor.LazyAttributeLoadingInterceptor; +import org.hibernate.engine.internal.ManagedTypeHelper; import org.hibernate.engine.spi.EntityHolder; import org.hibernate.engine.spi.EntityKey; import org.hibernate.engine.spi.EntityUniqueKey; import org.hibernate.engine.spi.PersistenceContext; +import org.hibernate.engine.spi.PersistentAttributeInterceptable; import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.graph.GraphSemantic; +import org.hibernate.graph.spi.AppliedGraph; +import org.hibernate.graph.spi.AttributeNodeImplementor; import org.hibernate.internal.log.LoggingHelper; import org.hibernate.metamodel.mapping.ModelPart; import org.hibernate.metamodel.mapping.internal.ToOneAttributeMapping; @@ -37,6 +42,7 @@ import org.checkerframework.checker.nullness.qual.Nullable; +import static org.hibernate.bytecode.enhance.spi.LazyPropertyInitializer.UNFETCHED_PROPERTY; import static org.hibernate.sql.results.graph.entity.internal.EntityInitializerImpl.determineConcreteEntityDescriptor; /** @@ -170,30 +176,53 @@ public void resolveInstance(EntityDelayedFetchInitializerData data) { concreteDescriptor = entityPersister; } - final PersistenceContext persistenceContext = session.getPersistenceContextInternal(); - if ( selectByUniqueKey ) { - final String uniqueKeyPropertyName = referencedModelPart.getReferencedPropertyName(); - final Type uniqueKeyPropertyType = ( referencedModelPart.getReferencedPropertyName() == null ) ? - concreteDescriptor.getIdentifierType() : - session.getFactory() - .getReferencedPropertyType( - concreteDescriptor.getEntityName(), - uniqueKeyPropertyName - ); - - final EntityUniqueKey euk = new EntityUniqueKey( - concreteDescriptor.getEntityName(), - uniqueKeyPropertyName, - data.entityIdentifier, - uniqueKeyPropertyType, - session.getFactory() - ); - Object instance = persistenceContext.getEntity( euk ); - if ( instance == null ) { - // For unique-key mappings, we always use bytecode-laziness if possible, - // because we can't generate a proxy based on the unique key yet - if ( referencedModelPart.isLazy() ) { - instance = LazyPropertyInitializer.UNFETCHED_PROPERTY; + initialize( data, null, concreteDescriptor ); + } + } + + protected void initialize(EntityDelayedFetchInitializerData data, @Nullable EntityKey entityKey, EntityPersister concreteDescriptor) { + final RowProcessingState rowProcessingState = data.getRowProcessingState(); + final SharedSessionContractImplementor session = rowProcessingState.getSession(); + final PersistenceContext persistenceContext = session.getPersistenceContextInternal(); + if ( selectByUniqueKey ) { + final String uniqueKeyPropertyName = referencedModelPart.getReferencedPropertyName(); + final Type uniqueKeyPropertyType = ( referencedModelPart.getReferencedPropertyName() == null ) ? + concreteDescriptor.getIdentifierType() : + session.getFactory() + .getReferencedPropertyType( + concreteDescriptor.getEntityName(), + uniqueKeyPropertyName + ); + + final EntityUniqueKey euk = new EntityUniqueKey( + concreteDescriptor.getEntityName(), + uniqueKeyPropertyName, + data.entityIdentifier, + uniqueKeyPropertyType, + session.getFactory() + ); + Object instance = persistenceContext.getEntity( euk ); + if ( instance == null ) { + // For unique-key mappings, we always use bytecode-laziness if possible, + // because we can't generate a proxy based on the unique key yet + if ( referencedModelPart.isLazy() ) { + instance = UNFETCHED_PROPERTY; + } + else { + // Try to load a PersistentAttributeInterceptable. If we get one, we can add the lazy + // field to the interceptor. If we don't get one, we load the entity by unique key. + PersistentAttributeInterceptable persistentAttributeInterceptable = null; + if ( getParent().isEntityInitializer() && isLazyByGraph( rowProcessingState ) ) { + final Object resolvedInstance = + getParent().asEntityInitializer().getResolvedInstance( rowProcessingState ); + persistentAttributeInterceptable = + ManagedTypeHelper.asPersistentAttributeInterceptableOrNull( resolvedInstance ); + } + + if ( persistentAttributeInterceptable != null && persistentAttributeInterceptable.$$_hibernate_getInterceptor() instanceof LazyAttributeLoadingInterceptor) { + final LazyAttributeLoadingInterceptor persistentAttributeInterceptor = (LazyAttributeLoadingInterceptor) persistentAttributeInterceptable.$$_hibernate_getInterceptor(); + persistentAttributeInterceptor.addLazyFieldByGraph( navigablePath.getLocalName() ); + instance = UNFETCHED_PROPERTY; } else { instance = concreteDescriptor.loadByUniqueKey( @@ -209,39 +238,54 @@ public void resolveInstance(EntityDelayedFetchInitializerData data) { } } } - if ( instance != null ) { - instance = persistenceContext.proxyFor( instance ); - } - data.setInstance( instance ); + } + if ( instance != null ) { + instance = persistenceContext.proxyFor( instance ); + } + data.setInstance( instance ); + } + else { + final EntityKey ek = entityKey == null ? + new EntityKey( data.entityIdentifier, concreteDescriptor ) : + entityKey; + final EntityHolder holder = persistenceContext.getEntityHolder( ek ); + final Object instance; + if ( holder != null && holder.getEntity() != null ) { + instance = persistenceContext.proxyFor( holder, concreteDescriptor ); + } + // For primary key based mappings we only use bytecode-laziness if the attribute is optional, + // because the non-optionality implies that it is safe to have a proxy + else if ( referencedModelPart.isOptional() && referencedModelPart.isLazy() ) { + instance = UNFETCHED_PROPERTY; } else { - final EntityKey entityKey = new EntityKey( data.entityIdentifier, concreteDescriptor ); - final EntityHolder holder = persistenceContext.getEntityHolder( entityKey ); - final Object instance; - if ( holder != null && holder.getEntity() != null ) { - instance = persistenceContext.proxyFor( holder, concreteDescriptor ); - } - // For primary key based mappings we only use bytecode-laziness if the attribute is optional, - // because the non-optionality implies that it is safe to have a proxy - else if ( referencedModelPart.isOptional() && referencedModelPart.isLazy() ) { - instance = LazyPropertyInitializer.UNFETCHED_PROPERTY; - } - else { - instance = session.internalLoad( - concreteDescriptor.getEntityName(), - data.entityIdentifier, - false, - false - ); - - final LazyInitializer lazyInitializer = HibernateProxy.extractLazyInitializer( instance ); - if ( lazyInitializer != null ) { - lazyInitializer.setUnwrap( referencedModelPart.isUnwrapProxy() && concreteDescriptor.isInstrumented() ); - } + instance = session.internalLoad( + concreteDescriptor.getEntityName(), + data.entityIdentifier, + false, + false + ); + + final LazyInitializer lazyInitializer = HibernateProxy.extractLazyInitializer( instance ); + if ( lazyInitializer != null ) { + lazyInitializer.setUnwrap( referencedModelPart.isUnwrapProxy() && concreteDescriptor.isInstrumented() ); } - data.setInstance( instance ); } + data.setInstance( instance ); + } + } + + private boolean isLazyByGraph(RowProcessingState rowProcessingState) { + final AppliedGraph appliedGraph = rowProcessingState.getQueryOptions().getAppliedGraph(); + if ( appliedGraph != null && appliedGraph.getSemantic() == GraphSemantic.FETCH ) { + final AttributeNodeImplementor attributeNode = appliedGraph.getGraph() + .findAttributeNode( navigablePath.getLocalName() ); + if ( attributeNode != null && attributeNode.getAttributeDescriptor() == getInitializedPart().asAttributeMapping() ) { + return false; + } + return true; } + return false; } @Override @@ -255,9 +299,28 @@ public void resolveInstance(Object instance, EntityDelayedFetchInitializerData d // This initializer is done initializing, since this is only invoked for delayed or select initializers data.setState( State.INITIALIZED ); data.setInstance( instance ); - final RowProcessingState rowProcessingState = data.getRowProcessingState(); + final var rowProcessingState = data.getRowProcessingState(); + final var session = rowProcessingState.getSession(); + final var entityDescriptor = getEntityDescriptor(); + data.entityIdentifier = entityDescriptor.getIdentifier( instance, session ); + + final var entityKey = new EntityKey( data.entityIdentifier, entityDescriptor ); + final var entityHolder = session.getPersistenceContextInternal().getEntityHolder( + entityKey + ); + + if ( entityHolder == null || entityHolder.getEntity() != instance && entityHolder.getProxy() != instance ) { + // the existing entity instance is detached or transient + if ( entityHolder != null ) { + final var managed = entityHolder.getManagedObject(); + data.entityIdentifier = entityHolder.getEntityKey().getIdentifier(); + data.setInstance( managed ); + } + else { + initialize( data, entityKey, entityDescriptor ); + } + } if ( keyIsEager ) { - data.entityIdentifier = getEntityDescriptor().getIdentifier( instance, rowProcessingState.getSession() ); final Initializer initializer = identifierAssembler.getInitializer(); assert initializer != null; initializer.resolveInstance( data.entityIdentifier, rowProcessingState ); diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntityInitializerImpl.java b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntityInitializerImpl.java index fac0a5c17889..7188f84f814a 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntityInitializerImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntityInitializerImpl.java @@ -25,6 +25,7 @@ import org.hibernate.cache.spi.access.AccessType; import org.hibernate.cache.spi.access.EntityDataAccess; import org.hibernate.cache.spi.entry.CacheEntry; +import org.hibernate.engine.internal.ForeignKeys; import org.hibernate.engine.spi.EntityEntry; import org.hibernate.engine.spi.EntityHolder; import org.hibernate.engine.spi.EntityKey; @@ -77,6 +78,7 @@ import org.hibernate.sql.results.jdbc.spi.JdbcValuesSourceProcessingOptions; import org.hibernate.sql.results.jdbc.spi.RowProcessingState; import org.hibernate.stat.spi.StatisticsImplementor; +import org.hibernate.type.ManyToOneType; import org.hibernate.type.Type; import org.hibernate.type.descriptor.java.MutabilityPlan; @@ -86,8 +88,7 @@ import static org.hibernate.bytecode.enhance.spi.LazyPropertyInitializer.UNFETCHED_PROPERTY; import static org.hibernate.engine.internal.ManagedTypeHelper.asPersistentAttributeInterceptable; import static org.hibernate.engine.internal.ManagedTypeHelper.isPersistentAttributeInterceptable; -import static org.hibernate.internal.log.LoggingHelper.toLoggableString; -import static org.hibernate.metamodel.mapping.ForeignKeyDescriptor.Nature.TARGET; +import static org.hibernate.internal.util.NullnessUtil.castNonNull; import static org.hibernate.proxy.HibernateProxy.extractLazyInitializer; /** @@ -132,6 +133,7 @@ public class EntityInitializerImpl extends AbstractInitializer rowIdAssembler; private final DomainResultAssembler[][] assemblers; + private final @Nullable Initializer[] allInitializers; private final @Nullable Initializer[][] subInitializers; private final @Nullable Initializer[][] subInitializersForResolveFromInitialized; private final @Nullable Initializer[][] collectionContainingSubInitializers; @@ -278,8 +280,10 @@ public EntityInitializerImpl( ? rowIdResult.createResultAssembler( this, creationState ) : null; + final int fetchableCount = entityDescriptor.getNumberOfFetchables(); final Collection subMappingTypes = rootEntityDescriptor.getSubMappingTypes(); final DomainResultAssembler[][] assemblers = new DomainResultAssembler[subMappingTypes.size() + 1][]; + final Initializer[] allInitializers = new Initializer[fetchableCount]; final Initializer[][] subInitializers = new Initializer[subMappingTypes.size() + 1][]; final Initializer[][] eagerSubInitializers = new Initializer[subMappingTypes.size() + 1][]; final Initializer[][] collectionContainingSubInitializers = new Initializer[subMappingTypes.size() + 1][]; @@ -295,8 +299,7 @@ public EntityInitializerImpl( } boolean hasLazySubInitializers = false; - final int size = entityDescriptor.getNumberOfFetchables(); - for ( int i = 0; i < size; i++ ) { + for ( int i = 0; i < fetchableCount; i++ ) { final AttributeMapping attributeMapping = entityDescriptor.getFetchable( i ).asAttributeMapping(); final Fetch fetch = resultDescriptor.findFetch( attributeMapping ); final DomainResultAssembler stateAssembler = fetch == null @@ -309,12 +312,13 @@ public EntityInitializerImpl( final Initializer subInitializer = stateAssembler.getInitializer(); if ( subInitializer != null ) { + allInitializers[i] = subInitializer; if ( subInitializers[subclassId] == null ) { - subInitializers[subclassId] = new Initializer[size]; - eagerSubInitializers[subclassId] = new Initializer[size]; - collectionContainingSubInitializers[subclassId] = new Initializer[size]; - lazySets[subclassId] = new BitSet( size ); - maybeLazySets[subclassId] = new BitSet( size ); + subInitializers[subclassId] = new Initializer[fetchableCount]; + eagerSubInitializers[subclassId] = new Initializer[fetchableCount]; + collectionContainingSubInitializers[subclassId] = new Initializer[fetchableCount]; + lazySets[subclassId] = new BitSet( fetchableCount ); + maybeLazySets[subclassId] = new BitSet( fetchableCount ); } subInitializers[subclassId][stateArrayPosition] = subInitializer; if ( subInitializer.isEager() ) { @@ -348,11 +352,11 @@ public EntityInitializerImpl( updatableAttributeMutabilityPlans[subMappingType.getSubclassId()][stateArrayPosition] = updatableAttributeMutabilityPlans[subclassId][stateArrayPosition]; if ( subInitializer != null ) { if ( subInitializers[subMappingType.getSubclassId()] == null ) { - subInitializers[subMappingType.getSubclassId()] = new Initializer[size]; - eagerSubInitializers[subMappingType.getSubclassId()] = new Initializer[size]; - collectionContainingSubInitializers[subMappingType.getSubclassId()] = new Initializer[size]; - lazySets[subMappingType.getSubclassId()] = new BitSet(size); - maybeLazySets[subMappingType.getSubclassId()] = new BitSet(size); + subInitializers[subMappingType.getSubclassId()] = new Initializer[fetchableCount]; + eagerSubInitializers[subMappingType.getSubclassId()] = new Initializer[fetchableCount]; + collectionContainingSubInitializers[subMappingType.getSubclassId()] = new Initializer[fetchableCount]; + lazySets[subMappingType.getSubclassId()] = new BitSet(fetchableCount); + maybeLazySets[subMappingType.getSubclassId()] = new BitSet(fetchableCount); } subInitializers[subMappingType.getSubclassId()][stateArrayPosition] = subInitializer; eagerSubInitializers[subMappingType.getSubclassId()][stateArrayPosition] = eagerSubInitializers[subclassId][stateArrayPosition]; @@ -410,6 +414,7 @@ public EntityInitializerImpl( } this.assemblers = assemblers; + this.allInitializers = allInitializers; this.subInitializers = subInitializers; this.subInitializersForResolveFromInitialized = rootEntityDescriptor.getBytecodeEnhancementMetadata().isEnhancedForLazyLoading() ? subInitializers @@ -564,7 +569,7 @@ protected void resolveKey(EntityInitializerData data, boolean entityKeyOnly) { } if ( oldEntityKey != null && previousRowReuse && oldEntityInstance != null - && areKeysEqual( oldEntityKey.getIdentifier(), id ) ) { + && areKeysEqual( oldEntityKey.getIdentifier(), id ) && !oldEntityHolder.isDetached() ) { data.setState( State.INITIALIZED ); data.entityKey = oldEntityKey; data.setInstance( oldEntityInstance ); @@ -610,6 +615,8 @@ private boolean areKeysEqual(Object key1, Object key2) { protected void resolveInstanceSubInitializers(EntityInitializerData data) { final int subclassId = data.concreteDescriptor.getSubclassId(); final EntityEntry entityEntry = data.entityHolder.getEntityEntry(); + assert entityEntry != null : "This method should only be called if the entity is already initialized"; + final Initializer[] initializers; final ImmutableBitSet maybeLazySet; if ( data.entityHolder.getEntityInitializer() == this ) { @@ -627,9 +634,9 @@ protected void resolveInstanceSubInitializers(EntityInitializerData data) { } } final RowProcessingState rowProcessingState = data.getRowProcessingState(); - assert entityEntry == rowProcessingState.getSession() - .getPersistenceContextInternal() - .getEntry( data.entityInstanceForNotify ); + final PersistenceContext persistenceContext = rowProcessingState.getSession() + .getPersistenceContextInternal(); + assert entityEntry == persistenceContext.getEntry( data.entityInstanceForNotify ); final Object[] loadedState = entityEntry.getLoadedState(); final Object[] state; if ( loadedState == null ) { @@ -899,31 +906,80 @@ public void resolveInstance(Object instance, EntityInitializerData data) { setMissing( data ); return; } - data.setInstance( instance ); final LazyInitializer lazyInitializer = extractLazyInitializer( instance ); final RowProcessingState rowProcessingState = data.getRowProcessingState(); final SharedSessionContractImplementor session = rowProcessingState.getSession(); final PersistenceContext persistenceContext = session.getPersistenceContextInternal(); if ( lazyInitializer == null ) { // Entity is most probably initialized - data.entityInstanceForNotify = instance; data.concreteDescriptor = session.getEntityPersister( null, instance ); resolveEntityKey( data, data.concreteDescriptor.getIdentifier( instance, session ) ); - data.entityHolder = persistenceContext.getEntityHolder( data.entityKey ); - if ( data.entityHolder == null ) { - // Entity was most probably removed in the same session without setting the reference to null - resolveKey( data ); - assert data.getState() == State.MISSING; - assert referencedModelPart instanceof ToOneAttributeMapping - && ( (ToOneAttributeMapping) referencedModelPart ).getSideNature() == TARGET; - return; + data.entityHolder = persistenceContext.claimEntityHolderIfPossible( + data.entityKey, + null, + rowProcessingState.getJdbcValuesSourceProcessingState(), + this + ); + if ( data.entityHolder.getManagedObject() == null ) { + final EntityEntry entry = persistenceContext.getEntry( instance ); // make sure an EntityEntry exists + if ( entry == null ) { + // We cannot reuse an entity instance that has no entry in the PC, + // this can happen if the parent entity contained a detached instance. + // We need to create a new instance in this case (see resolveEntityInstance1) + instance = resolveEntityInstance( data ); + } + else { + // Entity was most probably removed in the same session without setting this association to null. + // Since this load request can happen through `find()` which doesn't auto-flush on association joins, + // the entity must be fully initialized, even if it is removed already + data.entityHolder = persistenceContext.claimEntityHolderIfPossible( + data.entityKey, + instance, + rowProcessingState.getJdbcValuesSourceProcessingState(), + this + ); + } + } + else if ( data.entityHolder.getEntity() == null ) { + assert data.entityHolder.getProxy() != instance; + instance = resolveEntityInstance( data ); + data.entityKey = data.entityHolder.getEntityKey(); + if ( data.entityHolder.getProxy() != null ) { + castNonNull( extractLazyInitializer( data.entityHolder.getProxy() ) ).setImplementation( instance ); + } + } + else if ( data.entityHolder.getEntity() != instance ) { + // The instance contained in the parent entity is different from the managed persistent instance + // currently in the persistence context. We should always initialize the managed one in this case. + instance = data.entityHolder.getEntity(); + data.entityKey = data.entityHolder.getEntityKey(); + } + + data.entityInstanceForNotify = instance; + + if ( data.concreteDescriptor.getBytecodeEnhancementMetadata().isEnhancedForLazyLoading() + && isPersistentAttributeInterceptable( data.entityInstanceForNotify ) + && getAttributeInterceptor( data.entityInstanceForNotify ) instanceof EnhancementAsProxyLazinessInterceptor + && !( (EnhancementAsProxyLazinessInterceptor) getAttributeInterceptor( data.entityInstanceForNotify ) ).isInitialized() ) { + data.setState( State.RESOLVED ); + } + else { + // If the entity initializer is null, we know the entity is fully initialized, + // otherwise it will be initialized by some other initializer + data.setState( data.entityHolder.getEntityInitializer() == null ? State.INITIALIZED : State.RESOLVED ); + } + + if ( data.getState() == State.RESOLVED ) { + data.entityHolder = persistenceContext.claimEntityHolderIfPossible( + data.entityKey, + data.entityInstanceForNotify, + rowProcessingState.getJdbcValuesSourceProcessingState(), + this + ); } - // If the entity initializer is null, we know the entity is fully initialized, - // otherwise it will be initialized by some other initializer - data.setState( data.entityHolder.getEntityInitializer() == null ? State.INITIALIZED : State.RESOLVED ); } else if ( lazyInitializer.isUninitialized() ) { data.setState( State.RESOLVED ); @@ -932,25 +988,58 @@ else if ( lazyInitializer.isUninitialized() ) { ? entityDescriptor : determineConcreteEntityDescriptor( rowProcessingState, discriminatorAssembler, entityDescriptor ); assert data.concreteDescriptor != null; - resolveEntityKey( data, lazyInitializer.getIdentifier() ); + resolveEntityKey( data, lazyInitializer.getInternalIdentifier() ); data.entityHolder = persistenceContext.claimEntityHolderIfPossible( data.entityKey, null, rowProcessingState.getJdbcValuesSourceProcessingState(), this ); + // Resolve and potentially create the entity instance - data.entityInstanceForNotify = resolveEntityInstance( data ); - lazyInitializer.setImplementation( data.entityInstanceForNotify ); - registerLoadingEntity( data, data.entityInstanceForNotify ); + if ( data.entityHolder.getProxy() == instance ) { + data.entityInstanceForNotify = resolveEntityInstance( data ); + lazyInitializer.setImplementation( data.entityInstanceForNotify ); + } + else if ( data.entityHolder.getEntity() == null ) { + data.entityInstanceForNotify = resolveEntityInstance( data ); + if ( data.entityHolder.getProxy() != null ) { + castNonNull( extractLazyInitializer( data.entityHolder.getProxy() ) ).setImplementation( data.entityInstanceForNotify ); + } + } + else { + data.entityInstanceForNotify = data.entityHolder.getEntity(); + data.setState( data.entityHolder.isInitialized() ? State.INITIALIZED : State.RESOLVED ); + } } else { - data.setState( State.INITIALIZED ); - data.entityInstanceForNotify = lazyInitializer.getImplementation(); - data.concreteDescriptor = session.getEntityPersister( null, data.entityInstanceForNotify ); - resolveEntityKey( data, lazyInitializer.getIdentifier() ); + final var implementation = lazyInitializer.getImplementation(); + data.concreteDescriptor = session.getEntityPersister( null, implementation ); + resolveEntityKey( data, lazyInitializer.getInternalIdentifier() ); data.entityHolder = persistenceContext.getEntityHolder( data.entityKey ); + if ( data.entityHolder.getProxy() == instance ) { + data.entityInstanceForNotify = implementation; + // Even though the lazyInitializer reports it is initialized, check if the entity holder reports initialized, + // because in a nested initialization scenario, this nested initializer must initialize the entity + data.setState( data.entityHolder.isInitialized() ? State.INITIALIZED : State.RESOLVED ); + } + else if ( data.entityHolder.getEntity() == null ) { + data.entityInstanceForNotify = resolveEntityInstance( data ); + data.entityKey = data.entityHolder.getEntityKey(); + if ( data.entityHolder.getProxy() != null ) { + castNonNull( extractLazyInitializer( data.entityHolder.getProxy() ) ).setImplementation( data.entityInstanceForNotify ); + } + data.setState( State.RESOLVED ); + } + else { + data.entityInstanceForNotify = data.entityHolder.getEntity(); + data.entityKey = data.entityHolder.getEntityKey(); + data.setState( data.entityHolder.isInitialized() ? State.INITIALIZED : State.RESOLVED ); + } } + + data.setInstance( data.entityHolder.getManagedObject() ); + if ( identifierAssembler != null ) { final Initializer initializer = identifierAssembler.getInitializer(); if ( initializer != null ) { @@ -1073,6 +1162,10 @@ else if ( isResultInitializer() ) { else if ( data.entityHolder.getEntityInitializer() != this ) { data.setState( State.INITIALIZED ); } + else if ( data.shallowCached ) { + // For shallow cached entities, only the id is available, so ensure we load the data immediately + data.setInstance( data.entityInstanceForNotify = resolveEntityInstance( data ) ); + } } else if ( ( entityFromExecutionContext = getEntityFromExecutionContext( data ) ) != null ) { // This is the entity to refresh, so don't set the state to initialized @@ -1185,7 +1278,7 @@ protected Object resolveEntityInstance(EntityInitializerData data) { return resolved; } else { - if ( rowProcessingState.isQueryCacheHit() && entityDescriptor.useShallowQueryCacheLayout() ) { + if ( data.shallowCached ) { // We must load the entity this way, because the query cache entry contains only the primary key data.setState( State.INITIALIZED ); final SharedSessionContractImplementor session = rowProcessingState.getSession(); @@ -1545,11 +1638,22 @@ protected void registerPossibleUniqueKeyEntries( // one used here, which it will be if ( resolvedEntityState[index] != null ) { + final Object key; + if ( type instanceof ManyToOneType ) { + key = ForeignKeys.getEntityIdentifierIfNotUnsaved( + ( (ManyToOneType) type ).getAssociatedEntityName(), + resolvedEntityState[index], + session + ); + } + else { + key = resolvedEntityState[index]; + } final EntityUniqueKey entityUniqueKey = new EntityUniqueKey( data.concreteDescriptor.getRootEntityDescriptor().getEntityName(), //polymorphism comment above ukName, - resolvedEntityState[index], + key, type, session.getFactory() ); @@ -1586,10 +1690,25 @@ public void resolveState(EntityInitializerData data) { if ( rowIdAssembler != null ) { rowIdAssembler.resolveState( data.getRowProcessingState() ); } + if ( data.concreteDescriptor == null ) { + data.concreteDescriptor = data.defaultConcreteDescriptor; + if ( data.concreteDescriptor == null ) { + data.concreteDescriptor = determineConcreteEntityDescriptor( + data.getRowProcessingState(), + castNonNull( discriminatorAssembler ), + entityDescriptor + ); + if ( data.concreteDescriptor == null ) { + // this should imply the entity is missing + return; + } + } + } resolveEntityState( data ); } protected void resolveEntityState(EntityInitializerData data) { + assert data.concreteDescriptor != null; final RowProcessingState rowProcessingState = data.getRowProcessingState(); for ( final DomainResultAssembler assembler : assemblers[data.concreteDescriptor.getSubclassId()] ) { if ( assembler != null ) { @@ -1715,11 +1834,9 @@ protected void forEachSubInitializer(BiConsumer, RowProcessingSta } final EntityInitializerData entityInitializerData = (EntityInitializerData) data; if ( entityInitializerData.concreteDescriptor == null ) { - for ( Initializer[] initializers : subInitializers ) { - for ( Initializer initializer : initializers ) { - if ( initializer != null ) { - consumer.accept( initializer, rowProcessingState ); - } + for ( Initializer initializer : allInitializers ) { + if ( initializer != null ) { + consumer.accept( initializer, rowProcessingState ); } } } @@ -1732,11 +1849,20 @@ protected void forEachSubInitializer(BiConsumer, RowProcessingSta } } + public static PersistentAttributeInterceptor getAttributeInterceptor(Object entity) { + return asPersistentAttributeInterceptable( entity ).$$_hibernate_getInterceptor(); + } + @Override public String toString() { return "EntityJoinedFetchInitializer(" + LoggingHelper.toLoggableString( getNavigablePath() ) + ")"; } + @Override + public Object getResolvedInstanceNoProxy(EntityInitializerData data) { + return data.entityInstanceForNotify; + } + //######################### // For Hibernate Reactive //######################### diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntitySelectFetchInitializer.java b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntitySelectFetchInitializer.java index 2f2c1c0c758d..d9bbd27f8276 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntitySelectFetchInitializer.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/graph/entity/internal/EntitySelectFetchInitializer.java @@ -161,27 +161,44 @@ public void resolveInstance(Object instance, Data data) { data.setInstance( null ); } else { - final RowProcessingState rowProcessingState = data.getRowProcessingState(); - final LazyInitializer lazyInitializer = extractLazyInitializer( data.getInstance() ); + final var rowProcessingState = data.getRowProcessingState(); + final var session = rowProcessingState.getSession(); + final var persistenceContext = session.getPersistenceContextInternal(); + final LazyInitializer lazyInitializer = extractLazyInitializer( instance ); if ( lazyInitializer == null ) { data.setState( State.INITIALIZED ); - if ( keyIsEager ) { - data.entityIdentifier = concreteDescriptor.getIdentifier( instance, rowProcessingState.getSession() ); - } + data.entityIdentifier = concreteDescriptor.getIdentifier( instance, session ); } else if ( lazyInitializer.isUninitialized() ) { data.setState( State.RESOLVED ); - if ( keyIsEager ) { - data.entityIdentifier = lazyInitializer.getIdentifier(); - } + data.entityIdentifier = lazyInitializer.getInternalIdentifier(); } else { data.setState( State.INITIALIZED ); - if ( keyIsEager ) { - data.entityIdentifier = lazyInitializer.getIdentifier(); + data.entityIdentifier = lazyInitializer.getInternalIdentifier(); + } + + final var entityKey = new EntityKey( data.entityIdentifier, concreteDescriptor ); + final var entityHolder = persistenceContext.getEntityHolder( + entityKey + ); + + if ( entityHolder == null || entityHolder.getEntity() != instance && entityHolder.getProxy() != instance ) { + // the existing entity instance is detached or transient + if ( entityHolder != null ) { + final var managed = entityHolder.getManagedObject(); + data.setInstance( managed ); + data.entityIdentifier = entityHolder.getEntityKey().getIdentifier(); + data.setState( entityHolder.isInitialized() ? State.INITIALIZED : State.RESOLVED ); + } + else { + initialize( data, null, session, persistenceContext ); } } - data.setInstance( instance ); + else { + data.setInstance( instance ); + } + if ( keyIsEager ) { final Initializer initializer = keyAssembler.getInitializer(); assert initializer != null; @@ -206,10 +223,16 @@ public void initializeInstance(Data data) { protected void initialize(EntitySelectFetchInitializerData data) { final RowProcessingState rowProcessingState = data.getRowProcessingState(); final SharedSessionContractImplementor session = rowProcessingState.getSession(); + final PersistenceContext persistenceContext = session.getPersistenceContextInternal(); final EntityKey entityKey = new EntityKey( data.entityIdentifier, concreteDescriptor ); + initialize( data, persistenceContext.getEntityHolder( entityKey ), session, persistenceContext ); + } - final PersistenceContext persistenceContext = session.getPersistenceContextInternal(); - final EntityHolder holder = persistenceContext.getEntityHolder( entityKey ); + protected void initialize( + EntitySelectFetchInitializerData data, + @Nullable EntityHolder holder, + SharedSessionContractImplementor session, + PersistenceContext persistenceContext) { if ( holder != null ) { data.setInstance( persistenceContext.proxyFor( holder, concreteDescriptor ) ); if ( holder.getEntityInitializer() == null ) { @@ -256,8 +279,8 @@ else if ( data.getInstance() == null ) { } persistenceContext.claimEntityHolderIfPossible( new EntityKey( data.entityIdentifier, concreteDescriptor ), - instance, - rowProcessingState.getJdbcValuesSourceProcessingState(), + null, + data.getRowProcessingState().getJdbcValuesSourceProcessingState(), this ); } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/internal/ResultsHelper.java b/hibernate-core/src/main/java/org/hibernate/sql/results/internal/ResultsHelper.java index 41f984c3c2b4..101991d67aae 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/internal/ResultsHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/internal/ResultsHelper.java @@ -49,6 +49,7 @@ import org.hibernate.sql.results.spi.RowReader; import org.hibernate.sql.results.spi.RowTransformer; import org.hibernate.stat.spi.StatisticsImplementor; +import org.hibernate.type.EntityType; /** * @author Steve Ebersole @@ -204,7 +205,7 @@ private static void addCollectionToCache( ); boolean isPutFromLoad = true; - if ( collectionDescriptor.getElementType().isAssociationType() ) { + if ( collectionDescriptor.getElementType() instanceof EntityType ) { final EntityPersister entityPersister = ( (QueryableCollection) collectionDescriptor ).getElementPersister(); for ( Object id : entry.getState() ) { if ( persistenceContext.wasInsertedDuringTransaction( entityPersister, id ) ) { diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/jdbc/internal/DeferredResultSetAccess.java b/hibernate-core/src/main/java/org/hibernate/sql/results/jdbc/internal/DeferredResultSetAccess.java index 8965fb2603cd..dcf3e7ef7778 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/jdbc/internal/DeferredResultSetAccess.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/jdbc/internal/DeferredResultSetAccess.java @@ -17,6 +17,7 @@ import org.hibernate.dialect.Dialect; import org.hibernate.dialect.pagination.LimitHandler; import org.hibernate.dialect.pagination.NoopLimitHandler; +import org.hibernate.engine.jdbc.spi.JdbcCoordinator; import org.hibernate.engine.jdbc.spi.SqlStatementLogger; import org.hibernate.engine.spi.SessionEventListenerManager; import org.hibernate.engine.spi.SessionFactoryImplementor; @@ -324,8 +325,8 @@ protected LockMode determineFollowOnLockMode(LockOptions lockOptions) { @Override public void release() { - final LogicalConnectionImplementor logicalConnection = getPersistenceContext().getJdbcCoordinator() - .getLogicalConnection(); + final JdbcCoordinator jdbcCoordinator = getPersistenceContext().getJdbcCoordinator(); + final LogicalConnectionImplementor logicalConnection = jdbcCoordinator.getLogicalConnection(); if ( resultSet != null ) { logicalConnection.getResourceRegistry().release( resultSet, preparedStatement ); resultSet = null; @@ -334,9 +335,8 @@ public void release() { if ( preparedStatement != null ) { logicalConnection.getResourceRegistry().release( preparedStatement ); preparedStatement = null; + jdbcCoordinator.afterStatementExecution(); } - - logicalConnection.afterStatement(); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/jdbc/internal/JdbcValuesCacheHit.java b/hibernate-core/src/main/java/org/hibernate/sql/results/jdbc/internal/JdbcValuesCacheHit.java index 62662d8f5e28..a2902861ea7c 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/jdbc/internal/JdbcValuesCacheHit.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/jdbc/internal/JdbcValuesCacheHit.java @@ -176,12 +176,12 @@ public Object getCurrentRowValue(int valueIndex) { if ( valueIndexesToCacheIndexes == null ) { return ( (Object[]) row )[valueIndex]; } - else if ( row.getClass() != Object[].class ) { - assert valueIndexesToCacheIndexes[valueIndex] == 0; - return row; + else if ( row instanceof Object[] ) { + return ( (Object[]) row )[valueIndexesToCacheIndexes[valueIndex]]; } else { - return ( (Object[]) row )[valueIndexesToCacheIndexes[valueIndex]]; + assert valueIndexesToCacheIndexes[valueIndex] == 0; + return row; } } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/jdbc/internal/JdbcValuesMappingProducerProviderStandard.java b/hibernate-core/src/main/java/org/hibernate/sql/results/jdbc/internal/JdbcValuesMappingProducerProviderStandard.java index fa95470f6d4f..151bdb7fc1d8 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/jdbc/internal/JdbcValuesMappingProducerProviderStandard.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/jdbc/internal/JdbcValuesMappingProducerProviderStandard.java @@ -9,9 +9,15 @@ import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.query.results.ResultSetMapping; import org.hibernate.query.results.ResultSetMappingImpl; +import org.hibernate.sql.ast.spi.SqlSelection; +import org.hibernate.sql.ast.tree.select.QueryGroup; +import org.hibernate.sql.ast.tree.select.QueryPart; import org.hibernate.sql.ast.tree.select.SelectStatement; import org.hibernate.sql.results.jdbc.spi.JdbcValuesMappingProducer; import org.hibernate.sql.results.jdbc.spi.JdbcValuesMappingProducerProvider; +import org.hibernate.type.descriptor.jdbc.NullJdbcType; + +import java.util.List; /** * Standard JdbcValuesMappingProducerProvider implementation @@ -28,10 +34,20 @@ public class JdbcValuesMappingProducerProviderStandard implements JdbcValuesMapp public JdbcValuesMappingProducer buildMappingProducer( SelectStatement sqlAst, SessionFactoryImplementor sessionFactory) { - return new JdbcValuesMappingProducerStandard( - sqlAst.getQuerySpec().getSelectClause().getSqlSelections(), - sqlAst.getDomainResultDescriptors() - ); + return new JdbcValuesMappingProducerStandard( getSelections( sqlAst ), sqlAst.getDomainResultDescriptors() ); + } + + private static List getSelections(SelectStatement selectStatement) { + if ( selectStatement.getQueryPart() instanceof QueryGroup ) { + final QueryGroup queryGroup = (QueryGroup) selectStatement.getQueryPart(); + for ( QueryPart queryPart : queryGroup.getQueryParts() ) { + if ( !(queryPart.getFirstQuerySpec().getSelectClause().getSqlSelections() + .get( 0 ).getExpressionType().getSingleJdbcMapping().getJdbcType() instanceof NullJdbcType) ) { + return queryPart.getFirstQuerySpec().getSelectClause().getSqlSelections(); + } + } + } + return selectStatement.getQuerySpec().getSelectClause().getSqlSelections(); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/jdbc/internal/JdbcValuesMappingResolutionImpl.java b/hibernate-core/src/main/java/org/hibernate/sql/results/jdbc/internal/JdbcValuesMappingResolutionImpl.java index 6fc595bbca4e..5abda091a405 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/jdbc/internal/JdbcValuesMappingResolutionImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/jdbc/internal/JdbcValuesMappingResolutionImpl.java @@ -6,7 +6,7 @@ */ package org.hibernate.sql.results.jdbc.internal; -import java.util.ArrayList; +import java.util.LinkedHashSet; import org.hibernate.sql.results.graph.DomainResultAssembler; import org.hibernate.sql.results.graph.Initializer; @@ -39,7 +39,7 @@ private JdbcValuesMappingResolutionImpl( } private static Initializer[] getResultInitializers(DomainResultAssembler[] resultAssemblers) { - final ArrayList> initializers = new ArrayList<>( resultAssemblers.length ); + final LinkedHashSet> initializers = new LinkedHashSet<>( resultAssemblers.length ); for ( DomainResultAssembler resultAssembler : resultAssemblers ) { resultAssembler.forEachResultAssembler( (initializer, list) -> list.add( initializer ), initializers ); } diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/jdbc/internal/JdbcValuesResultSetImpl.java b/hibernate-core/src/main/java/org/hibernate/sql/results/jdbc/internal/JdbcValuesResultSetImpl.java index 715940b17d1d..724ded4d5f80 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/jdbc/internal/JdbcValuesResultSetImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/jdbc/internal/JdbcValuesResultSetImpl.java @@ -366,7 +366,7 @@ else if ( rowToCacheSize < 1 ) { } else { final Object[] rowToCache = new Object[rowToCacheSize]; - for ( int i = 0; i < currentRowJdbcValues.length; i++ ) { + for ( int i = 0; i < valueIndexesToCacheIndexes.length; i++ ) { final int cacheIndex = valueIndexesToCacheIndexes[i]; if ( cacheIndex != -1 ) { rowToCache[cacheIndex] = initializedIndexes.get( i ) ? currentRowJdbcValues[i] : null; diff --git a/hibernate-core/src/main/java/org/hibernate/sql/results/jdbc/internal/StandardJdbcValuesMapping.java b/hibernate-core/src/main/java/org/hibernate/sql/results/jdbc/internal/StandardJdbcValuesMapping.java index e76dba82f463..adfc3a5bc52d 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/results/jdbc/internal/StandardJdbcValuesMapping.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/results/jdbc/internal/StandardJdbcValuesMapping.java @@ -54,19 +54,23 @@ public StandardJdbcValuesMapping( List> domainResults) { this.sqlSelections = sqlSelections; this.domainResults = domainResults; - - final int rowSize = sqlSelections.size(); + // The native query might select more columns than the number of selections, + // so we need to compute the effective row size from the max values array position + int maxPosition = -1; + boolean needsResolve = false; + for ( SqlSelection sqlSelection : sqlSelections ) { + maxPosition = Math.max( maxPosition, sqlSelection.getValuesArrayPosition() ); + needsResolve = needsResolve + || sqlSelection instanceof SqlSelectionImpl && ( (SqlSelectionImpl) sqlSelection ).needsResolve(); + } + final int rowSize = maxPosition + 1; final BitSet valueIndexesToCache = new BitSet( rowSize ); for ( DomainResult domainResult : domainResults ) { domainResult.collectValueIndexesToCache( valueIndexesToCache ); } final int[] valueIndexesToCacheIndexes = new int[rowSize]; int cacheIndex = 0; - boolean needsResolve = false; - for ( int i = 0; i < valueIndexesToCacheIndexes.length; i++ ) { - final SqlSelection sqlSelection = sqlSelections.get( i ); - needsResolve = needsResolve - || sqlSelection instanceof SqlSelectionImpl && ( (SqlSelectionImpl) sqlSelection ).needsResolve(); + for ( int i = 0; i < rowSize; i++ ) { if ( valueIndexesToCache.get( i ) ) { valueIndexesToCacheIndexes[i] = cacheIndex++; } diff --git a/hibernate-core/src/main/java/org/hibernate/tool/schema/internal/SchemaCreatorImpl.java b/hibernate-core/src/main/java/org/hibernate/tool/schema/internal/SchemaCreatorImpl.java index 78ad8d519003..9afc9185e961 100644 --- a/hibernate-core/src/main/java/org/hibernate/tool/schema/internal/SchemaCreatorImpl.java +++ b/hibernate-core/src/main/java/org/hibernate/tool/schema/internal/SchemaCreatorImpl.java @@ -60,7 +60,9 @@ import static org.hibernate.cfg.AvailableSettings.HBM2DDL_IMPORT_FILES; import static org.hibernate.cfg.AvailableSettings.HBM2DDL_LOAD_SCRIPT_SOURCE; import static org.hibernate.cfg.AvailableSettings.JAKARTA_HBM2DDL_LOAD_SCRIPT_SOURCE; +import static org.hibernate.cfg.SchemaToolingSettings.HBM2DDL_SKIP_DEFAULT_IMPORT_FILE; import static org.hibernate.internal.util.collections.CollectionHelper.setOfSize; +import static org.hibernate.internal.util.config.ConfigurationHelper.getBoolean; import static org.hibernate.internal.util.config.ConfigurationHelper.getString; import static org.hibernate.tool.schema.internal.Helper.applyScript; import static org.hibernate.tool.schema.internal.Helper.applySqlStrings; @@ -597,11 +599,24 @@ private void applyImportSources( commandExtractor, dialect, formatter, - hasDefaultImportFileScriptBeenExecuted ? "" : DEFAULT_IMPORT_FILE, + hasDefaultImportFileScriptBeenExecuted ? "" : getDefaultImportFile( options ), targets ); } + private String getDefaultImportFile(ExecutionOptions options) { + if ( skipDefaultFileImport( options ) ) { + return ""; + } + else { + return DEFAULT_IMPORT_FILE; + } + } + + private static boolean skipDefaultFileImport(ExecutionOptions options) { + return getBoolean( HBM2DDL_SKIP_DEFAULT_IMPORT_FILE, options.getConfigurationValues(), false ); + } + /** * In principle, we should format the commands in the import script if the * {@code format} parameter is {@code true}, and since it's supposed to be @@ -642,16 +657,19 @@ private boolean applyImportScript( formatter, targets ); - return containsDefaultImportFile( importScriptInput ); + return containsDefaultImportFile( importScriptInput, options ); } else { return false; } } - private boolean containsDefaultImportFile(ScriptSourceInput importScriptInput) { + private boolean containsDefaultImportFile(ScriptSourceInput importScriptInput,ExecutionOptions options ) { + if ( skipDefaultFileImport( options ) ) { + return false; + } final URL defaultImportFileUrl = getClassLoaderService().locateResource( DEFAULT_IMPORT_FILE ); - return defaultImportFileUrl != null && importScriptInput.containsScript(defaultImportFileUrl); + return defaultImportFileUrl != null && importScriptInput.containsScript( defaultImportFileUrl ); } /** diff --git a/hibernate-core/src/main/java/org/hibernate/tuple/AbstractNonIdentifierAttribute.java b/hibernate-core/src/main/java/org/hibernate/tuple/AbstractNonIdentifierAttribute.java index 5183f8bdb25a..088ccae7978a 100644 --- a/hibernate-core/src/main/java/org/hibernate/tuple/AbstractNonIdentifierAttribute.java +++ b/hibernate-core/src/main/java/org/hibernate/tuple/AbstractNonIdentifierAttribute.java @@ -7,6 +7,7 @@ package org.hibernate.tuple; import org.hibernate.FetchMode; +import org.hibernate.annotations.OnDeleteAction; import org.hibernate.engine.spi.CascadeStyle; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.persister.walking.spi.AttributeSource; @@ -94,6 +95,11 @@ public CascadeStyle getCascadeStyle() { return attributeInformation.getCascadeStyle(); } + @Override + public OnDeleteAction getOnDeleteAction() { + return attributeInformation.getOnDeleteAction(); + } + @Override public FetchMode getFetchMode() { return attributeInformation.getFetchMode(); diff --git a/hibernate-core/src/main/java/org/hibernate/tuple/BaselineAttributeInformation.java b/hibernate-core/src/main/java/org/hibernate/tuple/BaselineAttributeInformation.java index fed498a92b28..b7b497c1d515 100644 --- a/hibernate-core/src/main/java/org/hibernate/tuple/BaselineAttributeInformation.java +++ b/hibernate-core/src/main/java/org/hibernate/tuple/BaselineAttributeInformation.java @@ -7,6 +7,7 @@ package org.hibernate.tuple; import org.hibernate.FetchMode; +import org.hibernate.annotations.OnDeleteAction; import org.hibernate.engine.spi.CascadeStyle; /** @@ -21,6 +22,7 @@ public class BaselineAttributeInformation { private final boolean nullable; private final boolean dirtyCheckable; private final boolean versionable; + private final OnDeleteAction onDeleteAction; private final CascadeStyle cascadeStyle; private final FetchMode fetchMode; @@ -32,6 +34,7 @@ public BaselineAttributeInformation( boolean dirtyCheckable, boolean versionable, CascadeStyle cascadeStyle, + OnDeleteAction onDeleteAction, FetchMode fetchMode) { this.lazy = lazy; this.insertable = insertable; @@ -40,6 +43,7 @@ public BaselineAttributeInformation( this.dirtyCheckable = dirtyCheckable; this.versionable = versionable; this.cascadeStyle = cascadeStyle; + this.onDeleteAction = onDeleteAction; this.fetchMode = fetchMode; } @@ -75,6 +79,10 @@ public FetchMode getFetchMode() { return fetchMode; } + public OnDeleteAction getOnDeleteAction() { + return onDeleteAction; + } + public static class Builder { private boolean lazy; private boolean insertable; @@ -83,6 +91,7 @@ public static class Builder { private boolean dirtyCheckable; private boolean versionable; private CascadeStyle cascadeStyle; + private OnDeleteAction onDeleteAction; private FetchMode fetchMode; public Builder setLazy(boolean lazy) { @@ -120,6 +129,11 @@ public Builder setCascadeStyle(CascadeStyle cascadeStyle) { return this; } + public Builder setOnDeleteAction(OnDeleteAction onDeleteAction) { + this.onDeleteAction = onDeleteAction; + return this; + } + public Builder setFetchMode(FetchMode fetchMode) { this.fetchMode = fetchMode; return this; @@ -134,6 +148,7 @@ public BaselineAttributeInformation createInformation() { dirtyCheckable, versionable, cascadeStyle, + onDeleteAction, fetchMode ); } diff --git a/hibernate-core/src/main/java/org/hibernate/tuple/NonIdentifierAttribute.java b/hibernate-core/src/main/java/org/hibernate/tuple/NonIdentifierAttribute.java index df3f580fc750..18de4823b048 100644 --- a/hibernate-core/src/main/java/org/hibernate/tuple/NonIdentifierAttribute.java +++ b/hibernate-core/src/main/java/org/hibernate/tuple/NonIdentifierAttribute.java @@ -7,6 +7,7 @@ package org.hibernate.tuple; import org.hibernate.FetchMode; +import org.hibernate.annotations.OnDeleteAction; import org.hibernate.engine.spi.CascadeStyle; /** @@ -34,5 +35,7 @@ public interface NonIdentifierAttribute extends Attribute { CascadeStyle getCascadeStyle(); + OnDeleteAction getOnDeleteAction(); + FetchMode getFetchMode(); } diff --git a/hibernate-core/src/main/java/org/hibernate/tuple/PropertyFactory.java b/hibernate-core/src/main/java/org/hibernate/tuple/PropertyFactory.java index c8272bcb17ea..e787729b22b8 100644 --- a/hibernate-core/src/main/java/org/hibernate/tuple/PropertyFactory.java +++ b/hibernate-core/src/main/java/org/hibernate/tuple/PropertyFactory.java @@ -20,8 +20,12 @@ import org.hibernate.tuple.entity.EntityBasedBasicAttribute; import org.hibernate.tuple.entity.EntityBasedCompositionAttribute; import org.hibernate.tuple.entity.VersionProperty; +import org.hibernate.type.AnyType; import org.hibernate.type.AssociationType; +import org.hibernate.type.CollectionType; +import org.hibernate.type.ComponentType; import org.hibernate.type.CompositeType; +import org.hibernate.type.EntityType; import org.hibernate.type.Type; /** @@ -97,6 +101,7 @@ public static VersionProperty buildVersionProperty( .setDirtyCheckable( property.isUpdateable() && !lazy ) .setVersionable( property.isOptimisticLocked() ) .setCascadeStyle( property.getCascadeStyle() ) + .setOnDeleteAction( property.getOnDeleteAction() ) .createInformation() ); } @@ -167,6 +172,7 @@ public static NonIdentifierAttribute buildEntityBasedAttribute( .setDirtyCheckable( alwaysDirtyCheck || property.isUpdateable() ) .setVersionable( property.isOptimisticLocked() ) .setCascadeStyle( property.getCascadeStyle() ) + .setOnDeleteAction( property.getOnDeleteAction() ) .setFetchMode( property.getValue().getFetchMode() ) .createInformation() ); @@ -186,6 +192,7 @@ public static NonIdentifierAttribute buildEntityBasedAttribute( .setDirtyCheckable( alwaysDirtyCheck || property.isUpdateable() ) .setVersionable( property.isOptimisticLocked() ) .setCascadeStyle( property.getCascadeStyle() ) + .setOnDeleteAction( property.getOnDeleteAction() ) .setFetchMode( property.getValue().getFetchMode() ) .createInformation() ); @@ -207,6 +214,7 @@ public static NonIdentifierAttribute buildEntityBasedAttribute( .setDirtyCheckable( alwaysDirtyCheck || property.isUpdateable() ) .setVersionable( property.isOptimisticLocked() ) .setCascadeStyle( property.getCascadeStyle() ) + .setOnDeleteAction( property.getOnDeleteAction() ) .setFetchMode( property.getValue().getFetchMode() ) .createInformation() ); @@ -218,22 +226,19 @@ public static NonIdentifierAttribute buildEntityBasedAttribute( } private static NonIdentifierAttributeNature decode(Type type) { - if ( type.isAssociationType() ) { - - if ( type.isComponentType() ) { - // an any type is both an association and a composite... - return NonIdentifierAttributeNature.ANY; - } - - return type.isCollectionType() - ? NonIdentifierAttributeNature.COLLECTION - : NonIdentifierAttributeNature.ENTITY; + if ( type instanceof CollectionType ) { + return NonIdentifierAttributeNature.COLLECTION; + } + else if ( type instanceof EntityType ) { + return NonIdentifierAttributeNature.ENTITY; + } + else if ( type instanceof AnyType ) { + return NonIdentifierAttributeNature.ANY; + } + else if ( type instanceof ComponentType ) { + return NonIdentifierAttributeNature.COMPOSITE; } else { - if ( type.isComponentType() ) { - return NonIdentifierAttributeNature.COMPOSITE; - } - return NonIdentifierAttributeNature.BASIC; } } diff --git a/hibernate-core/src/main/java/org/hibernate/tuple/StandardProperty.java b/hibernate-core/src/main/java/org/hibernate/tuple/StandardProperty.java index dd672af71e59..cdc25d1a802e 100644 --- a/hibernate-core/src/main/java/org/hibernate/tuple/StandardProperty.java +++ b/hibernate-core/src/main/java/org/hibernate/tuple/StandardProperty.java @@ -7,6 +7,7 @@ package org.hibernate.tuple; import org.hibernate.FetchMode; +import org.hibernate.annotations.OnDeleteAction; import org.hibernate.engine.spi.CascadeStyle; import org.hibernate.type.Type; @@ -39,6 +40,7 @@ public StandardProperty( boolean checkable, boolean versionable, CascadeStyle cascadeStyle, + OnDeleteAction onDeleteAction, FetchMode fetchMode) { super( null, @@ -54,6 +56,7 @@ public StandardProperty( .setDirtyCheckable( checkable ) .setVersionable( versionable ) .setCascadeStyle( cascadeStyle ) + .setOnDeleteAction( onDeleteAction ) .setFetchMode( fetchMode ) .createInformation() ); diff --git a/hibernate-core/src/main/java/org/hibernate/tuple/entity/CompositeGeneratorBuilder.java b/hibernate-core/src/main/java/org/hibernate/tuple/entity/CompositeGeneratorBuilder.java index c5e2afddcfc2..455426f6abf6 100644 --- a/hibernate-core/src/main/java/org/hibernate/tuple/entity/CompositeGeneratorBuilder.java +++ b/hibernate-core/src/main/java/org/hibernate/tuple/entity/CompositeGeneratorBuilder.java @@ -171,7 +171,7 @@ public Object generate(SharedSessionContractImplementor session, Object owner, O final Object[] generatedValues = new Object[size]; for ( int i = 0; i < size; i++ ) { final Generator generator = generators.get(i); - if ( generator != null ) { + if ( generator != null && generator.getEventTypes().contains( eventType ) ) { generatedValues[i] = ((BeforeExecutionGenerator) generator) .generate( session, owner, null, eventType ); } @@ -182,7 +182,7 @@ public Object generate(SharedSessionContractImplementor session, Object owner, O else { for ( int i = 0; i < size; i++ ) { final Generator generator = generators.get(i); - if ( generator != null ) { + if ( generator != null && generator.getEventTypes().contains( eventType ) ) { final Object value = descriptor.getValue( currentValue, i ); final Object generatedValue = ((BeforeExecutionGenerator) generator) .generate( session, owner, value, eventType ); diff --git a/hibernate-core/src/main/java/org/hibernate/tuple/entity/EntityMetamodel.java b/hibernate-core/src/main/java/org/hibernate/tuple/entity/EntityMetamodel.java index adcdc3b5cc91..534437680543 100644 --- a/hibernate-core/src/main/java/org/hibernate/tuple/entity/EntityMetamodel.java +++ b/hibernate-core/src/main/java/org/hibernate/tuple/entity/EntityMetamodel.java @@ -19,6 +19,7 @@ import org.hibernate.HibernateException; import org.hibernate.MappingException; import org.hibernate.annotations.NotFoundAction; +import org.hibernate.annotations.OnDeleteAction; import org.hibernate.boot.spi.MetadataImplementor; import org.hibernate.bytecode.enhance.spi.interceptor.EnhancementHelper; import org.hibernate.bytecode.internal.BytecodeEnhancementMetadataNonPojoImpl; @@ -55,8 +56,11 @@ import org.hibernate.type.CompositeType; import org.hibernate.type.EntityType; import org.hibernate.type.ManyToOneType; +import org.hibernate.type.OneToOneType; import org.hibernate.type.Type; +import org.checkerframework.checker.nullness.qual.Nullable; + import static java.util.Collections.singleton; import static org.hibernate.internal.CoreLogging.messageLogger; import static org.hibernate.internal.util.ReflectHelper.isAbstractClass; @@ -64,6 +68,7 @@ import static org.hibernate.internal.util.collections.ArrayHelper.toIntArray; import static org.hibernate.internal.util.collections.CollectionHelper.toSmallMap; import static org.hibernate.internal.util.collections.CollectionHelper.toSmallSet; +import static org.hibernate.tuple.PropertyFactory.buildIdentifierAttribute; /** * Centralizes metamodel information about an entity. @@ -95,6 +100,7 @@ public class EntityMetamodel implements Serializable { // temporary ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ private final String[] propertyNames; private final Type[] propertyTypes; + private final @Nullable Type[] dirtyCheckablePropertyTypes; private final boolean[] propertyLaziness; private final boolean[] propertyUpdateability; private final boolean[] nonlazyPropertyUpdateability; @@ -102,6 +108,7 @@ public class EntityMetamodel implements Serializable { private final boolean[] propertyInsertability; private final boolean[] propertyNullability; private final boolean[] propertyVersionability; + private final OnDeleteAction[] propertyOnDeleteActions; private final CascadeStyle[] cascadeStyles; // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -170,21 +177,19 @@ public EntityMetamodel( subclassId = persistentClass.getSubclassId(); - identifierAttribute = PropertyFactory.buildIdentifierAttribute( - persistentClass, - sessionFactory.getGenerator( rootName ) - ); + final Generator idgenerator = sessionFactory.getGenerator( rootName ); + identifierAttribute = buildIdentifierAttribute( persistentClass, idgenerator ); versioned = persistentClass.isVersioned(); final boolean collectionsInDefaultFetchGroupEnabled = creationContext.getSessionFactoryOptions().isCollectionsInDefaultFetchGroupEnabled(); + final boolean supportsCascadeDelete = creationContext.getDialect().supportsCascadeDelete(); if ( persistentClass.hasPojoRepresentation() ) { final Component identifierMapperComponent = persistentClass.getIdentifierMapper(); final CompositeType nonAggregatedCidMapper; final Set idAttributeNames; - if ( identifierMapperComponent != null ) { nonAggregatedCidMapper = (CompositeType) identifierMapperComponent.getType(); idAttributeNames = new HashSet<>( ); @@ -217,6 +222,7 @@ public EntityMetamodel( // temporary ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ propertyNames = new String[propertySpan]; propertyTypes = new Type[propertySpan]; + dirtyCheckablePropertyTypes = new Type[propertySpan]; propertyUpdateability = new boolean[propertySpan]; propertyInsertability = new boolean[propertySpan]; nonlazyPropertyUpdateability = new boolean[propertySpan]; @@ -224,6 +230,7 @@ public EntityMetamodel( propertyNullability = new boolean[propertySpan]; propertyVersionability = new boolean[propertySpan]; propertyLaziness = new boolean[propertySpan]; + propertyOnDeleteActions = new OnDeleteAction[propertySpan]; cascadeStyles = new CascadeStyle[propertySpan]; // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -306,6 +313,9 @@ public EntityMetamodel( propertyNames[i] = attribute.getName(); final Type propertyType = attribute.getType(); propertyTypes[i] = propertyType; + if ( attribute.isDirtyCheckable() && !( propertyType instanceof OneToOneType ) ) { + dirtyCheckablePropertyTypes[i] = propertyType; + } propertyNullability[i] = attribute.isNullable(); propertyUpdateability[i] = attribute.isUpdateable(); propertyInsertability[i] = attribute.isInsertable(); @@ -313,7 +323,7 @@ public EntityMetamodel( nonlazyPropertyUpdateability[i] = attribute.isUpdateable() && !lazy; propertyCheckability[i] = propertyUpdateability[i] || propertyType.isAssociationType() && ( (AssociationType) propertyType ).isAlwaysDirtyChecked(); - + propertyOnDeleteActions[i] = supportsCascadeDelete ? attribute.getOnDeleteAction() : null; cascadeStyles[i] = attribute.getCascadeStyle(); // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -589,7 +599,7 @@ public Set getSubclassEntityNames() { } private static boolean indicatesCollection(Type type) { - if ( type.isCollectionType() ) { + if ( type instanceof CollectionType ) { return true; } else if ( type.isComponentType() ) { @@ -604,7 +614,7 @@ else if ( type.isComponentType() ) { } private static boolean indicatesOwnedCollection(Type type, MetadataImplementor metadata) { - if ( type.isCollectionType() ) { + if ( type instanceof CollectionType ) { final CollectionType collectionType = (CollectionType) type; return !metadata.getCollectionBinding( collectionType.getRole() ).isInverse(); } @@ -794,6 +804,10 @@ public Type[] getPropertyTypes() { return propertyTypes; } + public @Nullable Type[] getDirtyCheckablePropertyTypes() { + return dirtyCheckablePropertyTypes; + } + public boolean[] getPropertyLaziness() { return propertyLaziness; } @@ -852,4 +866,8 @@ public boolean isInstrumented() { public BytecodeEnhancementMetadata getBytecodeEnhancementMetadata() { return bytecodeEnhancementMetadata; } + + public OnDeleteAction[] getPropertyOnDeleteActions() { + return propertyOnDeleteActions; + } } diff --git a/hibernate-core/src/main/java/org/hibernate/type/AbstractType.java b/hibernate-core/src/main/java/org/hibernate/type/AbstractType.java index d67d3026ec96..2d9e196e121d 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/AbstractType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/AbstractType.java @@ -126,7 +126,8 @@ public Object replace( } private boolean needsReplacement(ForeignKeyDirection foreignKeyDirection) { - if ( isAssociationType() ) { + // Collection and OneToOne are the only associations that could be TO_PARENT + if ( this instanceof CollectionType || this instanceof OneToOneType ) { final AssociationType associationType = (AssociationType) this; return associationType.getForeignKeyDirection() == foreignKeyDirection; } diff --git a/hibernate-core/src/main/java/org/hibernate/type/AnyType.java b/hibernate-core/src/main/java/org/hibernate/type/AnyType.java index 67fc1cb09c55..64bfa3f109d2 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/AnyType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/AnyType.java @@ -21,6 +21,7 @@ import org.hibernate.MappingException; import org.hibernate.PropertyNotFoundException; import org.hibernate.TransientObjectException; +import org.hibernate.annotations.OnDeleteAction; import org.hibernate.bytecode.enhance.spi.LazyPropertyInitializer; import org.hibernate.engine.spi.CascadeStyle; import org.hibernate.engine.spi.CascadeStyles; @@ -149,7 +150,7 @@ private Object extractIdentifier(Object entity, SessionFactoryImplementor factor final EntityPersister concretePersister = guessEntityPersister( entity, factory ); return concretePersister == null ? null - : concretePersister.getIdentifier( entity, null ); + : concretePersister.getIdentifier( entity, (SharedSessionContractImplementor) null ); } private EntityPersister guessEntityPersister(Object object, SessionFactoryImplementor factory) { @@ -410,6 +411,11 @@ public CascadeStyle getCascadeStyle(int i) { return CascadeStyles.NONE; } + @Override + public OnDeleteAction getOnDeleteAction(int index) { + return OnDeleteAction.NO_ACTION; + } + @Override public FetchMode getFetchMode(int i) { return FetchMode.SELECT; diff --git a/hibernate-core/src/main/java/org/hibernate/type/BasicArrayType.java b/hibernate-core/src/main/java/org/hibernate/type/BasicArrayType.java index a2bf4c36a264..b19df61aded6 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/BasicArrayType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/BasicArrayType.java @@ -28,7 +28,28 @@ public class BasicArrayType public BasicArrayType(BasicType baseDescriptor, JdbcType arrayJdbcType, JavaType arrayTypeDescriptor) { super( arrayJdbcType, arrayTypeDescriptor ); this.baseDescriptor = baseDescriptor; - this.name = baseDescriptor.getName() + "[]"; + this.name = determineArrayTypeName( baseDescriptor ); + } + + static String determineElementTypeName(BasicType baseDescriptor) { + final String elementName = baseDescriptor.getName(); + switch ( elementName ) { + case "boolean": + case "byte": + case "char": + case "short": + case "int": + case "long": + case "float": + case "double": + return Character.toUpperCase( elementName.charAt( 0 ) ) + elementName.substring( 1 ); + default: + return elementName; + } + } + + static String determineArrayTypeName(BasicType baseDescriptor) { + return determineElementTypeName( baseDescriptor ) + "[]"; } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/type/BasicCollectionType.java b/hibernate-core/src/main/java/org/hibernate/type/BasicCollectionType.java index bd40f9e17ff2..f86771705457 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/BasicCollectionType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/BasicCollectionType.java @@ -14,6 +14,8 @@ import org.hibernate.type.descriptor.jdbc.JdbcType; import org.hibernate.type.descriptor.jdbc.JdbcTypeIndicators; +import static org.hibernate.type.BasicArrayType.determineElementTypeName; + /** * A type that maps between {@link java.sql.Types#ARRAY ARRAY} and {@code Collection} * @@ -36,18 +38,19 @@ public BasicCollectionType( } private static String determineName(BasicCollectionJavaType collectionTypeDescriptor, BasicType baseDescriptor) { + final String elementTypeName = determineElementTypeName( baseDescriptor ); switch ( collectionTypeDescriptor.getSemantics().getCollectionClassification() ) { case BAG: case ID_BAG: - return "Collection<" + baseDescriptor.getName() + ">"; + return "Collection<" + elementTypeName + ">"; case LIST: - return "List<" + baseDescriptor.getName() + ">"; + return "List<" + elementTypeName + ">"; case SET: - return "Set<" + baseDescriptor.getName() + ">"; + return "Set<" + elementTypeName + ">"; case SORTED_SET: - return "SortedSet<" + baseDescriptor.getName() + ">"; + return "SortedSet<" + elementTypeName + ">"; case ORDERED_SET: - return "OrderedSet<" + baseDescriptor.getName() + ">"; + return "OrderedSet<" + elementTypeName + ">"; } return null; } diff --git a/hibernate-core/src/main/java/org/hibernate/type/BasicTypeRegistry.java b/hibernate-core/src/main/java/org/hibernate/type/BasicTypeRegistry.java index d994435cb1c7..3ec99729df2d 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/BasicTypeRegistry.java +++ b/hibernate-core/src/main/java/org/hibernate/type/BasicTypeRegistry.java @@ -13,6 +13,7 @@ import org.hibernate.HibernateException; import org.hibernate.Internal; +import org.hibernate.MappingException; import org.hibernate.internal.CoreLogging; import org.hibernate.internal.CoreMessageLogger; import org.hibernate.internal.util.StringHelper; @@ -68,59 +69,46 @@ private BasicType resolveTypeReference(String name) { if ( typeReference == null ) { return null; } - if ( !name.equals( typeReference.getName() ) ) { + else if ( !name.equals( typeReference.getName() ) ) { final BasicType basicType = typesByName.get( typeReference.getName() ); if ( basicType != null ) { return basicType; } } - final JavaType javaType = typeConfiguration.getJavaTypeRegistry().getDescriptor( - typeReference.getBindableJavaType() - ); - final JdbcType jdbcType = typeConfiguration.getJdbcTypeRegistry().getDescriptor( - typeReference.getSqlTypeCode() - ); - final BasicType type; + + return createBasicType( name, typeReference ); + } + + private BasicType createBasicType(String name, BasicTypeReference typeReference) { + final JavaType javaType = + typeConfiguration.getJavaTypeRegistry() + .getDescriptor( typeReference.getBindableJavaType() ); + final JdbcType jdbcType = + typeConfiguration.getJdbcTypeRegistry() + .getDescriptor( typeReference.getSqlTypeCode() ); + final BasicType createdType = createBasicType( typeReference, javaType, jdbcType ); + primeRegistryEntry( createdType ); + typesByName.put( typeReference.getName(), createdType ); + typesByName.put( name, createdType ); + return createdType; + } + + private static BasicType createBasicType( + BasicTypeReference typeReference, JavaType javaType, JdbcType jdbcType) { + final String name = typeReference.getName(); if ( typeReference.getConverter() == null ) { - if ( typeReference.isForceImmutable() ) { - type = new ImmutableNamedBasicTypeImpl<>( - javaType, - jdbcType, - typeReference.getName() - ); - } - else { - type = new NamedBasicTypeImpl<>( - javaType, - jdbcType, - typeReference.getName() - ); - } + return typeReference.isForceImmutable() + ? new ImmutableNamedBasicTypeImpl<>( javaType, jdbcType, name ) + : new NamedBasicTypeImpl<>( javaType, jdbcType, name ); } else { - //noinspection unchecked - final BasicValueConverter converter = (BasicValueConverter) typeReference.getConverter(); + final BasicValueConverter converter = typeReference.getConverter(); assert javaType == converter.getDomainJavaType(); - if ( typeReference.isForceImmutable() ) { - type = new CustomMutabilityConvertedBasicTypeImpl<>( - typeReference.getName(), - jdbcType, - converter, - ImmutableMutabilityPlan.instance() - ); - } - else { - type = new ConvertedBasicTypeImpl<>( - typeReference.getName(), - jdbcType, - converter - ); - } + return typeReference.isForceImmutable() + ? new CustomMutabilityConvertedBasicTypeImpl<>( name, jdbcType, converter, + ImmutableMutabilityPlan.instance() ) + : new ConvertedBasicTypeImpl<>( name, jdbcType, converter ); } - primeRegistryEntry( type ); - typesByName.put( typeReference.getName(), type ); - typesByName.put( name, type ); - return type; } public BasicType getRegisteredType(java.lang.reflect.Type javaType) { @@ -144,10 +132,7 @@ public BasicType resolve(java.lang.reflect.Type javaType, int sqlTypeCode } public BasicType resolve(JavaType javaType, int sqlTypeCode) { - return resolve( - javaType, - typeConfiguration.getJdbcTypeRegistry().getDescriptor( sqlTypeCode ) - ); + return resolve( javaType, typeConfiguration.getJdbcTypeRegistry().getDescriptor( sqlTypeCode ) ); } /** @@ -158,38 +143,44 @@ public BasicType resolve(JavaType javaType, JdbcType jdbcType) { return resolve( javaType, jdbcType, - () -> { - if ( javaType instanceof BasicPluralJavaType && jdbcType instanceof ArrayJdbcType ) { - //noinspection unchecked - final BasicPluralJavaType pluralJavaType = (BasicPluralJavaType) javaType; - final BasicType elementType = resolve( - pluralJavaType.getElementJavaType(), - ( (ArrayJdbcType) jdbcType ).getElementJdbcType() - ); - final BasicType resolvedType = pluralJavaType.resolveType( - typeConfiguration, - typeConfiguration.getCurrentBaseSqlTypeIndicators().getDialect(), - elementType, - null, - typeConfiguration.getCurrentBaseSqlTypeIndicators() - ); - if ( resolvedType instanceof BasicPluralType ) { - register( resolvedType ); - } - //noinspection unchecked - return (BasicType) resolvedType; - } - return new BasicTypeImpl<>( javaType, jdbcType ); - } + () -> resolvedType( javaType, jdbcType ) ); } - public BasicType resolve(JavaType javaType, JdbcType jdbcType, String baseTypeName) { - return resolve( - javaType, - jdbcType, - () -> new NamedBasicTypeImpl<>( javaType, jdbcType, baseTypeName ) + private BasicType resolvedType(JavaType javaType, JdbcType jdbcType) { + if ( javaType instanceof BasicPluralJavaType && jdbcType instanceof ArrayJdbcType ) { + //noinspection unchecked + return (BasicType) resolvedType( (ArrayJdbcType) jdbcType, (BasicPluralJavaType) javaType ); + } + else { + return new BasicTypeImpl<>( javaType, jdbcType ); + } + } + + private BasicType resolvedType(ArrayJdbcType arrayType, BasicPluralJavaType castPluralJavaType) { + final BasicType elementType = resolve( castPluralJavaType.getElementJavaType(), arrayType.getElementJdbcType() ); + final BasicType resolvedType = castPluralJavaType.resolveType( + typeConfiguration, + typeConfiguration.getCurrentBaseSqlTypeIndicators().getDialect(), + elementType, + null, + typeConfiguration.getCurrentBaseSqlTypeIndicators() ); + if ( resolvedType instanceof BasicPluralType ) { + register( resolvedType ); + } + else if ( resolvedType == null ) { + final Class elementJavaTypeClass = elementType.getJavaTypeDescriptor().getJavaTypeClass(); + if ( elementJavaTypeClass != null && elementJavaTypeClass.isArray() && elementJavaTypeClass != byte[].class ) { + // No support for nested arrays, except for byte[][] + throw new MappingException( "Nested arrays (with the exception of byte[][]) are not supported" ); + } + } + return resolvedType; + } + + public BasicType resolve(JavaType javaType, JdbcType jdbcType, String baseTypeName) { + return resolve( javaType, jdbcType, () -> new NamedBasicTypeImpl<>( javaType, jdbcType, baseTypeName ) ); } /** @@ -197,34 +188,48 @@ public BasicType resolve(JavaType javaType, JdbcType jdbcType, String * JdbcType combo or create (and register) one. */ public BasicType resolve(JavaType javaType, JdbcType jdbcType, Supplier> creator) { - final Map, BasicType> typeByJavaTypeForJdbcType = registryValues.computeIfAbsent( - jdbcType, - key -> new ConcurrentHashMap<>() - ); + final BasicType registeredBasicType = registryForJdbcType( jdbcType ).get( javaType ); + //noinspection unchecked + return registeredBasicType != null + ? (BasicType) registeredBasicType + : createIfUnregistered( javaType, jdbcType, creator ); + } - final BasicType foundBasicType = typeByJavaTypeForJdbcType.get( javaType ); - if ( foundBasicType != null ) { - //noinspection unchecked - return (BasicType) foundBasicType; - } + private BasicType createIfUnregistered( + JavaType javaType, + JdbcType jdbcType, + Supplier> creator) { // Before simply creating the type, we try to find if there is a registered type for this java type, // and if so, if the jdbc type descriptor matches. Unless it does, we at least reuse the name final BasicType registeredType = getRegisteredType( javaType.getJavaType() ); - if ( registeredType != null && registeredType.getJdbcType() == jdbcType && registeredType.getMappedJavaType() == javaType ) { + if ( registeredTypeMatches( javaType, jdbcType, registeredType ) ) { return registeredType; } - final BasicType createdBasicType = creator.get(); - typeByJavaTypeForJdbcType.put( javaType, createdBasicType ); - - // if we are still building mappings, register this ad-hoc type - // via a unique code. this is to support envers - try { - typeConfiguration.getMetadataBuildingContext().getBootstrapContext() - .registerAdHocBasicType( createdBasicType ); + else { + final BasicType createdType = creator.get(); + register( javaType, jdbcType, createdType ); + return createdType; } - catch (Exception ignore) { + } + + private static boolean registeredTypeMatches(JavaType javaType, JdbcType jdbcType, BasicType registeredType) { + return registeredType != null + && registeredType.getJdbcType() == jdbcType + && registeredType.getMappedJavaType() == javaType; + } + + private void register(JavaType javaType, JdbcType jdbcType, BasicType createdType) { + if ( createdType != null ) { + registryForJdbcType( jdbcType ).put( javaType, createdType ); + // if we are still building mappings, register this adhoc + // type via a unique code. (This is to support Envers.) + try { + typeConfiguration.getMetadataBuildingContext().getBootstrapContext() + .registerAdHocBasicType( createdType ); + } + catch (Exception ignore) { + } } - return createdBasicType; } @@ -260,16 +265,12 @@ public void register(BasicType type, String... keys) { } private void applyOrOverwriteEntry(BasicType type) { - final Map, BasicType> typeByJavaTypeForJdbcType = registryValues.computeIfAbsent( - type.getJdbcType(), - jdbcType -> new ConcurrentHashMap<>() - ); - - final BasicType existing = typeByJavaTypeForJdbcType.put( type.getMappedJavaType(), type ); + final JdbcType jdbcType = type.getJdbcType(); + final BasicType existing = registryForJdbcType( jdbcType ).put( type.getMappedJavaType(), type ); if ( existing != null ) { LOG.debugf( "BasicTypeRegistry registration overwritten (%s + %s); previous =`%s`", - type.getJdbcType().getFriendlyName(), + jdbcType.getFriendlyName(), type.getJavaTypeDescriptor(), existing ); @@ -360,26 +361,25 @@ public void addPrimeEntry(BasicTypeReference type, String legacyTypeClassName } private void primeRegistryEntry(BasicType type) { - final Map, BasicType> typeByJavaTypeForJdbcType = registryValues.computeIfAbsent( - type.getJdbcType(), - jdbcType -> new ConcurrentHashMap<>() - ); - - final BasicType existing = typeByJavaTypeForJdbcType.get( type.getMappedJavaType() ); - + final JdbcType jdbcType = type.getJdbcType(); + final BasicType existing = registryForJdbcType( jdbcType ).get( type.getMappedJavaType() ); if ( existing != null ) { LOG.debugf( "Skipping registration of BasicType (%s + %s); still priming. existing = %s", - type.getJdbcType().getFriendlyName(), + jdbcType.getFriendlyName(), type.getJavaTypeDescriptor(), existing ); } else { - typeByJavaTypeForJdbcType.put( type.getMappedJavaType(), type ); + registryForJdbcType( jdbcType ).put( type.getMappedJavaType(), type ); } } + private Map, BasicType> registryForJdbcType(JdbcType jdbcType) { + return registryValues.computeIfAbsent( jdbcType, key -> new ConcurrentHashMap<>() ); + } + private void applyRegistrationKeys(BasicType type, String[] keys) { for ( String key : keys ) { // be safe... diff --git a/hibernate-core/src/main/java/org/hibernate/type/CollectionType.java b/hibernate-core/src/main/java/org/hibernate/type/CollectionType.java index 2e6e1253958f..ef2c37f24262 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/CollectionType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/CollectionType.java @@ -25,6 +25,7 @@ import org.hibernate.Internal; import org.hibernate.MappingException; import org.hibernate.collection.spi.AbstractPersistentCollection; +import org.hibernate.collection.spi.PersistentArrayHolder; import org.hibernate.collection.spi.PersistentCollection; import org.hibernate.engine.spi.CollectionEntry; import org.hibernate.engine.spi.CollectionKey; @@ -469,7 +470,7 @@ public String getAssociatedEntityName(SessionFactoryImplementor factory) QueryableCollection collectionPersister = (QueryableCollection) factory.getRuntimeMetamodels().getMappingMetamodel().getCollectionDescriptor( role ); - if ( !collectionPersister.getElementType().isEntityType() ) { + if ( !( collectionPersister.getElementType() instanceof EntityType ) ) { throw new MappingException( "collection was not an association: " + collectionPersister.getRole() @@ -638,6 +639,39 @@ public Object replace( final Object owner, final Map copyCache) throws HibernateException { if ( original == null ) { + if ( target == null ) { + return null; + } + if ( target instanceof Collection ) { + ( (Collection) target ).clear(); + return target; + } + else if ( target instanceof Map ) { + ( (Map) target ).clear(); + return target; + } + else { + final PersistenceContext persistenceContext = session.getPersistenceContext(); + final PersistentCollection collectionHolder = persistenceContext + .getCollectionHolder( target ); + if ( collectionHolder != null ) { + if ( collectionHolder instanceof PersistentArrayHolder ) { + PersistentArrayHolder persistentArrayHolder = (PersistentArrayHolder) collectionHolder; + persistenceContext.removeCollectionHolder( target ); + persistentArrayHolder.beginRead(); + persistentArrayHolder.injectLoadedState( + persistenceContext.getCollectionEntry( collectionHolder ) + .getLoadedPersister() + .getAttributeMapping(), null + ); + persistentArrayHolder.endRead(); + persistentArrayHolder.dirty(); + persistenceContext.addCollectionHolder( collectionHolder ); + return persistentArrayHolder.getArray(); + } + } + } + return null; } if ( !Hibernate.isInitialized( original ) ) { diff --git a/hibernate-core/src/main/java/org/hibernate/type/ComponentType.java b/hibernate-core/src/main/java/org/hibernate/type/ComponentType.java index b9703191657f..75f88a927324 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/ComponentType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/ComponentType.java @@ -20,6 +20,7 @@ import org.hibernate.PropertyNotFoundException; import org.hibernate.Remove; import org.hibernate.boot.spi.MetadataBuildingContext; +import org.hibernate.annotations.OnDeleteAction; import org.hibernate.bytecode.enhance.spi.LazyPropertyInitializer; import org.hibernate.engine.spi.CascadeStyle; import org.hibernate.engine.spi.CascadeStyles; @@ -63,6 +64,7 @@ public class ComponentType extends AbstractType implements CompositeTypeImplemen private final int[] originalPropertyOrder; protected final int propertySpan; private final CascadeStyle[] cascade; + private final OnDeleteAction[] onDeleteAction; private final FetchMode[] joinedFetch; private final int discriminatorColumnSpan; @@ -92,11 +94,18 @@ public ComponentType(Component component, int[] originalPropertyOrder, boolean m this.propertySpan = component.getPropertySpan(); this.originalPropertyOrder = originalPropertyOrder; final Value discriminator = component.getDiscriminator(); - this.propertyNames = new String[propertySpan + ( component.isPolymorphic() ? 1 : 0 )]; - this.propertyTypes = new Type[propertySpan + ( component.isPolymorphic() ? 1 : 0 )]; - this.propertyNullability = new boolean[propertySpan + ( component.isPolymorphic() ? 1 : 0 )]; - this.cascade = new CascadeStyle[propertySpan + ( component.isPolymorphic() ? 1 : 0 )]; - this.joinedFetch = new FetchMode[propertySpan + ( component.isPolymorphic() ? 1 : 0 )]; + final int length = propertySpan + (component.isPolymorphic() ? 1 : 0); + this.propertyNames = new String[length]; + this.propertyTypes = new Type[length]; + this.propertyNullability = new boolean[length]; + this.cascade = new CascadeStyle[length]; + this.onDeleteAction = new OnDeleteAction[length]; + this.joinedFetch = new FetchMode[length]; + + final boolean supportsCascadeDelete = + component.getBuildingContext().getMetadataCollector() + .getDatabase().getDialect() + .supportsCascadeDelete(); int i = 0; for ( Property property : component.getProperties() ) { @@ -105,6 +114,7 @@ public ComponentType(Component component, int[] originalPropertyOrder, boolean m this.propertyNullability[i] = property.isOptional(); this.cascade[i] = property.getCascadeStyle(); this.joinedFetch[i] = property.getValue().getFetchMode(); + onDeleteAction[i] = supportsCascadeDelete ? property.getOnDeleteAction() : null; if ( !property.isOptional() ) { hasNotNullProperty = true; } @@ -440,7 +450,7 @@ public Object[] getPropertyValues(Object component, SharedSessionContractImpleme @Override public Object[] getPropertyValues(Object component) { if (component == null) { - return new Object[propertySpan]; + return new Object[propertySpan + discriminatorColumnSpan]; } else if ( component instanceof Object[] ) { // A few calls to hashCode pass the property values already in an @@ -594,6 +604,11 @@ public CascadeStyle getCascadeStyle(int i) { return cascade[i]; } + @Override + public OnDeleteAction getOnDeleteAction(int i) { + return onDeleteAction[i]; + } + @Override public boolean isMutable() { return mutable; diff --git a/hibernate-core/src/main/java/org/hibernate/type/CompositeType.java b/hibernate-core/src/main/java/org/hibernate/type/CompositeType.java index 344bad6f4253..a86fdec3c1df 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/CompositeType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/CompositeType.java @@ -10,6 +10,7 @@ import org.hibernate.FetchMode; import org.hibernate.HibernateException; +import org.hibernate.annotations.OnDeleteAction; import org.hibernate.engine.spi.CascadeStyle; import org.hibernate.engine.spi.SharedSessionContractImplementor; @@ -124,6 +125,17 @@ default Object replacePropertyValues(Object component, Object[] values, SharedSe */ CascadeStyle getCascadeStyle(int index); + /** + * Retrieve the on delete action of the indicated component property. + * + * @param index The property index, + * + * @return The cascade style. + * + * @since 7.0 + */ + OnDeleteAction getOnDeleteAction(int index); + /** * Retrieve the fetch mode of the indicated component property. * diff --git a/hibernate-core/src/main/java/org/hibernate/type/ConvertedBasicArrayType.java b/hibernate-core/src/main/java/org/hibernate/type/ConvertedBasicArrayType.java index d592694b3746..448504e01102 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/ConvertedBasicArrayType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/ConvertedBasicArrayType.java @@ -16,6 +16,8 @@ import org.hibernate.type.descriptor.jdbc.JdbcType; import org.hibernate.type.descriptor.jdbc.JdbcTypeIndicators; +import static org.hibernate.type.BasicArrayType.determineArrayTypeName; + /** * Given a {@link BasicValueConverter} for an array type, * @@ -50,7 +52,7 @@ public ConvertedBasicArrayType( this.jdbcValueExtractor = (ValueExtractor) arrayJdbcType.getExtractor( converter.getRelationalJavaType() ); this.jdbcLiteralFormatter = (JdbcLiteralFormatter) arrayJdbcType.getJdbcLiteralFormatter( converter.getRelationalJavaType() ); this.baseDescriptor = baseDescriptor; - this.name = baseDescriptor.getName() + "[]"; + this.name = determineArrayTypeName( baseDescriptor ); } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/type/EntityType.java b/hibernate-core/src/main/java/org/hibernate/type/EntityType.java index 3cbaf9b8e8a7..ba67db9a7c1b 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/EntityType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/EntityType.java @@ -277,14 +277,14 @@ else if ( y == null ) { final Object xId = extractIdentifier( x, factory ); final Object yId = extractIdentifier( y, factory ); - return getIdentifierType( factory ).compare( xId, yId ); + return getIdentifierType( factory ).compare( xId, yId, factory ); } private Object extractIdentifier(Object entity, SessionFactoryImplementor factory) { final EntityPersister concretePersister = getAssociatedEntityPersister( factory ); return concretePersister == null ? null - : concretePersister.getIdentifier( entity, null ); + : concretePersister.getIdentifier( entity, (SharedSessionContractImplementor) null ); } @Override @@ -352,8 +352,8 @@ public int getHashCode(Object x, SessionFactoryImplementor factory) { } else { final Class mappedClass = persister.getMappedClass(); - if ( mappedClass.isAssignableFrom( x.getClass() ) ) { - id = persister.getIdentifier( x, null ); + if ( mappedClass.isInstance( x ) ) { + id = persister.getIdentifier( x, (SharedSessionContractImplementor) null ); } else { id = x; @@ -363,8 +363,15 @@ public int getHashCode(Object x, SessionFactoryImplementor factory) { } else { assert uniqueKeyPropertyName != null; + final Object uniqueKey; final Type keyType = persister.getPropertyType( uniqueKeyPropertyName ); - return keyType.getHashCode( x, factory ); + if ( keyType.getReturnedClass().isInstance( x ) ) { + uniqueKey = x; + } + else { + uniqueKey = persister.getPropertyValue( x, uniqueKeyPropertyName ); + } + return keyType.getHashCode( uniqueKey, factory ); } } @@ -379,39 +386,62 @@ public boolean isEqual(Object x, Object y, SessionFactoryImplementor factory) { } final EntityPersister persister = getAssociatedEntityPersister( factory ); - final Class mappedClass = persister.getMappedClass(); - Object xid; - final LazyInitializer lazyInitializerX = extractLazyInitializer( x ); - if ( lazyInitializerX != null ) { - xid = lazyInitializerX.getInternalIdentifier(); - } - else { - if ( mappedClass.isAssignableFrom( x.getClass() ) ) { - xid = persister.getIdentifier( x, null ); + if ( isReferenceToPrimaryKey() ) { + final Class mappedClass = persister.getMappedClass(); + Object xid; + final LazyInitializer lazyInitializerX = extractLazyInitializer( x ); + if ( lazyInitializerX != null ) { + xid = lazyInitializerX.getInternalIdentifier(); } else { - //JPA 2 case where @IdClass contains the id and not the associated entity - xid = x; + if ( mappedClass.isInstance( x ) ) { + xid = persister.getIdentifier( x, (SharedSessionContractImplementor) null ); + } + else { + //JPA 2 case where @IdClass contains the id and not the associated entity + xid = x; + } } - } - Object yid; - final LazyInitializer lazyInitializerY = extractLazyInitializer( y ); - if ( lazyInitializerY != null ) { - yid = lazyInitializerY.getInternalIdentifier(); + Object yid; + final LazyInitializer lazyInitializerY = extractLazyInitializer( y ); + if ( lazyInitializerY != null ) { + yid = lazyInitializerY.getInternalIdentifier(); + } + else { + if ( mappedClass.isInstance( y ) ) { + yid = persister.getIdentifier( y, (SharedSessionContractImplementor) null ); + } + else { + //JPA 2 case where @IdClass contains the id and not the associated entity + yid = y; + } + } + + // Check for reference equality first as the type-specific checks by IdentifierType are sometimes non-trivial + return ( xid == yid ) || persister.getIdentifierType().isEqual( xid, yid, factory ); } else { - if ( mappedClass.isAssignableFrom( y.getClass() ) ) { - yid = persister.getIdentifier( y, null ); + assert uniqueKeyPropertyName != null; + final Object xUniqueKey; + final Type keyType = persister.getPropertyType( uniqueKeyPropertyName ); + if ( keyType.getReturnedClass().isInstance( x ) ) { + xUniqueKey = x; } else { - //JPA 2 case where @IdClass contains the id and not the associated entity - yid = y; + xUniqueKey = persister.getPropertyValue( x, uniqueKeyPropertyName ); } - } - // Check for reference equality first as the type-specific checks by IdentifierType are sometimes non-trivial - return ( xid == yid ) || persister.getIdentifierType().isEqual( xid, yid, factory ); + final Object yUniqueKey; + if ( keyType.getReturnedClass().isInstance( y ) ) { + yUniqueKey = y; + } + else { + yUniqueKey = persister.getPropertyValue( y, uniqueKeyPropertyName ); + } + return (xUniqueKey == yUniqueKey) + || keyType.isEqual( xUniqueKey, yUniqueKey, factory ); + } } /** @@ -497,7 +527,7 @@ else if ( isPersistentAttributeInterceptable( value ) ) { // we need to dig a little deeper, as that property might also be // an entity type, in which case we need to resolve its identifier final Type type = entityPersister.getPropertyType( uniqueKeyPropertyName ); - if ( type.isEntityType() ) { + if ( type instanceof EntityType ) { return ( (EntityType) type ).getIdentifier( propertyValue, session ); } else { @@ -522,7 +552,7 @@ else if ( value == null ) { // we need to dig a little deeper, as that property might also be // an entity type, in which case we need to resolve its identifier final Type type = entityPersister.getPropertyType( uniqueKeyPropertyName ); - if ( type.isEntityType() ) { + if ( type instanceof EntityType ) { return ( (EntityType) type ).getIdentifier( propertyValue, sessionFactory ); } else { @@ -564,7 +594,7 @@ public String toLoggableString(Object value, SessionFactoryImplementor factory) id = lazyInitializer.getInternalIdentifier(); } else { - id = persister.getIdentifier( value, null ); + id = persister.getIdentifier( value, (SharedSessionContractImplementor) null ); } result.append( '#' ) @@ -647,7 +677,7 @@ public final Type getIdentifierOrUniqueKeyType(Mapping factory) throws MappingEx } else { final Type type = factory.getReferencedPropertyType( getAssociatedEntityName(), uniqueKeyPropertyName ); - if ( type.isEntityType() ) { + if ( type instanceof EntityType ) { return ( (EntityType) type ).getIdentifierOrUniqueKeyType( factory ); } else { @@ -701,8 +731,13 @@ protected final Object resolveIdentifier(Object id, SharedSessionContractImpleme getAssociatedEntityPersister( session.getFactory() ) .isInstrumented(); - final Object proxyOrEntity = - session.internalLoad( getAssociatedEntityName(), id, isEager( overridingEager ), isNullable() ); + final boolean isEager = isEager( overridingEager ); + // If the association is lazy, retrieve the concrete type if required + final String entityName = isEager ? getAssociatedEntityName() + : getAssociatedEntityPersister( session.getFactory() ).resolveConcreteProxyTypeForId( id, session ) + .getEntityName(); + + final Object proxyOrEntity = session.internalLoad( entityName, id, isEager, isNullable() ); final LazyInitializer lazyInitializer = extractLazyInitializer( proxyOrEntity ); if ( lazyInitializer != null ) { diff --git a/hibernate-core/src/main/java/org/hibernate/type/ManyToOneType.java b/hibernate-core/src/main/java/org/hibernate/type/ManyToOneType.java index 3c27da133a2f..eefe40170f89 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/ManyToOneType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/ManyToOneType.java @@ -249,7 +249,7 @@ public boolean isDirty( } Object oldid = getIdentifier( old, session ); Object newid = getIdentifier( current, session ); - return getIdentifierType( session ).isDirty( oldid, newid, session ); + return getIdentifierOrUniqueKeyType( session.getFactory() ).isDirty( oldid, newid, session ); } @Override @@ -267,7 +267,7 @@ public boolean isDirty( } Object oldid = getIdentifier( old, session ); Object newid = getIdentifier( current, session ); - return getIdentifierType( session ).isDirty( oldid, newid, checkable, session ); + return getIdentifierOrUniqueKeyType( session.getFactory() ).isDirty( oldid, newid, checkable, session ); } } diff --git a/hibernate-core/src/main/java/org/hibernate/type/SqlTypes.java b/hibernate-core/src/main/java/org/hibernate/type/SqlTypes.java index 081e60f283b3..3ff9013eb131 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/SqlTypes.java +++ b/hibernate-core/src/main/java/org/hibernate/type/SqlTypes.java @@ -678,8 +678,9 @@ public class SqlTypes { /** - * A type code representing an {@code embedding vector} type for databases like - * {@link org.hibernate.dialect.PostgreSQLDialect PostgreSQL} and {@link org.hibernate.dialect.OracleDialect Oracle 23ai}. + * A type code representing an {@code embedding vector} type for databases + * like {@link org.hibernate.dialect.PostgreSQLDialect PostgreSQL}, + * {@link org.hibernate.dialect.OracleDialect Oracle 23ai} and {@link org.hibernate.dialect.MariaDBDialect MariaDB}. * An embedding vector essentially is a {@code float[]} with a fixed size. * * @since 6.4 diff --git a/hibernate-core/src/main/java/org/hibernate/type/TypeHelper.java b/hibernate-core/src/main/java/org/hibernate/type/TypeHelper.java index 9223fff3a31a..867ba6881aee 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/TypeHelper.java +++ b/hibernate-core/src/main/java/org/hibernate/type/TypeHelper.java @@ -193,12 +193,12 @@ public static Object[] replaceAssociations( final Type type = types[i]; // AnyType is both a CompositeType and an AssociationType // but here we want to treat it as an association - if ( type.isAssociationType() ) { + if ( type instanceof EntityType || type instanceof CollectionType || type instanceof AnyType ) { copied[i] = types[i].replace( currentOriginal, target[i], session, owner, copyCache, foreignKeyDirection ); } else { - if ( type.isComponentType() ) { - final CompositeType compositeType = (CompositeType) type; + if ( type instanceof ComponentType ) { + final ComponentType compositeType = (ComponentType) type; if ( target[i] != null ) { // need to extract the component values and check for subtype replacements... final Object[] objects = replaceCompositeAssociations( @@ -224,7 +224,7 @@ private static Object[] replaceCompositeAssociations( Map copyCache, ForeignKeyDirection foreignKeyDirection, Object target, Object currentOriginal, - CompositeType compositeType) { + ComponentType compositeType) { final Type[] subtypes = compositeType.getSubtypes(); return replaceAssociations( currentOriginal == null diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/DateTimeUtils.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/DateTimeUtils.java index 42487e0c4af5..df7934e6141a 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/DateTimeUtils.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/DateTimeUtils.java @@ -477,7 +477,10 @@ public static T truncateToPrecision(T temporal, int precisi /** * Do the same conversion that databases do when they encounter a timestamp with a higher precision * than what is supported by a column, which is to round the excess fractions. + * + * @deprecated Use {@link #adjustToDefaultPrecision(Temporal, Dialect)} instead */ + @Deprecated(forRemoval = true, since = "6.6.1") public static T roundToDefaultPrecision(T temporal, Dialect d) { final int defaultTimestampPrecision = d.getDefaultTimestampPrecision(); if ( defaultTimestampPrecision >= 9 || !temporal.isSupported( ChronoField.NANO_OF_SECOND ) ) { @@ -491,6 +494,9 @@ public static T roundToDefaultPrecision(T temporal, Dialect } public static T roundToSecondPrecision(T temporal, int precision) { + if ( precision >= 9 || !temporal.isSupported( ChronoField.NANO_OF_SECOND ) ) { + return temporal; + } if ( precision == 0 ) { //noinspection unchecked return temporal.get( ChronoField.NANO_OF_SECOND ) >= 500_000_000L diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/AbstractArrayJavaType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/AbstractArrayJavaType.java index c0de85c7e4d5..5f4ecba47f65 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/AbstractArrayJavaType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/AbstractArrayJavaType.java @@ -20,7 +20,6 @@ import org.hibernate.type.descriptor.java.spi.UnknownBasicJavaType; import org.hibernate.type.descriptor.jdbc.JdbcType; import org.hibernate.type.descriptor.jdbc.JdbcTypeIndicators; -import org.hibernate.type.internal.BasicTypeImpl; import org.hibernate.type.spi.TypeConfiguration; public abstract class AbstractArrayJavaType extends AbstractClassJavaType @@ -49,7 +48,8 @@ public JdbcType getRecommendedJdbcType(JdbcTypeIndicators indicators) { // Always determine the recommended type to make sure this is a valid basic java type return indicators.getTypeConfiguration().getJdbcTypeRegistry().resolveTypeConstructorDescriptor( indicators.getPreferredSqlTypeCodeForArray(), - new BasicTypeImpl<>( componentJavaType, componentJavaType.getRecommendedJdbcType( indicators ) ), + indicators.getTypeConfiguration().getBasicTypeRegistry().resolve( + componentJavaType, componentJavaType.getRecommendedJdbcType( indicators ) ), ColumnTypeInformation.EMPTY ); } diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/BasicPluralJavaType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/BasicPluralJavaType.java index e0401ad4bd70..eb4f00529a90 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/BasicPluralJavaType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/BasicPluralJavaType.java @@ -19,7 +19,7 @@ * Descriptor for a basic plural Java type. * A basic plural type represents a type, that is mapped to a single column instead of multiple rows. * This is used for array or collection types, that are backed by e.g. SQL array or JSON/XML DDL types. - * + *

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

    + * Note that this operation is only used when the program sets a null or negative + * number as the value of the entity version field. It is not called when the + * program sets the version field to a sensible-looking version. * * @param length The length of the type * @param precision The precision of the type diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/spi/BasicCollectionJavaType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/spi/BasicCollectionJavaType.java index 70a7d653d021..31b679dfd8b8 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/spi/BasicCollectionJavaType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/spi/BasicCollectionJavaType.java @@ -40,7 +40,6 @@ import org.hibernate.type.descriptor.java.MutabilityPlan; import org.hibernate.type.descriptor.jdbc.JdbcType; import org.hibernate.type.descriptor.jdbc.JdbcTypeIndicators; -import org.hibernate.type.internal.BasicTypeImpl; import org.hibernate.type.spi.TypeConfiguration; /** @@ -78,7 +77,8 @@ public JdbcType getRecommendedJdbcType(JdbcTypeIndicators indicators) { // (even though we only use this inside the if block, we want it to throw here if something wrong) return indicators.getTypeConfiguration().getJdbcTypeRegistry().resolveTypeConstructorDescriptor( indicators.getPreferredSqlTypeCodeForArray(), - new BasicTypeImpl<>( componentJavaType, componentJavaType.getRecommendedJdbcType( indicators ) ), + indicators.getTypeConfiguration().getBasicTypeRegistry().resolve( + componentJavaType, componentJavaType.getRecommendedJdbcType( indicators ) ), ColumnTypeInformation.EMPTY ); } diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/spi/EmbeddableAggregateJavaType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/spi/EmbeddableAggregateJavaType.java index 812bfa9d45c5..1fd0388fc5a7 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/spi/EmbeddableAggregateJavaType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/spi/EmbeddableAggregateJavaType.java @@ -85,6 +85,9 @@ public X unwrap(T value, Class type, WrapperOptions options) { @Override public T wrap(X value, WrapperOptions options) { + if ( value == null ) { + return null; + } if ( getJavaTypeClass().isInstance( value ) ) { //noinspection unchecked return (T) value; diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/spi/JavaTypeRegistry.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/spi/JavaTypeRegistry.java index 37870323b34a..c075fa202559 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/spi/JavaTypeRegistry.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/java/spi/JavaTypeRegistry.java @@ -106,8 +106,9 @@ public JavaType resolveDescriptor(Type javaType, Supplier> cr } final JavaType created = creator.get(); - descriptorsByType.put( javaType, created ); - return created; + final JavaType cachedNew = descriptorsByType.putIfAbsent( javaType, created ); + //noinspection unchecked + return cachedNew == null ? created : (JavaType) cachedNew; } public JavaType resolveDescriptor(Type javaType) { diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/ArrayJdbcType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/ArrayJdbcType.java index d4c005df983a..47e97626e141 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/ArrayJdbcType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/ArrayJdbcType.java @@ -128,13 +128,18 @@ protected String getElementTypeName(JavaType javaType, SharedSessionContractI .getSizeStrategy() .resolveSize( elementJdbcType, elementJavaType, null, null, null ); final DdlTypeRegistry ddlTypeRegistry = session.getTypeConfiguration().getDdlTypeRegistry(); - final String typeName = ddlTypeRegistry.getDescriptor( elementJdbcType.getDdlTypeCode() ) - .getTypeName( size, new BasicTypeImpl<>( elementJavaType, elementJdbcType), ddlTypeRegistry ); - int cutIndex = typeName.indexOf( '(' ); - if ( cutIndex > 0 ) { + final String typeName = + ddlTypeRegistry.getDescriptor( elementJdbcType.getDdlTypeCode() ) + .getTypeName( size, new BasicTypeImpl<>( elementJavaType, elementJdbcType), ddlTypeRegistry ); + + final int cutIndexBegin = typeName.indexOf( '(' ); + if ( cutIndexBegin > 0 ) { + final int cutIndexEnd = typeName.lastIndexOf( ')' ); + assert cutIndexEnd > cutIndexBegin; // getTypeName for this case required length, etc, parameters. // Cut them out and use database defaults. - return typeName.substring( 0, cutIndex ); + // e.g. "timestamp($p) with timezone" becomes "timestamp with timezone" + return typeName.substring( 0, cutIndexBegin ) + typeName.substring( cutIndexEnd + 1 ); } else { return typeName; @@ -185,19 +190,28 @@ protected Object[] getArray( } protected X getArray(BasicExtractor extractor, java.sql.Array array, WrapperOptions options) throws SQLException { - if ( array != null && getElementJdbcType() instanceof AggregateJdbcType ) { - final AggregateJdbcType aggregateJdbcType = (AggregateJdbcType) getElementJdbcType(); + final JdbcType jdbcType = getElementJdbcType(); + if (array != null + && jdbcType instanceof AggregateJdbcType + && ((AggregateJdbcType) jdbcType).getEmbeddableMappingType() != null) { + final AggregateJdbcType aggregateJdbcType = (AggregateJdbcType) jdbcType; final EmbeddableMappingType embeddableMappingType = aggregateJdbcType.getEmbeddableMappingType(); final Object rawArray = array.getArray(); final Object[] domainObjects = new Object[Array.getLength( rawArray )]; for ( int i = 0; i < domainObjects.length; i++ ) { - final Object[] aggregateRawValues = aggregateJdbcType.extractJdbcValues( Array.get( rawArray, i ), options ); - final StructAttributeValues attributeValues = StructHelper.getAttributeValues( - embeddableMappingType, - aggregateRawValues, - options - ); - domainObjects[i] = instantiate( embeddableMappingType, attributeValues, options.getSessionFactory() ); + final Object rawJdbcValue = Array.get(rawArray, i); + if (rawJdbcValue == null) { + domainObjects[i] = null; + } + else { + final Object[] aggregateRawValues = aggregateJdbcType.extractJdbcValues(rawJdbcValue, options); + final StructAttributeValues attributeValues = StructHelper.getAttributeValues( + embeddableMappingType, + aggregateRawValues, + options + ); + domainObjects[i] = instantiate( embeddableMappingType, attributeValues, options.getSessionFactory() ); + } } return extractor.getJavaType().wrap( domainObjects, options ); } diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/ObjectNullResolvingJdbcType.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/ObjectNullResolvingJdbcType.java index 00e6a3f14d7c..b1fd1bdb0178 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/ObjectNullResolvingJdbcType.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/ObjectNullResolvingJdbcType.java @@ -42,13 +42,27 @@ public ValueBinder getBinder(JavaType javaType) { @Override protected void doBindNull(PreparedStatement st, int index, WrapperOptions options) throws SQLException { - st.setNull( index, st.getParameterMetaData().getParameterType( index ) ); + if ( options.getDialect().supportsBindingNullForSetObject() ) { + st.setObject( index, null ); + } + else { + final int sqlType = options.getDialect().supportsBindingNullSqlTypeForSetNull() ? Types.NULL + : st.getParameterMetaData().getParameterType( index ); + st.setNull( index, sqlType ); + } } @Override protected void doBindNull(CallableStatement st, String name, WrapperOptions options) throws SQLException { - st.setNull( name, Types.JAVA_OBJECT ); + if ( options.getDialect().supportsBindingNullForSetObject() ) { + st.setObject( name, null ); + } + else { + final int sqlType = options.getDialect().supportsBindingNullSqlTypeForSetNull() ? Types.NULL + : Types.JAVA_OBJECT; + st.setNull( name, sqlType ); + } } @Override diff --git a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/spi/JdbcTypeRegistry.java b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/spi/JdbcTypeRegistry.java index 2760d9665968..496787aa2f37 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/spi/JdbcTypeRegistry.java +++ b/hibernate-core/src/main/java/org/hibernate/type/descriptor/jdbc/spi/JdbcTypeRegistry.java @@ -9,6 +9,7 @@ import java.io.Serializable; import java.sql.Types; import java.util.Locale; +import java.util.Map; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; @@ -271,7 +272,17 @@ private JdbcType resolveTypeConstructorDescriptor( public boolean hasRegisteredDescriptor(int jdbcTypeCode) { return descriptorMap.containsKey( jdbcTypeCode ) || JdbcTypeNameMapper.isStandardTypeCode( jdbcTypeCode ) - || JdbcTypeFamilyInformation.INSTANCE.locateJdbcTypeFamilyByTypeCode( jdbcTypeCode ) != null; + || JdbcTypeFamilyInformation.INSTANCE.locateJdbcTypeFamilyByTypeCode( jdbcTypeCode ) != null + || locateConstructedJdbcType( jdbcTypeCode ); + } + + private boolean locateConstructedJdbcType(int jdbcTypeCode) { + for ( TypeConstructedJdbcTypeKey key : typeConstructorDescriptorMap.keySet() ) { + if ( key.typeCode == jdbcTypeCode ) { + return true; + } + } + return false; } public JdbcTypeConstructor getConstructor(int jdbcTypeCode) { diff --git a/hibernate-core/src/main/java/org/hibernate/type/format/FormatMapperCreationContext.java b/hibernate-core/src/main/java/org/hibernate/type/format/FormatMapperCreationContext.java new file mode 100644 index 000000000000..ae0f803ed975 --- /dev/null +++ b/hibernate-core/src/main/java/org/hibernate/type/format/FormatMapperCreationContext.java @@ -0,0 +1,20 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.type.format; + +import org.hibernate.Incubating; +import org.hibernate.boot.spi.BootstrapContext; + + +/** + * The creation context for {@link FormatMapper} that is passed as constructor argument to implementations. + */ +@Incubating +public interface FormatMapperCreationContext { + BootstrapContext getBootstrapContext(); + +} diff --git a/hibernate-core/src/main/java/org/hibernate/type/format/jackson/JacksonIntegration.java b/hibernate-core/src/main/java/org/hibernate/type/format/jackson/JacksonIntegration.java index 10460b012e51..0ee2d0072611 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/format/jackson/JacksonIntegration.java +++ b/hibernate-core/src/main/java/org/hibernate/type/format/jackson/JacksonIntegration.java @@ -6,7 +6,15 @@ */ package org.hibernate.type.format.jackson; +import java.util.List; + +import com.fasterxml.jackson.databind.Module; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.checkerframework.checker.nullness.qual.Nullable; + +import org.hibernate.boot.registry.classloading.spi.ClassLoaderService; import org.hibernate.type.format.FormatMapper; +import org.hibernate.type.format.FormatMapperCreationContext; public final class JacksonIntegration { @@ -14,8 +22,7 @@ public final class JacksonIntegration { // when GraalVM native image is initializing them. private static final boolean JACKSON_XML_AVAILABLE = ableToLoadJacksonXMLMapper(); private static final boolean JACKSON_JSON_AVAILABLE = ableToLoadJacksonJSONMapper(); - private static final JacksonXmlFormatMapper XML_FORMAT_MAPPER = JACKSON_XML_AVAILABLE ? new JacksonXmlFormatMapper() : null; - private static final JacksonJsonFormatMapper JSON_FORMAT_MAPPER = JACKSON_JSON_AVAILABLE ? new JacksonJsonFormatMapper() : null; + private JacksonIntegration() { //To not be instantiated: static helpers only @@ -29,12 +36,22 @@ private static boolean ableToLoadJacksonXMLMapper() { return canLoad( "com.fasterxml.jackson.dataformat.xml.XmlMapper" ); } - public static FormatMapper getXMLJacksonFormatMapperOrNull() { - return XML_FORMAT_MAPPER; + public static @Nullable FormatMapper getXMLJacksonFormatMapperOrNull(FormatMapperCreationContext creationContext) { + return JACKSON_XML_AVAILABLE + ? new JacksonXmlFormatMapper( creationContext ) + : null; + } + + public static @Nullable FormatMapper getJsonJacksonFormatMapperOrNull(FormatMapperCreationContext creationContext) { + return JACKSON_JSON_AVAILABLE + ? new JacksonJsonFormatMapper( creationContext ) + : null; } - public static FormatMapper getJsonJacksonFormatMapperOrNull() { - return JSON_FORMAT_MAPPER; + public static @Nullable FormatMapper getJsonJacksonFormatMapperOrNull() { + return JACKSON_JSON_AVAILABLE + ? new JacksonJsonFormatMapper() + : null; } private static boolean canLoad(String name) { @@ -50,4 +67,28 @@ private static boolean canLoad(String name) { return false; } } + + static List loadModules(FormatMapperCreationContext creationContext) { + final ClassLoader classLoader = JacksonIntegration.class.getClassLoader(); + final ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); + if ( contextClassLoader != null && classLoader != contextClassLoader ) { + try { + // The context class loader represents the application class loader in a Jakarta EE deployment. + // We have to check if the ObjectMapper that is visible to Hibernate ORM is the same that is visible + // to the application class loader. Only if it is, we can use the application class loader or rather + // our AggregatedClassLoader for loading Jackson Module via ServiceLoader, as otherwise the loaded + // Jackson Module instances would have a different class loader, leading to a ClassCastException. + if ( ObjectMapper.class == contextClassLoader.loadClass( "com.fasterxml.jackson.databind.ObjectMapper" ) ) { + return creationContext.getBootstrapContext() + .getServiceRegistry() + .requireService( ClassLoaderService.class ) + .>workWithClassLoader( ObjectMapper::findModules ); + } + } + catch (ClassNotFoundException | LinkageError e) { + // Ignore if the context/application class loader doesn't know Jackson classes + } + } + return ObjectMapper.findModules( classLoader ); + } } diff --git a/hibernate-core/src/main/java/org/hibernate/type/format/jackson/JacksonJsonFormatMapper.java b/hibernate-core/src/main/java/org/hibernate/type/format/jackson/JacksonJsonFormatMapper.java index fe4b41b4f9b9..5c5b37081ab8 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/format/jackson/JacksonJsonFormatMapper.java +++ b/hibernate-core/src/main/java/org/hibernate/type/format/jackson/JacksonJsonFormatMapper.java @@ -6,12 +6,16 @@ */ package org.hibernate.type.format.jackson; +import com.fasterxml.jackson.databind.Module; + import org.hibernate.type.format.AbstractJsonFormatMapper; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; +import org.hibernate.type.format.FormatMapperCreationContext; import java.lang.reflect.Type; +import java.util.List; /** * @author Christian Beikov @@ -24,7 +28,15 @@ public final class JacksonJsonFormatMapper extends AbstractJsonFormatMapper { private final ObjectMapper objectMapper; public JacksonJsonFormatMapper() { - this(new ObjectMapper().findAndRegisterModules()); + this( ObjectMapper.findModules( JacksonJsonFormatMapper.class.getClassLoader() ) ); + } + + public JacksonJsonFormatMapper(FormatMapperCreationContext creationContext) { + this( JacksonIntegration.loadModules( creationContext ) ); + } + + private JacksonJsonFormatMapper(List modules) { + this(new ObjectMapper().registerModules( modules )); } public JacksonJsonFormatMapper(ObjectMapper objectMapper) { diff --git a/hibernate-core/src/main/java/org/hibernate/type/format/jackson/JacksonXmlFormatMapper.java b/hibernate-core/src/main/java/org/hibernate/type/format/jackson/JacksonXmlFormatMapper.java index a463d5903565..59193b46789d 100644 --- a/hibernate-core/src/main/java/org/hibernate/type/format/jackson/JacksonXmlFormatMapper.java +++ b/hibernate-core/src/main/java/org/hibernate/type/format/jackson/JacksonXmlFormatMapper.java @@ -8,8 +8,11 @@ import java.io.IOException; import java.lang.reflect.Type; +import java.util.List; import java.util.ArrayList; +import com.fasterxml.jackson.databind.Module; + import org.hibernate.type.descriptor.WrapperOptions; import org.hibernate.type.descriptor.java.JavaType; import org.hibernate.type.format.FormatMapper; @@ -24,6 +27,7 @@ import com.fasterxml.jackson.databind.module.SimpleModule; import com.fasterxml.jackson.dataformat.xml.XmlMapper; import com.fasterxml.jackson.dataformat.xml.ser.ToXmlGenerator; +import org.hibernate.type.format.FormatMapperCreationContext; /** * @author Christian Beikov @@ -35,17 +39,25 @@ public final class JacksonXmlFormatMapper implements FormatMapper { private final ObjectMapper objectMapper; public JacksonXmlFormatMapper() { - this( createXmlMapper() ); + this( + createXmlMapper( ObjectMapper.findModules( JacksonXmlFormatMapper.class.getClassLoader() ) ) + ); + } + + public JacksonXmlFormatMapper(FormatMapperCreationContext creationContext) { + this( + createXmlMapper( JacksonIntegration.loadModules( creationContext ) ) + ); } public JacksonXmlFormatMapper(ObjectMapper objectMapper) { this.objectMapper = objectMapper; } - private static XmlMapper createXmlMapper() { + private static XmlMapper createXmlMapper(List modules) { final XmlMapper xmlMapper = new XmlMapper(); // needed to automatically find and register Jackson's jsr310 module for java.time support - xmlMapper.findAndRegisterModules(); + xmlMapper.registerModules( modules ); xmlMapper.configure( SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false ); xmlMapper.enable( ToXmlGenerator.Feature.WRITE_NULLS_AS_XSI_NIL ); // Workaround for null vs empty string handling inside arrays, diff --git a/hibernate-core/src/test/java/org/hibernate/dialect/DB2iDialectTest.java b/hibernate-core/src/test/java/org/hibernate/dialect/DB2iDialectTest.java new file mode 100644 index 000000000000..4167d5d9dd76 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/dialect/DB2iDialectTest.java @@ -0,0 +1,69 @@ +package org.hibernate.dialect; + +import org.hibernate.testing.orm.junit.JiraKey; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class DB2iDialectTest { + + private static final String EXPECTED_FOR_UPDATE = " for update with rs"; + private static final String EXPECTED_FOR_UPDATE_SKIP_LOCK = " for update with rs skip locked data"; + + private DB2iDialect dialect; + + @BeforeEach + void setUp() { + dialect = new DB2iDialect(); + } + + @Test + @JiraKey("HHH-18560") + void getForUpdateString() { + String actual = dialect.getForUpdateString(); + assertEquals(EXPECTED_FOR_UPDATE, actual); + } + + @Test + @JiraKey("HHH-18560") + void getForUpdateSkipLockedString() { + String actual = dialect.getForUpdateSkipLockedString(); + assertEquals(EXPECTED_FOR_UPDATE_SKIP_LOCK, actual); + } + + @Test + @JiraKey("HHH-18560") + void testGetForUpdateSkipLockedString() { + String actual = dialect.getForUpdateSkipLockedString("alias"); + assertEquals(EXPECTED_FOR_UPDATE_SKIP_LOCK, actual); + } + + @Test + @JiraKey("HHH-18560") + void getWriteLockString_skiplocked() { + String actual = dialect.getWriteLockString(-2); + assertEquals(EXPECTED_FOR_UPDATE_SKIP_LOCK, actual); + } + + @Test + @JiraKey("HHH-18560") + void getWriteLockString() { + String actual = dialect.getWriteLockString(0); + assertEquals(EXPECTED_FOR_UPDATE, actual); + } + + @Test + @JiraKey("HHH-18560") + void getReadLockString() { + String actual = dialect.getReadLockString(0); + assertEquals(EXPECTED_FOR_UPDATE, actual); + } + + @Test + @JiraKey("HHH-18560") + void getReadLockString_skipLocked() { + String actual = dialect.getReadLockString(-2); + assertEquals(EXPECTED_FOR_UPDATE_SKIP_LOCK, actual); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/dialect/MySQLDialectDatabaseVersionTest.java b/hibernate-core/src/test/java/org/hibernate/dialect/MySQLDialectDatabaseVersionTest.java new file mode 100644 index 000000000000..466180eac632 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/dialect/MySQLDialectDatabaseVersionTest.java @@ -0,0 +1,97 @@ +package org.hibernate.dialect; + +import java.util.Map; +import org.hibernate.engine.jdbc.dialect.spi.DialectResolutionInfo; +import org.hibernate.testing.RequiresDialect; +import org.hibernate.testing.TestForIssue; +import org.hibernate.testing.junit4.BaseUnitTestCase; +import org.junit.Test; + + +import static org.junit.Assert.assertEquals; + +@RequiresDialect(MySQLDialect.class) +@TestForIssue(jiraKey = "HHH-18518") +public class MySQLDialectDatabaseVersionTest extends BaseUnitTestCase { + + @Test + public void versionWithSuffix() { + String version = "8.0.37-azure"; + Dialect dialect = new MySQLDialect( new TestingMySQLDialectResolutionInfo( version ) ); + + assertEquals(8, dialect.getVersion().getMajor()); + assertEquals(0, dialect.getVersion().getMinor()); + assertEquals(37, dialect.getVersion().getMicro()); + } + + @Test + public void releaseVersion() { + String version = "8.0.37"; + Dialect dialect = new MySQLDialect( new TestingMySQLDialectResolutionInfo( version ) ); + + assertEquals(8, dialect.getVersion().getMajor()); + assertEquals(0, dialect.getVersion().getMinor()); + assertEquals(37, dialect.getVersion().getMicro()); + } + + static final class TestingMySQLDialectResolutionInfo implements DialectResolutionInfo { + private final String databaseVersion; + + TestingMySQLDialectResolutionInfo(String databaseVersion) { + this.databaseVersion = databaseVersion; + } + + + @Override + public String getDatabaseName() { + return "MySQL"; + } + + @Override + public String getDatabaseVersion() { + return this.databaseVersion; + } + + @Override + public int getDatabaseMajorVersion() { + return 8; + } + + @Override + public int getDatabaseMinorVersion() { + return 0; + } + + @Override + public String getDriverName() { + return "MySQL JDBC Driver"; + } + + @Override + public int getDriverMajorVersion() { + return 8; + } + + @Override + public int getDriverMinorVersion() { + return 3; + } + + @Override + public String getSQLKeywords() { + return ""; + } + + @Override + public String toString() { + return "8.3.0"; + } + + @Override + public Map getConfigurationValues() { + return Map.of(); + } + + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/engine/internal/StatisticalLoggingSessionEventListenerTest.java b/hibernate-core/src/test/java/org/hibernate/engine/internal/StatisticalLoggingSessionEventListenerTest.java new file mode 100644 index 000000000000..e8c9065c691c --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/engine/internal/StatisticalLoggingSessionEventListenerTest.java @@ -0,0 +1,213 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.engine.internal; + +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.internal.CoreMessageLogger; + +import org.hibernate.testing.logger.LoggerInspectionRule; +import org.hibernate.testing.logger.Triggerable; +import org.hibernate.testing.orm.domain.gambit.BasicEntity; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.Setting; +import org.junit.Rule; +import org.junit.jupiter.api.Test; + +import org.jboss.logging.Logger; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * @author Christian BÃŧrgi + */ +@DomainModel(annotatedClasses = { + BasicEntity.class +}) +@SessionFactory(generateStatistics = true) +@ServiceRegistry(settings = { + @Setting(name = AvailableSettings.STATEMENT_BATCH_SIZE, value = "10"), + @Setting(name = AvailableSettings.ORDER_INSERTS, value = "true"), + @Setting(name = AvailableSettings.USE_QUERY_CACHE, value = "true"), + @Setting(name = AvailableSettings.USE_SECOND_LEVEL_CACHE, value = "true") +}) +@Jira("https://hibernate.atlassian.net/browse/HHH-18513") +public class StatisticalLoggingSessionEventListenerTest { + + @Rule + public LoggerInspectionRule logInspection = new LoggerInspectionRule( + Logger.getMessageLogger( + CoreMessageLogger.class, + StatisticalLoggingSessionEventListener.class.getName() + ) ); + + @Test + void testSessionMetricsLog(SessionFactoryScope scope) { + Triggerable triggerable = logInspection.watchForLogMessages( "Session Metrics {" ); + long startTime = System.nanoTime(); + + scope.inTransaction( session -> { + session.persist( new BasicEntity( 1, "fooData" ) ); + session.persist( new BasicEntity( 2, "fooData2" ) ); + session.flush(); + session.clear(); + for ( int i = 0; i < 2; i++ ) { + assertThat( session.createQuery( "select data from BasicEntity e where e.id=1", String.class ) + .setCacheable( true ) + .getSingleResult() ).isEqualTo( "fooData" ); + } + } ); + long sessionNanoDuration = System.nanoTime() - startTime; + + List messages = triggerable.triggerMessages(); + assertThat( messages ).hasSize( 1 ); + String sessionMetricsLog = messages.get( 0 ); + + // acquiring JDBC connections + SessionMetric acquiringJdbcConnectionsMetric = extractMetric( + sessionMetricsLog, + "([0-9]+) nanoseconds spent acquiring ([0-9]+) JDBC connections" + ); + assertThat( acquiringJdbcConnectionsMetric.getDuration() ).isGreaterThan( 0L ) + .isLessThan( sessionNanoDuration ); + + // releasing JDBC connections + SessionMetric releasingJdbcConnectionsMetric = extractMetric( + sessionMetricsLog, + "([0-9]+) nanoseconds spent releasing ([0-9]+) JDBC connections" + ); + assertThat( releasingJdbcConnectionsMetric.getDuration() ).isGreaterThan( 0L ) + .isLessThan( sessionNanoDuration ); + + // preparing JDBC statements + SessionMetric preparingJdbcStatmentsMetric = extractMetric( + sessionMetricsLog, + "([0-9]+) nanoseconds spent preparing ([0-9]+) JDBC statements" + ); + assertThat( preparingJdbcStatmentsMetric.getDuration() ).isGreaterThan( 0L ) + .isLessThan( sessionNanoDuration ); + + // executing JDBC statements + SessionMetric executingJdbcStatmentsMetric = extractMetric( + sessionMetricsLog, + "([0-9]+) nanoseconds spent executing ([0-9]+) JDBC statements" + ); + assertThat( executingJdbcStatmentsMetric.getDuration() ).isGreaterThan( 0L ) + .isLessThan( sessionNanoDuration ); + + // executing JDBC batches + SessionMetric executingJdbcBatchesMetric = extractMetric( + sessionMetricsLog, + "([0-9]+) nanoseconds spent executing ([0-9]+) JDBC batches" + ); + assertThat( executingJdbcBatchesMetric.getDuration() ).isGreaterThan( 0L ) + .isLessThan( sessionNanoDuration ); + + // performing L2C puts + SessionMetric performingL2CPutsMetric = extractMetric( + sessionMetricsLog, + "([0-9]+) nanoseconds spent performing ([0-9]+) L2C puts" + ); + assertThat( performingL2CPutsMetric.getDuration() ).isGreaterThan( 0L ) + .isLessThan( sessionNanoDuration ); + + // performing L2C hits + SessionMetric performingL2CHitsMetric = extractMetric( + sessionMetricsLog, + "([0-9]+) nanoseconds spent performing ([0-9]+) L2C hits" + ); + assertThat( performingL2CHitsMetric.getDuration() ).isGreaterThan( 0L ) + .isLessThan( sessionNanoDuration ); + + // performing L2C misses + SessionMetric performingL2CMissesMetric = extractMetric( + sessionMetricsLog, + "([0-9]+) nanoseconds spent performing ([0-9]+) L2C misses" + ); + assertThat( performingL2CMissesMetric.getDuration() ).isGreaterThan( 0L ) + .isLessThan( sessionNanoDuration ); + + // executing flushes + SessionMetric executingFlushesMetric = extractMetric( + sessionMetricsLog, + "([0-9]+) nanoseconds spent executing ([0-9]+) flushes" + ); + assertThat( executingFlushesMetric.getDuration() ).isGreaterThan( 0L ) + .isLessThan( sessionNanoDuration ); + + // executing pre-partial-flushes + SessionMetric executingPrePartialFlushesMetric = extractMetric( + sessionMetricsLog, + "([0-9]+) nanoseconds spent executing ([0-9]+) pre-partial-flushes" + ); + assertThat( executingPrePartialFlushesMetric.getDuration() ).isGreaterThan( 0L ) + .isLessThan( sessionNanoDuration ); + + // executing partial-flushes + SessionMetric executingPartialFlushesMetric = extractMetric( + sessionMetricsLog, + "([0-9]+) nanoseconds spent executing ([0-9]+) partial-flushes" + ); + assertThat( executingPartialFlushesMetric.getDuration() ).isGreaterThan( 0L ) + .isLessThan( sessionNanoDuration ); + + // Number of metrics + List metricList = List.of( + acquiringJdbcConnectionsMetric, + releasingJdbcConnectionsMetric, + preparingJdbcStatmentsMetric, + executingJdbcStatmentsMetric, + executingJdbcBatchesMetric, + performingL2CPutsMetric, + performingL2CHitsMetric, + performingL2CMissesMetric, + executingFlushesMetric, + executingPrePartialFlushesMetric, + executingPartialFlushesMetric + ); + int numberOfMetrics = metricList.size(); + // Number of lines + assertThat( sessionMetricsLog.lines().count() ) + .as( "The StatisticalLoggingSessionEventListener should write a line per metric (" + + numberOfMetrics + " lines) plus a header and a footer (2 lines)" ) + .isEqualTo( numberOfMetrics + 2 ); + // Total time + long sumDuration = metricList.stream().map( SessionMetric::getDuration ).mapToLong( Long::longValue ).sum(); + assertThat( sumDuration ).isLessThanOrEqualTo( sessionNanoDuration ); + } + + private SessionMetric extractMetric(String logMessage, String regex) { + Pattern pattern = Pattern.compile( regex ); + Matcher matcher = pattern.matcher( logMessage ); + assertTrue( matcher.find() ); + return new SessionMetric( Long.parseLong( matcher.group( 1 ) ), Integer.parseInt( matcher.group( 2 ) ) ); + } + + private static class SessionMetric { + private final long duration; + private final int count; + + public SessionMetric(long duration, int count) { + this.duration = duration; + this.count = count; + } + + public long getDuration() { + return duration; + } + + public int getCount() { + return count; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/CreationUpdatedTimestampInEmbeddableTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/CreationUpdatedTimestampInEmbeddableTest.java new file mode 100644 index 000000000000..1cf0fa0b94a5 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/CreationUpdatedTimestampInEmbeddableTest.java @@ -0,0 +1,86 @@ +package org.hibernate.orm.test.annotations; + +import jakarta.persistence.*; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.annotations.UpdateTimestamp; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.time.LocalDateTime; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel( + annotatedClasses = { + CreationUpdatedTimestampInEmbeddableTest.Event.class, + CreationUpdatedTimestampInEmbeddableTest.History.class, + } +) +@SessionFactory +class CreationUpdatedTimestampInEmbeddableTest { + + @BeforeEach + void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + Event event = new Event(); + event.id = 1L; + event.name = "conference"; + session.persist( event ); + } ); + } + + @AfterEach + void tearDown(SessionFactoryScope scope) { + scope.inTransaction( session -> { + session.createMutationQuery( "delete Event" ).executeUpdate(); + } ); + } + + @Test + void test(SessionFactoryScope scope) { + LocalDateTime created = scope.fromTransaction( session -> { + Event fruit = session.get( Event.class, 1L ); + return fruit.history.created; + } ); + + scope.inTransaction( session -> { + Event event = session.get( Event.class, 1L ); + event.name = "concert"; + } ); + + scope.inTransaction( session -> { + Event event = session.get( Event.class, 1L ); + assertThat( event.history.created ).isEqualTo( created ); + assertThat( event.history.updated ).isNotEqualTo( created ); + assertThat( event.name ).isEqualTo( "concert" ); + } ); + } + + @Entity(name = "Event") + public static class Event { + + @Id + public Long id; + + public String name; + + @Embedded + public History history; + + } + + @Embeddable + public static class History { + @Column + @CreationTimestamp + public LocalDateTime created; + + @Column + @UpdateTimestamp + public LocalDateTime updated; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/attributeoverride/MapAttributeOverrideTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/attributeoverride/MapAttributeOverrideTest.java new file mode 100644 index 000000000000..af9db96ccaa4 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/attributeoverride/MapAttributeOverrideTest.java @@ -0,0 +1,45 @@ +package org.hibernate.orm.test.annotations.attributeoverride; + +import java.time.LocalTime; +import java.util.Map; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.metamodel.EntityType; +import jakarta.persistence.metamodel.MapAttribute; + +import static jakarta.persistence.metamodel.Type.PersistenceType.BASIC; +import static jakarta.persistence.metamodel.Type.PersistenceType.EMBEDDABLE; +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel( + annotatedClasses = { + Schedule.class, + Route.class + } +) +@SessionFactory +class MapAttributeOverrideTest { + + @Test + @JiraKey("HHH-18516") + void testMapOfEmbeddableKeysWithAttributeOverridesAndBasicValues(SessionFactoryScope sessionFactoryScope) { + sessionFactoryScope.inTransaction(session -> { + EntityType scheduleType = session.getMetamodel().entity(Schedule.class); + MapAttribute departuresMapAttribute = scheduleType.getMap("departures"); + // Presence of @AttributeOverride-s for the key should only affect the type of the key, but not the type of the value + assertThat(departuresMapAttribute.getKeyType().getPersistenceType()).isEqualTo(EMBEDDABLE); + assertThat(departuresMapAttribute.getElementType().getPersistenceType()).isEqualTo(BASIC); + + session.persist(new Schedule(Map.of( + new Route("Hamburg", "Vienna"), LocalTime.NOON, + new Route("Warsaw", "Barcelona"), LocalTime.MIDNIGHT + ))); + assertThat(session.createQuery("FROM Schedule s", Schedule.class).getResultCount()).isOne(); + }); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/attributeoverride/Route.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/attributeoverride/Route.java new file mode 100644 index 000000000000..1e3951ea1c58 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/attributeoverride/Route.java @@ -0,0 +1,18 @@ +package org.hibernate.orm.test.annotations.attributeoverride; + +import jakarta.persistence.Embeddable; + +@Embeddable +public class Route { + + private String origin; + private String destination; + + public Route() { + } + + public Route(String origin, String destination) { + this.origin = origin; + this.destination = destination; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/attributeoverride/Schedule.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/attributeoverride/Schedule.java new file mode 100644 index 000000000000..1e5f0356219e --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/attributeoverride/Schedule.java @@ -0,0 +1,32 @@ +package org.hibernate.orm.test.annotations.attributeoverride; + +import java.time.LocalTime; +import java.util.Map; + +import jakarta.persistence.AttributeOverride; +import jakarta.persistence.Column; +import jakarta.persistence.ElementCollection; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; + +@Entity +public class Schedule { + + @Id + @GeneratedValue + private Long id; + + @ElementCollection + @Column(name = "time") + @AttributeOverride(name = "key.origin", column = @Column(name = "orig")) + @AttributeOverride(name = "key.destination", column = @Column(name = "dest")) + private Map departures; + + public Schedule() { + } + + public Schedule(Map departures) { + this.departures = departures; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/basic/ListOfByteArrayTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/basic/ListOfByteArrayTest.java new file mode 100644 index 000000000000..4bcfc592ede2 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/basic/ListOfByteArrayTest.java @@ -0,0 +1,36 @@ +package org.hibernate.orm.test.annotations.basic; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import org.hibernate.MappingException; +import org.hibernate.testing.orm.junit.DialectFeatureChecks; +import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.Jpa; +import org.hibernate.testing.orm.junit.RequiresDialectFeature; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +@JiraKey("HHH-17739") +@Jpa(annotatedClasses = ListOfByteArrayTest.Broken.class) +@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsStandardArrays.class) +public class ListOfByteArrayTest { + @Test void test(EntityManagerFactoryScope scope) { + try { + scope.getEntityManagerFactory(); + fail(); + } + catch (MappingException e) { + assertTrue( e.getMessage().contains("binaryList") ); + } + } + @Entity + static class Broken { + @Id long id; + List binaryList; // this is not supported + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/basic/ListOfStringTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/basic/ListOfStringTest.java new file mode 100644 index 000000000000..71711123647a --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/basic/ListOfStringTest.java @@ -0,0 +1,41 @@ +package org.hibernate.orm.test.annotations.basic; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import org.hibernate.testing.orm.junit.DialectFeatureChecks; +import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.Jpa; +import org.hibernate.testing.orm.junit.RequiresDialectFeature; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@JiraKey("HHH-17739") +@Jpa(annotatedClasses = ListOfStringTest.Unbroken.class) +@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsStandardArrays.class) +public class ListOfStringTest { + @Test void test(EntityManagerFactoryScope scope) { + scope.inTransaction(entityManager -> { + entityManager.persist( new Unbroken( List.of("hello", "world") ) ); + }); + scope.inTransaction(entityManager -> { + assertEquals( List.of("hello", "world"), + entityManager.find(Unbroken.class, 0).stringList ); + }); + + } + @Entity + static class Unbroken { + @Id long id; + List stringList; // this should be OK + + Unbroken(List stringList) { + this.stringList = stringList; + } + Unbroken() { + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/beanvalidation/CollectionActionsValidationTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/beanvalidation/CollectionActionsValidationTest.java new file mode 100644 index 000000000000..47e4acbf8001 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/beanvalidation/CollectionActionsValidationTest.java @@ -0,0 +1,257 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.annotations.beanvalidation; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.ElementCollection; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToMany; +import jakarta.persistence.ValidationMode; +import jakarta.validation.ConstraintViolation; +import jakarta.validation.ConstraintViolationException; +import jakarta.validation.Path; +import jakarta.validation.constraints.Future; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.Size; +import org.hibernate.SessionFactory; +import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.Jpa; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@Jpa(annotatedClasses = { + CollectionActionsValidationTest.TestEntity.class, + CollectionActionsValidationTest.ChildEntity.class, +}, validationMode = ValidationMode.AUTO) +@Jira( "https://hibernate.atlassian.net/browse/HHH-19232" ) +public class CollectionActionsValidationTest { + @Test + public void testPersistEmpty(EntityManagerFactoryScope scope) { + scope.inTransaction( entityManager -> { + final ConstraintViolationException e = assertThrows( ConstraintViolationException.class, () -> { + final TestEntity entity = new TestEntity( 2L ); + assertThat( entity.getNotEmptySet() ).isNull(); + assertThat( entity.getMinSizeList() ).isNull(); + entityManager.persist( entity ); + entityManager.flush(); + } ); + assertThat( e.getConstraintViolations() ).hasSize( 1 ); + assertThat( getPropertyPaths( e ) ).containsOnly( "notEmptySet" ); + } ); + } + + @Test + public void testPersistInvalidChild(EntityManagerFactoryScope scope) { + scope.inTransaction( entityManager -> { + final ConstraintViolationException e = assertThrows( ConstraintViolationException.class, () -> { + final TestEntity entity = new TestEntity( 2L ); + entity.setNotEmptySet( Set.of( new ChildEntity( 2L, "" ) ) ); + entity.setMinSizeList( List.of( "test" ) ); + entityManager.persist( entity ); + entityManager.flush(); + } ); + assertThat( e.getConstraintViolations() ).hasSize( 1 ); + assertThat( getPropertyPaths( e ) ).containsOnly( "name" ); + } ); + } + + @Test + public void testUpdateEmptyUsingGetter(EntityManagerFactoryScope scope) { + scope.inTransaction( entityManager -> { + final ConstraintViolationException e = assertThrows( ConstraintViolationException.class, () -> { + final TestEntity entity = entityManager.find( TestEntity.class, 1L ); + entity.getNotEmptySet().clear(); + entityManager.flush(); + } ); + assertThat( e.getConstraintViolations() ).hasSize( 1 ); + assertThat( getPropertyPaths( e ) ).containsOnly( "notEmptySet" ); + + entityManager.clear(); + + final ConstraintViolationException e2 = assertThrows( ConstraintViolationException.class, () -> { + final TestEntity entity = entityManager.find( TestEntity.class, 1L ); + entity.getMinSizeList().clear(); + entityManager.flush(); + } ); + assertThat( e2.getConstraintViolations() ).hasSize( 1 ); + assertThat( getPropertyPaths( e2 ) ).containsOnly( "minSizeList" ); + } ); + } + + @Test + public void testUpdateEmptyUsingSetter(EntityManagerFactoryScope scope) { + scope.inTransaction( entityManager -> { + final ConstraintViolationException e = assertThrows( ConstraintViolationException.class, () -> { + final TestEntity entity = entityManager.find( TestEntity.class, 1L ); + entity.setNotEmptySet( Set.of() ); + entityManager.flush(); + } ); + assertThat( e.getConstraintViolations() ).hasSize( 1 ); + assertThat( getPropertyPaths( e ) ).containsOnly( "notEmptySet" ); + + entityManager.clear(); + + final ConstraintViolationException e2 = assertThrows( ConstraintViolationException.class, () -> { + final TestEntity entity = entityManager.find( TestEntity.class, 1L ); + entity.setMinSizeList( List.of() ); + entityManager.flush(); + } ); + assertThat( e2.getConstraintViolations() ).hasSize( 1 ); + assertThat( getPropertyPaths( e2 ) ).containsOnly( "minSizeList" ); + } ); + } + + @Test + public void testUpdateNull(EntityManagerFactoryScope scope) { + scope.inTransaction( entityManager -> { + final TestEntity entity = new TestEntity( 3L ); + entity.setNotEmptySet( Set.of( new ChildEntity( 3L, "child_3" ) ) ); + entity.setMinSizeList( List.of( "three" ) ); + entityManager.persist( entity ); + } ); + scope.inTransaction( entityManager -> { + final ConstraintViolationException e = assertThrows( ConstraintViolationException.class, () -> { + final TestEntity entity = entityManager.find( TestEntity.class, 3L ); + entity.setNotEmptySet( null ); + entityManager.flush(); + } ); + assertThat( e.getConstraintViolations() ).hasSize( 1 ); + assertThat( getPropertyPaths( e ) ).containsOnly( "notEmptySet" ); + } ); + } + + @Test + public void testUpdateInvalidChild(EntityManagerFactoryScope scope) { + scope.inTransaction( entityManager -> { + final ConstraintViolationException e = assertThrows( ConstraintViolationException.class, () -> { + final TestEntity entity = entityManager.find( TestEntity.class, 1L ); + final ChildEntity child = entity.getNotEmptySet().iterator().next(); + child.setName( "" ); + entityManager.flush(); + } ); + assertThat( e.getConstraintViolations() ).hasSize( 1 ); + assertThat( getPropertyPaths( e ) ).containsOnly( "name" ); + } ); + } + + @Test + public void testUpdateCollectionUsingGetterAndBasicProperty(EntityManagerFactoryScope scope) { + scope.inTransaction( entityManager -> { + final ConstraintViolationException e = assertThrows( ConstraintViolationException.class, () -> { + final TestEntity entity = entityManager.find( TestEntity.class, 1L ); + entity.getNotEmptySet().clear(); + entity.setExpiryDate( LocalDate.now().minusDays( 1L ) ); + entityManager.flush(); + } ); + assertThat( e.getConstraintViolations() ).hasSize( 2 ); + assertThat( getPropertyPaths( e ) ).containsOnly( "notEmptySet", "expiryDate" ); + } ); + } + + private static List getPropertyPaths(ConstraintViolationException e) { + return e.getConstraintViolations().stream().map( ConstraintViolation::getPropertyPath ).map( Path::toString ) + .collect( Collectors.toList() ); + } + + @BeforeEach + public void setUp(EntityManagerFactoryScope scope) { + scope.inTransaction( entityManager -> { + final TestEntity a = new TestEntity( 1L ); + a.setNotEmptySet( Set.of( new ChildEntity( 1L, "child_1" ) ) ); + a.setMinSizeList( List.of( "one" ) ); + entityManager.persist( a ); + } ); + } + + @AfterEach + public void tearDown(EntityManagerFactoryScope scope) { + scope.getEntityManagerFactory().unwrap( SessionFactory.class ).getSchemaManager().truncateMappedObjects(); + } + + @Entity(name = "TestEntity") + static class TestEntity { + @Id + private Long id; + + @ManyToMany(cascade = CascadeType.PERSIST) + @NotEmpty + private Set notEmptySet; + + @ElementCollection + @Size(min = 1) + private List minSizeList; + + @Future + private LocalDate expiryDate = LocalDate.now().plusMonths( 1L ); + + public TestEntity() { + } + + public TestEntity(Long id) { + this.id = id; + } + + public Set getNotEmptySet() { + return notEmptySet; + } + + public void setNotEmptySet(Set notEmptySet) { + this.notEmptySet = notEmptySet; + } + + public List getMinSizeList() { + return minSizeList; + } + + public void setMinSizeList(List minSizeList) { + this.minSizeList = minSizeList; + } + + public LocalDate getExpiryDate() { + return expiryDate; + } + + public void setExpiryDate(LocalDate updateDate) { + this.expiryDate = updateDate; + } + } + + @Entity(name = "ChildEntity") + static class ChildEntity { + @Id + private Long id; + + @NotBlank + private String name; + + public ChildEntity() { + } + + public ChildEntity(Long id, String name) { + this.id = id; + this.name = name; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/cid/CompositeIdFkGeneratedValueTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/cid/CompositeIdFkGeneratedValueTest.java index 794e6c8f934e..0030b1bf3dd4 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/cid/CompositeIdFkGeneratedValueTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/cid/CompositeIdFkGeneratedValueTest.java @@ -29,7 +29,7 @@ /** * This tests the design demonstrated in the user + * 'https://docs.hibernate.org/orm/5.4/userguide/html_single/Hibernate_User_Guide.html#identifiers-composite-nonaggregated'>user * guide, example "@{@link IdClass} with partial identifier generation using @{@link GeneratedValue}". The * getters and setters have been omitted for clarity of the code. A separate test has been made for * {@link GenerationType#SEQUENCE}, {@link GenerationType#TABLE}, and diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/cid/EmbeddedIdLazyOneToOneCriteriaQueryTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/cid/EmbeddedIdLazyOneToOneCriteriaQueryTest.java new file mode 100644 index 000000000000..1c0b21f53f0d --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/cid/EmbeddedIdLazyOneToOneCriteriaQueryTest.java @@ -0,0 +1,119 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.annotations.cid; + +import jakarta.persistence.Embeddable; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToOne; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Root; +import org.hibernate.Hibernate; +import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel(annotatedClasses = { + EmbeddedIdLazyOneToOneCriteriaQueryTest.EntityA.class, + EmbeddedIdLazyOneToOneCriteriaQueryTest.EntityB.class, +}) +@SessionFactory +@Jira("https://hibernate.atlassian.net/browse/HHH-19687") +@BytecodeEnhanced +public class EmbeddedIdLazyOneToOneCriteriaQueryTest { + + @Test + public void query(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final CriteriaBuilder builder = session.getCriteriaBuilder(); + final CriteriaQuery criteriaQuery = builder.createQuery( EntityA.class ); + final Root root = criteriaQuery.from( EntityA.class ); + criteriaQuery.where( root.get( "id" ).in( 1 ) ); + criteriaQuery.select( root ); + + final List entities = session.createQuery( criteriaQuery ).getResultList(); + assertThat( entities ).hasSize( 1 ); + assertThat( Hibernate.isPropertyInitialized( entities.get( 0 ), "entityB" ) ).isFalse(); + } ); + } + + @BeforeAll + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final EntityA entityA = new EntityA( 1 ); + session.persist( entityA ); + final EntityB entityB = new EntityB( new EntityBId( entityA ) ); + session.persist( entityB ); + } ); + } + + @AfterAll + public void tearDown(SessionFactoryScope scope) { + scope.inTransaction( session -> session.getSessionFactory().getSchemaManager().truncateMappedObjects() ); + } + + @Entity(name = "EntityA") + static class EntityA { + + @Id + private Integer id; + + @OneToOne(mappedBy = "id.entityA", fetch = FetchType.LAZY) + private EntityB entityB; + + public EntityA() { + } + + public EntityA(Integer id) { + this.id = id; + } + + } + + @Entity(name = "EntityB") + static class EntityB { + + @EmbeddedId + private EntityBId id; + + public EntityB() { + } + + public EntityB(EntityBId id) { + this.id = id; + } + + } + + @Embeddable + static class EntityBId { + + @OneToOne(fetch = FetchType.LAZY) + private EntityA entityA; + + public EntityBId() { + } + + public EntityBId(EntityA entityA) { + this.entityA = entityA; + } + + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/cid/NestedCompositeIdWithOrderedUpdatesTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/cid/NestedCompositeIdWithOrderedUpdatesTest.java new file mode 100644 index 000000000000..4c3284067cc0 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/cid/NestedCompositeIdWithOrderedUpdatesTest.java @@ -0,0 +1,250 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.annotations.cid; + + +import jakarta.persistence.*; +import org.hibernate.cfg.BatchSettings; +import org.hibernate.testing.orm.junit.*; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.fail; + +/** + * @author Torsten Landmann + * @author Jan Schatteman + */ +@ServiceRegistry( + settings = { + @Setting(name = BatchSettings.ORDER_UPDATES, value = "true") + } +) +@DomainModel( + annotatedClasses = { + NestedCompositeIdWithOrderedUpdatesTest.A.class, + NestedCompositeIdWithOrderedUpdatesTest.AId.class, + NestedCompositeIdWithOrderedUpdatesTest.B.class, + NestedCompositeIdWithOrderedUpdatesTest.BId.class, + NestedCompositeIdWithOrderedUpdatesTest.C.class + } +) +@SessionFactory +public class NestedCompositeIdWithOrderedUpdatesTest { + + @Test + public void testUpdateOrderingWithNestedCompositeIds(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + // set up entities + C c1 = new C(); + c1.setCvalue("sample_c1"); + session.persist(c1); + C c2 = new C(); + c2.setCvalue("sample_c2"); + session.persist(c2); + + B b1 = new B(); + b1.setId(new BId(c1, "b1_key")); + b1.setBvalue("sample_b1"); + session.persist(b1); + B b2 = new B(); + b2.setId(new BId(c2, "b2_key")); + b2.setBvalue("sample_b2"); + session.persist(b2); + + A a1 = new A(); + a1.setId(new AId(b1, "a1")); + a1.setAvalue("sample_a1"); + session.persist(a1); + A a2 = new A(); + a2.setId(new AId(b2, "a2")); + a2.setAvalue("sample_a2"); + session.persist(a2); + } + ); + + try { + scope.inTransaction( + session -> { + TypedQuery query = session.createQuery("select a from A a", A.class); + List aList = query.getResultList(); + for (A curA : aList) { + curA.setAvalue(curA.getAvalue() + "_modified"); + session.persist(curA); + } + } + ); + } catch (UnsupportedOperationException uoe) { + fail("Shouldn't throw an UnsupportedOperationException!"); + } + } + + @Entity(name = "A") + public static class A + { + @EmbeddedId + private AId id; + + private String avalue; + + public AId getId() + { + return id; + } + + public void setId(AId id) + { + this.id=id; + } + + public String getAvalue() + { + return avalue; + } + + public void setAvalue(String avalue) + { + this.avalue=avalue; + } + } + + @Embeddable + public static class AId + { + @ManyToOne(cascade={}, // cascade nothing + fetch=FetchType.LAZY, + optional=false) + private B b; + + /** + * "key" won't work because h2 database considers it a reserved word, and hibernate doesn't escape it. + * furthermore, the variable name must be after {@link #b}, alphabetically, to account for hibernate's internal sorting. + */ + private String zkey; + + public AId() + { + } + + public AId(B b, String key) + { + this.b=b; + this.zkey=key; + } + + public B getB() + { + return b; + } + + public void setB(B b) + { + this.b=b; + } + + public String getZkey() + { + return zkey; + } + + public void setZkey(String zkey) + { + this.zkey=zkey; + } + } + + @Entity(name = "B") + public static class B + { + @EmbeddedId + private BId id; + + private String bvalue; + + public BId getId() + { + return id; + } + + public void setId(BId id) + { + this.id=id; + } + + public String getBvalue() + { + return bvalue; + } + + public void setBvalue(String bvalue) + { + this.bvalue=bvalue; + } + } + + @Embeddable + public static class BId + { + @ManyToOne(cascade={}, // cascade nothing + fetch=FetchType.LAZY, + optional=false) + private C c; + + /** + * "key" won't work because h2 database considers it a reserved word, and hibernate doesn't escape it. + * furthermore, the variable name must be after {@link #c}, alphabetically, to account for hibernate's internal sorting. + */ + private String zkey; + + public BId() + { + } + + public BId(C c, String key) + { + this.c=c; + this.zkey=key; + } + + public C getC() + { + return c; + } + + public void setC(C c) + { + this.c=c; + } + + public String getZkey() + { + return zkey; + } + + public void setZkey(String zkey) + { + this.zkey=zkey; + } + } + + @Entity(name = "C") + public static class C + { + @Id + private String cvalue; + + public String getCvalue() + { + return cvalue; + } + + public void setCvalue(String cvalue) + { + this.cvalue=cvalue; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/collectionelement/TestCollectionWithOrderInsertAndOrderUpdates.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/collectionelement/TestCollectionWithOrderInsertAndOrderUpdates.java new file mode 100644 index 000000000000..9883e425c188 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/collectionelement/TestCollectionWithOrderInsertAndOrderUpdates.java @@ -0,0 +1,272 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.annotations.collectionelement; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.JoinColumns; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import org.hibernate.cfg.BatchSettings; +import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.Setting; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + + +@DomainModel( + annotatedClasses = { + TestCollectionWithOrderInsertAndOrderUpdates.ChildEntity.class, + TestCollectionWithOrderInsertAndOrderUpdates.ParentEntity.class, + TestCollectionWithOrderInsertAndOrderUpdates.AnotherEntity.class, + TestCollectionWithOrderInsertAndOrderUpdates.ThirdEntity.class, + } +) +@SessionFactory +@ServiceRegistry( + settings = { + @Setting(name = BatchSettings.ORDER_INSERTS, value = "true"), + @Setting(name = BatchSettings.ORDER_UPDATES, value = "true"), + } +) +@BytecodeEnhanced(runNotEnhancedAsWell = true) +@JiraKey("HHH-19585") +public class TestCollectionWithOrderInsertAndOrderUpdates { + + @BeforeEach + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + ParentEntity parentEntity = new ParentEntity( 1L, "Parent 1", 0L ); + parentEntity.addChild( new ChildEntity( 1L, "Child 1" ) ); + session.persist( parentEntity ); + + ParentEntity parentEntity2 = new ParentEntity( 2l, "Parent 2", 0L ); + parentEntity2.addChild( new ChildEntity( 2L, "Child 2" ) ); + session.persist( parentEntity2 ); + + ParentEntity parentEntity3 = new ParentEntity( 3l, "Parent 3", 0L ); + session.persist( parentEntity3 ); + + AnotherEntity anotherEntity = new AnotherEntity( 1L, "1", 1L, parentEntity ); + + AnotherEntity anotherEntity2 = new AnotherEntity( 2L, "2", 2L, parentEntity2 ); + AnotherEntity anotherEntity3 = new AnotherEntity( 3L, "3", 3L, parentEntity3 ); + + ThirdEntity thirdEntity = new ThirdEntity( 1L, "123" ); + thirdEntity.addAnotherEntity( anotherEntity ); + thirdEntity.addAnotherEntity( anotherEntity2 ); + thirdEntity.addAnotherEntity( anotherEntity3 ); + + session.persist( thirdEntity ); + } + ); + } + + @Test + public void testGetReference(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + ThirdEntity thirdEntity = session.getReference( ThirdEntity.class, 1L ); + for ( AnotherEntity thirdEntityDetail : thirdEntity.getAnotherEntities() ) { + thirdEntityDetail.getParentEntity().getName(); + } + } + ); + + scope.inTransaction( + session -> { + ParentEntity parentEntity = session.find( ParentEntity.class, 1L ); + assertThat( parentEntity.getChildren().size() ).isEqualTo( 1 ); + + ParentEntity parentEntity2 = session.find( ParentEntity.class, 2L ); + assertThat( parentEntity2.getChildren().size() ).isEqualTo( 1 ); + + ParentEntity parentEntity3 = session.find( ParentEntity.class, 3L ); + assertThat( parentEntity3.getChildren().size() ).isEqualTo( 0 ); + } + ); + } + + @AfterEach + public void tearDown(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } + + @Entity(name = "ChildEntity") + public static class ChildEntity { + @Id + private Long id; + + private String name; + + public ChildEntity() { + } + + public ChildEntity(Long id, String name) { + this.id = id; + this.name = name; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + + @Entity(name = "ParentEntity") + public static class ParentEntity { + @Id + private Long id; + + private String name; + + @Column(name = "secondid") + private Long secondId; + + @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL) + @JoinColumns({ + @JoinColumn(name = "parentid", referencedColumnName = "id"), + @JoinColumn(name = "secondid", referencedColumnName = "secondid") + }) + private List children; + + public ParentEntity() { + } + + public ParentEntity(Long id, String name, Long secondId) { + this.id = id; + this.name = name; + this.secondId = secondId; + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + public List getChildren() { + return children; + } + + public void addChild(ChildEntity childEntity) { + if ( this.children == null ) { + this.children = new ArrayList<>(); + } + this.children.add( childEntity ); + } + } + + @Entity(name = "AnotherEntity") + public static class AnotherEntity { + @Id + private Long id; + + private String name; + + @Column(name = "parentid") + private Long parentId; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "parentid", insertable = false, updatable = false, referencedColumnName = "id") + private ParentEntity parentEntity; + + public AnotherEntity() { + } + + public AnotherEntity(Long id, String name, Long parentId, ParentEntity parentEntity) { + this.id = id; + this.name = name; + this.parentId = parentId; + this.parentEntity = parentEntity; + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + public Long getParentId() { + return parentId; + } + + public ParentEntity getParentEntity() { + return parentEntity; + } + } + + @Entity(name = "ThirdEntity") + public static class ThirdEntity { + @Id + private Long id; + + private String name; + + @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL) + @JoinColumn(name = "thirdEntityHeader", referencedColumnName = "id") + private List anotherEntities; + + public ThirdEntity() { + } + + public ThirdEntity(Long id, String name) { + this.id = id; + this.name = name; + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + public List getAnotherEntities() { + return anotherEntities; + } + + public void addAnotherEntity(AnotherEntity anotherEntity) { + if ( this.anotherEntities == null ) { + this.anotherEntities = new ArrayList<>(); + } + this.anotherEntities.add( anotherEntity ); + } + } + + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/comment/CommentElementCollectionTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/comment/CommentElementCollectionTest.java new file mode 100644 index 000000000000..b32b25120536 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/comment/CommentElementCollectionTest.java @@ -0,0 +1,81 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.orm.test.annotations.comment; + + +import jakarta.persistence.ElementCollection; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.OrderColumn; +import org.hibernate.annotations.Comment; +import org.hibernate.boot.Metadata; +import org.hibernate.boot.spi.BootstrapContext; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.integrator.spi.Integrator; +import org.hibernate.mapping.PersistentClass; +import org.hibernate.service.spi.SessionFactoryServiceRegistry; +import org.hibernate.testing.orm.junit.BootstrapServiceRegistry; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Test; + +import java.util.Collection; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@JiraKey(value = "HHH-19011") +@DomainModel(annotatedClasses = {CommentElementCollectionTest.MainEntity.class}) +@SessionFactory +@BootstrapServiceRegistry(integrators = CommentElementCollectionTest.MetadataIntegrator.class) +public class CommentElementCollectionTest { + private static Metadata METADATA; + + @Test + public void testTableCommentsPlacement(SessionFactoryScope scope) { + scope.inSession(session -> { + Collection entityBindings = METADATA.getEntityBindings(); + assertThat( entityBindings.iterator().next().getTable().getComment() ).isEqualTo( + MainEntity.EXPECTED_MAIN_ENTITY_TABLE_COMMENT ); + + Collection collectionBindings = METADATA.getCollectionBindings(); + assertThat( collectionBindings.iterator().next().getCollectionTable().getComment() ).isEqualTo( + MainEntity.EXPECTED_ELEMENT_COLLECTION_TABLE_COMMENT ); + }); + } + + @Entity + @Comment(MainEntity.EXPECTED_MAIN_ENTITY_TABLE_COMMENT) + static class MainEntity { + public static final String EXPECTED_MAIN_ENTITY_TABLE_COMMENT = "Expected main entity table level comment"; + public static final String EXPECTED_ELEMENT_COLLECTION_TABLE_COMMENT = "Expected element collection table level comment"; + + @Id + @GeneratedValue + public long id; + + @ElementCollection + @OrderColumn(name = "elementOrder", nullable = false) + @Comment(EXPECTED_ELEMENT_COLLECTION_TABLE_COMMENT) + public List embeddableValues; + } + + public static class MetadataIntegrator implements Integrator { + @Override + public void integrate(Metadata metadata, BootstrapContext bootstrapContext, SessionFactoryImplementor sessionFactory) { + METADATA = metadata; + } + + @Override + public void disintegrate(SessionFactoryImplementor sessionFactory, SessionFactoryServiceRegistry serviceRegistry) { + // do nothing + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/derivedidentities/OneToManyEmbeddableId.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/derivedidentities/OneToManyEmbeddableId.java new file mode 100644 index 000000000000..66906a093907 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/derivedidentities/OneToManyEmbeddableId.java @@ -0,0 +1,176 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.annotations.derivedidentities; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@JiraKey("HHH-18702") +@DomainModel( + annotatedClasses = { + OneToManyEmbeddableId.Parent.class, + OneToManyEmbeddableId.FirstChild.class, + OneToManyEmbeddableId.SecondChild.class, + } +) +@SessionFactory +public class OneToManyEmbeddableId { + private static final BigDecimal FIRST_CHILD_CODE = new BigDecimal( 2 ); + + @BeforeAll + public static void init(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Parent parent = new Parent( BigDecimal.TEN, "Lio" ); + FirstChild firstChild = new FirstChild( parent, BigDecimal.ONE, FIRST_CHILD_CODE ); + SecondChild secondChild = new SecondChild( firstChild, BigDecimal.TEN, "Al" ); + firstChild.addChild( secondChild ); + session.persist( parent ); + session.persist( firstChild ); + session.persist( secondChild ); + } + ); + } + + @Test + public void propertyNavigationTest(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + FirstChild firstChild = session.createQuery( + "select f from FirstChild f where f.code = :code", FirstChild.class ) + .setParameter( "code", FIRST_CHILD_CODE ).getSingleResult(); + + assertThat( firstChild ).isNotNull(); + + assertThat( firstChild.getChildren() ).hasSize( 1 ); + } + ); + + } + + @Entity(name = "TestEntity") + public static class Parent { + @Id + private BigDecimal id; + + private String name; + + public Parent() { + } + + public Parent(BigDecimal id, String name) { + this.id = id; + this.name = name; + } + + @OneToMany(mappedBy = "id.parent") + private List children = new ArrayList<>(); + + void addChild(FirstChild firstChild) { + children.add( firstChild ); + } + } + + @Entity(name = "FirstChild") + public static class FirstChild { + @EmbeddedId + private FirstChildId id; + + @Column(name = "FIRST_CHILD_CODE") + private BigDecimal code; + + @OneToMany(mappedBy = "id.firstChild") + private List children = new ArrayList<>(); + + public FirstChildId getId() { + return id; + } + + public FirstChild() { + } + + public FirstChild(Parent parent, BigDecimal bigDecimalNum, BigDecimal code) { + this.id = new FirstChildId( bigDecimalNum, parent ); + parent.addChild( this ); + this.code = code; + } + + public List getChildren() { + return children; + } + + void addChild(SecondChild child) { + children.add( child ); + } + } + + @Embeddable + public static class FirstChildId { + private BigDecimal bigDecimalNum; + + @ManyToOne() + private Parent parent; + + public FirstChildId() { + } + + public FirstChildId(BigDecimal bigDecimalNum, Parent parent) { + this.bigDecimalNum = bigDecimalNum; + this.parent = parent; + } + } + + @Entity(name = "SecondChild") + public static class SecondChild { + @EmbeddedId + private SecondChildId id; + + private String name; + + public SecondChild() { + } + + public SecondChild(FirstChild firstChild, BigDecimal bigDecimalNum, String name) { + this.id = new SecondChildId( bigDecimalNum, firstChild ); + this.name = name; + } + } + + @Embeddable + public static class SecondChildId { + private BigDecimal bigDecimalNum; + + @ManyToOne + @JoinColumn(name = "FIRST_CHILD_CODE", referencedColumnName = "FIRST_CHILD_CODE") + private FirstChild firstChild; + + public SecondChildId() { + } + + public SecondChildId(BigDecimal bigDecimalNum, FirstChild firstChild) { + this.bigDecimalNum = bigDecimalNum; + this.firstChild = firstChild; + } + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/entity/Forest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/entity/Forest.java index 93aa841e523b..2d9814b4d373 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/entity/Forest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/entity/Forest.java @@ -27,6 +27,7 @@ import org.hibernate.annotations.SelectBeforeUpdate; import org.hibernate.annotations.Where; +import jakarta.persistence.Column; import jakarta.persistence.Convert; import jakarta.persistence.ElementCollection; import jakarta.persistence.Entity; @@ -63,6 +64,7 @@ public class Forest { @OptimisticLock(excluded=true) @JdbcTypeCode( Types.LONGVARCHAR ) + @Column(length = 10000) public String getLongDescription() { return longDescription; } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/naturalid/ByteArrayNaturalIdTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/naturalid/ByteArrayNaturalIdTest.java new file mode 100644 index 000000000000..641f397ed077 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/naturalid/ByteArrayNaturalIdTest.java @@ -0,0 +1,85 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.annotations.naturalid; + +import org.hibernate.annotations.NaturalId; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + + +@DomainModel( + annotatedClasses = { + ByteArrayNaturalIdTest.TestEntity.class, + } +) +@SessionFactory +@JiraKey("HHH-18409") +public class ByteArrayNaturalIdTest { + + private static final String NATURAL_ID_1 = "N1"; + + @AfterEach + public void tearDown(SessionFactoryScope scope) { + scope.inTransaction( + session -> + session.createMutationQuery( "delete TestEntity" ).executeUpdate() + ); + } + + @Test + public void testSelectByNaturalId(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + TestEntity entity = new TestEntity( 1L, new byte[] { 1, 2 }, NATURAL_ID_1 ); + session.persist( entity ); + TestEntity testEntity = session.byNaturalId( TestEntity.class ) + .using( "naturalId2", NATURAL_ID_1 ) + .using( "naturalId1", new byte[] { 1, 2 } ) + .load(); + + assertThat( testEntity ).as( "Loading the entity by its natural id failed" ).isNotNull(); + TestEntity testEntity2 = session.byNaturalId( TestEntity.class ) + .using( "naturalId2", NATURAL_ID_1 ) + .using( "naturalId1", new byte[] { 1, 3 } ) + .load(); + assertThat( testEntity2 ).as( "Loading the entity using wrong natural id failed" ).isNull(); + + } + ); + } + + @Entity(name = "TestEntity") + public static class TestEntity { + @Id + private Long id; + + @NaturalId + private byte[] naturalId1; + + @NaturalId + private String naturalId2; + + public TestEntity() { + } + + public TestEntity(Long id, byte[] naturalId1, String naturalId2) { + this.id = id; + this.naturalId1 = naturalId1; + this.naturalId2 = naturalId2; + } + + + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/refcolnames/secondary/RefToSecondaryTableTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/refcolnames/secondary/RefToSecondaryTableTest.java index 2ae3ccc0bc6a..405b3606b9d7 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/refcolnames/secondary/RefToSecondaryTableTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/refcolnames/secondary/RefToSecondaryTableTest.java @@ -5,10 +5,8 @@ import org.hibernate.testing.orm.junit.SessionFactory; import org.hibernate.testing.orm.junit.SessionFactoryScope; import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; -//@Disabled("test produces broken SQL and issue needs to be fixed") @TestForIssue(jiraKey = "HHH-15933") @SessionFactory @DomainModel(annotatedClasses = { Split.class, Reference.class }) diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/uniqueconstraint/UniqueConstraintTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/uniqueconstraint/UniqueConstraintTest.java index 5b2c2f935f2c..8a14234ba0a0 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/uniqueconstraint/UniqueConstraintTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/annotations/uniqueconstraint/UniqueConstraintTest.java @@ -7,6 +7,7 @@ package org.hibernate.orm.test.annotations.uniqueconstraint; import org.hibernate.JDBCException; +import org.hibernate.community.dialect.InformixDialect; import org.hibernate.dialect.SybaseDialect; import org.hibernate.testing.orm.junit.DomainModel; @@ -27,6 +28,9 @@ */ @DomainModel( annotatedClasses = { Room.class, Building.class, House.class } ) @SessionFactory +@SkipForDialect( dialectClass = InformixDialect.class, + matchSubTypes = true, + reason = "Informix does not properly support unique constraints on nullable columns" ) @SkipForDialect( dialectClass = SybaseDialect.class, matchSubTypes = true, reason = "Sybase does not properly support unique constraints on nullable columns" ) diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/array/StructArrayWithNullElementTestDemoTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/array/StructArrayWithNullElementTestDemoTest.java new file mode 100644 index 000000000000..e44cf3906826 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/array/StructArrayWithNullElementTestDemoTest.java @@ -0,0 +1,87 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.array; + +import jakarta.persistence.Embeddable; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import org.hibernate.annotations.Struct; +import org.hibernate.testing.orm.junit.*; +import org.hibernate.testing.orm.junit.DialectFeatureChecks.SupportsStructAggregate; +import org.hibernate.testing.orm.junit.DialectFeatureChecks.SupportsTypedArrays; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +@SessionFactory +@DomainModel(annotatedClasses = { + StructArrayWithNullElementTestDemoTest.Book.class, + StructArrayWithNullElementTestDemoTest.Author.class +}) +@RequiresDialectFeature(feature = SupportsStructAggregate.class) +@RequiresDialectFeature(feature = SupportsTypedArrays.class) +class StructArrayWithNullElementTestDemoTest { + + @Test + void test(SessionFactoryScope scope) { + scope.inTransaction( session -> { + var book = new Book(); + book.id = 1; + book.authors = Arrays.asList( + new Author( "John", "Smith" ), + null + ); + session.persist( book ); + } ); + + scope.inSession( session -> { + final var book = session.find( Book.class, 1 ); + assertEquals( 2, book.authors.size() ); + assertEquals( new Author( "John", "Smith" ), book.authors.get( 0 ) ); + assertNull( book.authors.get( 1 ) ); + } ); + } + + @Entity(name = "Book") + @Table(name = "books") + static class Book { + @Id + int id; + List authors; + } + + @Embeddable + @Struct(name = "Author") + static final class Author { + String firstName; + String lastName; + + public Author() { + } + + public Author(String firstName, String lastName) { + this.firstName = firstName; + this.lastName = lastName; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + Author author = (Author) o; + return Objects.equals(firstName, author.firstName) && Objects.equals(lastName, author.lastName); + } + + @Override + public int hashCode() { + return Objects.hash(firstName, lastName); + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/associations/ManyToOneUniqueKeyReferenceWithCustomIdTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/associations/ManyToOneUniqueKeyReferenceWithCustomIdTest.java new file mode 100644 index 000000000000..b8d92ec659ee --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/associations/ManyToOneUniqueKeyReferenceWithCustomIdTest.java @@ -0,0 +1,219 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.orm.test.associations; + +import java.io.Serializable; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; +import java.util.Objects; + +import org.hibernate.HibernateException; +import org.hibernate.annotations.NaturalId; +import org.hibernate.annotations.Type; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.usertype.EnhancedUserType; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; + +/** + * @author Kowsar Atazadeh + */ +@SessionFactory +@DomainModel(annotatedClasses = + { + ManyToOneUniqueKeyReferenceWithCustomIdTest.Phone.class, + ManyToOneUniqueKeyReferenceWithCustomIdTest.User.class + }) +@JiraKey("HHH-18764") +public class ManyToOneUniqueKeyReferenceWithCustomIdTest { + + @Test + void test(SessionFactoryScope scope) { + scope.inTransaction( session -> { + Phone phone = new Phone(); + + User user1 = new User( new CustomId( "u1" ), "Kowsar" ); + session.persist( user1 ); + phone.setUser( user1 ); + session.persist( phone ); + + User user2 = new User( new CustomId( "u2" ), "Someone" ); + session.persist( user2 ); + phone.setUser( user2 ); + session.persist( phone ); + } ); + } + + @Entity(name = "Phone") + static class Phone { + @Id + @GeneratedValue + Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(referencedColumnName = "name", nullable = false) + User user; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public User getUser() { + return user; + } + + public void setUser(User user) { + this.user = user; + } + } + + @Entity(name = "_User") + static class User { + @Id + @Type(CustomIdType.class) + CustomId id; + + @NaturalId + String name; + + public User() { + } + + public User(CustomId id, String name) { + this.id = id; + this.name = name; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public CustomId getId() { + return id; + } + + public void setId(CustomId id) { + this.id = id; + } + } + + static class CustomIdType implements EnhancedUserType { + @Override + public String toSqlLiteral(CustomId value) { + return "'" + value.toString() + "'"; + } + + @Override + public String toString(CustomId value) throws HibernateException { + return value.toString(); + } + + @Override + public void nullSafeSet( + PreparedStatement st, CustomId value, int position, + SharedSessionContractImplementor session) throws SQLException { + st.setObject( position, value.toString(), getSqlType() ); + } + + @Override + public CustomId nullSafeGet(ResultSet rs, int position, SharedSessionContractImplementor session, Object owner) + throws SQLException { + String idValue = rs.getString( position ); + return idValue != null ? fromStringValue( idValue ) : null; + } + + @Override + public CustomId fromStringValue(CharSequence sequence) throws HibernateException { + return new CustomId( sequence.toString() ); + } + + @Override + public int getSqlType() { + return Types.VARCHAR; + } + + @Override + public Class returnedClass() { + return CustomId.class; + } + + @Override + public CustomId deepCopy(CustomId value) { + return new CustomId( value.getId() ); + } + + @Override + public boolean isMutable() { + return false; + } + + @Override + public Serializable disassemble(CustomId value) { + return value; + } + + @Override + public CustomId assemble(Serializable cached, Object owner) { + return (CustomId) cached; + } + + @Override + public boolean equals(CustomId x, CustomId y) { + return Objects.equals( x, y ); + } + + @Override + public int hashCode(CustomId x) { + return x != null ? x.hashCode() : 0; + } + } + + static class CustomId implements Serializable { + private String id; + + public CustomId() { + } + + public CustomId(String id) { + this.id = id; + } + + @Override + public String toString() { + return id; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/batch/BatchAndUserTypeIdCollectionTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/batch/BatchAndUserTypeIdCollectionTest.java new file mode 100644 index 000000000000..fcd918452e04 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/batch/BatchAndUserTypeIdCollectionTest.java @@ -0,0 +1,319 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.batch; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import org.assertj.core.api.Assertions; +import org.hibernate.HibernateException; +import org.hibernate.annotations.BatchSize; +import org.hibernate.annotations.Type; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.testing.jdbc.SQLStatementInspector; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.Setting; +import org.hibernate.type.descriptor.converter.spi.BasicValueConverter; +import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.descriptor.java.StringJavaType; +import org.hibernate.type.internal.UserTypeJavaTypeWrapper; +import org.hibernate.usertype.EnhancedUserType; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.io.Serializable; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Set; + +import static org.hibernate.type.SqlTypes.VARCHAR; + +@ServiceRegistry( + settings = { + @Setting( name = AvailableSettings.DIALECT_NATIVE_PARAM_MARKERS, value = "false" ) + } +) +@DomainModel( + annotatedClasses = { + BatchAndUserTypeIdCollectionTest.Child.class, + BatchAndUserTypeIdCollectionTest.Parent.class + } +) +@SessionFactory( + useCollectingStatementInspector = true +) + +public class BatchAndUserTypeIdCollectionTest { + + @BeforeAll + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + for (long i = 1L; i < 11; i++) { + Parent parent = new Parent( new Parent.ParentId( "parent-" + i ) ); + Child child1 = new Child( i * 100L + 1L, parent ); + Child child2 = new Child( i * 100L + 2L, parent ); + Child child3 = new Child( i * 100L + 3L, parent ); + Child child4 = new Child( i * 100L + 4L, parent ); + Child child5 = new Child( i * 100L + 5L, parent ); + Child child6 = new Child( i * 100L + 6L, parent ); + Child child7 = new Child( i * 100L + 7L, parent ); + Child child8 = new Child( i * 100L + 8L, parent ); + Child child9 = new Child( i * 100L + 9L, parent ); + Child child10 = new Child( i * 100L + 10L, parent ); + Child child11 = new Child( i * 100L + 11L, parent ); + session.persist( parent ); + } + } + ); + } + + @Test + public void testBatchInitializeChildCollection(SessionFactoryScope scope){ + SQLStatementInspector statementInspector = (SQLStatementInspector) scope.getStatementInspector(); + scope.inTransaction( + session -> { + statementInspector.clear(); + final List list = session.createSelectionQuery( "from Parent", Parent.class ) + .getResultList(); + list.get( 0 ).getChildren().size(); + statementInspector.assertExecutedCount( 2 ); + Assertions.assertThat( statementInspector.getSqlQueries().get( 0 ) ).doesNotContain( "?" ); + if ( scope.getSessionFactory().getJdbcServices().getDialect().useArrayForMultiValuedParameters() ) { + Assertions.assertThat( statementInspector.getSqlQueries().get( 1 ) ).containsOnlyOnce( "?" ); + } + else { + Assertions.assertThat( statementInspector.getSqlQueries().get( 1 ) ).containsOnlyOnce( "in (?,?,?,?,?)" ); + } + } + ); + } + + @Entity(name = "Child") + @Table(name = "child_table") + public static class Child { + @Id + private Long id; + + private String name; + + @ManyToOne + @JoinColumn(name = "parent_id") + private Parent parent; + + public Child() { + } + + public Child(Long id, Parent parent) { + this.id = id; + this.name = String.valueOf( id ); + this.parent = parent; + parent.addChild( this ); + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + public Parent getParent() { + return parent; + } + + } + + @Entity(name = "Parent") + @Table(name = "parents") + public static class Parent { + @Id + @Type(ParentIdUserType.class) + private ParentId id; + + private String name; + + @BatchSize(size = 5) + @OneToMany(mappedBy = "parent", fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) + public Set children = new HashSet<>(); + + public Parent() { + } + + public Parent(ParentId id) { + this.id = id; + this.name = String.valueOf( id ); + } + + public ParentId getId() { + return id; + } + + public String getName() { + return name; + } + + public Set getChildren() { + return children; + } + + public void addChild(Child child){ + children.add( child ); + } + + public static class ParentId implements Serializable { + + private static final long serialVersionUID = 1L; + + private final String id; + + @Override + public String toString() { + return id; + } + + public ParentId(String id) { + this.id = id; + } + + public String getId() { + return id; + } + + @Override + public boolean equals(Object o) { + if ( o == null || getClass() != o.getClass() ) { + return false; + } + ParentId parentId = (ParentId) o; + return Objects.equals( id, parentId.id ); + } + + @Override + public int hashCode() { + return Objects.hashCode( id ); + } + } + + + static class ParentIdUserType implements EnhancedUserType { + @Override + public int getSqlType() { + return VARCHAR; + } + + @Override + public Class returnedClass() { + return ParentId.class; + } + + @Override + public boolean equals(ParentId x, ParentId y) { + return Objects.equals(x, y); + } + + @Override + public int hashCode(ParentId x) { + return x.hashCode(); + } + + @Override + public ParentId nullSafeGet(ResultSet rs, int position, + SharedSessionContractImplementor session, Object owner) + throws SQLException { + String string = rs.getString( position ); + return rs.wasNull() ? null : new ParentId(string); + } + + @Override + public void nullSafeSet(PreparedStatement st, ParentId value, int index, + SharedSessionContractImplementor session) + throws SQLException { + if ( value == null ) { + st.setNull(index, VARCHAR); + } + else { + st.setString(index, value.getId()); + } + } + + @Override + public boolean isMutable() { + return false; + } + + @Override + public ParentId deepCopy(ParentId parentId) { + return parentId; //ParentId is immutable + } + + @Override + public Serializable disassemble(ParentId parentId) { + return parentId; //ParentId is immutable + } + + @Override + public ParentId assemble(Serializable cached, Object owner) { + return (ParentId) cached; //ParentId is immutable + } + + @Override + public String toSqlLiteral(ParentId parentId) { + return parentId.getId(); + } + + @Override + public String toString(ParentId parentId) throws HibernateException { + return parentId.getId(); + } + + @Override + public ParentId fromStringValue(CharSequence sequence) throws HibernateException { + return new ParentId(sequence.toString()); + } + + @Override + public BasicValueConverter getValueConverter() { + return (BasicValueConverter) new BasicValueConverter() { + @Override + public String toRelationalValue(ParentId value) { + return value == null ? null : value.getId(); + } + + @Override + public ParentId toDomainValue(String dbData) { + return dbData == null ? null : new ParentId(dbData); + } + + @Override + public JavaType getDomainJavaType() { + return new UserTypeJavaTypeWrapper<>(ParentIdUserType.this); + } + + @Override + public JavaType getRelationalJavaType() { + return StringJavaType.INSTANCE; + } + }; + } + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/batch/BatchOffOnlyForOptimisticallyLocked.java b/hibernate-core/src/test/java/org/hibernate/orm/test/batch/BatchOffOnlyForOptimisticallyLocked.java new file mode 100644 index 000000000000..a8a11f01a558 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/batch/BatchOffOnlyForOptimisticallyLocked.java @@ -0,0 +1,75 @@ +package org.hibernate.orm.test.batch; + +import java.io.Serializable; +import java.util.List; + +import org.hibernate.cfg.BatchSettings; + +import org.hibernate.dialect.MariaDBDialect; +import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.Jpa; +import org.hibernate.testing.orm.junit.Setting; +import org.hibernate.testing.orm.junit.SkipForDialect; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.OptimisticLockException; +import jakarta.persistence.RollbackException; +import jakarta.persistence.Version; + +@Jpa( + annotatedClasses = BatchOffOnlyForOptimisticallyLocked.Something.class, + properties = { + @Setting(name = BatchSettings.STATEMENT_BATCH_SIZE, value = "10"), + @Setting(name = BatchSettings.BATCH_VERSIONED_DATA, value = "false") + } +) +@JiraKey( "HHH-18621" ) +@SkipForDialect(dialectClass = MariaDBDialect.class, majorVersion = 11, minorVersion = 6, microVersion = 2, reason = "MariaDB will throw an error DB_RECORD_CHANGED when acquiring a lock on a record that have changed") +public class BatchOffOnlyForOptimisticallyLocked { + @Test + public void testMultiUpdateOfConcurrentlyModified(EntityManagerFactoryScope scope) { + scope.inTransaction( em -> { + em.persist( new Something( "First" ) ); + em.persist( new Something( "Second" ) ); + } ); + + final RollbackException ex = Assertions.assertThrows( RollbackException.class, () -> { + scope.inTransaction( em -> { + final List subjects = em.createQuery( "select s from Something s", Something.class ) + .getResultList(); + scope.inTransaction( + competitorEm -> competitorEm.find( Something.class, subjects.get( 0 ).id ).name = "Outrun" + ); + for ( Something something : subjects ) { + something.name += " modified"; + } + } ); + } ); + Assertions.assertInstanceOf( OptimisticLockException.class, ex.getCause(), "The cause of rollback" ); + Assertions.assertNotNull( ( (OptimisticLockException) ex.getCause() ).getEntity(), "OLE references an entity" ); + } + + //Has to be Serializable, otherwise it is not deemed safe to include in the OLE. + @Entity(name = "Something") + public static class Something implements Serializable { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + public Long id; + public String name; + @Version + public long version; + + public Something() { + } + + public Something(String name) { + this.name = name; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/batch/BatchOptimisticLockingTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/batch/BatchOptimisticLockingTest.java index 17708e7471d0..caebfc592047 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/batch/BatchOptimisticLockingTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/batch/BatchOptimisticLockingTest.java @@ -17,6 +17,7 @@ import org.hibernate.dialect.CockroachDialect; import org.hibernate.dialect.OracleDialect; +import org.hibernate.dialect.MariaDBDialect; import org.hibernate.testing.junit4.BaseNonConfigCoreFunctionalTestCase; import org.junit.Test; @@ -72,7 +73,7 @@ public void testBatchAndOptimisticLocking() { } ); try { - inTransaction( (session) -> { + inTransaction( session -> { List persons = session .createSelectionQuery( "select p from Person p", Person.class ) .getResultList(); @@ -109,10 +110,18 @@ public void testBatchAndOptimisticLocking() { ); } else { - assertEquals( - "Batch update returned unexpected row count from update [1]; actual row count: 0; expected: 1; statement executed: update Person set name=?,version=? where id=? and version=?", - expected.getMessage() - ); + if ( getDialect() instanceof MariaDBDialect && getDialect().getVersion().isAfter( 11, 6, 2 )) { + assertTrue( + expected.getMessage() + .contains( "Record has changed since last read in table 'Person'" ) + ); + } else { + assertTrue( + expected.getMessage() + .startsWith( + "Batch update returned unexpected row count from update [1]; actual row count: 0; expected: 1; statement executed: update Person set name=?,version=? where id=? and version=?" ) + ); + } } } } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/boot/database/qualfiedTableNaming/DefaultCatalogAndSchemaTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/boot/database/qualfiedTableNaming/DefaultCatalogAndSchemaTest.java index 9a0b383913ef..81536f20816f 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/boot/database/qualfiedTableNaming/DefaultCatalogAndSchemaTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/boot/database/qualfiedTableNaming/DefaultCatalogAndSchemaTest.java @@ -34,6 +34,7 @@ import org.hibernate.boot.spi.BootstrapContext; import org.hibernate.boot.spi.MetadataImplementor; import org.hibernate.cfg.AvailableSettings; +import org.hibernate.community.dialect.InformixDialect; import org.hibernate.dialect.Dialect; import org.hibernate.dialect.SQLServerDialect; import org.hibernate.dialect.SybaseDialect; @@ -309,12 +310,17 @@ public void createSchema_fromSessionFactory() { @Test @SkipForDialect(value = SQLServerDialect.class, - comment = "SQL Server and Sybase support catalogs but their implementation of DatabaseMetaData" + comment = "SQL Server support catalogs but their implementation of DatabaseMetaData" + " throws exceptions when calling getSchemas/getTables with a non-existing catalog," + " which results in nasty errors when generating an update script" + " and some catalogs don't exist.") @SkipForDialect(value = SybaseDialect.class, - comment = "SQL Server and Sybase support catalogs but their implementation of DatabaseMetaData" + comment = "Sybase support catalogs but their implementation of DatabaseMetaData" + + " throws exceptions when calling getSchemas/getTables with a non-existing catalog," + + " which results in nasty errors when generating an update script" + + " and some catalogs don't exist.") + @SkipForDialect(value = InformixDialect.class, + comment = "Informix support catalogs but their implementation of DatabaseMetaData" + " throws exceptions when calling getSchemas/getTables with a non-existing catalog," + " which results in nasty errors when generating an update script" + " and some catalogs don't exist.") @@ -772,8 +778,8 @@ String patternStringForNameWithDifferentQualifier(String patternStringForName) { } private String patternStringForQualifier() { - return ( catalog != null ? Pattern.quote( catalog + "." ) : "" ) - + ( schema != null ? Pattern.quote( schema + "." ) : "" ); + return ( catalog != null ? Pattern.quote( catalog ) + "." : "" ) + + ( schema != null ? Pattern.quote( schema ) + "." : "" ); } } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bootstrap/binding/annotations/embedded/EmbeddableA.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bootstrap/binding/annotations/embedded/EmbeddableA.java index 04dc114022e4..41740cea5ddf 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/bootstrap/binding/annotations/embedded/EmbeddableA.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bootstrap/binding/annotations/embedded/EmbeddableA.java @@ -17,11 +17,11 @@ */ @Embeddable public class EmbeddableA { - + @Embedded @AttributeOverrides({@AttributeOverride(name = "embedAttrB" , column = @Column(table = "TableB"))}) private EmbeddableB embedB; - + @Column(table = "TableB") private String embedAttrA; public EmbeddableB getEmbedB() { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/ForeignPackageSuperclassAccessorTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/ForeignPackageSuperclassAccessorTest.java new file mode 100644 index 000000000000..78f5fa5c179b --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/ForeignPackageSuperclassAccessorTest.java @@ -0,0 +1,33 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.bytecode; + +import org.hibernate.orm.test.bytecode.foreignpackage.ConcreteEntity; + +import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Test; + +@SessionFactory +@DomainModel(annotatedClasses = { + ConcreteEntity.class, + SuperclassEntity.class +}) +@Jira("https://hibernate.atlassian.net/browse/HHH-19369") +@BytecodeEnhanced(runNotEnhancedAsWell = true) +public class ForeignPackageSuperclassAccessorTest { + + @Test + public void test(SessionFactoryScope scope) { + scope.inTransaction( session -> { + session.find( SuperclassEntity.class, 1L ); + } ); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/SuperclassEntity.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/SuperclassEntity.java new file mode 100644 index 000000000000..ac4bc286b95c --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/SuperclassEntity.java @@ -0,0 +1,30 @@ +package org.hibernate.orm.test.bytecode; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Inheritance; +import jakarta.persistence.InheritanceType; + +@Entity +@Inheritance(strategy = InheritanceType.JOINED) +public class SuperclassEntity { + @Id + protected long id; + protected String name; + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/access/HierarchyPropertyAccessTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/access/HierarchyPropertyAccessTest.java new file mode 100644 index 000000000000..b60561d80eb2 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/access/HierarchyPropertyAccessTest.java @@ -0,0 +1,148 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.bytecode.enhancement.access; + +import jakarta.persistence.Access; +import jakarta.persistence.AccessType; +import jakarta.persistence.DiscriminatorColumn; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.MappedSuperclass; +import jakarta.persistence.Transient; +import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel(annotatedClasses = { + HierarchyPropertyAccessTest.AbstractSuperclass.class, + HierarchyPropertyAccessTest.ParentEntity.class, + HierarchyPropertyAccessTest.ChildEntity.class, +}) +@SessionFactory +@Jira( "https://hibernate.atlassian.net/browse/HHH-19140" ) +@Jira( "https://hibernate.atlassian.net/browse/HHH-19059" ) +@BytecodeEnhanced +public class HierarchyPropertyAccessTest { + @Test + public void testParent(SessionFactoryScope scope) { + assertThat( scope.getSessionFactory().getMappingMetamodel().findEntityDescriptor( ParentEntity.class ) + .getBytecodeEnhancementMetadata().isEnhancedForLazyLoading() ).isTrue(); + scope.inTransaction( session -> session.persist( new ParentEntity( 1L, "field", "transient: property" ) ) ); + + scope.inTransaction( session -> { + final ParentEntity entity = session.find( ParentEntity.class, 1L ); + assertThat( entity.getPersistProperty() ).isEqualTo( "property" ); + assertThat( entity.getProperty() ).isEqualTo( "transient: property" ); + assertThat( entity.getSuperProperty() ).isEqualTo( 8 ); + + entity.setProperty( "transient: updated" ); + } ); + + scope.inTransaction( session -> { + final ParentEntity entity = session.find( ParentEntity.class, 1L ); + assertThat( entity.getPersistProperty() ).isEqualTo( "updated" ); + assertThat( entity.getProperty() ).isEqualTo( "transient: updated" ); + } ); + } + + @Test + public void testChild(SessionFactoryScope scope) { + assertThat( scope.getSessionFactory().getMappingMetamodel().findEntityDescriptor( ChildEntity.class ) + .getBytecodeEnhancementMetadata().isEnhancedForLazyLoading() ).isTrue(); + scope.inTransaction( session -> session.persist( new ChildEntity( 2L, "field", "transient: property" ) ) ); + + scope.inTransaction( session -> { + ChildEntity entity = session.find( ChildEntity.class, 2L ); + assertThat( entity.getPersistProperty() ).isEqualTo( "property" ); + assertThat( entity.getProperty() ).isEqualTo( "transient: property" ); + assertThat( entity.getSuperProperty() ).isEqualTo( 8 ); + + entity.setProperty( "transient: updated" ); + } ); + + scope.inTransaction( session -> { + ChildEntity entity = session.find( ChildEntity.class, 2L ); + assertThat( entity.getPersistProperty() ).isEqualTo( "updated" ); + assertThat( entity.getProperty() ).isEqualTo( "transient: updated" ); + } ); + } + + @AfterAll + public void cleanup(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } + + @MappedSuperclass + static abstract class AbstractSuperclass { + protected Integer superProperty; + } + + @Entity(name = "ParentEntity") + @DiscriminatorColumn(name = "entity_type") + static class ParentEntity extends AbstractSuperclass { + @Id + private Long id; + + private String field; + + private String persistProperty; + + @Transient + private String property; + + public ParentEntity() { + } + + public ParentEntity(Long id, String field, String property) { + this.id = id; + this.field = field; + this.property = property; + } + + @Access(AccessType.PROPERTY) + public String getPersistProperty() { + this.persistProperty = this.property.substring( 11 ); + return this.persistProperty; + } + + public void setPersistProperty(String persistProperty) { + this.property = "transient: " + persistProperty; + this.persistProperty = persistProperty; + } + + public String getProperty() { + return this.property; + } + + public void setProperty(String property) { + this.property = property; + } + + @Access(AccessType.PROPERTY) + public Integer getSuperProperty() { + return getPersistProperty().length(); + } + + public void setSuperProperty(Integer superProperty) { + this.superProperty = superProperty; + } + } + + @Entity(name = "ChildEntity") + static class ChildEntity extends ParentEntity { + public ChildEntity() { + } + + public ChildEntity(Long id, String field, String property) { + super( id, field, property ); + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/access/InvalidPropertyNameTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/access/InvalidPropertyNameTest.java new file mode 100644 index 000000000000..461ea8cb9861 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/access/InvalidPropertyNameTest.java @@ -0,0 +1,172 @@ +package org.hibernate.orm.test.bytecode.enhancement.access; + +import jakarta.persistence.Access; +import jakarta.persistence.AccessType; +import jakarta.persistence.Basic; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.Transient; +import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.FailureExpected; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel( + annotatedClasses = { + InvalidPropertyNameTest.SomeEntity.class, + InvalidPropertyNameTest.SomeEntityWithFalsePositive.class + } +) +@SessionFactory +@BytecodeEnhanced +public class InvalidPropertyNameTest { + + + @Test + @FailureExpected(jiraKey = "HHH-16572") + @JiraKey("HHH-16572") + public void test(SessionFactoryScope scope) { + scope.inTransaction( session -> { + session.persist( new SomeEntity( 1L, "field", "property" ) ); + } ); + + scope.inTransaction( session -> { + SomeEntity entity = session.get( SomeEntity.class, 1L ); + assertThat( entity.property ).isEqualTo( "from getter: property" ); + + entity.setPropertyMethod( "updated" ); + } ); + + scope.inTransaction( session -> { + SomeEntity entity = session.get( SomeEntity.class, 1L ); + assertThat( entity.property ).isEqualTo( "from getter: updated" ); + } ); + } + + @Test + @JiraKey("HHH-18832") + public void testNoFalsePositive(SessionFactoryScope scope) { + scope.inTransaction( session -> { + session.persist( new SomeEntityWithFalsePositive( 1L, "property1-initial", "property2-initial" ) ); + } ); + + // Before HHH-18832 was fixed, lazy-loading enhancement was (incorrectly) skipped, + // resulting at best in `property1` being null in the code below, + // at worst in other errors such as java.lang.NoSuchMethodError: 'java.lang.String org.hibernate.orm.test.bytecode.enhancement.access.InvalidPropertyNameTest$SomeEntityWithFalsePositive.$$_hibernate_read_property1()' + // (see https://hibernate.zulipchat.com/#narrow/channel/132094-hibernate-orm-dev/topic/HHH-16572/near/481330806) + scope.inTransaction( session -> { + SomeEntityWithFalsePositive entity = session.getReference( SomeEntityWithFalsePositive.class, 1L ); + // Lazy-loading triggered by field access + // Proves bytecode enhancement is effective + assertThat( entity.property1 ).isEqualTo( "property1-initial" ); + } ); + + scope.inTransaction( session -> { + SomeEntityWithFalsePositive entity = session.getReference( SomeEntityWithFalsePositive.class, 1L ); + // Proves bytecode enhancement is effective even for the transient method + assertThat( entity.getProperty() ).isEqualTo( "property1-initial property2-initial" ); + } ); + } + + @AfterEach + public void cleanup(SessionFactoryScope scope) { + scope.inTransaction( session -> { + session.createQuery( "delete from SomeEntity" ).executeUpdate(); + session.createQuery( "delete from SomeEntityWithFalsePositive" ).executeUpdate(); + } ); + } + + @Entity(name = "SomeEntity") + @Table(name = "SOME_ENTITY") + static class SomeEntity { + @Id + Long id; + + @Basic + String field; + + String property; + + public SomeEntity() { + } + + public SomeEntity(Long id, String field, String property) { + this.id = id; + this.field = field; + this.property = property; + } + + /** + * The following property accessor methods are purposely named incorrectly to + * not match the "property" field. The HHH-16572 change ensures that + * this entity is not (bytecode) enhanced. Eventually further changes will be made + * such that this entity is enhanced in which case the FailureExpected can be removed + * and the cleanup() uncommented. + */ + @Basic + @Access(AccessType.PROPERTY) + public String getPropertyMethod() { + return "from getter: " + property; + } + + public void setPropertyMethod(String property) { + this.property = property; + } + } + + @Entity(name = "SomeEntityWithFalsePositive") + static class SomeEntityWithFalsePositive { + + private Long id; + + private String property1; + + private String property2; + + public SomeEntityWithFalsePositive() { + } + + public SomeEntityWithFalsePositive(Long id, String property1, String property2) { + this.id = id; + this.property1 = property1; + this.property2 = property2; + } + + @Id + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public String getProperty1() { + return property1; + } + + public void setProperty1(String property1) { + this.property1 = property1; + } + + public String getProperty2() { + return property2; + } + + public void setProperty2(String property2) { + this.property2 = property2; + } + + @Transient + public String getProperty() { + return property1 + " " + property2; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/access/PropertyAccessMemberTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/access/PropertyAccessMemberTest.java new file mode 100644 index 000000000000..fca1ed7f3efa --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/access/PropertyAccessMemberTest.java @@ -0,0 +1,99 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.bytecode.enhancement.access; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.metamodel.Attribute; +import jakarta.persistence.metamodel.ManagedType; +import jakarta.persistence.metamodel.Metamodel; +import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Test; + +import java.lang.reflect.Member; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * @author Scott Marlow + */ +@SessionFactory +@DomainModel(annotatedClasses = { + PropertyAccessMemberTest.PropertyEntity.class, + PropertyAccessMemberTest.FieldEntity.class +}) +@BytecodeEnhanced +public class PropertyAccessMemberTest { + @Test + public void testPropertyAccessMember(SessionFactoryScope scope) { + scope.inTransaction( entityManager -> { + final Metamodel metaModel = entityManager.getMetamodel(); + final ManagedType managedType = metaModel.managedType( PropertyEntity.class ); + final Attribute attribute = managedType.getDeclaredAttribute( "total" ); + final Member member = attribute.getJavaMember(); + assertEquals( "getTotal", member.getName() ); + } ); + } + + @Test + public void testFieldAccessMember(SessionFactoryScope scope) { + scope.inTransaction( entityManager -> { + final Metamodel metaModel = entityManager.getMetamodel(); + final ManagedType managedType = metaModel.managedType( FieldEntity.class ); + final Attribute attribute = managedType.getDeclaredAttribute( "total" ); + final Member member = attribute.getJavaMember(); + assertEquals( "total", member.getName() ); + } ); + } + + @Entity(name = "PropertyEntity") + static class PropertyEntity { + private int id; + private int total; + + @Id + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public int getTotal() { + return total; + } + + public void setTotal(int total) { + this.total = total; + } + } + + @Entity(name = "FieldEntity") + static class FieldEntity { + @Id + private int id; + private int total; + + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + public int getTotal() { + return total; + } + + public void setTotal(int total) { + this.total = total; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/access/UnsupportedEnhancementStrategyTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/access/UnsupportedEnhancementStrategyTest.java new file mode 100644 index 000000000000..d131692062a9 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/access/UnsupportedEnhancementStrategyTest.java @@ -0,0 +1,290 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.bytecode.enhancement.access; + +import jakarta.persistence.Access; +import jakarta.persistence.AccessType; +import jakarta.persistence.Basic; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.MappedSuperclass; +import jakarta.persistence.PostLoad; +import jakarta.persistence.PrePersist; +import jakarta.persistence.Transient; + +import org.hibernate.bytecode.enhance.internal.bytebuddy.EnhancerImpl; +import org.hibernate.bytecode.enhance.spi.EnhancementContext; +import org.hibernate.bytecode.enhance.spi.EnhancementException; +import org.hibernate.bytecode.enhance.spi.Enhancer; +import org.hibernate.bytecode.enhance.spi.UnsupportedEnhancementStrategy; +import org.hibernate.bytecode.internal.bytebuddy.ByteBuddyState; +import org.hibernate.bytecode.spi.ByteCodeHelper; +import org.hibernate.testing.bytecode.enhancement.EnhancerTestContext; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.JiraKey; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.io.InputStream; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@JiraKey("HHH-18833") +public class UnsupportedEnhancementStrategyTest { + + @Test + public void skip() throws IOException { + var context = new EnhancerTestContext() { + @Override + public UnsupportedEnhancementStrategy getUnsupportedEnhancementStrategy() { + // This is currently the default, but we don't care about what's the default here + return UnsupportedEnhancementStrategy.SKIP; + } + }; + byte[] enhancedBytes = doEnhance( SomeEntity.class, context ); + assertThat( enhancedBytes ).isNull(); // null means "not enhanced" + } + + @Test + public void fail() throws IOException { + var context = new UnsupportedEnhancerContext(); + assertThatThrownBy( () -> doEnhance( SomeEntity.class, context ) ).isInstanceOf( EnhancementException.class ) + .hasMessageContainingAll( + String.format( + "Enhancement of [%s] failed because no field named [%s] could be found for property accessor method [%s].", + SomeEntity.class.getName(), + "propertyMethod", + "getPropertyMethod" + ), "To fix this, make sure all property accessor methods have a matching field." + ); + assertThatThrownBy( () -> doEnhance( SomeOtherEntity.class, context ) ).isInstanceOf( EnhancementException.class ) + .hasMessageContainingAll( + String.format( + "Enhancement of [%s] failed because no field named [%s] could be found for property accessor method [%s].", + SomeOtherEntity.class.getName(), + "propertyMethod", + "setPropertyMethod" + ), "To fix this, make sure all property accessor methods have a matching field." + ); + } + + @Test + @SuppressWarnings("deprecation") + public void legacy() throws IOException { + var context = new EnhancerTestContext() { + @Override + public UnsupportedEnhancementStrategy getUnsupportedEnhancementStrategy() { + // This is currently the default, but we don't care about what's the default here + return UnsupportedEnhancementStrategy.LEGACY; + } + }; + byte[] enhancedBytes = doEnhance( SomeEntity.class, context ); + assertThat( enhancedBytes ).isNotNull(); // non-null means enhancement _was_ performed + } + + @Test + @Jira( "https://hibernate.atlassian.net/browse/HHH-18903" ) + @Jira( "https://hibernate.atlassian.net/browse/HHH-18904" ) + public void testEntityListeners() throws IOException { + // non-null means check passed and enhancement _was_ performed + assertThat( doEnhance( EventListenersEntity.class, new UnsupportedEnhancerContext() ) ).isNotNull(); + } + + @Test + @Jira( "https://hibernate.atlassian.net/browse/HHH-18903" ) + @Jira( "https://hibernate.atlassian.net/browse/HHH-18904" ) + public void testAccessTypeFieldEntity() throws IOException { + var context = new UnsupportedEnhancerContext(); + // non-null means check passed and enhancement _was_ performed + assertThat( doEnhance( ExplicitAccessTypeFieldEntity.class, context ) ).isNotNull(); + assertThat( doEnhance( ImplicitAccessTypeFieldEntity.class, context ) ).isNotNull(); + } + + @Test + @Jira( "https://hibernate.atlassian.net/browse/HHH-19059" ) + public void testAccessTypePropertyInherited() throws IOException { + var context = new UnsupportedEnhancerContext(); + // non-null means check passed and enhancement _was_ performed + assertThat( doEnhance( PropertyAccessInheritedEntity.class, context ) ).isNotNull(); + } + + private static byte[] doEnhance(Class entityClass, EnhancementContext context) throws IOException { + final ByteBuddyState byteBuddyState = new ByteBuddyState(); + final Enhancer enhancer = new EnhancerImpl( context, byteBuddyState ); + return enhancer.enhance( entityClass.getName(), getAsBytes( entityClass ) ); + } + + private static byte[] getAsBytes(Class clazz) throws IOException { + final String classFile = clazz.getName().replace( '.', '/' ) + ".class"; + try (InputStream classFileStream = clazz.getClassLoader().getResourceAsStream( classFile )) { + return ByteCodeHelper.readByteCode( classFileStream ); + } + } + + static class UnsupportedEnhancerContext extends EnhancerTestContext { + @Override + public UnsupportedEnhancementStrategy getUnsupportedEnhancementStrategy() { + return UnsupportedEnhancementStrategy.FAIL; + } + } + + @Entity(name = "SomeEntity") + static class SomeEntity { + @Id + Long id; + + @Basic + String field; + + String property; + + /** + * The following property accessor methods are purposely named incorrectly to + * not match the "property" field. The HHH-16572 change ensures that + * this entity is not (bytecode) enhanced. Eventually further changes will be made + * such that this entity is enhanced in which case the FailureExpected can be removed + * and the cleanup() uncommented. + */ + @Basic + @Access(AccessType.PROPERTY) + public String getPropertyMethod() { + return "from getter: " + property; + } + + public void setPropertyMethod(String property) { + this.property = property; + } + } + + @Entity(name = "SomeOtherEntity") + static class SomeOtherEntity { + @Id + Long id; + + @Basic + String field; + + String property; + @Access(AccessType.PROPERTY) + public void setPropertyMethod(String property) { + this.property = property; + } + } + + @Entity(name = "EventListenersEntity") + static class EventListenersEntity { + private UUID id; + + private String status = "new"; + + @Id + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + // special case, we should let it through + public UUID get() { + return id; + } + + @PrePersist + public void setId() { + id = UUID.randomUUID(); + } + + @Transient + public String getState() { + return status; + } + + @PostLoad + public void setState(String state) { + status = "loaded"; + } + + @Transient + public boolean isLoaded() { + return status.equals( "loaded" ); + } + } + + @Entity(name = "ExplicitAccessTypeFieldEntity") + @Access( AccessType.FIELD ) + static class ExplicitAccessTypeFieldEntity { + @Id + private Long id; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long get() { + return id; + } + + public String getSomething() { + return "something"; + } + } + + @Entity(name = "ImplicitAccessTypeFieldEntity") + static class ImplicitAccessTypeFieldEntity { + @Id + private Long id; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long get() { + return id; + } + + public String getAnother() { + return "another"; + } + } + + @MappedSuperclass + static abstract class AbstractSuperclass { + protected String property; + } + + @Entity(name="PropertyAccessInheritedEntity") + static class PropertyAccessInheritedEntity extends AbstractSuperclass { + private Long id; + + @Id + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getProperty() { + return property; + } + + public void setProperty(String property) { + this.property = property; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/basic/ReloadAssociatedEntitiesTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/basic/ReloadAssociatedEntitiesTest.java new file mode 100644 index 000000000000..35acb17bfded --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/basic/ReloadAssociatedEntitiesTest.java @@ -0,0 +1,415 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.orm.test.bytecode.enhancement.basic; + +import java.util.HashSet; +import java.util.Set; + +import org.hibernate.Hibernate; + +import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.MappedSuperclass; +import jakarta.persistence.OneToMany; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel( + annotatedClasses = { + ReloadAssociatedEntitiesTest.SimpleOne.class, + ReloadAssociatedEntitiesTest.SimpleTwo.class, + ReloadAssociatedEntitiesTest.SimpleThree.class, + ReloadAssociatedEntitiesTest.ConcreteOne.class, + ReloadAssociatedEntitiesTest.ConcreteTwo.class, + ReloadAssociatedEntitiesTest.ConcreteThree.class, + ReloadAssociatedEntitiesTest.AbsOne.class, + ReloadAssociatedEntitiesTest.AbsTwo.class, + } +) +@SessionFactory +@BytecodeEnhanced(runNotEnhancedAsWell = true) +@JiraKey("HHH-18565") +public class ReloadAssociatedEntitiesTest { + + private Long oneId; + private Long threeId; + private Long simpleOneId; + private Long simpleThreeId; + + @BeforeEach + public void before(SessionFactoryScope scope) { + scope.inTransaction( s -> { + final var three = new ConcreteThree(); + final var two = new ConcreteTwo( "two", three ); + final var one = new ConcreteOne( "one", two ); + two.getOnes().add( one ); + three.getTwos().add( two ); + + s.persist( one ); + s.persist( two ); + s.persist( three ); + oneId = one.getId(); + threeId = three.getId(); + + final var simpleThree = new SimpleThree( "simple three" ); + final var simpleTwo = new SimpleTwo( "simple two", simpleThree ); + final var simpleOne = new SimpleOne( "simple one", simpleTwo ); + + s.persist( simpleOne ); + s.persist( simpleTwo ); + s.persist( simpleThree ); + simpleOneId = simpleOne.getId(); + simpleThreeId = simpleThree.getId(); + } ); + } + + @AfterEach + void after(SessionFactoryScope scope) { + scope.inTransaction( session -> { + session.createMutationQuery( session.getCriteriaBuilder().createCriteriaDelete( ConcreteOne.class ) ) + .executeUpdate(); + session.createMutationQuery( session.getCriteriaBuilder().createCriteriaDelete( ConcreteTwo.class ) ) + .executeUpdate(); + session.createMutationQuery( session.getCriteriaBuilder().createCriteriaDelete( ConcreteThree.class ) ) + .executeUpdate(); + } ); + } + + @Test + public void reloadToOneFromSimpleEntity(SessionFactoryScope scope) { + scope.inTransaction( s -> { + SimpleOne one = s.createQuery( + "select o from SimpleOne o join fetch o.two t where o.id = :oneId", + SimpleOne.class + ) + .setParameter( "oneId", simpleOneId ).getSingleResult(); + + assertThat( one ).isNotNull(); + assertThat( Hibernate.isInitialized( one ) ).isTrue(); + assertThat( Hibernate.isPropertyInitialized( one, "two" ) ).isTrue(); + SimpleTwo two = one.getTwo(); + assertThat( Hibernate.isInitialized( two.getThree() ) ).isFalse(); + + SimpleOne one2 = s.createQuery( + "select o from SimpleOne o join fetch o.two t join fetch t.three rh where o.id = :oneId", + SimpleOne.class + ) + .setParameter( "oneId", simpleOneId ).getSingleResult(); + + assertThat( one2 ).isNotNull(); + assertThat( one2 ).isSameAs( one ); + assertThat( Hibernate.isInitialized( one2 ) ).isTrue(); + assertThat( Hibernate.isPropertyInitialized( one2, "two" ) ).isTrue(); + SimpleTwo two2 = one2.getTwo(); + assertThat( two2 ).isSameAs( two ); + assertThat( Hibernate.isInitialized( two2.getThree() ) ).isTrue(); + } ); + } + + @Test + public void reloadToOneFromParameterizedEntity(SessionFactoryScope scope) { + scope.inTransaction( s -> { + ConcreteOne one = s.createQuery( + "select o from ConcreteOne o join fetch o.two t where o.id = :oneId", + ConcreteOne.class + ) + .setParameter( "oneId", oneId ).getSingleResult(); + + assertThat( one ).isNotNull(); + + ConcreteOne one2 = s.createQuery( + "select o from ConcreteOne o join fetch o.two t join fetch t.three rh where o.id = :oneId", + ConcreteOne.class + ) + .setParameter( "oneId", oneId ).getSingleResult(); + + assertThat( one2 ).isNotNull(); + assertThat( one2 ).isSameAs( one ); + } ); + } + + @Test + public void reloadToManyFromParameterizedEntity(SessionFactoryScope scope) { + scope.inTransaction( s -> { + ConcreteThree three = s.createQuery( + "select t from ConcreteThree t join t.twos tw where t.id = :threeId", + ConcreteThree.class + ) + .setParameter( "threeId", threeId ).getSingleResult(); + + assertThat( three ).isNotNull(); + + ConcreteThree three1 = s.createQuery( + "select t from ConcreteThree t join fetch t.twos tw join fetch tw.ones o where t.id = :threeId", + ConcreteThree.class + ) + .setParameter( "threeId", threeId ).getSingleResult(); + + assertThat( three1 ).isNotNull(); + assertThat( three1 ).isSameAs( three ); + } ); + } + + @Test + public void reloadToManyFromSimpleEntity(SessionFactoryScope scope) { + scope.inTransaction( s -> { + SimpleThree three = s.createQuery( + "select t from SimpleThree t join t.twos tw where t.id = :threeId", + SimpleThree.class + ) + .setParameter( "threeId", simpleThreeId ).getSingleResult(); + + assertThat( three ).isNotNull(); + + SimpleThree three1 = s.createQuery( + "select t from SimpleThree t join fetch t.twos tw join fetch tw.ones o where t.id = :threeId", + SimpleThree.class + ) + .setParameter( "threeId", simpleThreeId ).getSingleResult(); + + assertThat( three1 ).isNotNull(); + assertThat( three1 ).isSameAs( three ); + } ); + } + + @Entity(name = "ConcreteOne") + public static class ConcreteOne extends AbsOne { + + public ConcreteOne() { + } + + public ConcreteOne(String name, ConcreteTwo two) { + super( name, two ); + two.getOnes().add( this ); + } + } + + @MappedSuperclass + public static abstract class AbsOne> { + @Id + @GeneratedValue + private Long id; + + private String name; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "two_id") + private TWO two; + + public AbsOne() { + } + + public AbsOne(String name, TWO two) { + this.two = two; + this.name = name; + } + + public Long getId() { + return id; + } + + public TWO getTwo() { + return two; + } + + public String getName() { + return name; + } + } + + @Entity(name = "ConcreteTwo") + public static class ConcreteTwo extends AbsTwo { + public ConcreteTwo() { + } + + public ConcreteTwo(String name, ConcreteThree concreteThree) { + super( name, concreteThree ); + } + } + + @MappedSuperclass + public static abstract class AbsTwo, THREE> { + @Id + @GeneratedValue + private Long id; + + private String name; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "three_id") + private THREE three; + + @OneToMany(mappedBy = "two") + private Set ones = new HashSet<>(); + + public AbsTwo() { + } + + public AbsTwo(String name, THREE three) { + this.name = name; + this.three = three; + } + + public Long getId() { + return id; + } + + + public THREE getThree() { + return three; + } + + + public Set getOnes() { + return ones; + } + + } + + @Entity(name = "ConcreteThree") + public static class ConcreteThree { + @Id + @GeneratedValue + private Long id; + + private String name; + + @OneToMany(mappedBy = "three") + private Set twos = new HashSet<>(); + + public ConcreteThree() { + } + + public ConcreteThree(String name, Set twos) { + this.name = name; + this.twos = twos; + } + + public Long getId() { + return id; + } + + public Set getTwos() { + return twos; + } + + public String getName() { + return name; + } + } + + @Entity(name = "SimpleOne") + public static class SimpleOne { + @Id + @GeneratedValue + private Long id; + + private String name; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "two_id") + private SimpleTwo two; + + public SimpleOne() { + } + + public SimpleOne(String name, SimpleTwo two) { + this.name = name; + this.two = two; + two.ones.add( this ); + } + + public Long getId() { + return id; + } + + + public SimpleTwo getTwo() { + return two; + } + + } + + @Entity(name = "SimpleTwo") + public static class SimpleTwo { + @Id + @GeneratedValue + private Long id; + + private String name; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "three_id") + private SimpleThree three; + + @OneToMany(mappedBy = "two") + private Set ones = new HashSet<>(); + + public SimpleTwo() { + } + + public SimpleTwo(String name, SimpleThree three) { + this.name = name; + this.three = three; + three.twos.add( this ); + } + + public Long getId() { + return id; + } + + public SimpleThree getThree() { + return three; + } + + public Set getOnes() { + return ones; + } + + } + + @Entity(name = "SimpleThree") + public static class SimpleThree { + @Id + @GeneratedValue + private Long id; + + private String name; + + @OneToMany(mappedBy = "three") + private Set twos = new HashSet<>(); + + public SimpleThree() { + } + + public SimpleThree(String name) { + this.name = name; + } + + public Long getId() { + return id; + } + + public Set getTwos() { + return twos; + } + + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/batch/BatchLazyProxyTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/batch/BatchLazyProxyTest.java new file mode 100644 index 000000000000..86ccfe27c532 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/batch/BatchLazyProxyTest.java @@ -0,0 +1,209 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.bytecode.enhancement.batch; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import org.hibernate.Hibernate; +import org.hibernate.annotations.BatchSize; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.graph.GraphSemantic; +import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.Setting; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +@DomainModel( + annotatedClasses = { + BatchLazyProxyTest.User.class, + BatchLazyProxyTest.UserInfo.class, + BatchLazyProxyTest.Phone.class, + } + +) +@SessionFactory +@ServiceRegistry( + settings = { + @Setting(name = AvailableSettings.DEFAULT_BATCH_FETCH_SIZE, value = "100") + + } +) +@JiraKey("HHH-18645") +@BytecodeEnhanced(runNotEnhancedAsWell = true) +public class BatchLazyProxyTest { + + @BeforeEach + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + UserInfo info = new UserInfo( "info" ); + Phone phone = new Phone( "123456" ); + info.addPhone( phone ); + User user = new User( 1L, "user1", info ); + session.persist( user ); + } + ); + } + + @AfterEach + public void tearDown(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + session.createMutationQuery( "delete User" ).executeUpdate(); + session.createMutationQuery( "delete Phone" ).executeUpdate(); + session.createMutationQuery( "delete UserInfo" ).executeUpdate(); + } + ); + } + + @Test + public void testBatchInitialize(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + User user = session.createQuery( "select u from User u where u.id = :id", User.class ) + .setEntityGraph( session.createEntityGraph( User.class ), GraphSemantic.FETCH ) + .setParameter( "id", 1L ) + .getSingleResult(); + assertThat( Hibernate.isInitialized( user.getInfo() ) ).isFalse(); + session.createQuery( "select u from User u where u.id = :id", User.class ) + .setParameter( "id", 1L ) + .getSingleResult(); + assertThat( Hibernate.isInitialized( user.getInfo() ) ).isTrue(); + } + ); + } + + @Entity(name = "User") + @Table(name = "USER_TABLE") + @BatchSize(size = 5) + public static class User { + + @Id + private Long id; + + @Column + private String name; + + @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.EAGER) + @JoinColumn(name = "INFO_ID", referencedColumnName = "ID") + private UserInfo info; + + public User() { + } + + public User(long id, String name, UserInfo info) { + this.id = id; + this.name = name; + this.info = info; + info.user = this; + } + + public long getId() { + return id; + } + + public String getName() { + return name; + } + + public UserInfo getInfo() { + return info; + } + } + + @Entity(name = "UserInfo") + public static class UserInfo { + @Id + @GeneratedValue + private Long id; + + @OneToOne(mappedBy = "info", fetch = FetchType.LAZY) + private User user; + + private String info; + + @OneToMany(mappedBy = "info", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + private List phoneList; + + public long getId() { + return id; + } + + public UserInfo() { + } + + public UserInfo(String info) { + this.info = info; + } + + public User getUser() { + return user; + } + + public String getInfo() { + return info; + } + + public List getPhoneList() { + return phoneList; + } + + public void addPhone(Phone phone) { + if ( phoneList == null ) { + phoneList = new ArrayList<>(); + } + this.phoneList.add( phone ); + phone.info = this; + } + } + + @Entity(name = "Phone") + public static class Phone { + @Id + @Column(name = "PHONE_NUMBER") + private String number; + + @ManyToOne + @JoinColumn(name = "INFO_ID") + private UserInfo info; + + public Phone() { + } + + public Phone(String number) { + this.number = number; + } + + public String getNumber() { + return number; + } + + public UserInfo getInfo() { + return info; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/batch/RefreshAndBatchTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/batch/RefreshAndBatchTest.java new file mode 100644 index 000000000000..499fa1582035 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/batch/RefreshAndBatchTest.java @@ -0,0 +1,205 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.bytecode.enhancement.batch; + +import java.util.ArrayList; +import java.util.List; + +import org.hibernate.Hibernate; +import org.hibernate.annotations.BatchSize; +import org.hibernate.cfg.AvailableSettings; + +import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.Setting; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +@DomainModel( + annotatedClasses = { + RefreshAndBatchTest.User.class, + RefreshAndBatchTest.UserInfo.class, + RefreshAndBatchTest.Phone.class, + } + +) +@SessionFactory +@ServiceRegistry( + settings = { + @Setting(name = AvailableSettings.DEFAULT_BATCH_FETCH_SIZE, value = "100") + + } +) +@JiraKey("HHH-18608") +@BytecodeEnhanced(runNotEnhancedAsWell = true) +public class RefreshAndBatchTest { + + @BeforeEach + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + UserInfo info = new UserInfo( "info" ); + Phone phone = new Phone( "123456" ); + info.addPhone( phone ); + User user = new User( 1L, "user1", info ); + session.persist( user ); + } + ); + } + + @AfterEach + public void tearDown(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + session.createMutationQuery( "delete User" ).executeUpdate(); + session.createMutationQuery( "delete Phone" ).executeUpdate(); + session.createMutationQuery( "delete UserInfo" ).executeUpdate(); + } + ); + } + + @Test + public void testRefresh(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + User user = session.createQuery( "select u from User u where u.id = :id", User.class ) + .setParameter( "id", 1L ) + .getSingleResult(); + assertThat( Hibernate.isInitialized( user.getInfo() ) ).isFalse(); + session.refresh( user.getInfo() ); + assertThat( Hibernate.isInitialized( user.getInfo() ) ).isTrue(); + } + ); + } + + @Entity(name = "User") + @Table(name = "USER_TABLE") + @BatchSize(size = 5) + public static class User { + + @Id + private Long id; + + @Column + private String name; + + @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY) + @JoinColumn(name = "INFO_ID", referencedColumnName = "ID") + private UserInfo info; + + public User() { + } + + public User(long id, String name, UserInfo info) { + this.id = id; + this.name = name; + this.info = info; + info.user = this; + } + + public long getId() { + return id; + } + + public String getName() { + return name; + } + + public UserInfo getInfo() { + return info; + } + } + + @Entity(name = "UserInfo") + public static class UserInfo { + @Id + @GeneratedValue + private Long id; + + @OneToOne(mappedBy = "info", fetch = FetchType.LAZY) + private User user; + + private String info; + + @OneToMany(mappedBy = "info", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + private List phoneList; + + public long getId() { + return id; + } + + public UserInfo() { + } + + public UserInfo(String info) { + this.info = info; + } + + public User getUser() { + return user; + } + + public String getInfo() { + return info; + } + + public List getPhoneList() { + return phoneList; + } + + public void addPhone(Phone phone) { + if ( phoneList == null ) { + phoneList = new ArrayList<>(); + } + this.phoneList.add( phone ); + phone.info = this; + } + } + + @Entity(name = "Phone") + public static class Phone { + @Id + @Column(name = "PHONE_NUMBER") + private String number; + + @ManyToOne + @JoinColumn(name = "INFO_ID") + private UserInfo info; + + public Phone() { + } + + public Phone(String number) { + this.number = number; + } + + public String getNumber() { + return number; + } + + public UserInfo getInfo() { + return info; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/batch/RefreshAndBatchTest2.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/batch/RefreshAndBatchTest2.java new file mode 100644 index 000000000000..849033fb6b7d --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/batch/RefreshAndBatchTest2.java @@ -0,0 +1,209 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.bytecode.enhancement.batch; + +import java.util.ArrayList; +import java.util.List; + +import org.hibernate.Hibernate; +import org.hibernate.annotations.BatchSize; +import org.hibernate.cfg.AvailableSettings; + +import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.Setting; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +@DomainModel( + annotatedClasses = { + RefreshAndBatchTest2.User.class, + RefreshAndBatchTest2.UserInfo.class, + RefreshAndBatchTest2.Phone.class, + } + +) +@SessionFactory +@ServiceRegistry( + settings = { + @Setting(name = AvailableSettings.DEFAULT_BATCH_FETCH_SIZE, value = "100") + + } +) +@JiraKey("HHH-18608") +@BytecodeEnhanced(runNotEnhancedAsWell = true) +public class RefreshAndBatchTest2 { + + @BeforeEach + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + User parentUser = new User( 1L, "user1", null ); + session.persist( parentUser ); + UserInfo info = new UserInfo( "info" ); + Phone phone = new Phone( "123456" ); + info.addPhone( phone ); + info.parentUser = parentUser; + User user = new User( 2L, "user1", info ); + session.persist( user ); + } + ); + } + + @AfterEach + public void tearDown(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + session.createMutationQuery( "update UserInfo set parentUser = null" ).executeUpdate(); + session.createMutationQuery( "delete User" ).executeUpdate(); + session.createMutationQuery( "delete Phone" ).executeUpdate(); + session.createMutationQuery( "delete UserInfo" ).executeUpdate(); + } + ); + } + + @Test + public void testRefresh(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + User user = session.createQuery( "select u from User u where u.id = :id", User.class ) + .setParameter( "id", 2L ) + .getSingleResult(); + assertThat( Hibernate.isInitialized( user.getInfo() ) ).isFalse(); + session.refresh( user.getInfo() ); + assertThat( Hibernate.isInitialized( user.getInfo() ) ).isTrue(); + } + ); + } + + @Entity(name = "User") + @Table(name = "USER_TABLE") + @BatchSize(size = 5) + public static class User { + + @Id + private Long id; + + @Column + private String name; + + @ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY) + private UserInfo info; + + public User() { + } + + public User(long id, String name, UserInfo info) { + this.id = id; + this.name = name; + if ( info != null ) { + this.info = info; + info.parentUser = this; + } + } + + public long getId() { + return id; + } + + public String getName() { + return name; + } + + public UserInfo getInfo() { + return info; + } + } + + @Entity(name = "UserInfo") + public static class UserInfo { + @Id + @GeneratedValue + private Long id; + + @ManyToOne + private User parentUser; + + private String info; + + @OneToMany(mappedBy = "info", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + private List phoneList; + + public long getId() { + return id; + } + + public UserInfo() { + } + + public UserInfo(String info) { + this.info = info; + } + + public User getParentUser() { + return parentUser; + } + + public String getInfo() { + return info; + } + + public List getPhoneList() { + return phoneList; + } + + public void addPhone(Phone phone) { + if ( phoneList == null ) { + phoneList = new ArrayList<>(); + } + this.phoneList.add( phone ); + phone.info = this; + } + } + + @Entity(name = "Phone") + public static class Phone { + @Id + @Column(name = "PHONE_NUMBER") + private String number; + + @ManyToOne + @JoinColumn(name = "INFO_ID") + private UserInfo info; + + public Phone() { + } + + public Phone(String number) { + this.number = number; + } + + public String getNumber() { + return number; + } + + public UserInfo getInfo() { + return info; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/detached/RemoveDetachedInstanceTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/detached/RemoveDetachedInstanceTest.java new file mode 100644 index 000000000000..c4d16a1a78ec --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/detached/RemoveDetachedInstanceTest.java @@ -0,0 +1,231 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.bytecode.enhancement.detached; + +import java.util.ArrayList; +import java.util.List; + +import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OrderColumn; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel( + annotatedClasses = { + RemoveDetachedInstanceTest.Parent.class, + RemoveDetachedInstanceTest.Child.class, + RemoveDetachedInstanceTest.ParentChild.class + } +) +@SessionFactory +@BytecodeEnhanced(runNotEnhancedAsWell = true) +@JiraKey("HHH-18631") +public class RemoveDetachedInstanceTest { + private static final Long PARENT_ID = 1L; + private static final Long CHILD_ID = 2L; + private static final Long PARENT_CHILD_ID = 3L; + + @BeforeEach + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + Parent parent = new Parent( PARENT_ID, "parent" ); + Child child = new Child( CHILD_ID, "child" ); + ParentChild parentChild = new ParentChild( PARENT_CHILD_ID, parent, child ); + + session.persist( parent ); + session.persist( child ); + session.persist( parentChild ); + } ); + } + + @AfterEach + public void tearDown(SessionFactoryScope scope) { + scope.inTransaction( session -> { + session.createMutationQuery( "delete from ParentChild" ).executeUpdate(); + session.createMutationQuery( "delete from Child" ).executeUpdate(); + session.createMutationQuery( "delete from Parent" ).executeUpdate(); + } ); + } + + @Test + void testRemoveDetachedInstance(SessionFactoryScope scope) { + ParentChild parentChild = scope.fromTransaction( session -> session.get( ParentChild.class, PARENT_CHILD_ID ) ); + assertThat( parentChild ).isNotNull(); + + scope.inTransaction( session -> { + session.remove( parentChild ); + Parent parent = session.get( Parent.class, PARENT_ID ); + assertThat( parent ).isNotNull(); + List pc = parent.getChildren(); + assertThat( pc ).isNotNull(); + assertThat( pc.size() ).isEqualTo( 1 ); + assertThat( pc.get( 0 ) ).isSameAs( parentChild ); + Child child = session.get( Child.class, CHILD_ID ); + assertThat( child ).isNotNull(); + } ); + + scope.inTransaction( session -> { + ParentChild pc = session.get( ParentChild.class, PARENT_CHILD_ID ); + assertThat( pc ).isNull(); + Parent parent = session.get( Parent.class, PARENT_ID ); + assertThat( parent ).isNotNull(); + assertThat( parent.getChildren() ).isEmpty(); + Child child = session.get( Child.class, CHILD_ID ); + assertThat( child ).isNotNull(); + assertThat( child.getChildren() ).isEmpty(); + + } ); + } + + @Test + void testRemoveDetachedInstance2(SessionFactoryScope scope) { + ParentChild parentChild = scope.fromTransaction( session -> session.get( ParentChild.class, PARENT_CHILD_ID ) ); + assertThat( parentChild ).isNotNull(); + + scope.inTransaction( session -> { + session.remove( parentChild ); + session.remove( parentChild.getChild() ); + Parent parent = session.get( Parent.class, PARENT_ID ); + assertThat( parent ).isNotNull(); + List pc = parent.getChildren(); + assertThat( pc ).isNotNull(); + assertThat( pc.size() ).isEqualTo( 1 ); + assertThat( pc.get( 0 ) ).isSameAs( parentChild ); + Child child = session.get( Child.class, CHILD_ID ); + assertThat( child ).isNull(); + } ); + + scope.inTransaction( session -> { + ParentChild pc = session.get( ParentChild.class, PARENT_CHILD_ID ); + assertThat( pc ).isNull(); + Parent parent = session.get( Parent.class, PARENT_ID ); + assertThat( parent ).isNotNull(); + assertThat( parent.getChildren() ).isEmpty(); + Child child = session.get( Child.class, CHILD_ID ); + assertThat( child ).isNull(); + } ); + } + + @Entity(name = "Parent") + public static class Parent { + @Id + private Long id; + + private String name; + + @OneToMany(mappedBy = "parent", fetch = FetchType.EAGER) + List children = new ArrayList<>(); + + public Parent() { + } + + public Parent(Long id, String name) { + this.id = id; + this.name = name; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public List getChildren() { + return children; + } + + public void setChildren(List children) { + this.children = children; + } + } + + @Entity(name = "ParentChild") + public static class ParentChild { + @Id + private Long id; + + @ManyToOne + private Parent parent; + + @ManyToOne + private Child child; + + public ParentChild() { + } + + public ParentChild(Long id, Parent parent, Child child) { + this.id = id; + this.parent = parent; + this.child = child; + parent.getChildren().add( this ); + child.getChildren().add( this ); + } + + public Long getId() { + return id; + } + + public Parent getParent() { + return parent; + } + + public Child getChild() { + return child; + } + + } + + @Entity(name = "Child") + public static class Child { + + @Id + private Long id; + + private String name; + + @OneToMany(mappedBy = "child") + @OrderColumn + List children = new ArrayList<>(); + + public Child() { + } + + public Child(Long id, String name) { + this.id = id; + this.name = name; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public List getChildren() { + return children; + } + + public void setChildren(List children) { + this.children = children; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/detached/collection/DetachedCollectionInitializationJoinFetchTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/detached/collection/DetachedCollectionInitializationJoinFetchTest.java new file mode 100644 index 000000000000..83b3490cbc3d --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/detached/collection/DetachedCollectionInitializationJoinFetchTest.java @@ -0,0 +1,159 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.bytecode.enhancement.detached.collection; + +import java.util.ArrayList; +import java.util.List; + +import org.hibernate.Hibernate; +import org.hibernate.collection.spi.PersistentCollection; +import org.hibernate.engine.spi.CollectionKey; +import org.hibernate.engine.spi.SessionImplementor; + +import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToMany; +import jakarta.persistence.OrderColumn; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel(annotatedClasses = { + DetachedCollectionInitializationJoinFetchTest.EntityA.class, + DetachedCollectionInitializationJoinFetchTest.EntityB.class, +}) +@SessionFactory +@BytecodeEnhanced(runNotEnhancedAsWell = true) +@Jira("https://hibernate.atlassian.net/browse/HHH-19910") +public class DetachedCollectionInitializationJoinFetchTest { + @Test + public void testTransientInstance(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityA = session.find( EntityA.class, 1L ); + Hibernate.initialize( entityA.getB() ); // initialize the collection + session.clear(); + + fetchQuery( new ArrayList<>( entityA.getB() ), session ); + } ); + } + + @Test + public void testUninitializedDetachedInstance(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityA = session.find( EntityA.class, 1L ); + session.clear(); + + fetchQuery( entityA.b, session ); + } ); + } + + @Test + public void testInitializedDetachedInstance(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityA = session.find( EntityA.class, 1L ); + Hibernate.initialize( entityA.getB() ); // initialize the collection + session.clear(); + + fetchQuery( entityA.getB(), session ); + } ); + } + + @BeforeEach + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB1 = new EntityB(); + entityB1.id = 1L; + entityB1.name = "b_1"; + session.persist( entityB1 ); + final var entityB2 = new EntityB(); + entityB2.id = 2L; + entityB2.name = "b_2"; + session.persist( entityB2 ); + final var entityA = new EntityA(); + entityA.id = 1L; + entityA.b.add( entityB1 ); + entityA.b.add( entityB2 ); + session.persist( entityA ); + } ); + } + + @AfterEach + public void tearDown(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } + + private void fetchQuery(List b, SessionImplementor session) { + final var entityA = new EntityA(); + entityA.id = 2L; + entityA.setB( b ); + session.persist( entityA ); + + // If persist triggers lazy initialization, the EntityB instances will be persistent + final boolean wasB1Managed = session.contains( b.get( 0 ) ); + final boolean wasB2Managed = session.contains( b.get( 1 ) ); + + final var result = session.createQuery( + "from EntityA a left join fetch a.b where a.id = 2", + EntityA.class + ).getSingleResult(); + + // We always need to initialize the collection on flush + assertThat( Hibernate.isInitialized( b ) ).isTrue(); + + final var descriptor = session.getSessionFactory() + .getMappingMetamodel() + .getCollectionDescriptor( EntityA.class.getName() + ".b" ); + final PersistentCollection collection = session.getPersistenceContextInternal() + .getCollection( new CollectionKey( descriptor, entityA.id ) ); + assertThat( Hibernate.isInitialized( collection ) ).isTrue(); + // Currently, the collection instance is re-used if we find a detached PersistentCollection. + // Not making any assertion here, as also always wrapping the value in a new instance would be acceptable + // assertThat( collection ).isNotSameAs( b ); + + // The detached instances in the collection should not be the same as the + // managed instances initialized in the persistence context. + assertThat( result.getB().get( 0 ) == session.getReference( EntityB.class, 1L ) ) + .isEqualTo( wasB1Managed ); + assertThat( result.getB().get( 1 ) == session.getReference( EntityB.class, 2L ) ) + .isEqualTo( wasB2Managed ); + } + + @Entity(name = "EntityA") + static class EntityA { + @Id + private Long id; + + private String name; + + @ManyToMany(fetch = FetchType.LAZY) + @OrderColumn + private List b = new ArrayList<>(); + + public List getB() { + return b; + } + + public void setB(List b) { + this.b = b; + } + } + + @Entity(name = "EntityB") + static class EntityB { + @Id + private Long id; + + private String name; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/detached/collection/DetachedNonJoinedCollectionInitializationJoinFetchTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/detached/collection/DetachedNonJoinedCollectionInitializationJoinFetchTest.java new file mode 100644 index 000000000000..aa8e6ff4be99 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/detached/collection/DetachedNonJoinedCollectionInitializationJoinFetchTest.java @@ -0,0 +1,162 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.bytecode.enhancement.detached.collection; + +import java.util.ArrayList; +import java.util.List; + +import org.hibernate.Hibernate; +import org.hibernate.annotations.Fetch; +import org.hibernate.annotations.FetchMode; +import org.hibernate.collection.spi.PersistentCollection; +import org.hibernate.engine.spi.CollectionKey; +import org.hibernate.engine.spi.SessionImplementor; + +import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToMany; +import jakarta.persistence.OrderColumn; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel(annotatedClasses = { + DetachedNonJoinedCollectionInitializationJoinFetchTest.EntityA.class, + DetachedNonJoinedCollectionInitializationJoinFetchTest.EntityB.class, +}) +@SessionFactory +@BytecodeEnhanced(runNotEnhancedAsWell = true) +@Jira("https://hibernate.atlassian.net/browse/HHH-19910") +public class DetachedNonJoinedCollectionInitializationJoinFetchTest { + @Test + public void testTransientInstance(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityA = session.find( EntityA.class, 1L ); + Hibernate.initialize( entityA.getB() ); // initialize the collection + session.clear(); + + fetchQuery( new ArrayList<>( entityA.getB() ), session ); + } ); + } + + @Test + public void testUninitializedDetachedInstance(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityA = session.find( EntityA.class, 1L ); + session.clear(); + + fetchQuery( entityA.b, session ); + } ); + } + + @Test + public void testInitializedDetachedInstance(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityA = session.find( EntityA.class, 1L ); + Hibernate.initialize( entityA.getB() ); // initialize the collection + session.clear(); + + fetchQuery( entityA.getB(), session ); + } ); + } + + @BeforeEach + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB1 = new EntityB(); + entityB1.id = 1L; + entityB1.name = "b_1"; + session.persist( entityB1 ); + final var entityB2 = new EntityB(); + entityB2.id = 2L; + entityB2.name = "b_2"; + session.persist( entityB2 ); + final var entityA = new EntityA(); + entityA.id = 1L; + entityA.b.add( entityB1 ); + entityA.b.add( entityB2 ); + session.persist( entityA ); + } ); + } + + @AfterEach + public void tearDown(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } + + private void fetchQuery(List b, SessionImplementor session) { + final var entityA = new EntityA(); + entityA.id = 2L; + entityA.setB( b ); + session.persist( entityA ); + + // If persist triggers lazy initialization, the EntityB instances will be persistent + final boolean wasB1Managed = session.contains( b.get( 0 ) ); + final boolean wasB2Managed = session.contains( b.get( 1 ) ); + + final var result = session.createQuery( + "from EntityA a where a.id = 2", + EntityA.class + ).getSingleResult(); + + // We always need to initialize the collection on flush + assertThat( Hibernate.isInitialized( b ) ).isTrue(); + + final var descriptor = session.getSessionFactory() + .getMappingMetamodel() + .getCollectionDescriptor( EntityA.class.getName() + ".b" ); + final PersistentCollection collection = session.getPersistenceContextInternal() + .getCollection( new CollectionKey( descriptor, entityA.id ) ); + assertThat( Hibernate.isInitialized( collection ) ).isTrue(); + // Currently, the collection instance is re-used if we find a detached PersistentCollection. + // Not making any assertion here, as also always wrapping the value in a new instance would be acceptable + // assertThat( collection ).isNotSameAs( b ); + + // The detached instances in the collection should not be the same as the + // managed instances initialized in the persistence context. + assertThat( result.getB().get( 0 ) == session.getReference( EntityB.class, 1L ) ) + .isEqualTo( wasB1Managed ); + assertThat( result.getB().get( 1 ) == session.getReference( EntityB.class, 2L ) ) + .isEqualTo( wasB2Managed ); + } + + @Entity(name = "EntityA") + static class EntityA { + @Id + private Long id; + + private String name; + + @ManyToMany(fetch = FetchType.EAGER) + @OrderColumn + @Fetch( FetchMode.SELECT ) + private List b = new ArrayList<>(); + + public List getB() { + return b; + } + + public void setB(List b) { + this.b = b; + } + } + + @Entity(name = "EntityB") + static class EntityB { + @Id + private Long id; + + private String name; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/detached/reference/DetachedReferenceInitializationAnyFetchTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/detached/reference/DetachedReferenceInitializationAnyFetchTest.java new file mode 100644 index 000000000000..09fd8d620957 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/detached/reference/DetachedReferenceInitializationAnyFetchTest.java @@ -0,0 +1,224 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.bytecode.enhancement.detached.reference; + +import org.hibernate.Hibernate; +import org.hibernate.annotations.Any; +import org.hibernate.annotations.AnyDiscriminatorValue; +import org.hibernate.annotations.AnyKeyJavaClass; +import org.hibernate.engine.spi.PrimeAmongSecondarySupertypes; +import org.hibernate.engine.spi.SessionImplementor; + +import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel(annotatedClasses = { + DetachedReferenceInitializationAnyFetchTest.EntityA.class, + DetachedReferenceInitializationAnyFetchTest.EntityB.class, +}) +@SessionFactory +@BytecodeEnhanced(runNotEnhancedAsWell = true) +@Jira("https://hibernate.atlassian.net/browse/HHH-19910") +public class DetachedReferenceInitializationAnyFetchTest { + @Test + public void testDetachedAndPersistentEntity(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.find( EntityB.class, 1L ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.find( EntityB.class, 1L ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedEntityAndPersistentInitializedProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.find( EntityB.class, 1L ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.getReference( EntityB.class, 1L ); + Hibernate.initialize( ignored ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedEntityAndPersistentProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.find( EntityB.class, 1L ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.getReference( EntityB.class, 1L ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedProxyAndPersistentEntity(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.getReference( EntityB.class, 1L ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.find( EntityB.class, 1L ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedProxyAndPersistentInitializedProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.getReference( EntityB.class, 1L ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.getReference( EntityB.class, 1L ); + Hibernate.initialize( ignored ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedAndPersistentProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.getReference( EntityB.class, 1L ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.getReference( EntityB.class, 1L ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedInitializedProxyAndPersistentEntity(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.getReference( EntityB.class, 1L ); + Hibernate.initialize( entityB ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.find( EntityB.class, 1L ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedAndPersistentInitializedProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.getReference( EntityB.class, 1L ); + Hibernate.initialize( entityB ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.getReference( EntityB.class, 1L ); + Hibernate.initialize( ignored ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedInitializedProxyAndPersistentProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.getReference( EntityB.class, 1L ); + Hibernate.initialize( entityB ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.getReference( EntityB.class, 1L ); + + fetchQuery( entityB, session ); + } ); + } + + @BeforeAll + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = new EntityB(); + entityB.id = 1L; + entityB.name = "b_1"; + session.persist( entityB ); + } ); + } + + @AfterEach + public void tearDown(SessionFactoryScope scope) { + scope.inTransaction( session -> { + session.createMutationQuery( "delete from EntityA" ).executeUpdate(); + } ); + } + + private void fetchQuery(EntityB entityB, SessionImplementor session) { + final var entityA = new EntityA(); + entityA.id = 1L; + entityA.b = entityB; + session.persist( entityA ); + + final var wasDetachedInitialized = Hibernate.isInitialized( entityB ); + + final var id = session.getSessionFactory().getPersistenceUnitUtil().getIdentifier( entityB ); + final var reference = session.getReference( EntityB.class, id ); + final var wasManagedInitialized = Hibernate.isInitialized( reference ); + + final var result = session.createQuery( + "from EntityA a", + EntityA.class + ).getSingleResult(); + + assertThat( Hibernate.isInitialized( entityB ) ).isEqualTo( wasDetachedInitialized ); + assertThat( result.b ).isSameAs( entityB ); + + // We cannot create a proxy for the non-enhanced case + assertThat( Hibernate.isInitialized( reference ) ).isEqualTo( wasManagedInitialized || !( reference instanceof PrimeAmongSecondarySupertypes ) ); + assertThat( reference ).isNotSameAs( entityB ); + } + + @Entity(name = "EntityA") + static class EntityA { + @Id + private Long id; + + @Any(fetch = FetchType.LAZY) + @AnyKeyJavaClass(Long.class) + @JoinColumn(name = "b_id") //the foreign key column + @Column(name = "b_type") //the discriminator column + @AnyDiscriminatorValue(discriminator = "B", entity = EntityB.class) + private Object b; + } + + @Entity(name = "EntityB") + static class EntityB { + @Id + private Long id; + + private String name; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/detached/reference/DetachedReferenceInitializationBatchFetchTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/detached/reference/DetachedReferenceInitializationBatchFetchTest.java new file mode 100644 index 000000000000..faa21feafa00 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/detached/reference/DetachedReferenceInitializationBatchFetchTest.java @@ -0,0 +1,217 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.bytecode.enhancement.detached.reference; + +import org.hibernate.Hibernate; +import org.hibernate.annotations.BatchSize; +import org.hibernate.engine.spi.SessionImplementor; + +import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel(annotatedClasses = { + DetachedReferenceInitializationBatchFetchTest.EntityA.class, + DetachedReferenceInitializationBatchFetchTest.EntityB.class, +}) +@SessionFactory +@BytecodeEnhanced(runNotEnhancedAsWell = true) +@Jira("https://hibernate.atlassian.net/browse/HHH-19910") +public class DetachedReferenceInitializationBatchFetchTest { + @Test + public void testDetachedAndPersistentEntity(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.find( EntityB.class, 1L ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.find( EntityB.class, 1L ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedEntityAndPersistentInitializedProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.find( EntityB.class, 1L ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.getReference( EntityB.class, 1L ); + Hibernate.initialize( ignored ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedEntityAndPersistentProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.find( EntityB.class, 1L ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.getReference( EntityB.class, 1L ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedProxyAndPersistentEntity(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.getReference( EntityB.class, 1L ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.find( EntityB.class, 1L ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedProxyAndPersistentInitializedProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.getReference( EntityB.class, 1L ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.getReference( EntityB.class, 1L ); + Hibernate.initialize( ignored ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedAndPersistentProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.getReference( EntityB.class, 1L ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.getReference( EntityB.class, 1L ); + + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedInitializedProxyAndPersistentEntity(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.getReference( EntityB.class, 1L ); + Hibernate.initialize( entityB ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.find( EntityB.class, 1L ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedAndPersistentInitializedProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.getReference( EntityB.class, 1L ); + Hibernate.initialize( entityB ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.getReference( EntityB.class, 1L ); + Hibernate.initialize( ignored ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedInitializedProxyAndPersistentProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.getReference( EntityB.class, 1L ); + Hibernate.initialize( entityB ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.getReference( EntityB.class, 1L ); + + fetchQuery( entityB, session ); + } ); + } + + @BeforeAll + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = new EntityB(); + entityB.id = 1L; + entityB.name = "b_1"; + session.persist( entityB ); + } ); + } + + @AfterEach + public void tearDown(SessionFactoryScope scope) { + scope.inTransaction( session -> { + session.createMutationQuery( "delete from EntityA" ).executeUpdate(); + } ); + } + + private void fetchQuery(EntityB entityB, SessionImplementor session) { + final var entityA = new EntityA(); + entityA.id = 1L; + entityA.b = entityB; + session.persist( entityA ); + final var entityA2 = new EntityA(); + entityA2.id = 2L; + session.persist( entityA2 ); + + final var wasInitialized = Hibernate.isInitialized( entityB ); + + final var result = session.createQuery( + "from EntityA a order by a.id", + EntityA.class + ).getResultList().get( 0 ); + + assertThat( Hibernate.isInitialized( entityB ) ).isEqualTo( wasInitialized ); + assertThat( result.b ).isSameAs( entityB ); + + final var id = session.getSessionFactory().getPersistenceUnitUtil().getIdentifier( entityB ); + final var reference = session.getReference( EntityB.class, id ); + assertThat( Hibernate.isInitialized( reference ) ).isTrue(); + assertThat( reference ).isNotSameAs( entityB ); + } + + @Entity(name = "EntityA") + static class EntityA { + @Id + private Long id; + + @ManyToOne + private EntityB b; + } + + @BatchSize( size = 10 ) + @Entity(name = "EntityB") + static class EntityB { + @Id + private Long id; + + private String name; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/detached/reference/DetachedReferenceInitializationDelayedFetchTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/detached/reference/DetachedReferenceInitializationDelayedFetchTest.java new file mode 100644 index 000000000000..7f938114dc18 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/detached/reference/DetachedReferenceInitializationDelayedFetchTest.java @@ -0,0 +1,216 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.bytecode.enhancement.detached.reference; + +import org.hibernate.Hibernate; +import org.hibernate.engine.spi.SessionImplementor; + +import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel(annotatedClasses = { + DetachedReferenceInitializationDelayedFetchTest.EntityA.class, + DetachedReferenceInitializationDelayedFetchTest.EntityB.class, +}) +@SessionFactory +@BytecodeEnhanced(runNotEnhancedAsWell = true) +@Jira("https://hibernate.atlassian.net/browse/HHH-19910") +public class DetachedReferenceInitializationDelayedFetchTest { + @Test + public void testDetachedAndPersistentEntity(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.find( EntityB.class, 1L ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.find( EntityB.class, 1L ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedEntityAndPersistentInitializedProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.find( EntityB.class, 1L ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.getReference( EntityB.class, 1L ); + Hibernate.initialize( ignored ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedEntityAndPersistentProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.find( EntityB.class, 1L ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.getReference( EntityB.class, 1L ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedProxyAndPersistentEntity(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.getReference( EntityB.class, 1L ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.find( EntityB.class, 1L ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedProxyAndPersistentInitializedProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.getReference( EntityB.class, 1L ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.getReference( EntityB.class, 1L ); + Hibernate.initialize( ignored ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedAndPersistentProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.getReference( EntityB.class, 1L ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.getReference( EntityB.class, 1L ); + + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedInitializedProxyAndPersistentEntity(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.getReference( EntityB.class, 1L ); + Hibernate.initialize( entityB ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.find( EntityB.class, 1L ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedAndPersistentInitializedProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.getReference( EntityB.class, 1L ); + Hibernate.initialize( entityB ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.getReference( EntityB.class, 1L ); + Hibernate.initialize( ignored ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedInitializedProxyAndPersistentProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.getReference( EntityB.class, 1L ); + Hibernate.initialize( entityB ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.getReference( EntityB.class, 1L ); + + fetchQuery( entityB, session ); + } ); + } + + @BeforeAll + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = new EntityB(); + entityB.id = 1L; + entityB.name = "b_1"; + session.persist( entityB ); + } ); + } + + @AfterEach + public void tearDown(SessionFactoryScope scope) { + scope.inTransaction( session -> { + session.createMutationQuery( "delete from EntityA" ).executeUpdate(); + } ); + } + + private void fetchQuery(EntityB entityB, SessionImplementor session) { + final var entityA = new EntityA(); + entityA.id = 1L; + entityA.b = entityB; + session.persist( entityA ); + + final var wasDetachedInitialized = Hibernate.isInitialized( entityB ); + + final var id = session.getSessionFactory().getPersistenceUnitUtil().getIdentifier( entityB ); + final var reference = session.getReference( EntityB.class, id ); + final var wasManagedInitialized = Hibernate.isInitialized( reference ); + + final var result = session.createQuery( + "from EntityA a", + EntityA.class + ).getSingleResult(); + + assertThat( Hibernate.isInitialized( entityB ) ).isEqualTo( wasDetachedInitialized ); + assertThat( result.b ).isSameAs( entityB ); + + + assertThat( Hibernate.isInitialized( reference ) ).isEqualTo( wasManagedInitialized ); + assertThat( reference ).isNotSameAs( entityB ); + } + + @Entity(name = "EntityA") + static class EntityA { + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private EntityB b; + } + + @Entity(name = "EntityB") + static class EntityB { + @Id + private Long id; + + private String name; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/detached/reference/DetachedReferenceInitializationEagerFetchTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/detached/reference/DetachedReferenceInitializationEagerFetchTest.java new file mode 100644 index 000000000000..a7e88e517203 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/detached/reference/DetachedReferenceInitializationEagerFetchTest.java @@ -0,0 +1,212 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.bytecode.enhancement.detached.reference; + +import org.hibernate.Hibernate; +import org.hibernate.engine.spi.SessionImplementor; + +import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel(annotatedClasses = { + DetachedReferenceInitializationEagerFetchTest.EntityA.class, + DetachedReferenceInitializationEagerFetchTest.EntityB.class, +}) +@SessionFactory +@BytecodeEnhanced(runNotEnhancedAsWell = true) +@Jira("https://hibernate.atlassian.net/browse/HHH-19910") +public class DetachedReferenceInitializationEagerFetchTest { + @Test + public void testDetachedAndPersistentEntity(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.find( EntityB.class, 1L ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.find( EntityB.class, 1L ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedEntityAndPersistentInitializedProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.find( EntityB.class, 1L ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.getReference( EntityB.class, 1L ); + Hibernate.initialize( ignored ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedEntityAndPersistentProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.find( EntityB.class, 1L ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.getReference( EntityB.class, 1L ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedProxyAndPersistentEntity(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.getReference( EntityB.class, 1L ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.find( EntityB.class, 1L ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedProxyAndPersistentInitializedProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.getReference( EntityB.class, 1L ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.getReference( EntityB.class, 1L ); + Hibernate.initialize( ignored ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedAndPersistentProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.getReference( EntityB.class, 1L ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.getReference( EntityB.class, 1L ); + + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedInitializedProxyAndPersistentEntity(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.getReference( EntityB.class, 1L ); + Hibernate.initialize( entityB ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.find( EntityB.class, 1L ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedAndPersistentInitializedProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.getReference( EntityB.class, 1L ); + Hibernate.initialize( entityB ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.getReference( EntityB.class, 1L ); + Hibernate.initialize( ignored ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedInitializedProxyAndPersistentProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.getReference( EntityB.class, 1L ); + Hibernate.initialize( entityB ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.getReference( EntityB.class, 1L ); + + fetchQuery( entityB, session ); + } ); + } + + @BeforeAll + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = new EntityB(); + entityB.id = 1L; + entityB.name = "b_1"; + session.persist( entityB ); + } ); + } + + @AfterEach + public void tearDown(SessionFactoryScope scope) { + scope.inTransaction( session -> { + session.createMutationQuery( "delete from EntityA" ).executeUpdate(); + } ); + } + + private void fetchQuery(EntityB entityB, SessionImplementor session) { + final var entityA = new EntityA(); + entityA.id = 1L; + entityA.b = entityB; + session.persist( entityA ); + + final var wasInitialized = Hibernate.isInitialized( entityB ); + + final var result = session.createQuery( + "from EntityA a", + EntityA.class + ).getSingleResult(); + + assertThat( Hibernate.isInitialized( entityB ) ).isEqualTo( wasInitialized ); + assertThat( result.b ).isSameAs( entityB ); + + final var id = session.getSessionFactory().getPersistenceUnitUtil().getIdentifier( entityB ); + final var reference = session.getReference( EntityB.class, id ); + assertThat( Hibernate.isInitialized( reference ) ).isTrue(); + assertThat( reference ).isNotSameAs( entityB ); + } + + @Entity(name = "EntityA") + static class EntityA { + @Id + private Long id; + + @ManyToOne + private EntityB b; + } + + @Entity(name = "EntityB") + static class EntityB { + @Id + private Long id; + + private String name; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/detached/reference/DetachedReferenceInitializationEagerUniqueFetchTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/detached/reference/DetachedReferenceInitializationEagerUniqueFetchTest.java new file mode 100644 index 000000000000..63c9370111dc --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/detached/reference/DetachedReferenceInitializationEagerUniqueFetchTest.java @@ -0,0 +1,182 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.bytecode.enhancement.detached.reference; + +import org.hibernate.Hibernate; +import org.hibernate.engine.spi.PrimeAmongSecondarySupertypes; +import org.hibernate.engine.spi.SessionImplementor; + +import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel(annotatedClasses = { + DetachedReferenceInitializationEagerUniqueFetchTest.EntityA.class, + DetachedReferenceInitializationEagerUniqueFetchTest.EntityB.class, +}) +@SessionFactory +@BytecodeEnhanced(runNotEnhancedAsWell = true) +@Jira("https://hibernate.atlassian.net/browse/HHH-19910") +public class DetachedReferenceInitializationEagerUniqueFetchTest { + @Test + public void testDetachedAndPersistentEntity(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.find( EntityB.class, 1L ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.find( EntityB.class, 1L ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedEntityAndPersistentInitializedProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.find( EntityB.class, 1L ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.getReference( EntityB.class, 1L ); + Hibernate.initialize( ignored ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedEntityAndPersistentProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.find( EntityB.class, 1L ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.getReference( EntityB.class, 1L ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedInitializedProxyAndPersistentEntity(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.getReference( EntityB.class, 1L ); + Hibernate.initialize( entityB ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.find( EntityB.class, 1L ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedAndPersistentInitializedProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.getReference( EntityB.class, 1L ); + Hibernate.initialize( entityB ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.getReference( EntityB.class, 1L ); + Hibernate.initialize( ignored ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedInitializedProxyAndPersistentProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.getReference( EntityB.class, 1L ); + Hibernate.initialize( entityB ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.getReference( EntityB.class, 1L ); + + fetchQuery( entityB, session ); + } ); + } + + @BeforeAll + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = new EntityB(); + entityB.id = 1L; + entityB.name = "b_1"; + session.persist( entityB ); + } ); + } + + @AfterEach + public void tearDown(SessionFactoryScope scope) { + scope.inTransaction( session -> { + session.createMutationQuery( "delete from EntityA" ).executeUpdate(); + } ); + } + + private void fetchQuery(EntityB entityB, SessionImplementor session) { + final var entityA = new EntityA(); + entityA.id = 1L; + entityA.b = entityB; + session.persist( entityA ); + + final var wasDetachedInitialized = Hibernate.isInitialized( entityB ); + + final var id = session.getSessionFactory().getPersistenceUnitUtil().getIdentifier( entityB ); + final var reference = session.getReference( EntityB.class, id ); + final var wasManagedInitialized = Hibernate.isInitialized( reference ); + + final var result = session.createQuery( + "from EntityA a", + EntityA.class + ).getSingleResult(); + + assertThat( Hibernate.isInitialized( entityB ) ).isEqualTo( wasDetachedInitialized ); + assertThat( result.b ).isSameAs( entityB ); + + // We cannot create a proxy for the non-enhanced case + assertThat( Hibernate.isInitialized( reference ) ).isEqualTo( wasManagedInitialized || !( reference instanceof PrimeAmongSecondarySupertypes ) ); + assertThat( reference ).isNotSameAs( entityB ); + } + + @Entity(name = "EntityA") + static class EntityA { + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "entityB_id", referencedColumnName = "uniqueValue") + private EntityB b; + } + + @Entity(name = "EntityB") + static class EntityB { + @Id + private Long id; + + @Column(unique = true) + private Long uniqueValue; + + private String name; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/detached/reference/DetachedReferenceInitializationJoinFetchTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/detached/reference/DetachedReferenceInitializationJoinFetchTest.java new file mode 100644 index 000000000000..45a6140c56a7 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/detached/reference/DetachedReferenceInitializationJoinFetchTest.java @@ -0,0 +1,213 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.bytecode.enhancement.detached.reference; + +import org.hibernate.Hibernate; +import org.hibernate.engine.spi.SessionImplementor; + +import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel(annotatedClasses = { + DetachedReferenceInitializationJoinFetchTest.EntityA.class, + DetachedReferenceInitializationJoinFetchTest.EntityB.class, +}) +@SessionFactory +@BytecodeEnhanced(runNotEnhancedAsWell = true) +@Jira("https://hibernate.atlassian.net/browse/HHH-19910") +public class DetachedReferenceInitializationJoinFetchTest { + @Test + public void testDetachedAndPersistentEntity(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.find( EntityB.class, 1L ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.find( EntityB.class, 1L ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedEntityAndPersistentInitializedProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.find( EntityB.class, 1L ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.getReference( EntityB.class, 1L ); + Hibernate.initialize( ignored ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedEntityAndPersistentProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.find( EntityB.class, 1L ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.getReference( EntityB.class, 1L ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedProxyAndPersistentEntity(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.getReference( EntityB.class, 1L ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.find( EntityB.class, 1L ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedProxyAndPersistentInitializedProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.getReference( EntityB.class, 1L ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.getReference( EntityB.class, 1L ); + Hibernate.initialize( ignored ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedAndPersistentProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.getReference( EntityB.class, 1L ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.getReference( EntityB.class, 1L ); + + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedInitializedProxyAndPersistentEntity(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.getReference( EntityB.class, 1L ); + Hibernate.initialize( entityB ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.find( EntityB.class, 1L ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedAndPersistentInitializedProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.getReference( EntityB.class, 1L ); + Hibernate.initialize( entityB ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.getReference( EntityB.class, 1L ); + Hibernate.initialize( ignored ); + + fetchQuery( entityB, session ); + } ); + } + + @Test + public void testDetachedInitializedProxyAndPersistentProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = session.getReference( EntityB.class, 1L ); + Hibernate.initialize( entityB ); + session.clear(); + + // put a different instance of EntityB in the persistence context + final var ignored = session.getReference( EntityB.class, 1L ); + + fetchQuery( entityB, session ); + } ); + } + + @BeforeAll + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var entityB = new EntityB(); + entityB.id = 1L; + entityB.name = "b_1"; + session.persist( entityB ); + } ); + } + + @AfterEach + public void tearDown(SessionFactoryScope scope) { + scope.inTransaction( session -> { + session.createMutationQuery( "delete from EntityA" ).executeUpdate(); + } ); + } + + private void fetchQuery(EntityB entityB, SessionImplementor session) { + final var entityA = new EntityA(); + entityA.id = 1L; + entityA.b = entityB; + session.persist( entityA ); + + final var wasInitialized = Hibernate.isInitialized( entityB ); + + final var result = session.createQuery( + "from EntityA a left join fetch a.b", + EntityA.class + ).getSingleResult(); + + assertThat( Hibernate.isInitialized( entityB ) ).isEqualTo( wasInitialized ); + assertThat( result.b ).isSameAs( entityB ); + + final var id = session.getSessionFactory().getPersistenceUnitUtil().getIdentifier( entityB ); + final var reference = session.getReference( EntityB.class, id ); + assertThat( Hibernate.isInitialized( reference ) ).isTrue(); + assertThat( reference ).isNotSameAs( entityB ); + } + + @Entity(name = "EntityA") + static class EntityA { + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + private EntityB b; + } + + @Entity(name = "EntityB") + static class EntityB { + @Id + private Long id; + + private String name; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/dirty/DirtyTrackingIdTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/dirty/DirtyTrackingIdTest.java new file mode 100644 index 000000000000..e645a7510c10 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/dirty/DirtyTrackingIdTest.java @@ -0,0 +1,114 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.bytecode.enhancement.dirty; + +import jakarta.persistence.Embeddable; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import org.hibernate.testing.bytecode.enhancement.EnhancementOptions; +import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Test; + +import java.util.Objects; + +import static org.assertj.core.api.Assertions.assertThat; + +@JiraKey("HHH-19206") +@DomainModel( + annotatedClasses = { + DirtyTrackingIdTest.MyEntity.class + } +) +@SessionFactory +@BytecodeEnhanced +@EnhancementOptions(lazyLoading = true, inlineDirtyChecking = true, extendedEnhancement = true) +public class DirtyTrackingIdTest { + + @Test + public void test(SessionFactoryScope scope) { + scope.inTransaction( session -> { + MyEntity myEntity = new MyEntity(); + myEntity.setAnId( new MyEntityId( 1L ) ); + myEntity.setData( "initial" ); + session.persist( myEntity ); + + // This is unnecessary, but should be harmless... + // Unfortunately it causes dirty checking to misbehave. + // Comment it, and the test will pass. + myEntity.setAnId( new MyEntityId( 1L ) ); + + myEntity.setData( "updated" ); + } ); + scope.inTransaction( session -> { + var entityFromDb = session.find( MyEntity.class, new MyEntityId( 1L ) ); + assertThat( entityFromDb.getData() ).isEqualTo( "updated" ); + } ); + } + + // --- // + + @Entity(name = "MyEntity") + public static class MyEntity { + // The name of this property must be (alphabetically) before the name of "data" to trigger the bug. + // Yes, it's weird. + @EmbeddedId + private MyEntityId anId; + private String data; + + public void setAnId(MyEntityId id) { + this.anId = id; + } + + public MyEntityId getAnId() { + return anId; + } + + public String getData() { + return data; + } + + public void setData(String name) { + this.data = name; + } + } + + @Embeddable + public static class MyEntityId { + private Long id; + + public MyEntityId() { + } + + public MyEntityId(Long id) { + this.id = id; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + @Override + public final boolean equals(Object o) { + if ( !(o instanceof MyEntityId) ) { + return false; + } + + return Objects.equals( id, ( (MyEntityId) o ).id ); + } + + @Override + public int hashCode() { + return Objects.hashCode( id ); + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/graph/LoadAndFetchGraphAssociationNotExplicitlySpecifiedTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/graph/LoadAndFetchGraphAssociationNotExplicitlySpecifiedTest.java new file mode 100644 index 000000000000..63111a85e015 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/graph/LoadAndFetchGraphAssociationNotExplicitlySpecifiedTest.java @@ -0,0 +1,443 @@ +package org.hibernate.orm.test.bytecode.enhancement.graph; + +import java.util.ArrayList; +import java.util.List; +import java.util.Set; +import java.util.function.Supplier; + +import org.hibernate.Hibernate; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.graph.GraphSemantic; + +import org.hibernate.testing.bytecode.enhancement.EnhancementOptions; +import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced; +import org.hibernate.testing.jdbc.SQLStatementInspector; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.Setting; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToOne; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; +import static org.hibernate.graph.GraphSemantic.FETCH; + +/** + * Checks that associations that are **not** explicitly specified in a fetch/load graph + * are correctly initialized (or not) according to the graph semantics, + * for several association topologies. + */ +@DomainModel( + annotatedClasses = { + LoadAndFetchGraphAssociationNotExplicitlySpecifiedTest.RootEntity.class, + LoadAndFetchGraphAssociationNotExplicitlySpecifiedTest.ContainedEntity.class + } +) +@SessionFactory(useCollectingStatementInspector = true) +@JiraKey("HHH-18489") +@BytecodeEnhanced() +@EnhancementOptions(lazyLoading = true) +@ServiceRegistry(settings = @Setting(name = AvailableSettings.MAX_FETCH_DEPTH, value = "")) +public class LoadAndFetchGraphAssociationNotExplicitlySpecifiedTest { + + @BeforeEach + void init(SessionFactoryScope scope) { + scope.inTransaction( session -> { + for ( long i = 0; i < 3; ++i ) { + RootEntity root = new RootEntity( i * 100 ); + + long j = i * 100; + root.setLazyOneToOneOwned( new ContainedEntity( ++j ) ); + root.setLazyManyToOneOwned( new ContainedEntity( ++j ) ); + root.setEagerOneToOneOwned( new ContainedEntity( ++j ) ); + root.setEagerManyToOneOwned( new ContainedEntity( ++j ) ); + + session.persist( root ); + + ContainedEntity contained; + + contained = new ContainedEntity( ++j ); + root.setLazyOneToOneUnowned( contained ); + contained.setInverseSideOfLazyOneToOneUnowned( root ); + session.persist( contained ); + + contained = new ContainedEntity( ++j ); + root.setEagerOneToOneUnowned( contained ); + contained.setInverseSideOfEagerOneToOneUnowned( root ); + session.persist( contained ); + } + } ); + } + + @AfterEach + void cleanUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + session.createMutationQuery( + "update ContainedEntity set" + + " inverseSideOfLazyOneToOneUnowned = null" + + ", inverseSideOfEagerOneToOneUnowned = null" + ).executeUpdate(); + session.createMutationQuery( "delete RootEntity" ).executeUpdate(); + session.createMutationQuery( "delete ContainedEntity" ).executeUpdate(); + } ); + } + + // Arguments for the parameterized test below + List queryWithEntityGraph() { + List args = new ArrayList<>(); + for ( GraphSemantic graphSemantic : GraphSemantic.values() ) { + for ( String propertyName : RootEntity.LAZY_PROPERTY_NAMES ) { + args.add( Arguments.of( graphSemantic, propertyName ) ); + } + for ( String propertyName : RootEntity.EAGER_PROPERTY_NAMES ) { + args.add( Arguments.of( graphSemantic, propertyName ) ); + } + } + // Also test without a graph, for reference + args.add( Arguments.of( null, null ) ); + return args; + } + + @Test + public void testWithFetchGraph(SessionFactoryScope scope) { + String propertySpecifiedInGraph = "eagerOneToOneOwned"; + scope.inTransaction( session -> { + var sqlStatementInspector = scope.getCollectingStatementInspector(); + sqlStatementInspector.clear(); + var query = session.createQuery( "select e from RootEntity e where id in (:ids)", RootEntity.class ) + .setFetchSize( 100 ) + // Selecting multiple entities to make sure we don't have side effects (e.g. some context shared across entity instances) + .setParameter( "ids", List.of( 0L, 100L, 200L ) ); + + var graph = session.createEntityGraph( RootEntity.class ); + graph.addAttributeNode( propertySpecifiedInGraph ); + query.applyGraph( graph, FETCH ); + + var resultList = query.list(); + assertThat( resultList ).isNotEmpty(); + for ( String propertyName : RootEntity.LAZY_PROPERTY_NAMES ) { + var expectInitialized = propertyName.equals( propertySpecifiedInGraph ); + assertAssociationInitialized( resultList, propertyName, expectInitialized, sqlStatementInspector ); + } + for ( String propertyName : RootEntity.EAGER_PROPERTY_NAMES ) { + var expectInitialized = propertyName.equals( propertySpecifiedInGraph ); + assertAssociationInitialized( resultList, propertyName, expectInitialized, sqlStatementInspector ); + } + } ); + } + + @ParameterizedTest + @MethodSource + public void queryWithEntityGraph(GraphSemantic graphSemantic, String propertySpecifiedInGraph, SessionFactoryScope scope) { + scope.inTransaction( session -> { + var sqlStatementInspector = scope.getCollectingStatementInspector(); + sqlStatementInspector.clear(); + var query = session.createQuery( "select e from RootEntity e where id in (:ids)", RootEntity.class ) + .setFetchSize( 100 ) + // Selecting multiple entities to make sure we don't have side effects (e.g. some context shared across entity instances) + .setParameter( "ids", List.of( 0L, 100L, 200L ) ); + + if ( graphSemantic != null ) { + var graph = session.createEntityGraph( RootEntity.class ); + graph.addAttributeNode( propertySpecifiedInGraph ); + query.applyGraph( graph, graphSemantic ); + } // else just run the query without a graph + + var resultList = query.list(); + assertThat( resultList ).isNotEmpty(); + for ( String propertyName : RootEntity.LAZY_PROPERTY_NAMES ) { + var expectInitialized = propertyName.equals( propertySpecifiedInGraph ); + assertAssociationInitialized( resultList, propertyName, expectInitialized, sqlStatementInspector ); + } + for ( String propertyName : RootEntity.EAGER_PROPERTY_NAMES ) { + var expectInitialized = propertyName.equals( propertySpecifiedInGraph ) + // Under LOAD semantics, or when not using graphs, + // eager properties also get loaded (even if not specified in the graph). + || GraphSemantic.LOAD.equals( graphSemantic ) || graphSemantic == null; + assertAssociationInitialized( resultList, propertyName, expectInitialized, sqlStatementInspector ); + } + } ); + } + + private void assertAssociationInitialized( + List resultList, + String propertyName, + boolean expectInitialized, + SQLStatementInspector sqlStatementInspector) { + for ( var rootEntity : resultList ) { + sqlStatementInspector.clear(); + if ( propertyName.endsWith( "Unowned" ) ) { + final Supplier supplier; + switch ( propertyName ) { + case ( "lazyOneToOneUnowned" ): + supplier = () -> rootEntity.getLazyOneToOneUnowned(); + break; + case ( "eagerOneToOneUnowned" ): + supplier = () -> rootEntity.getEagerOneToOneUnowned(); + break; + default: + supplier = null; + fail( "unknown association property name : " + propertyName ); + } + assertUnownedAssociationLazyness( + supplier, + rootEntity, + propertyName, + expectInitialized, + sqlStatementInspector + ); + } + else { + final Supplier supplier; + switch ( propertyName ) { + case "lazyOneToOneOwned": + supplier = () -> rootEntity.getLazyOneToOneOwned(); + break; + case "lazyManyToOneOwned": + supplier = () -> rootEntity.getLazyManyToOneOwned(); + break; + case "eagerOneToOneOwned": + supplier = () -> rootEntity.getEagerOneToOneOwned(); + break; + case "eagerManyToOneOwned": + supplier = () -> rootEntity.getEagerManyToOneOwned(); + break; + default: + supplier = null; + fail( "unknown association property name : " + propertyName ); + } + assertOwnedAssociationLazyness( + supplier, + propertyName, + expectInitialized, + sqlStatementInspector + ); + } + } + } + + private static void assertUnownedAssociationLazyness( + Supplier associationSupplier, + RootEntity rootEntity, + String associationName, + boolean expectInitialized, + SQLStatementInspector sqlStatementInspector) { + // for an unowned lazy association the value is null and accessing the association triggers its initialization + assertThat( Hibernate.isPropertyInitialized( rootEntity, associationName ) ) + .as( associationName + " association expected to be initialized ? expected is :" + expectInitialized + " but it's not " ) + .isEqualTo( expectInitialized ); + if ( !expectInitialized ) { + var containedEntity = associationSupplier.get(); + sqlStatementInspector.assertExecutedCount( 1 ); + assertThat( Hibernate.isInitialized( containedEntity ) ); + sqlStatementInspector.clear(); + + assertThat( containedEntity ).isNotNull(); + associationSupplier.get().getName(); + sqlStatementInspector.assertExecutedCount( 0 ); + } + } + + private static void assertOwnedAssociationLazyness( + Supplier associationSupplier, + String associationName, + boolean expectInitialized, + SQLStatementInspector sqlStatementInspector) { + // for an owned lazy association the value is an enhanced proxy, Hibernate.isPropertyInitialized( rootEntity, "lazyManyToOneOwned" ) returns true. + // accessing the association does not trigger its initialization + assertThat( Hibernate.isInitialized( associationSupplier.get() ) ) + .as( associationName + " association expected to be initialized ? expected is :" + expectInitialized + " but it's not " ) + .isEqualTo( expectInitialized ); + if ( !expectInitialized ) { + var containedEntity = associationSupplier.get(); + sqlStatementInspector.assertExecutedCount( 0 ); + + containedEntity.getName(); + sqlStatementInspector.assertExecutedCount( 1 ); + assertThat( Hibernate.isInitialized( containedEntity ) ).isTrue(); + sqlStatementInspector.clear(); + + assertThat( containedEntity ).isNotNull(); + associationSupplier.get().getName(); + sqlStatementInspector.assertExecutedCount( 0 ); + } + } + + @Entity(name = "RootEntity") + static class RootEntity { + + public static final Set LAZY_PROPERTY_NAMES = Set.of( + "lazyOneToOneOwned", "lazyManyToOneOwned", "lazyOneToOneUnowned" + ); + + public static final Set EAGER_PROPERTY_NAMES = Set.of( + "eagerOneToOneOwned", "eagerManyToOneOwned", "eagerOneToOneUnowned" + + ); + + @Id + private Long id; + + private String name; + + @OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST) + private ContainedEntity lazyOneToOneOwned; + + @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST) + private ContainedEntity lazyManyToOneOwned; + + @OneToOne(fetch = FetchType.LAZY, mappedBy = "inverseSideOfLazyOneToOneUnowned") + private ContainedEntity lazyOneToOneUnowned; + + @OneToOne(fetch = FetchType.EAGER, cascade = CascadeType.PERSIST) + private ContainedEntity eagerOneToOneOwned; + + @ManyToOne(fetch = FetchType.EAGER, cascade = CascadeType.PERSIST) + private ContainedEntity eagerManyToOneOwned; + + @OneToOne(fetch = FetchType.EAGER, mappedBy = "inverseSideOfEagerOneToOneUnowned") + private ContainedEntity eagerOneToOneUnowned; + + public RootEntity() { + } + + public RootEntity(Long id) { + this.id = id; + } + + @Override + public String toString() { + return "RootEntity#" + id; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public ContainedEntity getLazyOneToOneOwned() { + return lazyOneToOneOwned; + } + + public void setLazyOneToOneOwned(ContainedEntity lazyOneToOneOwned) { + this.lazyOneToOneOwned = lazyOneToOneOwned; + } + + public ContainedEntity getLazyManyToOneOwned() { + return lazyManyToOneOwned; + } + + public void setLazyManyToOneOwned(ContainedEntity lazyManyToOneOwned) { + this.lazyManyToOneOwned = lazyManyToOneOwned; + } + + public ContainedEntity getEagerOneToOneOwned() { + return eagerOneToOneOwned; + } + + public void setEagerOneToOneOwned(ContainedEntity eagerOneToOneOwned) { + this.eagerOneToOneOwned = eagerOneToOneOwned; + } + + public ContainedEntity getEagerManyToOneOwned() { + return eagerManyToOneOwned; + } + + public void setEagerManyToOneOwned(ContainedEntity eagerManyToOneOwned) { + this.eagerManyToOneOwned = eagerManyToOneOwned; + } + + public ContainedEntity getLazyOneToOneUnowned() { + return lazyOneToOneUnowned; + } + + public void setLazyOneToOneUnowned(ContainedEntity lazyOneToOneUnowned) { + this.lazyOneToOneUnowned = lazyOneToOneUnowned; + } + + public ContainedEntity getEagerOneToOneUnowned() { + return eagerOneToOneUnowned; + } + + public void setEagerOneToOneUnowned(ContainedEntity eagerOneToOneUnowned) { + this.eagerOneToOneUnowned = eagerOneToOneUnowned; + } + } + + @Entity(name = "ContainedEntity") + static class ContainedEntity { + + @Id + private Long id; + + private String name; + + @OneToOne(fetch = FetchType.LAZY) + private RootEntity inverseSideOfLazyOneToOneUnowned; + + @OneToOne(fetch = FetchType.LAZY) + private RootEntity inverseSideOfEagerOneToOneUnowned; + + public ContainedEntity() { + } + + public ContainedEntity(Long id) { + this.id = id; + this.name = "Name #" + id; + } + + @Override + public String toString() { + return "ContainedEntity#" + id; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public RootEntity getInverseSideOfLazyOneToOneUnowned() { + return inverseSideOfLazyOneToOneUnowned; + } + + public void setInverseSideOfLazyOneToOneUnowned(RootEntity inverseSideOfLazyOneToOneUnowned) { + this.inverseSideOfLazyOneToOneUnowned = inverseSideOfLazyOneToOneUnowned; + } + + public RootEntity getInverseSideOfEagerOneToOneUnowned() { + return inverseSideOfEagerOneToOneUnowned; + } + + public void setInverseSideOfEagerOneToOneUnowned(RootEntity inverseSideOfEagerOneToOneUnowned) { + this.inverseSideOfEagerOneToOneUnowned = inverseSideOfEagerOneToOneUnowned; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/graph/LoadAndFetchGraphCollectionsNotExplicitlySpecifiedTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/graph/LoadAndFetchGraphCollectionsNotExplicitlySpecifiedTest.java new file mode 100644 index 000000000000..1c8acb026ab0 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/graph/LoadAndFetchGraphCollectionsNotExplicitlySpecifiedTest.java @@ -0,0 +1,235 @@ +package org.hibernate.orm.test.bytecode.enhancement.graph; + +import java.util.ArrayList; +import java.util.List; + +import org.hibernate.Hibernate; +import org.hibernate.graph.GraphSemantic; + +import org.hibernate.testing.bytecode.enhancement.EnhancementOptions; +import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToMany; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel( + annotatedClasses = { + LoadAndFetchGraphCollectionsNotExplicitlySpecifiedTest.RootEntity.class, + LoadAndFetchGraphCollectionsNotExplicitlySpecifiedTest.ContainedEntity.class + } +) +@SessionFactory +@JiraKey("HHH-18489") +@BytecodeEnhanced(runNotEnhancedAsWell = true) +@EnhancementOptions(lazyLoading = true) +public class LoadAndFetchGraphCollectionsNotExplicitlySpecifiedTest { + + @BeforeEach + void init(SessionFactoryScope scope) { + scope.inTransaction( session -> { + for ( long i = 0; i < 3; ++i ) { + var root = new RootEntity( i * 100 ); + long j = i * 100; + session.persist( root ); + + var contained = new ContainedEntity( ++j ); + session.persist( contained ); + + root.addEagerContainedEntity( contained ); + + var contained2 = new ContainedEntity( ++j ); + session.persist( contained2 ); + + root.addLazyContainedEntity( contained2 ); + } + } ); + } + + @AfterEach + void cleanUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + session.createMutationQuery( "delete ContainedEntity" ).executeUpdate(); + session.createMutationQuery( "delete RootEntity" ).executeUpdate(); + } ); + } + + @Test + void queryWithFetchGraph(SessionFactoryScope scope) { + scope.inTransaction( session -> { + var query = session.createQuery( "select e from RootEntity e where id in (:ids)", RootEntity.class ) + .setFetchSize( 100 ) + // Selecting multiple entities to make sure we don't have side effects (e.g. some context shared across entity instances) + .setParameter( "ids", List.of( 0L, 100L, 200L ) ); + + var graph = session.createEntityGraph( RootEntity.class ); + graph.addAttributeNode( "lazyContainedEntities" ); + query.applyGraph( graph, GraphSemantic.FETCH ); + + var resultList = query.list(); + assertThat( resultList ).isNotEmpty(); + for ( var rootEntity : resultList ) { + // GraphSemantic.FETCH, so eagerContainedEntities is lazy because it's not present in the EntityGraph + assertThat( Hibernate.isInitialized( rootEntity.getEagerContainedEntities() )).isFalse(); + assertThat( Hibernate.isInitialized( rootEntity.getLazyContainedEntities() ) ).isTrue(); + } + } ); + } + + @Test + void queryWithLoadGraph(SessionFactoryScope scope) { + scope.inTransaction( session -> { + var query = session.createQuery( "select e from RootEntity e where id in (:ids)", RootEntity.class ) + .setFetchSize( 100 ) + // Selecting multiple entities to make sure we don't have side effects (e.g. some context shared across entity instances) + .setParameter( "ids", List.of( 0L, 100L, 200L ) ); + + var graph = session.createEntityGraph( RootEntity.class ); + graph.addAttributeNode( "lazyContainedEntities" ); + query.applyGraph( graph, GraphSemantic.LOAD ); + + var resultList = query.list(); + assertThat( resultList ).isNotEmpty(); + for ( var rootEntity : resultList ) { + // GraphSemantic.LOAD, eagerContainedEntities maintains is eagerness + assertThat( Hibernate.isInitialized( rootEntity.getEagerContainedEntities() )).isTrue(); + assertThat( Hibernate.isInitialized( rootEntity.getLazyContainedEntities() ) ).isTrue(); + } + } ); + } + + @Test + void queryWithNoEntityGraph(SessionFactoryScope scope) { + + scope.inTransaction( session -> { + var query = session.createQuery( "select e from RootEntity e where id in (:ids)", RootEntity.class ) + .setFetchSize( 100 ) + // Selecting multiple entities to make sure we don't have side effects (e.g. some context shared across entity instances) + .setParameter( "ids", List.of( 0L, 100L, 200L ) ); + + var resultList = query.list(); + assertThat( resultList ).isNotEmpty(); + for ( var rootEntity : resultList ) { + assertThat( Hibernate.isInitialized( rootEntity.getEagerContainedEntities() ) ).isTrue(); + assertThat( Hibernate.isInitialized( rootEntity.getLazyContainedEntities() ) ).isFalse(); + } + } ); + } + + @Entity(name = "RootEntity") + static class RootEntity { + + @Id + private Long id; + + private String name; + + @OneToMany(fetch = FetchType.EAGER) + @JoinColumn(name = "eager_id") + private List eagerContainedEntities; + + @OneToMany(fetch = FetchType.LAZY) + @JoinColumn(name = "lazy_id") + private List lazyContainedEntities; + + public RootEntity() { + } + + public RootEntity(Long id) { + this.id = id; + this.name = "Name #" + id; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public List getEagerContainedEntities() { + return eagerContainedEntities; + } + + public void setEagerContainedEntities(List eagerContainedEntities) { + this.eagerContainedEntities = eagerContainedEntities; + } + + public List getLazyContainedEntities() { + return lazyContainedEntities; + } + + public void setLazyContainedEntities(List lazyContainedEntities) { + this.lazyContainedEntities = lazyContainedEntities; + } + + public void addEagerContainedEntity(ContainedEntity containedEntity) { + if ( eagerContainedEntities == null ) { + eagerContainedEntities = new ArrayList<>(); + } + eagerContainedEntities.add( containedEntity ); + } + + public void addLazyContainedEntity(ContainedEntity containedEntity) { + if ( lazyContainedEntities == null ) { + lazyContainedEntities = new ArrayList<>(); + } + lazyContainedEntities.add( containedEntity ); + } + + @Override + public String toString() { + return "RootEntity#" + id; + } + } + + @Entity(name = "ContainedEntity") + static class ContainedEntity { + + @Id + private Long id; + + private String name; + + public ContainedEntity() { + } + + public ContainedEntity(Long id) { + this.id = id; + this.name = "Name #" + id; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String toString() { + return "ContainedEntity#" + id; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/lazy/LazyLoadingAndParameterizedInheritanceTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/lazy/LazyLoadingAndParameterizedInheritanceTest.java index 35523f23cc27..e4b2f01db285 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/lazy/LazyLoadingAndParameterizedInheritanceTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/lazy/LazyLoadingAndParameterizedInheritanceTest.java @@ -32,6 +32,7 @@ import java.util.Set; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatNoException; @DomainModel( annotatedClasses = { @@ -103,6 +104,14 @@ public void test(SessionFactoryScope scope) { } ); } + @Test + public void testCollectionWrite() { + Three three = new Three(); + Two two = new Two(); + assertThatNoException().isThrownBy(() -> three.setTwos(Set.of(two))); + assertThat(two.getThree()).isSameAs(three); + } + @Entity(name = "One") public static class One extends AbsOne { } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/lazy/LazyLoadingByEnhancerSetterTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/lazy/LazyLoadingByEnhancerSetterTest.java index c490d8068c9f..d98dd6c865f1 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/lazy/LazyLoadingByEnhancerSetterTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/lazy/LazyLoadingByEnhancerSetterTest.java @@ -10,7 +10,6 @@ import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced; import org.hibernate.testing.orm.junit.DomainModel; -import org.hibernate.testing.orm.junit.FailureExpected; import org.hibernate.testing.orm.junit.JiraKey; import org.hibernate.testing.orm.junit.ServiceRegistry; import org.hibernate.testing.orm.junit.SessionFactory; @@ -79,7 +78,7 @@ public void testField(SessionFactoryScope scope) { } @Test - @FailureExpected( jiraKey = "HHH-10747" ) + // failure doesn't occur with HHH-16572 change @FailureExpected( jiraKey = "HHH-10747" ) public void testProperty(SessionFactoryScope scope) { scope.inTransaction( s -> { ItemProperty input = new ItemProperty(); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/lazy/proxy/HHH20253Test.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/lazy/proxy/HHH20253Test.java new file mode 100644 index 000000000000..f42a68b0bdea --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/lazy/proxy/HHH20253Test.java @@ -0,0 +1,206 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.orm.test.bytecode.enhancement.lazy.proxy; + +import jakarta.persistence.*; +import org.hibernate.testing.bytecode.enhancement.EnhancementOptions; +import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced; +import org.hibernate.testing.orm.junit.*; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static jakarta.persistence.CascadeType.ALL; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hibernate.graph.GraphSemantic.FETCH; + +/** + * @author Wander Winkelhorst + */ + +@JiraKey( "HHH-20253" ) +@DomainModel( + annotatedClasses = { + HHH20253Test.CompanyEntity.class, + HHH20253Test.CompanyEmails.class, + HHH20253Test.Item.class + } +) +@ServiceRegistry +@SessionFactory +@BytecodeEnhanced +@EnhancementOptions( lazyLoading = true ) +class HHH20253Test { + + @Test + void testWithFetchGraph(SessionFactoryScope scope) { + scope.inTransaction( session -> { + var sqlStatementInspector = scope.getCollectingStatementInspector(); + sqlStatementInspector.clear(); + var query = session.createQuery( "select e from Item e", Item.class ) + .setFetchSize( 10 ); + + var graph = session.createEntityGraph( Item.class ); + graph.addAttributeNode("company"); + query.applyGraph( graph, FETCH ); + + var resultList = query.list(); + assertThat( resultList ).isNotEmpty(); + } ); + } + + @BeforeEach + void prepareTestData(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + + CompanyEntity company1 = new CompanyEntity(); + company1.setCompanyEmails( new CompanyEmails( company1 ) ); + session.persist( company1 ); + + CompanyEntity company2 = new CompanyEntity(); + company2.setCompanyEmails( new CompanyEmails( company2 ) ); + company2.setParent( company1 ); + session.persist( company2 ); + + Item item1 = new Item(); + item1.setCompany( company2 ); + session.persist( item1 ); + + Item item2 = new Item(); + item2.setCompany( company1 ); + session.persist( item2 ); + + session.flush(); + session.clear(); + } + ); + } + + @AfterEach + public void tearDown(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + session.createMutationQuery( "delete from Item" ).executeUpdate(); + } + ); + } + + @Entity + static class CompanyEntity { + + @Id + @GeneratedValue + @Column(name = "id", nullable = false) + private Long id; + + @OneToOne(mappedBy = "company", cascade = ALL, orphanRemoval = true) + private CompanyEmails companyEmails; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "parent_company_id") + private CompanyEntity parent; + + public CompanyEntity getParent() { + return parent; + } + + public void setParent(CompanyEntity parent) { + this.parent = parent; + } + + @Override + public String toString() { + return "RootEntity#" + id; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public void setCompanyEmails(CompanyEmails companyEmails) { + this.companyEmails = companyEmails; + } + } + + @Entity + static class CompanyEmails { + + @Id + @GeneratedValue + @Column(name = "id", nullable = false) + private Long id; + + @OneToOne + @JoinColumn(name = "company_id") + private CompanyEntity company; + + public CompanyEmails() { + } + + public CompanyEmails(CompanyEntity company) { + this.company = company; + } + + public CompanyEntity getCompany() { + return company; + } + + public void setCompany(CompanyEntity company) { + this.company = company; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + } + + @Entity(name = "Item") + static class Item { + + @Id + @GeneratedValue + private Long id; + + private String name; + + @ManyToOne(fetch = FetchType.LAZY) + private CompanyEntity company; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public CompanyEntity getCompany() { + return company; + } + + public void setCompany(CompanyEntity company) { + this.company = company; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/AncestorEntity.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/AncestorEntity.java new file mode 100644 index 000000000000..ab7cdffb8a4d --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/AncestorEntity.java @@ -0,0 +1,35 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.bytecode.enhancement.optimizer; + +import jakarta.persistence.Entity; +import jakarta.persistence.Inheritance; +import jakarta.persistence.InheritanceType; +import org.hibernate.orm.test.bytecode.enhancement.optimizer.parent.Ancestor; + +@Entity(name = "AncestorEntity") +@Inheritance(strategy = InheritanceType.JOINED) +public class AncestorEntity extends Ancestor { + + private Long id; + + private String field; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getField() { + return field; + } + + public void setField(String field) { + this.field = field; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/ChildEntity3.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/ChildEntity3.java new file mode 100644 index 000000000000..100191b43b98 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/ChildEntity3.java @@ -0,0 +1,20 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.bytecode.enhancement.optimizer; + +import jakarta.persistence.Entity; + +@Entity(name = "ChildEntity3") +public class ChildEntity3 extends AncestorEntity { + private String childField; + + public String getChildField() { + return childField; + } + + public void setChieldField(String childField) { + this.childField = childField; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/ChildEntity4.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/ChildEntity4.java new file mode 100644 index 000000000000..a8ed90f3ef5b --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/ChildEntity4.java @@ -0,0 +1,20 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.bytecode.enhancement.optimizer; + +import jakarta.persistence.Entity; + +@Entity(name = "ChildEntity4") +public class ChildEntity4 extends AncestorEntity { + private String childField; + + public String getChildField() { + return childField; + } + + public void setChieldField(String childField) { + this.childField = childField; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/ChildEntity5.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/ChildEntity5.java new file mode 100644 index 000000000000..ba7540fec1fa --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/ChildEntity5.java @@ -0,0 +1,20 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.bytecode.enhancement.optimizer; + +import jakarta.persistence.Entity; + +@Entity(name = "ChildEntity5") +public class ChildEntity5 extends AncestorEntity { + private String childField; + + public String getChildField() { + return childField; + } + + public void setChieldField(String childField) { + this.childField = childField; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/ChildEntity6.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/ChildEntity6.java new file mode 100644 index 000000000000..a996eb9089ed --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/ChildEntity6.java @@ -0,0 +1,20 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.bytecode.enhancement.optimizer; + +import jakarta.persistence.Entity; + +@Entity(name = "ChildEntity6") +public class ChildEntity6 extends AncestorEntity { + private String childField; + + public String getChildField() { + return childField; + } + + public void setChieldField(String childField) { + this.childField = childField; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/ChildEntity7.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/ChildEntity7.java new file mode 100644 index 000000000000..9a9775a53cd3 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/ChildEntity7.java @@ -0,0 +1,20 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.bytecode.enhancement.optimizer; + +import jakarta.persistence.Entity; + +@Entity(name = "ChildEntity7") +public class ChildEntity7 extends AncestorEntity { + private String childField; + + public String getChildField() { + return childField; + } + + public void setChildField(String childField) { + this.childField = childField; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/ChildEntity8.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/ChildEntity8.java new file mode 100644 index 000000000000..f6bdd29a2a29 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/ChildEntity8.java @@ -0,0 +1,20 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.bytecode.enhancement.optimizer; + +import jakarta.persistence.Entity; + +@Entity(name = "ChildEntity8") +public class ChildEntity8 extends AncestorEntity { + private String childField; + + public String getChildField() { + return childField; + } + + public void setChildField(String childField) { + this.childField = childField; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/ChildEntity9.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/ChildEntity9.java new file mode 100644 index 000000000000..b734abdd91ee --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/ChildEntity9.java @@ -0,0 +1,20 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.bytecode.enhancement.optimizer; + +import jakarta.persistence.Entity; + +@Entity(name = "ChildEntity9") +public class ChildEntity9 extends AncestorEntity { + private String childField; + + public String getChildField() { + return childField; + } + + public void setChildField(String childField) { + this.childField = childField; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/HierarchyBytecodeOptimizerMethodVisibilityTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/HierarchyBytecodeOptimizerMethodVisibilityTest.java new file mode 100644 index 000000000000..e6e9dd57eed4 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/HierarchyBytecodeOptimizerMethodVisibilityTest.java @@ -0,0 +1,68 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.bytecode.enhancement.optimizer; + +import org.hibernate.orm.test.bytecode.enhancement.optimizer.child.ChildEntity10; +import org.hibernate.orm.test.bytecode.enhancement.optimizer.parent.Ancestor; +import org.hibernate.orm.test.bytecode.enhancement.optimizer.parent.ChildEntity2; +import org.hibernate.query.Query; +import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel(annotatedClasses = { + Ancestor.class, + AncestorEntity.class, + ChildEntity2.class, + ChildEntity10.class +}) +@SessionFactory +@Jira("https://hibernate.atlassian.net/browse/HHH-19372") +@BytecodeEnhanced +public class HierarchyBytecodeOptimizerMethodVisibilityTest { + + @Test + public void testOptimizerSetPropertyValues(SessionFactoryScope scope) { + ChildEntity2 childEntity2 = new ChildEntity2(); + childEntity2.setId( 1L ); + childEntity2.setField( "field" ); + childEntity2.setChieldField( "childField" ); + + ChildEntity10 childEntity10 = new ChildEntity10(); + childEntity10.setId( 3L ); + childEntity10.setField( "field10" ); + childEntity10.setChieldField( "childField3" ); + + scope.inTransaction( session -> { + session.persist( childEntity2 ); + session.persist( childEntity10 ); + } ); + + scope.inTransaction( session -> { + Query query = session.createQuery( "select c from ChildEntity2 c where c.field = :field", + ChildEntity2.class ); + query.setParameter( "field", "field" ); + assertThat( query.uniqueResult() ).isNotNull(); + } ); + + scope.inTransaction( session -> { + Query query = session.createQuery( "select c from ChildEntity10 c where c.field = :field", + ChildEntity10.class ); + query.setParameter( "field", "field10" ); + assertThat( query.uniqueResult() ).isNotNull(); + } ); + } + + @AfterAll + public void cleanup(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/HierarchyBytecodeOptimizerOrderingTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/HierarchyBytecodeOptimizerOrderingTest.java new file mode 100644 index 000000000000..1493c992c53e --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/HierarchyBytecodeOptimizerOrderingTest.java @@ -0,0 +1,115 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.bytecode.enhancement.optimizer; + +import org.hibernate.orm.test.bytecode.enhancement.optimizer.parent.Ancestor; +import org.hibernate.query.Query; +import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel(annotatedClasses = { + Ancestor.class, + ParentEntity.class, + ChildEntity3.class, + ChildEntity4.class, + ChildEntity5.class, + ChildEntity6.class, + ChildEntity7.class, + ChildEntity8.class, + ChildEntity9.class +}) +@SessionFactory +@Jira("https://hibernate.atlassian.net/browse/HHH-19369") +@BytecodeEnhanced +public class HierarchyBytecodeOptimizerOrderingTest { + + @Test + public void testOptimizerSetPropertyValues(SessionFactoryScope scope) { + ChildEntity3 childEntity3 = new ChildEntity3(); + childEntity3.setId( 3L ); + childEntity3.setName( "child3" ); + childEntity3.setField( "field3" ); + childEntity3.setChieldField( "childField3" ); + + ChildEntity4 childEntity4 = new ChildEntity4(); + childEntity4.setId( 4L ); + childEntity4.setName( "child4" ); + childEntity4.setField( "field4" ); + childEntity4.setChieldField( "childField4" ); + + ChildEntity5 childEntity5 = new ChildEntity5(); + childEntity5.setId( 5L ); + childEntity5.setName( "child5" ); + childEntity5.setField( "field5" ); + childEntity5.setChieldField( "childField5" ); + + ChildEntity6 childEntity6 = new ChildEntity6(); + childEntity6.setId( 6L ); + childEntity6.setName( "child6" ); + childEntity6.setField( "field6" ); + childEntity6.setChieldField( "childField6" ); + + ChildEntity7 childEntity7 = new ChildEntity7(); + childEntity7.setId( 7L ); + childEntity7.setName( "child7" ); + childEntity7.setField( "field7" ); + childEntity7.setChildField( "childField7" ); + + scope.inTransaction( session -> { + session.persist( childEntity3 ); + session.persist( childEntity4 ); + session.persist( childEntity5 ); + session.persist( childEntity6 ); + session.persist( childEntity7 ); + } ); + + scope.inTransaction( session -> { + Query query = session.createQuery( "select c from ChildEntity3 c where c.field = :field", + ChildEntity3.class ); + query.setParameter( "field", "field3" ); + assertThat( query.uniqueResult() ).isNotNull(); + } ); + + scope.inTransaction( session -> { + Query query = session.createQuery( "select c from ChildEntity4 c where c.field = :field", + ChildEntity4.class ); + query.setParameter( "field", "field4" ); + assertThat( query.uniqueResult() ).isNotNull(); + } ); + + scope.inTransaction( session -> { + Query query = session.createQuery( "select c from ChildEntity5 c where c.field = :field", + ChildEntity5.class ); + query.setParameter( "field", "field5" ); + assertThat( query.uniqueResult() ).isNotNull(); + } ); + + scope.inTransaction( session -> { + Query query = session.createQuery( "select c from ChildEntity6 c where c.field = :field", + ChildEntity6.class ); + query.setParameter( "field", "field6" ); + assertThat( query.uniqueResult() ).isNotNull(); + } ); + + scope.inTransaction( session -> { + Query query = session.createQuery( + "select c from ChildEntity7 c where c.field = :field", ChildEntity7.class ); + query.setParameter( "field", "field7" ); + assertThat( query.uniqueResult() ).isNotNull(); + } ); + } + + @AfterAll + public void cleanup(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/HierarchyBytecodeOptimizerTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/HierarchyBytecodeOptimizerTest.java new file mode 100644 index 000000000000..a90b07190ef3 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/HierarchyBytecodeOptimizerTest.java @@ -0,0 +1,52 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.bytecode.enhancement.optimizer; + +import org.hibernate.orm.test.bytecode.enhancement.optimizer.child.ChildEntity; +import org.hibernate.query.Query; +import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel(annotatedClasses = { + ParentEntity.class, + ChildEntity.class, +}) +@SessionFactory +@Jira("https://hibernate.atlassian.net/browse/HHH-19372") +@BytecodeEnhanced +public class HierarchyBytecodeOptimizerTest { + + @Test + public void testOptimizerSetPropertyValues(SessionFactoryScope scope) { + ChildEntity childEntity = new ChildEntity(); + childEntity.setId( 1L ); + childEntity.setField( "field" ); + childEntity.setChieldField( "childField" ); + + scope.inTransaction( session -> { + session.persist( childEntity ); + } ); + + scope.inTransaction( session -> { + Query query = session.createQuery( "select c from ChildEntity c where c.field = :field", + ChildEntity.class ); + query.setParameter( "field", "field" ); + assertThat( query.uniqueResult() ).isNotNull(); + } ); + + } + + @AfterAll + public void cleanup(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/ParentEntity.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/ParentEntity.java new file mode 100644 index 000000000000..ee933b160ed0 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/ParentEntity.java @@ -0,0 +1,36 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.bytecode.enhancement.optimizer; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Inheritance; +import jakarta.persistence.InheritanceType; + +@Entity(name = "ParentEntity") +@Inheritance(strategy = InheritanceType.JOINED) +public class ParentEntity { + + @Id + private Long id; + + private String field; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getField() { + return field; + } + + public void setField(String field) { + this.field = field; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/child/ChildEntity.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/child/ChildEntity.java new file mode 100644 index 000000000000..d189fad30c9b --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/child/ChildEntity.java @@ -0,0 +1,22 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.bytecode.enhancement.optimizer.child; + +import org.hibernate.orm.test.bytecode.enhancement.optimizer.ParentEntity; + +import jakarta.persistence.Entity; + +@Entity(name = "ChildEntity") +public class ChildEntity extends ParentEntity { + private String childField; + + public String getChildField() { + return childField; + } + + public void setChieldField(String childField) { + this.childField = childField; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/child/ChildEntity10.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/child/ChildEntity10.java new file mode 100644 index 000000000000..f1a6756a8c20 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/child/ChildEntity10.java @@ -0,0 +1,21 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.bytecode.enhancement.optimizer.child; + +import jakarta.persistence.Entity; +import org.hibernate.orm.test.bytecode.enhancement.optimizer.AncestorEntity; + +@Entity(name = "ChildEntity10") +public class ChildEntity10 extends AncestorEntity { + private String childField; + + public String getChildField() { + return childField; + } + + public void setChieldField(String childField) { + this.childField = childField; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/parent/Ancestor.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/parent/Ancestor.java new file mode 100644 index 000000000000..15389f9b22fb --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/parent/Ancestor.java @@ -0,0 +1,35 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.bytecode.enhancement.optimizer.parent; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Inheritance; +import jakarta.persistence.InheritanceType; + +@Entity(name = "Parent") +@Inheritance(strategy = InheritanceType.JOINED) +public class Ancestor { + @Id + private Long id; + + private String name; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/parent/ChildEntity2.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/parent/ChildEntity2.java new file mode 100644 index 000000000000..05e4917d8c9e --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/optimizer/parent/ChildEntity2.java @@ -0,0 +1,21 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.bytecode.enhancement.optimizer.parent; + +import jakarta.persistence.Entity; +import org.hibernate.orm.test.bytecode.enhancement.optimizer.AncestorEntity; + +@Entity(name = "ChildEntity2") +public class ChildEntity2 extends AncestorEntity { + private String childField; + + public String getChildField() { + return childField; + } + + public void setChieldField(String childField) { + this.childField = childField; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/refresh/RefreshEntityWithLazyPropertyTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/refresh/RefreshEntityWithLazyPropertyTest.java new file mode 100644 index 000000000000..f0c4caa58508 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/refresh/RefreshEntityWithLazyPropertyTest.java @@ -0,0 +1,346 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.bytecode.enhancement.refresh; + +import jakarta.persistence.Basic; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import org.hibernate.annotations.Formula; +import org.hibernate.dialect.MySQLDialect; +import org.hibernate.dialect.SQLServerDialect; +import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.SkipForDialect; +import org.hibernate.testing.orm.junit.SkipForDialectGroup; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.HashSet; +import java.util.Set; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + + +@Jira("HHH-13377") +@DomainModel( + annotatedClasses = { + RefreshEntityWithLazyPropertyTest.Person.class, + RefreshEntityWithLazyPropertyTest.Course.class, + RefreshEntityWithLazyPropertyTest.Position.class} +) +@SessionFactory +@BytecodeEnhanced +@SkipForDialectGroup( + { + @SkipForDialect( dialectClass = MySQLDialect.class, matchSubTypes = true, reason = "does not support || as String concatenation"), + @SkipForDialect( dialectClass = SQLServerDialect.class, reason = "does not support || as String concatenation"), + } +) +public class RefreshEntityWithLazyPropertyTest { + + private static final Long PERSON_ID = 1L; + private static final Long ASSISTANT_PROFESSOR_POSITION_ID = 1L; + private static final Long PROFESSOR_POSITION_ID = 2L; + private static final String ASSISTANT_POSITION_DESCRIPTION = "Assistant Professor"; + private static final String POSITION_DESCRIPTION = "Professor"; + private static final String PROFESSOR_FIRST_NAME = "John"; + private static final String PROFESSOR_LAST_NAME = "Doe"; + + @BeforeEach + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + Position professorPosition = new Position( PROFESSOR_POSITION_ID, POSITION_DESCRIPTION ); + session.persist( professorPosition ); + + Position assistantProfessor = new Position( ASSISTANT_PROFESSOR_POSITION_ID, + ASSISTANT_POSITION_DESCRIPTION ); + session.persist( assistantProfessor ); + + Person person = new Person( PERSON_ID, PROFESSOR_FIRST_NAME, PROFESSOR_LAST_NAME, assistantProfessor, + professorPosition ); + session.persist( person ); + } ); + } + + @AfterEach + public void tearDown(SessionFactoryScope scope) { + scope.inTransaction( session -> { + session.createMutationQuery( "delete from Course" ).executeUpdate(); + session.createMutationQuery( "delete from Person" ).executeUpdate(); + session.createMutationQuery( "delete from Position" ).executeUpdate(); + } ); + } + + @Test + public void testRefreshOfLazyField(SessionFactoryScope scope) { + scope.inTransaction( session -> { + Person p = session.find( Person.class, PERSON_ID ); + assertThat( p.getLastName() ).isEqualTo( PROFESSOR_LAST_NAME ); + + String updatedLastName = "Johnson"; + session.createMutationQuery( "update Person p " + + "set p.lastName = :lastName " + + "where p.id = :id" + ) + .setParameter( "lastName", updatedLastName ) + .setParameter( "id", PERSON_ID ) + .executeUpdate(); + + session.refresh( p ); + assertThat( p.getLastName() ).isEqualTo( updatedLastName ); + } ); + } + + @Test + public void testRefreshOfLazyFormula(SessionFactoryScope scope) { + scope.inTransaction( session -> { + Person p = session.find( Person.class, PERSON_ID ); + assertThat( p.getFullName() ).isEqualTo( "John Doe" ); + + p.setLastName( "Johnson" ); + session.flush(); + session.refresh( p ); + assertThat( p.getFullName() ).isEqualTo( "John Johnson" ); + } ); + } + + @Test + public void testRefreshOfLazyOneToMany(SessionFactoryScope scope) { + scope.inTransaction( session -> { + Person p = session.find( Person.class, PERSON_ID ); + assertThat( p.getCourses().size() ).isEqualTo( 0 ); + + session.createMutationQuery( "insert into Course (id, title, person) values (:id, :title, :person) " ) + .setParameter( "id", 0 ) + .setParameter( "title", "Book Title" ) + .setParameter( "person", p ) + .executeUpdate(); + + session.refresh( p ); + assertThat( p.getCourses().size() ).isEqualTo( 1 ); + } ); + } + + @Test + public void testRefreshOfLazyManyToOne(SessionFactoryScope scope) { + scope.inTransaction( session -> { + Person p = session.find( Person.class, PERSON_ID ); + assertThat( p.getPosition().id ).isEqualTo( ASSISTANT_PROFESSOR_POSITION_ID ); + + Position professorPosition = session.find( Position.class, PROFESSOR_POSITION_ID ); + + session.createMutationQuery( + "update Person p " + + "set p.position = :position " + + "where p.id = :personId " + ) + .setParameter( "position", professorPosition ) + .setParameter( "personId", p.getId() ) + .executeUpdate(); + + session.refresh( p ); + assertThat( p.getPosition().id ).isEqualTo( PROFESSOR_POSITION_ID ); + + } ); + } + + @Test + public void testRefreshOfLazyManyToOneCascadeRefresh(SessionFactoryScope scope) { + scope.inTransaction( session -> { + Person p = session.find( Person.class, PERSON_ID ); + Position position = p.getPosition(); + assertThat( position.getId() ).isEqualTo( ASSISTANT_PROFESSOR_POSITION_ID ); + assertThat( position.getDescription() ).isEqualTo( ASSISTANT_POSITION_DESCRIPTION ); + + String newAssistantProfessorDescription = "Assistant Professor 2"; + session.createMutationQuery( + "update Position " + + "set description = :description " + + "where id = :id " + ) + .setParameter( "description", newAssistantProfessorDescription ) + .setParameter( "id", ASSISTANT_PROFESSOR_POSITION_ID ) + .executeUpdate(); + + session.refresh( p ); + // the association has been refreshed because it's annotated with `cascade = CascadeType.REFRESH` + assertThat( p.getPosition().getDescription() ).isEqualTo( newAssistantProfessorDescription ); + } ); + } + + @Test + public void testRefreshOfLazyManyToOneNoCascadeRefresh(SessionFactoryScope scope) { + scope.inTransaction( session -> { + Person p = session.find( Person.class, PERSON_ID ); + Position position = p.getPreviousPosition(); + assertThat( position.getId() ).isEqualTo( PROFESSOR_POSITION_ID ); + assertThat( position.getDescription() ).isEqualTo( POSITION_DESCRIPTION ); + + String newAssistantProfessorDescription = "Assistant Professor 2"; + session.createMutationQuery( + "update Position " + + "set description = :description " + + "where id = :id " + ) + .setParameter( "description", newAssistantProfessorDescription ) + .setParameter( "id", PROFESSOR_POSITION_ID ) + .executeUpdate(); + + session.refresh( p ); + // the association has not been refreshed because it's not annotated with `cascade = CascadeType.REFRESH` + assertThat( p.getPreviousPosition().getDescription() ).isEqualTo( POSITION_DESCRIPTION ); + } ); + } + + @Entity(name = "Person") + public static class Person { + + @Id + private Long id; + + private String firstName; + + @Basic(fetch = FetchType.LAZY) + private String lastName; + + @Basic(fetch = FetchType.LAZY) + @Formula("firstName || ' ' || lastName") + private String fullName; + + @OneToMany(mappedBy = "person", fetch = FetchType.LAZY, cascade = CascadeType.REFRESH, orphanRemoval = true) + private Set courses = new HashSet<>(); + + @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.REFRESH) + private Position position; + + @ManyToOne(fetch = FetchType.LAZY) + private Position previousPosition; + + protected Person() { + } + + public Person(Long id, String firstName, String lastName, Position position, Position previousPosition) { + this.id = id; + this.firstName = firstName; + this.lastName = lastName; + this.position = position; + this.previousPosition = previousPosition; + } + + public Long getId() { + return id; + } + + public String getFirstName() { + return firstName; + } + + public void setFirstName(String firstName) { + this.firstName = firstName; + } + + public String getLastName() { + return lastName; + } + + public void setLastName(String lastName) { + this.lastName = lastName; + } + + public String getFullName() { + return fullName; + } + + public Set getCourses() { + return courses; + } + + public Position getPosition() { + return position; + } + + public Position getPreviousPosition() { + return previousPosition; + } + } + + @Entity(name = "Course") + public static class Course { + + @Id + private Long id; + + private String title; + + @ManyToOne(fetch = FetchType.LAZY) + private Person person; + + protected Course() { + } + + public Course(Long id, String title, Person person) { + this.id = id; + this.title = title; + this.person = person; + } + + public Long getId() { + return id; + } + + public String getTitle() { + return title; + } + + public Person getPerson() { + return person; + } + } + + @Entity(name = "Position") + @Table(name = "POSITION_TABLE") + public static class Position { + + @Id + private Long id; + + @Basic(fetch = FetchType.LAZY) + private String description; + + public Position() { + } + + public Position(Long id, String description) { + this.id = id; + this.description = description; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getDescription() { + return description; + } + + public void setDescription(String description) { + this.description = description; + } + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/saveupdate/SaveUpdateTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/saveupdate/SaveUpdateTest.java new file mode 100644 index 000000000000..ca76457ee138 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/enhancement/saveupdate/SaveUpdateTest.java @@ -0,0 +1,242 @@ +package org.hibernate.orm.test.bytecode.enhancement.saveupdate; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Version; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +@DomainModel( + annotatedClasses = { + SaveUpdateTest.Parent.class, + SaveUpdateTest.Child.class, + SaveUpdateTest.Owned.class, + SaveUpdateTest.Owner.class, + } +) +@SessionFactory +@BytecodeEnhanced(runNotEnhancedAsWell = true) +public class SaveUpdateTest { + + @AfterEach + public void tearDown(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + session.createQuery( "delete from Child" ).executeUpdate(); + session.createQuery( "delete from Parent" ).executeUpdate(); + } + ); + } + + @Test + @JiraKey("HHH-18713") + public void testSaveUpdate(SessionFactoryScope scope) { + Parent parent = scope.fromTransaction( + session -> { + Parent p = new Parent( "a" ); + Child child = new Child( p, "b" ); + session.persist( p ); + return p; + + } + ); + + scope.inTransaction( session -> { + Child child2 = new Child( parent, "c" ); + session.saveOrUpdate( parent ); + } ); + + scope.inTransaction( session -> { + Parent saved = session.get( Parent.class, parent.getId() ); + assertThat( saved.getChildren().size() ).isEqualTo( 2 ); + } ); + } + + @Test + @JiraKey("HHH-18614") + public void testUpdate(SessionFactoryScope scope) { + Parent parent = scope.fromTransaction( + session -> { + Parent p = new Parent( "a" ); + Child child = new Child( p, "b" ); + session.persist( p ); + return p; + + } + ); + + scope.inTransaction( session -> { + Child child2 = new Child( parent, "c" ); + session.update( parent ); + } ); + + scope.inTransaction( session -> { + Parent saved = session.get( Parent.class, parent.getId() ); + assertThat( saved.getChildren().size() ).isEqualTo( 2 ); + } ); + } + + @Test + @JiraKey("HHH-18614") + void testUpdate2(SessionFactoryScope scope) { + Long ownerId = scope.fromTransaction( session -> { + Owner owner = new Owner( "a" ); + owner.addOwned( new Owned() ); + session.persist( owner ); + return owner.getId(); + } ); + + scope.inTransaction( session -> { + Owner owner2 = new Owner( ownerId, "a" ); + owner2.addOwned( new Owned() ); + session.update( owner2 ); + session.flush(); + } ); + } + + @Test + @JiraKey("HHH-18614") + void testUpdate3(SessionFactoryScope scope) { + Long ownerId = scope.fromTransaction( session -> { + Owner owner = new Owner( ); + owner.addOwned( new Owned() ); + session.persist( owner ); + return owner.getId(); + } ); + + scope.inTransaction( session -> { + Owner owner2 = new Owner( ); + owner2.id = ownerId; + owner2.owneds.add( new Owned() ); + session.update( owner2 ); + session.flush(); + } ); + } + + @Entity(name = "Parent") + public static class Parent { + + @Id + @GeneratedValue + public Long id; + + public Long getId() { + return id; + } + + private String name; + + @Version + @Column(name = "VERSION_COLUMN") + private long version; + + public Parent() { + } + + public Parent(String name) { + this.name = name; + } + + @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL) + private Set children = new LinkedHashSet<>(); + + public Set getChildren() { + return children; + } + } + + @Entity(name = "Child") + public static class Child { + + @Id + @GeneratedValue + public Long id; + + public Long getId() { + return id; + } + + @ManyToOne + private Parent parent; + + private String name; + + public Child() { + } + + public Child(Parent parent, String name) { + this.parent = parent; + parent.children.add( this ); + this.name = name; + } + } + + @Entity(name = "Owned") + public static class Owned { + + @Id + @GeneratedValue + private Long id; + + private String name; + + public Owned() { + } + + public Owned(String name) { + this.name = name; + } + } + + @Entity(name = "Owner") + public static class Owner { + + @Id + @GeneratedValue + private Long id; + + private String name; + + @OneToMany(cascade = CascadeType.ALL) + private List owneds = new ArrayList<>(); + + public Owner() { + } + + public Owner(String name) { + this.name = name; + } + + public Owner(Long id, String name) { + this.id = id; + this.name = name; + } + + public void addOwned(Owned owned) { + owneds.add( owned ); + } + + public Long getId() { + return id; + } + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/foreignpackage/ConcreteEntity.java b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/foreignpackage/ConcreteEntity.java new file mode 100644 index 000000000000..316de17c5e2f --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/bytecode/foreignpackage/ConcreteEntity.java @@ -0,0 +1,29 @@ +package org.hibernate.orm.test.bytecode.foreignpackage; + +import org.hibernate.orm.test.bytecode.SuperclassEntity; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; + +@Entity +public class ConcreteEntity extends SuperclassEntity { + @Id + protected long id; + protected String bname; + + public long getId() { + return id; + } + + public void setId(long id) { + this.id = id; + } + + public String getBname() { + return bname; + } + + public void setBname(String bname) { + this.bname = bname; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/cache/NaturalIdTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/cache/NaturalIdTest.java new file mode 100644 index 000000000000..ad0a4cf7e353 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/cache/NaturalIdTest.java @@ -0,0 +1,98 @@ +package org.hibernate.orm.test.cache; + +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; +import org.hibernate.annotations.NaturalId; +import org.hibernate.annotations.NaturalIdCache; +import org.hibernate.cfg.AvailableSettings; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.Setting; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + + +@DomainModel(annotatedClasses = { NaturalIdTest.TestEntity.class }) +@SessionFactory +@ServiceRegistry(settings = { + @Setting(name = AvailableSettings.USE_SECOND_LEVEL_CACHE, value = "true"), + @Setting(name = AvailableSettings.USE_QUERY_CACHE, value = "true"), +}) +@JiraKey("HHH_18511") +public class NaturalIdTest { + + @Test + void testGetNaturalIdentifierSnapshot(SessionFactoryScope scope) { + final var naturalId1 = "id1"; + final var naturalId2 = "id2"; + + scope.inTransaction( session -> { + var testEntity = new TestEntity( 1l, naturalId1, naturalId2, "abc" ); + session.persist( testEntity ); + session.flush(); + + var sessionFactory = session.getFactory(); + var entityPersister = sessionFactory.getRuntimeMetamodels() + .getMappingMetamodel() + .getEntityDescriptor( TestEntity.class ); + + Object[] naturalId = (Object[]) entityPersister.getNaturalIdentifierSnapshot( 1l, session ); + assertThat( naturalId[0] ).isEqualTo( naturalId1 ); + assertThat( naturalId[1] ).isEqualTo( naturalId2 ); + } ); + } + + @Entity(name = "CompositeNaturalIdModel") + @Table(name = "CompositeNaturalIdModel") + @NaturalIdCache + @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + public static class TestEntity { + + @Id + private Long id; + + @NaturalId + private String naturalId1; + + @NaturalId + private String naturalId2; + + private String name; + + public TestEntity() { + } + + public TestEntity(Long id, String naturalId1, String naturalId2, String name) { + this.id = id; + this.naturalId1 = naturalId1; + this.naturalId2 = naturalId2; + this.name = name; + } + + public Long getId() { + return id; + } + + public String getNaturalId1() { + return naturalId1; + } + + + public String getNaturalId2() { + return naturalId2; + } + + public String getName() { + return name; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/cache/QueryCacheIncompleteTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/cache/QueryCacheIncompleteTest.java new file mode 100644 index 000000000000..77575b06d239 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/cache/QueryCacheIncompleteTest.java @@ -0,0 +1,116 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.cache; + +import org.hibernate.CacheMode; +import org.hibernate.Session; +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; +import org.hibernate.cfg.AvailableSettings; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.Setting; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel(annotatedClasses = { + QueryCacheIncompleteTest.Admin.class, +}) +@SessionFactory +@ServiceRegistry( + settings = { + @Setting(name = AvailableSettings.USE_SECOND_LEVEL_CACHE, value = "true"), + @Setting(name = AvailableSettings.USE_QUERY_CACHE, value = "true"), + @Setting(name = AvailableSettings.QUERY_CACHE_LAYOUT, value = "FULL") + } +) +@JiraKey(value = "HHH-18689") +public class QueryCacheIncompleteTest { + + private Long adminId; + + @BeforeAll + public void setUp(SessionFactoryScope scope) { + adminId = scope.fromTransaction( + session -> { + Admin admin = new Admin(); + admin.setAge( 42 ); + session.persist( admin ); + return admin.getId(); + } + ); + } + + @Test + void testQueryWithEmbeddableParameter(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + // load uninitialized proxy + session.getReference( Admin.class, adminId ); + // load entity + var multiLoader = session.byMultipleIds( Admin.class ); + multiLoader.with( CacheMode.NORMAL ); + multiLoader.multiLoad( adminId ); + + // store in query cache + Admin admin = queryAdmin( session ); + assertThat( admin.getAge() ).isEqualTo( 42 ); + } + ); + + scope.inTransaction( + session -> { + // use query cache + Admin admin = queryAdmin( session ); + assertThat( admin.getAge() ).isEqualTo( 42 ); + } + ); + } + + private Admin queryAdmin(Session s) { + return s.createQuery( "from Admin", Admin.class ).setCacheable( true ).getSingleResult(); + } + + @Entity(name = "Admin") + @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + public static class Admin { + + @Id + @GeneratedValue + private Long id; + + @Column(nullable = false) + private int age; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/cid/CompositeIdAndMergeTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/cid/CompositeIdAndMergeTest.java new file mode 100644 index 000000000000..9dc2d41e5bd2 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/cid/CompositeIdAndMergeTest.java @@ -0,0 +1,162 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.cid; + +import java.util.ArrayList; +import java.util.List; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.IdClass; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +@DomainModel( + annotatedClasses = { + CompositeIdAndMergeTest.Order.class, + CompositeIdAndMergeTest.Invoice.class, + CompositeIdAndMergeTest.LineItem.class + } +) +@SessionFactory +@JiraKey("HHH-18131") +public class CompositeIdAndMergeTest { + + @Test + public void testMerge(SessionFactoryScope scope) { + Integer lineItemIndex = 2; + Order persistedOrder = scope.fromTransaction( + session -> { + Order order = new Order( "order" ); + session.persist( order ); + + Invoice invoice = new Invoice( "invoice" ); + LineItem lineItem = new LineItem( lineItemIndex ); + invoice.addLine( lineItem ); + order.setInvoice( invoice ); + + session.merge( order ); + return order; + } + ); + + scope.inTransaction( + session -> { + Order order = session.find( Order.class, persistedOrder.getId() ); + Invoice invoice = order.getInvoice(); + assertThat( invoice ).isNotNull(); + List lines = invoice.getLines(); + assertThat( lines.size() ).isEqualTo( 1 ); + assertThat( lines.get( 0 ).getIndex() ).isEqualTo( lineItemIndex ); + } + ); + } + + @Entity(name = "Order") + @Table(name = "order_table") + public static class Order { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(nullable = false) + private String description; + + @ManyToOne(cascade = { CascadeType.ALL }) + private Invoice invoice; + + public Order() { + } + + public Order(String description) { + this.description = description; + } + + public Long getId() { + return id; + } + + public Invoice getInvoice() { + return invoice; + } + + public void setInvoice(Invoice invoice) { + this.invoice = invoice; + } + } + + @Entity(name = "Invoice") + public static class Invoice { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @Column(name = "number_column") + private String number; + + @OneToMany(mappedBy = "invoice", cascade = { CascadeType.ALL }, orphanRemoval = true) + private List lines = new ArrayList<>(); + + public Invoice() { + } + + public Invoice(String number) { + this.number = number; + } + + public void addLine(LineItem line) { + lines.add( line ); + line.invoice = this; + } + + public List getLines() { + return lines; + } + } + + @Entity + @Table(name = "invoice_lines") + @IdClass(LineItemId.class) + public static class LineItem { + @Id + @ManyToOne + private Invoice invoice; + + @Id + @Column(name = "index_column") + private Integer index; + + public LineItem() { + } + + public LineItem(Integer index) { + this.index = index; + } + + public Integer getIndex() { + return index; + } + } + + public static class LineItemId { + private Long invoice; + private Integer index; + + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/collection/basic/DetachedElementTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/collection/basic/DetachedElementTest.java new file mode 100644 index 000000000000..c65f8e0676d7 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/collection/basic/DetachedElementTest.java @@ -0,0 +1,154 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.collection.basic; + + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import org.hibernate.Hibernate; +import org.hibernate.LazyInitializationException; +import org.hibernate.dialect.H2Dialect; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.RequiresDialect; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; + + +/** + * @author Jan Schatteman + */ +@DomainModel( + annotatedClasses = {DetachedElementTest.EntityWithList.class, DetachedElementTest.ListElement.class, + DetachedElementTest.EntityWithMap.class, DetachedElementTest.MapElement.class} +) +@SessionFactory +@RequiresDialect( H2Dialect.class ) +public class DetachedElementTest { + + @Test + public void testList(SessionFactoryScope scope) { + scope.inSession( + session -> { + session.getTransaction().begin(); + EntityWithList parent = new EntityWithList(1); + ListElement element = new ListElement(2, parent); + session.persist( parent ); + session.persist( element ); + session.getTransaction().commit(); + session.clear(); + + // shouldn't throw exceptions for these operations + assertEquals( 1, parent.children.size() ); + assertFalse( parent.children.isEmpty() ); + assertTrue( parent.children.contains(element) ); + assertTrue( parent.children.remove(element) ); + + assertThrows( LazyInitializationException.class, + () -> Hibernate.get(parent.children, 0) + ); + } + ); + } + + @Test + public void testMap(SessionFactoryScope scope) { + scope.inSession( + session -> { + session.getTransaction().begin(); + EntityWithMap parent = new EntityWithMap(1); + MapElement element = new MapElement(2, parent); + session.persist(parent); + session.persist(element); + session.getTransaction().commit(); + session.clear(); + + // shouldn't throw exceptions for these operations + assertEquals( 1, parent.children.size() ); + assertFalse( parent.children.isEmpty() ); + assertTrue( parent.children.containsKey(element.id) ); + assertTrue( parent.children.containsValue(element) ); + + assertThrows( LazyInitializationException.class, + () -> Hibernate.get(parent.children, 2L) + ); + } + ); + } + + @Entity + public static class EntityWithList { + @Id + long id; + + @OneToMany(mappedBy = "parent") + List children = new ArrayList<>(); + + public EntityWithList(int id) { + this.id = id; + } + + protected EntityWithList() {} + } + + @Entity + public static class ListElement { + @Id + long id; + + @ManyToOne + private EntityWithList parent; + + public ListElement(long id, EntityWithList parent) { + this.id = id; + this.parent = parent; + parent.children.add(this); + } + + protected ListElement() {} + } + + @Entity + public static class EntityWithMap { + @Id + long id; + + @OneToMany(mappedBy = "parent") + Map children = new HashMap<>(); + + public EntityWithMap(int id) { + this.id = id; + } + + protected EntityWithMap() {} + } + + @Entity + public static class MapElement { + @Id + long id; + + @ManyToOne + private EntityWithMap parent; + + public MapElement(long id, EntityWithMap parent) { + this.id = id; + this.parent = parent; + parent.children.put(id, this); + } + + protected MapElement() {} + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/collection/dereferenced/UnversionedCascadeDereferencedCollectionTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/collection/dereferenced/UnversionedCascadeDereferencedCollectionTest.java index c84dc138a3ba..1e2e07e40129 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/collection/dereferenced/UnversionedCascadeDereferencedCollectionTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/collection/dereferenced/UnversionedCascadeDereferencedCollectionTest.java @@ -36,6 +36,7 @@ import org.hibernate.testing.orm.junit.SessionFactoryScope; import org.junit.jupiter.api.Test; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; @@ -76,12 +77,9 @@ public void testMergeNullCollection(SessionFactoryScope scope) { scope.inTransaction( session -> { - UnversionedCascadeOne one = (UnversionedCascadeOne) session.merge( unversionedCascadeOne ); + UnversionedCascadeOne one = session.merge( unversionedCascadeOne ); - // after merging, one.getManies() should still be null; - // the EntityEntry loaded state should contain a PersistentCollection though. - - assertNull( one.getManies() ); + assertThat( one.getManies().size() ).isEqualTo( 0 ); EntityEntry eeOne = getEntityEntry( session, one ); AbstractPersistentCollection maniesEEOneStateOrig = (AbstractPersistentCollection) eeOne.getLoadedValue( "manies" ); @@ -109,27 +107,7 @@ public void testMergeNullCollection(SessionFactoryScope scope) { // Ensure the same EntityEntry is being used. assertSame( eeOne, getEntityEntry( session, one ) ); - // Ensure one.getManies() is still null. - assertNull( one.getManies() ); - - // Ensure CollectionEntry for maniesEEOneStateOrig is no longer in the PersistenceContext. - assertNull( getCollectionEntry( session, maniesEEOneStateOrig ) ); - - // Ensure the original CollectionEntry has role, persister, and key set to null. - assertNull( ceManiesOrig.getRole() ); - assertNull( ceManiesOrig.getLoadedPersister() ); - assertNull( ceManiesOrig.getKey() ); - - // Ensure the PersistentCollection (that was previously returned by eeOne.getLoadedState()) - // has key and role set to null. - assertNull( maniesEEOneStateOrig.getKey() ); - assertNull( maniesEEOneStateOrig.getRole() ); - - // Ensure eeOne.getLoadedState() returns null for collection after flush. - assertNull( eeOne.getLoadedValue( "manies" ) ); - - // Ensure the session in maniesEEOneStateOrig has been unset. - assertNull( maniesEEOneStateOrig.getSession() ); + assertThat( one.getManies().size() ).isEqualTo( 0 ); } ); } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/collection/dereferenced/UnversionedNoCascadeDereferencedCollectionTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/collection/dereferenced/UnversionedNoCascadeDereferencedCollectionTest.java index 04976c981c82..0a4613f42224 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/collection/dereferenced/UnversionedNoCascadeDereferencedCollectionTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/collection/dereferenced/UnversionedNoCascadeDereferencedCollectionTest.java @@ -36,6 +36,7 @@ import org.hibernate.testing.orm.junit.SessionFactoryScope; import org.junit.jupiter.api.Test; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; @@ -74,12 +75,10 @@ public void testMergeNullCollection(SessionFactoryScope scope) { scope.inTransaction( session -> { - UnversionedNoCascadeOne one = (UnversionedNoCascadeOne) session.merge( unversionedNoCascadeOne ); + UnversionedNoCascadeOne one = session.merge( unversionedNoCascadeOne ); - // after merging, one.getManies() should still be null; - // the EntityEntry loaded state should contain a PersistentCollection though. + assertThat( one.getManies().size() ).isEqualTo( 0 ); - assertNull( one.getManies() ); EntityEntry eeOne = getEntityEntry( session, one ); AbstractPersistentCollection maniesEEOneStateOrig = (AbstractPersistentCollection) eeOne.getLoadedValue( "manies" ); @@ -107,27 +106,8 @@ public void testMergeNullCollection(SessionFactoryScope scope) { // Ensure the same EntityEntry is being used. assertSame( eeOne, getEntityEntry( session, one ) ); - // Ensure one.getManies() is still null. - assertNull( one.getManies() ); - - // Ensure CollectionEntry for maniesEEOneStateOrig is no longer in the PersistenceContext. - assertNull( getCollectionEntry( session, maniesEEOneStateOrig ) ); - - // Ensure the original CollectionEntry has role, persister, and key set to null. - assertNull( ceManiesOrig.getRole() ); - assertNull( ceManiesOrig.getLoadedPersister() ); - assertNull( ceManiesOrig.getKey() ); + assertThat( one.getManies().size() ).isEqualTo( 0 ); - // Ensure the PersistentCollection (that was previously returned by eeOne.getLoadedState()) - // has key and role set to null. - assertNull( maniesEEOneStateOrig.getKey() ); - assertNull( maniesEEOneStateOrig.getRole() ); - - // Ensure eeOne.getLoadedState() returns null for collection after flush. - assertNull( eeOne.getLoadedValue( "manies" ) ); - - // Ensure the session in maniesEEOneStateOrig has been unset. - assertNull( maniesEEOneStateOrig.getSession() ); } ); } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/collection/dereferenced/VersionedCascadeDereferencedCollectionTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/collection/dereferenced/VersionedCascadeDereferencedCollectionTest.java index afd7c628d444..2bd4742cc590 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/collection/dereferenced/VersionedCascadeDereferencedCollectionTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/collection/dereferenced/VersionedCascadeDereferencedCollectionTest.java @@ -36,6 +36,7 @@ import org.hibernate.testing.orm.junit.SessionFactoryScope; import org.junit.jupiter.api.Test; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; @@ -76,12 +77,9 @@ public void testMergeNullCollection(SessionFactoryScope scope) { scope.inTransaction( session -> { - VersionedCascadeOne one = (VersionedCascadeOne) session.merge( versionedCascadeOne ); + VersionedCascadeOne one = session.merge( versionedCascadeOne ); - // after merging, one.getManies() should still be null; - // the EntityEntry loaded state should contain a PersistentCollection though. - - assertNull( one.getManies() ); + assertThat( one.getManies().size() ).isEqualTo( 0 ); EntityEntry eeOne = getEntityEntry( session, one ); AbstractPersistentCollection maniesEEOneStateOrig = (AbstractPersistentCollection) eeOne.getLoadedValue( "manies" ); @@ -109,28 +107,7 @@ public void testMergeNullCollection(SessionFactoryScope scope) { // Ensure the same EntityEntry is being used. assertSame( eeOne, getEntityEntry( session, one ) ); - // Ensure one.getManies() is still null. - assertNull( one.getManies() ); - - // Ensure CollectionEntry for maniesEEOneStateOrig is no longer in the PersistenceContext. - assertNull( getCollectionEntry( session, maniesEEOneStateOrig ) ); - - // Ensure the original CollectionEntry has role, persister, and key set to null. - assertNull( ceManiesOrig.getRole() ); - assertNull( ceManiesOrig.getLoadedPersister() ); - assertNull( ceManiesOrig.getKey() ); - - // Ensure the PersistentCollection (that was previously returned by eeOne.getLoadedState()) - // has key and role set to null. - assertNull( maniesEEOneStateOrig.getKey() ); - assertNull( maniesEEOneStateOrig.getRole() ); - - // Ensure eeOne.getLoadedState() returns null for collection after flush. - assertNull( eeOne.getLoadedValue( "manies" ) ); - - // Ensure the session in maniesEEOneStateOrig has been unset. - assertNull( maniesEEOneStateOrig.getSession() ); - + assertThat( one.getManies().size() ).isEqualTo( 0 ); } ); } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/collection/dereferenced/VersionedNoCascadeDereferencedCollectionTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/collection/dereferenced/VersionedNoCascadeDereferencedCollectionTest.java index 034974ba8e5d..aa9b45bcd999 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/collection/dereferenced/VersionedNoCascadeDereferencedCollectionTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/collection/dereferenced/VersionedNoCascadeDereferencedCollectionTest.java @@ -36,6 +36,7 @@ import org.hibernate.testing.orm.junit.SessionFactoryScope; import org.junit.jupiter.api.Test; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; @@ -76,12 +77,9 @@ public void testMergeNullCollection(SessionFactoryScope scope) { scope.inTransaction( session -> { - VersionedNoCascadeOne one = (VersionedNoCascadeOne) session.merge( versionedNoCascadeOne ); + VersionedNoCascadeOne one = session.merge( versionedNoCascadeOne ); - // after merging, one.getManies() should still be null; - // the EntityEntry loaded state should contain a PersistentCollection though. - - assertNull( one.getManies() ); + assertThat( one.getManies().size() ).isEqualTo( 0 ); EntityEntry eeOne = getEntityEntry( session, one ); AbstractPersistentCollection maniesEEOneStateOrig = (AbstractPersistentCollection) eeOne.getLoadedValue( "manies" ); @@ -109,27 +107,7 @@ public void testMergeNullCollection(SessionFactoryScope scope) { // Ensure the same EntityEntry is being used. assertSame( eeOne, getEntityEntry( session, one ) ); - // Ensure one.getManies() is still null. - assertNull( one.getManies() ); - - // Ensure CollectionEntry for maniesEEOneStateOrig is no longer in the PersistenceContext. - assertNull( getCollectionEntry( session, maniesEEOneStateOrig ) ); - - // Ensure the original CollectionEntry has role, persister, and key set to null. - assertNull( ceManiesOrig.getRole() ); - assertNull( ceManiesOrig.getLoadedPersister() ); - assertNull( ceManiesOrig.getKey() ); - - // Ensure the PersistentCollection (that was previously returned by eeOne.getLoadedState()) - // has key and role set to null. - assertNull( maniesEEOneStateOrig.getKey() ); - assertNull( maniesEEOneStateOrig.getRole() ); - - // Ensure eeOne.getLoadedState() returns null for collection after flush. - assertNull( eeOne.getLoadedValue( "manies" ) ); - - // Ensure the session in maniesEEOneStateOrig has been unset. - assertNull( maniesEEOneStateOrig.getSession() ); + assertThat( one.getManies().size() ).isEqualTo( 0 ); } ); } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/collection/multisession/MultipleSessionCollectionTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/collection/multisession/MultipleSessionCollectionTest.java index 0587129d6107..cdf6f14bb2a2 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/collection/multisession/MultipleSessionCollectionTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/collection/multisession/MultipleSessionCollectionTest.java @@ -37,12 +37,15 @@ import org.hibernate.HibernateException; import org.hibernate.collection.spi.AbstractPersistentCollection; import org.hibernate.collection.spi.PersistentCollection; +import org.hibernate.dialect.CockroachDialect; +import org.hibernate.dialect.HSQLDialect; import org.hibernate.engine.spi.CollectionEntry; import org.hibernate.testing.TestForIssue; import org.hibernate.testing.orm.junit.DomainModel; import org.hibernate.testing.orm.junit.SessionFactory; import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.SkipForDialect; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -284,6 +287,8 @@ public void testCopyPersistentCollectionReferenceBeforeFlush(SessionFactoryScope @Test @TestForIssue(jiraKey = "HHH-9518") + @SkipForDialect(dialectClass = HSQLDialect.class, reason = "The select triggered by the merge just hang without any exception") + @SkipForDialect(dialectClass = CockroachDialect.class, reason = "The merge in the second session causes a deadlock") public void testCopyPersistentCollectionReferenceAfterFlush(SessionFactoryScope scope) { Parent p = new Parent(); Child c = new Child(); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/columndiscriminator/Author.java b/hibernate-core/src/test/java/org/hibernate/orm/test/columndiscriminator/Author.java new file mode 100644 index 000000000000..a42591cad3e7 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/columndiscriminator/Author.java @@ -0,0 +1,45 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.columndiscriminator; + +import java.util.*; + +public class Author { + private Long id; + private String name; + private String email; + private List books = new ArrayList<>(); + + public Author(String name, String email) { + this.name = name; + this.email = email; + } + + protected Author() { + // default + } + + public Long id() { + return id; + } + + public String name() { + return name; + } + + public String email() { + return email; + } + + public List books() { + return books; + } + + public void addBook(Book book) { + books.add(book); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/columndiscriminator/Book.java b/hibernate-core/src/test/java/org/hibernate/orm/test/columndiscriminator/Book.java new file mode 100644 index 000000000000..ca16510368e3 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/columndiscriminator/Book.java @@ -0,0 +1,34 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.columndiscriminator; + +public class Book { + private Long id; + private String title; + private BookDetails details; + + public Book(String title, BookDetails details) { + this.title = title; + this.details = details; + } + + protected Book() { + // default + } + + public Long id() { + return id; + } + + public String title() { + return title; + } + + public BookDetails details() { + return details; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/columndiscriminator/BookDetails.java b/hibernate-core/src/test/java/org/hibernate/orm/test/columndiscriminator/BookDetails.java new file mode 100644 index 000000000000..77f8b389c71f --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/columndiscriminator/BookDetails.java @@ -0,0 +1,23 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.columndiscriminator; + +public abstract class BookDetails { + private String information; + + protected BookDetails(String information) { + this.information = information; + } + + protected BookDetails() { + // default + } + + public String information() { + return information; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/columndiscriminator/BoringBookDetails.java b/hibernate-core/src/test/java/org/hibernate/orm/test/columndiscriminator/BoringBookDetails.java new file mode 100644 index 000000000000..235dba94696b --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/columndiscriminator/BoringBookDetails.java @@ -0,0 +1,24 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.columndiscriminator; + +public class BoringBookDetails extends BookDetails { + private String boringInformation; + + public BoringBookDetails(String information, String boringInformation) { + super(information); + this.boringInformation = boringInformation; + } + + public BoringBookDetails() { + // default + } + + public String boringInformation() { + return boringInformation; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/columndiscriminator/ColumnDiscrimnatorWithSchemaTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/columndiscriminator/ColumnDiscrimnatorWithSchemaTest.java new file mode 100644 index 000000000000..d8f2d3384c2b --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/columndiscriminator/ColumnDiscrimnatorWithSchemaTest.java @@ -0,0 +1,47 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.columndiscriminator; + +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.cfg.SchemaToolingSettings; +import org.hibernate.testing.orm.junit.DialectFeatureChecks.SupportSchemaCreation; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.RequiresDialectFeature; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.Setting; +import org.junit.jupiter.api.Test; + +@DomainModel(xmlMappings = "org/hibernate/orm/test/columndiscriminator/orm.xml") +@ServiceRegistry(settings = { + @Setting(name = AvailableSettings.DEFAULT_SCHEMA, value = "GREET"), + @Setting(name = SchemaToolingSettings.JAKARTA_HBM2DDL_CREATE_SCHEMAS, value = "true") +}) +@SessionFactory +@RequiresDialectFeature(feature = SupportSchemaCreation.class) +class ColumnDiscrimnatorWithSchemaTest { + + private ColumnDiscrimnatorWithSchemaTest() { + } + + static ColumnDiscrimnatorWithSchemaTest createColumnDiscrimnatorWithSchemaTest() { + return new ColumnDiscrimnatorWithSchemaTest(); + } + + @Test + void testIt(SessionFactoryScope scope) { + scope.inTransaction( entityManager -> { + var book = new Book( "The Art of Computer Programming", + new SpecialBookDetails( "Hardcover", "Computer Science" ) ); + + var author = new Author( "Donald Knuth", "dn@cs.com" ); + author.addBook( book ); + entityManager.persist( author ); + } ); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/columndiscriminator/SpecialBookDetails.java b/hibernate-core/src/test/java/org/hibernate/orm/test/columndiscriminator/SpecialBookDetails.java new file mode 100644 index 000000000000..5cc837e63ebf --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/columndiscriminator/SpecialBookDetails.java @@ -0,0 +1,24 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.columndiscriminator; + +public class SpecialBookDetails extends BookDetails { + private String specialInformation; + + public SpecialBookDetails(String information, String specialInformation) { + super(information); + this.specialInformation = specialInformation; + } + + protected SpecialBookDetails() { + // default + } + + public String specialInformation() { + return specialInformation; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/component/StructComponentManyToOneCompositeTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/component/StructComponentManyToOneCompositeTest.java new file mode 100644 index 000000000000..e5e772a2879f --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/component/StructComponentManyToOneCompositeTest.java @@ -0,0 +1,175 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.orm.test.component; + +import jakarta.persistence.Embeddable; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import org.hibernate.Hibernate; +import org.hibernate.annotations.Struct; +import org.hibernate.testing.jdbc.SharedDriverManagerTypeCacheClearingIntegrator; +import org.hibernate.testing.orm.junit.BootstrapServiceRegistry; +import org.hibernate.testing.orm.junit.DialectFeatureChecks; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.RequiresDialectFeature; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@BootstrapServiceRegistry( + // Clear the type cache, otherwise we might run into ORA-21700: object does not exist or is marked for delete + integrators = SharedDriverManagerTypeCacheClearingIntegrator.class +) +@DomainModel( + annotatedClasses = { + StructComponentManyToOneCompositeTest.Book.class + } +) +@SessionFactory +@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsStructAggregate.class) +public class StructComponentManyToOneCompositeTest { + + @BeforeEach + public void setUp(SessionFactoryScope scope){ + scope.inTransaction( + session -> { + Book book1 = new Book(); + book1.id = 1L; + book1.id2 = 1L; + book1.title = "Hibernate 3"; + book1.author = new Author( "Gavin", null ); + + session.persist( book1 ); + + Book book2 = new Book(); + book2.id = 2L; + book2.id2 = 2L; + book2.title = "Hibernate 6"; + book2.author = new Author( "Steve", book1 ); + + session.persist( book2 ); + } + ); + } + + @AfterEach + public void tearDown(SessionFactoryScope scope){ + scope.inTransaction( + session -> + session.createQuery( "delete from Book" ).executeUpdate() + ); + } + + @Test + public void testGet(SessionFactoryScope scope){ + scope.inTransaction( + session -> { + Book book = session.createQuery( "from Book b where b.id = 2", Book.class ).getSingleResult(); + assertFalse( Hibernate.isInitialized( book.author.getFavoriteBook() ) ); + assertEquals( "Gavin", book.author.getFavoriteBook().getAuthor().getName() ); + } + ); + } + + @Test + public void testJoin(SessionFactoryScope scope){ + scope.inTransaction( + session -> { + Book book = session.createQuery( + "from Book b join fetch b.author.favoriteBook where b.id = 2", + Book.class + ).getSingleResult(); + assertTrue( Hibernate.isInitialized( book.author.getFavoriteBook() ) ); + assertEquals( "Gavin", book.author.getFavoriteBook().getAuthor().getName() ); + } + ); + } + + @Entity(name = "Book") + public static class Book { + @Id + private Long id; + @Id + private Long id2; + private String title; + private Author author; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getId2() { + return id2; + } + + public void setId2(Long id2) { + this.id2 = id2; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public Author getAuthor() { + return author; + } + + public void setAuthor(Author author) { + this.author = author; + } + } + + @Embeddable + @Struct( name = "author_type") + public static class Author { + + private String name; + @ManyToOne(fetch = FetchType.LAZY) + private Book favoriteBook; + + public Author() { + } + + public Author(String name, Book favoriteBook) { + this.name = name; + this.favoriteBook = favoriteBook; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Book getFavoriteBook() { + return favoriteBook; + } + + public void setFavoriteBook(Book favoriteBook) { + this.favoriteBook = favoriteBook; + } + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/dialect/PostgreSQLDialectTestCase.java b/hibernate-core/src/test/java/org/hibernate/orm/test/dialect/PostgreSQLDialectTestCase.java index 9a32b9dfe768..2debbd50823a 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/dialect/PostgreSQLDialectTestCase.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/dialect/PostgreSQLDialectTestCase.java @@ -11,6 +11,7 @@ import java.sql.SQLException; import org.hibernate.JDBCException; +import org.hibernate.Length; import org.hibernate.LockMode; import org.hibernate.LockOptions; import org.hibernate.PessimisticLockException; @@ -29,8 +30,14 @@ import org.hibernate.mapping.Table; import org.hibernate.mapping.UniqueKey; +import org.hibernate.metamodel.mapping.internal.SqlTypedMappingImpl; +import org.hibernate.query.sqm.function.SqmFunctionRegistry; +import org.hibernate.testing.orm.junit.DialectFeatureChecks; import org.hibernate.testing.TestForIssue; import org.hibernate.testing.junit4.BaseUnitTestCase; +import org.hibernate.testing.orm.junit.JiraKey; + +import org.hibernate.type.spi.TypeConfiguration; import org.junit.Test; import org.mockito.Mockito; @@ -153,6 +160,41 @@ public void testAlterTableDropConstraintString() { assertEquals("alter table if exists table_name drop constraint if exists unique_something", sql ); } + @Test + @JiraKey( value = "HHH-18780" ) + public void testTextVsVarchar() { + PostgreSQLDialect dialect = new PostgreSQLDialect(); + + final TypeConfiguration typeConfiguration = new TypeConfiguration(); + final SqmFunctionRegistry functionRegistry = new SqmFunctionRegistry(); + typeConfiguration.scope( new DialectFeatureChecks.FakeMetadataBuildingContext( typeConfiguration, functionRegistry ) ); + final DialectFeatureChecks.FakeTypeContributions typeContributions = new DialectFeatureChecks.FakeTypeContributions( typeConfiguration ); + final DialectFeatureChecks.FakeFunctionContributions functionContributions = new DialectFeatureChecks.FakeFunctionContributions( + dialect, + typeConfiguration, + functionRegistry + ); + dialect.contribute( typeContributions, typeConfiguration.getServiceRegistry() ); + dialect.initializeFunctionRegistry( functionContributions ); + final String varcharNullString = dialect.getSelectClauseNullString( + new SqlTypedMappingImpl( typeConfiguration.getBasicTypeForJavaType( String.class ) ), + typeConfiguration + ); + final String textNullString = dialect.getSelectClauseNullString( + new SqlTypedMappingImpl( + null, + (long) Length.LONG32, + null, + null, + null, + typeConfiguration.getBasicTypeForJavaType( String.class ) + ), + typeConfiguration + ); + assertEquals("cast(null as varchar)", varcharNullString); + assertEquals("cast(null as text)", textNullString); + } + private static class MockSqlStringGenerationContext implements SqlStringGenerationContext { @Override diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/dialect/resolver/DialectFactoryTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/dialect/resolver/DialectFactoryTest.java index 65b1c8d24de7..2f648db51892 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/dialect/resolver/DialectFactoryTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/dialect/resolver/DialectFactoryTest.java @@ -156,8 +156,8 @@ public void testPreregisteredDialects() { testDetermination( "DB2/SUN", DB2Dialect.class, resolver ); testDetermination( "DB2/LINUX390", DB2Dialect.class, resolver ); testDetermination( "DB2/AIX64", DB2Dialect.class, resolver ); - testDetermination( "DB2 UDB for AS/400", DB2Dialect.class, resolver ); - testDetermination( "DB2 UDB for AS/400", 7, 3, DB2Dialect.class, resolver ); + testDetermination( "DB2 UDB for AS/400", DB2iDialect.class, resolver ); + testDetermination( "DB2 UDB for AS/400", 7, 3, DB2iDialect.class, resolver ); testDetermination( "Oracle", 8, OracleDialect.class, resolver ); testDetermination( "Oracle", 9, OracleDialect.class, resolver ); testDetermination( "Oracle", 10, OracleDialect.class, resolver ); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/dynamicmap/CustomEntityNameResolverTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/dynamicmap/CustomEntityNameResolverTest.java new file mode 100644 index 000000000000..744674c5d945 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/dynamicmap/CustomEntityNameResolverTest.java @@ -0,0 +1,58 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.dynamicmap; + +import java.util.HashMap; +import java.util.Map; + +import org.hibernate.EntityNameResolver; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.cfg.Configuration; +import org.hibernate.engine.spi.SessionFactoryImplementor; + +import org.hibernate.testing.orm.junit.Jira; +import org.junit.jupiter.api.Test; + +import static org.hibernate.testing.transaction.TransactionUtil2.inTransaction; +import static org.hibernate.tool.schema.Action.ACTION_CREATE_THEN_DROP; + +/** + * @author Marco Belladelli + */ +@Jira( "https://hibernate.atlassian.net/browse/HHH-18486" ) +public class CustomEntityNameResolverTest { + @Test + public void test() { + final Configuration configuration = new Configuration(); + configuration.setProperty( AvailableSettings.HBM2DDL_AUTO, ACTION_CREATE_THEN_DROP ); + configuration.setProperty( AvailableSettings.SHOW_SQL, Boolean.TRUE.toString() ); + configuration.setProperty( AvailableSettings.FORMAT_SQL, Boolean.TRUE.toString() ); + configuration.addResource( "org/hibernate/orm/test/dynamicmap/artist.hbm.xml" ); + configuration.addEntityNameResolver( new HibernateEntityNameResolver() ); + try (final SessionFactoryImplementor sf = (SessionFactoryImplementor) configuration.buildSessionFactory()) { + inTransaction( sf, session -> { + final Map artistEntity = new HashMap<>(); + artistEntity.put( "id", 1 ); + artistEntity.put( "firstname", "John" ); + artistEntity.put( "lastname", "Doe" ); + // Persist the dynamic map entity + session.persist( artistEntity ); + } ); + sf.getSchemaManager().truncateMappedObjects(); + } + } + + static class HibernateEntityNameResolver implements EntityNameResolver { + @Override + public String resolveEntityName(Object entity) { + if ( entity instanceof Map ) { + return "artist"; + } + return null; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/embeddable/EmbeddableSameNestedNameSelectionTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/embeddable/EmbeddableSameNestedNameSelectionTest.java new file mode 100644 index 000000000000..93e99dad023b --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/embeddable/EmbeddableSameNestedNameSelectionTest.java @@ -0,0 +1,222 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.embeddable; + +import jakarta.persistence.AttributeOverride; +import jakarta.persistence.AttributeOverrides; +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Tuple; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel(annotatedClasses = { + EmbeddableSameNestedNameSelectionTest.Material.class, + EmbeddableSameNestedNameSelectionTest.Weight.class, + EmbeddableSameNestedNameSelectionTest.Length.class +}) +@SessionFactory +@Jira("https://hibernate.atlassian.net/browse/HHH-19962") +@Jira("https://hibernate.atlassian.net/browse/HHH-19985") +public class EmbeddableSameNestedNameSelectionTest { + + @Test + void testPlainEntity(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var result = session.createSelectionQuery( "from Material", Material.class ) + .getSingleResult(); + assertThat( result.getWeight().getValue() ).isEqualTo( "10" ); + assertThat( result.getLength().getValue() ).isEqualTo( "2" ); + } ); + } + + @Test + void testSubqueryEmbedded(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var result = session.createSelectionQuery( + "select q.weight as weight, q.length as length from (select m.weight as weight, m.length as length from Material m) q", + Tuple.class + ).getSingleResult(); + assertEmbeddedTuple( result ); + } ); + } + + @Test + void testEmbedded(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var result = session.createSelectionQuery( + "select m.weight as weight, m.length as length from Material m", + Tuple.class + ).getSingleResult(); + assertEmbeddedTuple( result ); + } ); + } + + private void assertEmbeddedTuple(Tuple result) { + final var weight = result.get( "weight", Weight.class ); + assertThat( weight.getValue() ).isEqualTo( "10" ); + assertThat( weight.getUnit() ).isEqualTo( WeightUnit.KILOGRAM ); + final var length = result.get( "length", Length.class ); + assertThat( length.getValue() ).isEqualTo( "2" ); + assertThat( length.getUnit() ).isEqualTo( LengthUnit.METER ); + } + + @Test + void testSubqueryScalar(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var result = session.createSelectionQuery( + "select q.weight, q.weight_unit, q.length, q.length_unit from " + + "(select m.weight.value as weight, m.weight.unit as weight_unit, m.length.value as length, m.length.unit as length_unit from Material m) q", + Tuple.class + ).getSingleResult(); + assertScalarTuple( result ); + } ); + } + + @Test + void testScalar(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var result = session.createSelectionQuery( + "select m.weight.value, m.weight.unit, m.length.value, m.length.unit from Material m", + Tuple.class + ).getSingleResult(); + assertScalarTuple( result ); + } ); + } + + private void assertScalarTuple(Tuple result) { + assertThat( result.get( 0, String.class ) ).isEqualTo( "10" ); + assertThat( result.get( 1, WeightUnit.class ) ).isEqualTo( WeightUnit.KILOGRAM ); + assertThat( result.get( 2, String.class ) ).isEqualTo( "2" ); + assertThat( result.get( 3, LengthUnit.class ) ).isEqualTo( LengthUnit.METER ); + } + + @BeforeAll + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> session.persist( + new Material( 1L, new Weight( "10", WeightUnit.KILOGRAM ), new Length( "2", LengthUnit.METER ) ) + ) ); + } + + @AfterAll + public void tearDown(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } + + @Entity(name = "Material") + static class Material { + @Id + private Long id; + + @Embedded + @AttributeOverrides({ + @AttributeOverride(name = "value", column = @Column(name = "weight_value")), + @AttributeOverride(name = "unit", column = @Column(name = "weight_unit")), + }) + private Weight weight; + + @Embedded + @AttributeOverrides({ + @AttributeOverride(name = "value", column = @Column(name = "length_value")), + @AttributeOverride(name = "unit", column = @Column(name = "length_unit")), + }) + private Length length; + + public Material() { + } + + public Material(Long id, Weight weight, Length length) { + this.id = id; + this.weight = weight; + this.length = length; + } + + public Long getId() { + return id; + } + + public Weight getWeight() { + return weight; + } + + public void setWeight(Weight weight) { + this.weight = weight; + } + + public Length getLength() { + return length; + } + + public void setLength(Length length) { + this.length = length; + } + } + + @Embeddable + static class Weight { + private String value; + + private WeightUnit unit; + + public Weight() { + } + + public Weight(String value, WeightUnit unit) { + this.value = value; + this.unit = unit; + } + + public String getValue() { + return value; + } + + public WeightUnit getUnit() { + return unit; + } + } + + enum WeightUnit { + KILOGRAM, + POUND + } + + @Embeddable + static class Length { + private String value; + + private LengthUnit unit; + + public Length() { + } + + public Length(String value, LengthUnit unit) { + this.value = value; + this.unit = unit; + } + + public String getValue() { + return value; + } + + public LengthUnit getUnit() { + return unit; + } + } + + enum LengthUnit { + METER, + FOOT + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/embeddable/EmbeddableWithJavaTypeTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/embeddable/EmbeddableWithJavaTypeTest.java new file mode 100644 index 000000000000..35df4dba7001 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/embeddable/EmbeddableWithJavaTypeTest.java @@ -0,0 +1,200 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.orm.test.embeddable; + +import jakarta.persistence.*; +import org.hibernate.annotations.JavaType; +import org.hibernate.query.spi.QueryImplementor; +import org.hibernate.testing.orm.junit.*; +import org.hibernate.type.AbstractSingleColumnStandardBasicType; +import org.hibernate.type.BasicType; +import org.hibernate.type.SqlTypes; +import org.hibernate.type.descriptor.WrapperOptions; +import org.hibernate.type.descriptor.java.AbstractClassJavaType; +import org.hibernate.type.descriptor.java.LocalDateJavaType; +import org.hibernate.type.descriptor.jdbc.DateJdbcType; +import org.hibernate.type.descriptor.jdbc.JdbcType; +import org.hibernate.type.descriptor.jdbc.JdbcTypeIndicators; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.time.LocalDate; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +@JiraKey("HHH-18898") +@DomainModel( + annotatedClasses = { + EmbeddableWithJavaTypeTest.EntityEmbedCustom.class, + EmbeddableWithJavaTypeTest.EntityEmbedNative.class + } +) +@SessionFactory +class EmbeddableWithJavaTypeTest implements SessionFactoryScopeAware { + + private SessionFactoryScope scope; + + @Override + public void injectSessionFactoryScope(SessionFactoryScope scope) { + this.scope = scope; + } + + // uses an embeddable with a custom java type + @ParameterizedTest + @ValueSource(strings = { + "select z from EntityEmbedCustom z where embedCustom.value=:datum", + "select z from EntityEmbedCustom z where :datum=embedCustom.value", + "select z from EntityEmbedCustom z where embedCustom=:datum", // this query failed with the bug + "select z from EntityEmbedCustom z where :datum=embedCustom", + "select z from EntityEmbedCustom z where embedCustom.value in (:datum)", + "select z from EntityEmbedCustom z where embedCustom in (:datum)" // failed as well + }) + void hhh18898Test_embedCustom(String hql) { + + // prepare + scope.inTransaction( session -> { + EntityEmbedCustom e = new EntityEmbedCustom(); + e.id = 1; + EmbedCustom datum = new EmbedCustom(); + datum.value = new MyDate( LocalDate.now() ); + e.embedCustom = datum; + session.persist( e ); + } ); + + // assert + scope.inTransaction( session -> { + QueryImplementor query = session.createQuery( hql, EntityEmbedCustom.class ); + query.setParameter( "datum", new MyDate( LocalDate.now() ), MyDateJavaType.TYPE ); + List resultList = query.getResultList(); + assertFalse( resultList.isEmpty() ); + assertEquals( LocalDate.now(), resultList.get( 0 ).embedCustom.value.wrapped ); + session.remove( resultList.get( 0 ) ); + } ); + } + + // uses an embeddable with a native java type + @ParameterizedTest + @ValueSource(strings = { + "select z from EntityEmbedNative z where embedNative.value=:datum", + "select z from EntityEmbedNative z where :datum=embedNative.value", + "select z from EntityEmbedNative z where embedNative=:datum", // this query failed with the bug + "select z from EntityEmbedNative z where :datum=embedNative", + "select z from EntityEmbedNative z where embedNative.value in (:datum)", + "select z from EntityEmbedNative z where embedNative in (:datum)" // failed as well + }) + void hhh18898Test_embedSingle(String hql) { + + // prepare + scope.inTransaction( session -> { + EntityEmbedNative e = new EntityEmbedNative(); + e.id = 1; + EmbedNative datum = new EmbedNative(); + datum.value = LocalDate.now(); + e.embedNative = datum; + session.persist( e ); + } ); + + // assert + scope.inTransaction( session -> { + QueryImplementor query = session.createQuery( hql, EntityEmbedNative.class ); + query.setParameter( "datum", LocalDate.now(), LocalDateJavaType.INSTANCE.getJavaType() ); + List resultList = query.getResultList(); + assertFalse( resultList.isEmpty() ); + assertEquals( LocalDate.now(), resultList.get( 0 ).embedNative.value ); + session.remove( resultList.get( 0 ) ); + } ); + } + + @Embeddable + public static class EmbedCustom { + + @Column(name = "DATUM") + @JavaType(MyDateJavaType.class) + MyDate value; + + } + + @Entity(name = "EntityEmbedCustom") + public static class EntityEmbedCustom { + + @Id + @Column(name = "id") + long id; + + @Embedded + EmbedCustom embedCustom; + } + + @Embeddable + public static class EmbedNative { + + @Column(name = "DATUM") + @JavaType(LocalDateJavaType.class) + LocalDate value; + } + + @Entity(name = "EntityEmbedNative") + public static class EntityEmbedNative { + + @Id + @Column(name = "id") + long id; + + @Embedded + EmbedNative embedNative; + } + + public static class MyDate { + private final LocalDate wrapped; + + public MyDate(LocalDate dateValue) { + wrapped = dateValue; + } + + public LocalDate toLocalDate() { + return wrapped; + } + } + + public static class MyDateJavaType extends AbstractClassJavaType { + private static final MyDateJavaType INSTANCE = new MyDateJavaType(); + public static final BasicType TYPE = new AbstractSingleColumnStandardBasicType<>( DateJdbcType.INSTANCE, + INSTANCE ) { + + @Override + public String getName() { + return "MyDateJavaType"; + } + }; + + protected MyDateJavaType() { + super( MyDate.class ); + } + + @Override + public X unwrap(final MyDate value, final Class type, final WrapperOptions options) { + LocalDate dateValue = (value == null ? null : value.toLocalDate()); + return LocalDateJavaType.INSTANCE.unwrap( dateValue, type, options ); + } + + @Override + public MyDate wrap(final X value, final WrapperOptions options) { + if ( value instanceof MyDate ) { + return (MyDate) value; + } + LocalDate dateValue = LocalDateJavaType.INSTANCE.wrap( value, options ); + return dateValue == null ? null : new MyDate( dateValue ); + } + + @Override + public JdbcType getRecommendedJdbcType(final JdbcTypeIndicators context) { + return context.getJdbcType( SqlTypes.DATE ); + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/engine/spi/LoadQueryInfluencersTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/engine/spi/LoadQueryInfluencersTest.java new file mode 100644 index 000000000000..d5379c597b3e --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/engine/spi/LoadQueryInfluencersTest.java @@ -0,0 +1,159 @@ +package org.hibernate.orm.test.engine.spi; + +import java.util.Set; + +import org.hibernate.annotations.BatchSize; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.engine.spi.LoadQueryInfluencers; +import org.hibernate.metamodel.model.domain.NavigableRole; +import org.hibernate.persister.collection.CollectionPersister; +import org.hibernate.persister.entity.EntityPersister; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.Setting; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@DomainModel(annotatedClasses = { + LoadQueryInfluencersTest.EntityWithBatchSize1.class, + LoadQueryInfluencersTest.ChildEntity.class +}) +@SessionFactory +@ServiceRegistry( + settings = { + @Setting(name = AvailableSettings.DEFAULT_BATCH_FETCH_SIZE, value = "100") + } +) +public class LoadQueryInfluencersTest { + + @Test + public void usesCustomEntityBatchSizeForEffectivelyBatchLoadable(SessionFactoryScope scope) { + scope.inSession( + (nonTransactedSession) -> { + LoadQueryInfluencers influencers = new LoadQueryInfluencers( nonTransactedSession.getSessionFactory() ); + assertTrue( influencers.getBatchSize() > 1, "Expecting default batch size > 1" ); + + EntityPersister persister = nonTransactedSession.getSessionFactory() + .getRuntimeMetamodels() + .getMappingMetamodel() + .getEntityDescriptor( EntityWithBatchSize1.class ); + + assertFalse( persister.isBatchLoadable(), "Incorrect persister batch loadable." ); + assertEquals( 1, influencers.effectiveBatchSize( persister ), "Incorrect effective batch size." ); + assertFalse( + influencers.effectivelyBatchLoadable( persister ), + "Incorrect effective batch loadable." + ); + } + ); + } + + @Test + public void usesCustomCollectionBatchSizeForEffectivelyBatchLoadable(SessionFactoryScope scope) { + scope.inSession( + (nonTransactedSession) -> { + LoadQueryInfluencers influencers = new LoadQueryInfluencers( nonTransactedSession.getSessionFactory() ); + assertTrue( influencers.getBatchSize() > 1, "Expecting default batch size > 1" ); + + EntityPersister entityPersister = nonTransactedSession.getSessionFactory() + .getRuntimeMetamodels() + .getMappingMetamodel() + .getEntityDescriptor( EntityWithBatchSize1.class ); + + NavigableRole collectionRole = entityPersister.getNavigableRole() + .append( "childrenWithBatchSize1" ); + + CollectionPersister persister = nonTransactedSession.getSessionFactory() + .getRuntimeMetamodels() + .getMappingMetamodel() + .findCollectionDescriptor( collectionRole.getFullPath() ); + + assertFalse( persister.isBatchLoadable(), "Incorrect persister batch loadable." ); + assertEquals( 1, influencers.effectiveBatchSize( persister ), "Incorrect effective batch size." ); + assertFalse( + influencers.effectivelyBatchLoadable( persister ), + "Incorrect effective batch loadable." + ); + } + ); + } + + + @Entity + @Table(name = "EntityWithBatchSize1") + @BatchSize(size = 1) + public class EntityWithBatchSize1 { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String name; + + @OneToMany + @BatchSize(size = 1) + private Set childrenWithBatchSize1; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Set getChildrenWithBatchSize1() { + return childrenWithBatchSize1; + } + + public void setChildrenWithBatchSize1(Set childrenWithBatchSize1) { + this.childrenWithBatchSize1 = childrenWithBatchSize1; + } + } + + @Entity + @Table(name = "ChildEntity") + public class ChildEntity { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String name; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/EntityGraphAndJoinFetchTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/EntityGraphAndJoinFetchTest.java new file mode 100644 index 000000000000..3ab3e03204f8 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/EntityGraphAndJoinFetchTest.java @@ -0,0 +1,209 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.entitygraph; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.IdClass; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.NamedAttributeNode; +import jakarta.persistence.NamedEntityGraph; +import jakarta.persistence.NamedSubgraph; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import org.hibernate.Hibernate; +import org.hibernate.jpa.SpecHints; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@DomainModel( + annotatedClasses = { + EntityGraphAndJoinFetchTest.OrderItem.class, + EntityGraphAndJoinFetchTest.Order.class, + EntityGraphAndJoinFetchTest.Product.class + } +) +@SessionFactory +@JiraKey( value = "HHH-19246") +public class EntityGraphAndJoinFetchTest { + + private static final Long ORDER_ID = 1l; + private static final Long PRODUCT_ID = 2l; + + private static final String NAMED_GRAPH_NAME = "Order.fetchAll"; + private static final String NAMED_SUBGRAPH_NAME = "OrderItem.fetchAll"; + + + @BeforeAll + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Order order = new Order( ORDER_ID, new BigDecimal( 1000 ) ); + Product product = new Product( PRODUCT_ID, "laptop" ); + OrderItem orderItem = new OrderItem( product, order ); + order.getItems().add( orderItem ); + + session.persist( order ); + session.persist( product ); + session.persist( orderItem ); + } + ); + } + + @Test + public void testJoinFetchBeingSubsetOfGraph(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Order order = session.createQuery( "FROM Order e LEFT JOIN FETCH e.items", Order.class ) + .setHint( + SpecHints.HINT_SPEC_LOAD_GRAPH, + scope.getSessionFactory().createEntityManager().getEntityGraph( NAMED_GRAPH_NAME ) + ) + .getSingleResult(); + assertTrue( Hibernate.isInitialized( order.getItems() ), "OrderItems have not been fetched" ); + assertEquals( 1, order.getItems().size(), "OrderItems have not been fetched" ); + assertTrue( Hibernate.isInitialized( order.getItems().iterator().next().getProduct() ), "Product has not been fetched" ); + } + ); + } + + @Entity(name = "OrderItem") + @IdClass(OrderItem.PK.class) + public static class OrderItem { + + @Id + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "product_id") + private Product product; + + @Id + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "order_id") + private Order order; + + public OrderItem() { + } + + public OrderItem(Product product, Order order) { + this.product = product; + this.order = order; + } + + public Product getProduct() { + return product; + } + + public Order getOrder() { + return order; + } + + public static class PK implements Serializable { + private Long product; + private Long order; + + @Override + public boolean equals(Object o) { + if ( this == o ) { + return true; + } + if ( o == null || getClass() != o.getClass() ) { + return false; + } + PK pk = (PK) o; + return Objects.equals( product, pk.product ) && Objects.equals( order, pk.order ); + } + + @Override + public int hashCode() { + return Objects.hash( product, order ); + } + } + } + + + @Entity(name = "Order") + @Table(name = "ORDER_TABLE") + @NamedEntityGraph( + name = NAMED_GRAPH_NAME, + attributeNodes = { + @NamedAttributeNode(value = "items", subgraph = NAMED_SUBGRAPH_NAME) + }, + subgraphs = { + @NamedSubgraph(name = NAMED_SUBGRAPH_NAME, attributeNodes = {@NamedAttributeNode("product")}) + } + ) + public static class Order { + + @Id + private Long id; + + private BigDecimal total; + @OneToMany(mappedBy = "order") + private Set items = new HashSet<>(); + + public Order() { + } + + public Order(Long id, BigDecimal total) { + this.id = id; + this.total = total; + } + + public Long getId() { + return id; + } + + public BigDecimal getTotal() { + return total; + } + + public Set getItems() { + return items; + } + } + + @Entity(name = "Product") + public static class Product { + + @Id + private Long id; + + private String name; + + public Product() { + } + + public Product(Long id, String name) { + this.id = id; + this.name = name; + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/FetchGraphCollectionOrderByAndCriteriaJoinTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/FetchGraphCollectionOrderByAndCriteriaJoinTest.java new file mode 100644 index 000000000000..cf69cbaf2e64 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/FetchGraphCollectionOrderByAndCriteriaJoinTest.java @@ -0,0 +1,331 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.entitygraph; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.NamedAttributeNode; +import jakarta.persistence.NamedEntityGraph; +import jakarta.persistence.NamedEntityGraphs; +import jakarta.persistence.NamedSubgraph; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OrderBy; +import jakarta.persistence.criteria.JoinType; +import jakarta.persistence.criteria.Predicate; +import org.hibernate.Hibernate; +import org.hibernate.graph.spi.RootGraphImplementor; +import org.hibernate.query.Query; +import org.hibernate.query.criteria.HibernateCriteriaBuilder; +import org.hibernate.query.criteria.JpaCriteriaQuery; +import org.hibernate.query.criteria.JpaJoin; +import org.hibernate.query.criteria.JpaRoot; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Random; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author baranyit@gmail.com + */ +@DomainModel(annotatedClasses = { + FetchGraphCollectionOrderByAndCriteriaJoinTest.Level1.class, + FetchGraphCollectionOrderByAndCriteriaJoinTest.Level2.class, + FetchGraphCollectionOrderByAndCriteriaJoinTest.Level3.class, +}) +@SessionFactory +@Jira( "https://hibernate.atlassian.net/browse/HHH-19207" ) +public class FetchGraphCollectionOrderByAndCriteriaJoinTest { + + @Test + public void testJoinAndFilter(SessionFactoryScope scope) { + executeTest( scope, true, true ); + } + + @Test + public void testNotJoinAndNotFilter(SessionFactoryScope scope) { + executeTest( scope, false, false ); + } + + /** + * This case describes the problem of using a fetch graph with a collection that has an @OrderBy clause + * and a criteria join without any usage. + *

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

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

    + * The required and the logical behaviour should be that the @OrderBy clause is applied to the + * collection as in the other test cases. If this problem occurs very difficult to find out the reason because + * this behaviour is not documented and the source code looks correct. + */ + @Test + public void testJoinAndNotFilter(SessionFactoryScope scope) { + executeTest( scope, true, false ); + } + + + private void executeTest(SessionFactoryScope scope, boolean directJoin, boolean filterOnJoin) { + scope.inTransaction( session -> { + HibernateCriteriaBuilder builder = session.getCriteriaBuilder(); + JpaCriteriaQuery criteriaQuery = builder.createQuery( Level1.class ); + JpaRoot root = criteriaQuery.from( Level1.class ); + + List predicates = new ArrayList<>(); + predicates.add( + builder.equal( root.get( "id" ), 1L ) + ); + + if ( directJoin || filterOnJoin ) { + // Directly add the join to the level2 and level3 entities + JpaJoin join = root.join( "children", JoinType.INNER ) + .join( "children", JoinType.LEFT ); + + if ( filterOnJoin ) { + predicates.add( + builder.gt( join.get( "id" ), 1L ) + ); + } + } + + // Add all defined predicates to the criteria query + criteriaQuery.where( builder.and( predicates.toArray(new Predicate[0]) ) ); + + // Set some default root ordering (not required for the test case) + criteriaQuery.orderBy( builder.asc( root.get( "id" ) ) ); + + // Create the TypedQuery with entity graph + RootGraphImplementor graph = session.getEntityGraph( "level1_loadAll" ); + Query query = session + .createQuery( criteriaQuery ) + .setHint( org.hibernate.jpa.SpecHints.HINT_SPEC_FETCH_GRAPH, graph ); + + // Parse the result as stream, but the problem occurs also with getResultList() + query.getResultStream().forEach( level1 -> { + + // Check ordering of Level2 entities + Long ordinalLevel2 = 0L; + assertThat( level1.getChildren() ).matches( Hibernate::isInitialized ); + for ( Level2 level2 : level1.getChildren() ) { + System.out.println( "Level2: " + level2.getOrdinal() ); + assertThat( level2.getOrdinal() ).isGreaterThan( ordinalLevel2 ); + ordinalLevel2 = level2.getOrdinal(); + + // Check ordering of Level3 entities + Long ordinalLevel3 = 0L; + assertThat( level2.getChildren() ).matches( Hibernate::isInitialized ); + for ( Level3 level3 : level2.getChildren() ) { + System.out.println( "Level3: " + level3.getOrdinal() ); + assertThat( level3.getOrdinal() ).isGreaterThan( ordinalLevel3 ); + ordinalLevel3 = level3.getOrdinal(); + } + } + } ); + } ); + } + + @BeforeAll + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + + final Iterator randomOrdinals = new Random().longs( 100, 999 ) + .distinct().limit( 200 ).boxed().iterator(); + + for ( long l1 = 1; l1 <= 5; l1++ ) { + final Level1 root = new Level1( l1 ); + + for ( long l2 = 1; l2 <= 5; l2++ ) { + final long l2Id = (l1 * 10) + l2; + final Level2 child2 = new Level2( root, l2Id, randomOrdinals.next() ); + + for ( long l3 = 1; l3 <= 5; l3++ ) { + final long l3Id = (l2Id * 10) + l3; + new Level3( child2, l3Id, randomOrdinals.next() ); + } + } + session.persist( root ); + } + } ); + } + + @AfterAll + public void tearDown(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } + + @Entity(name = "Level1") + @NamedEntityGraphs({ + @NamedEntityGraph( + name = "level1_loadAll", + attributeNodes = { + @NamedAttributeNode(value = "children", subgraph = "subgraph.children") + }, + subgraphs = { + @NamedSubgraph( + name = "subgraph.children", + attributeNodes = { + @NamedAttributeNode(value = "children") + } + ) + } + ) + }) + static class Level1 { + @Id + private Long id; + + @OneToMany(fetch = FetchType.LAZY, + mappedBy = "parent", + cascade = CascadeType.PERSIST) + @OrderBy("ordinal") + private Set children = new LinkedHashSet<>(); + + public Level1() { + } + + public Level1(Long id) { + this.id = id; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Set getChildren() { + return children; + } + + @Override + public String toString() { + return "Level1 #" + id; + } + } + + @Entity(name = "Level2") + static class Level2 { + @Id + Long id; + + Long ordinal; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "parent_id") + private Level1 parent; + + @OneToMany(fetch = FetchType.LAZY, + mappedBy = "parent", + cascade = CascadeType.PERSIST) + @OrderBy("ordinal") + private Set children = new LinkedHashSet<>(); + + public Level2() { + } + + public Level2(Level1 parent, Long id, Long ordinal) { + this.parent = parent; + this.id = id; + this.ordinal = ordinal; + parent.getChildren().add( this ); + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Level1 getParent() { + return parent; + } + + public void setParent(Level1 parent) { + this.parent = parent; + } + + public Set getChildren() { + return children; + } + + public Long getOrdinal() { + return ordinal; + } + + @Override + public String toString() { + return "Level1 #" + id + " $" + ordinal; + } + } + + @Entity(name = "Level3") + static class Level3 { + @Id + Long id; + + Long ordinal; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "parent_id") + private Level2 parent; + + public Level3() { + } + + public Level3(Level2 parent, Long id, Long ordinal) { + this.parent = parent; + this.id = id; + this.ordinal = ordinal; + parent.getChildren().add( this ); + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Level2 getParent() { + return parent; + } + + public void setParent(Level2 parent) { + this.parent = parent; + } + + public Long getOrdinal() { + return ordinal; + } + + @Override + public String toString() { + return "Level3 #" + id + " $" + ordinal; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/FindGraphCollectionOrderByTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/FindGraphCollectionOrderByTest.java new file mode 100644 index 000000000000..837f365223fd --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/entitygraph/FindGraphCollectionOrderByTest.java @@ -0,0 +1,235 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.entitygraph; + +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +import org.hibernate.Hibernate; +import org.hibernate.graph.spi.RootGraphImplementor; +import org.hibernate.jpa.AvailableHints; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.NamedAttributeNode; +import jakarta.persistence.NamedEntityGraph; +import jakarta.persistence.NamedEntityGraphs; +import jakarta.persistence.NamedSubgraph; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OrderBy; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Marco Belladelli + */ +@DomainModel( annotatedClasses = { + FindGraphCollectionOrderByTest.Level1.class, + FindGraphCollectionOrderByTest.Level2.class, + FindGraphCollectionOrderByTest.Level3.class, +} ) +@SessionFactory +@Jira( "https://hibernate.atlassian.net/browse/HHH-18436" ) +public class FindGraphCollectionOrderByTest { + @Test + public void testLoadGraphFind(SessionFactoryScope scope) { + executeTest( scope, AvailableHints.HINT_SPEC_LOAD_GRAPH, true ); + } + + @Test + public void testLoadGraphQuery(SessionFactoryScope scope) { + executeTest( scope, AvailableHints.HINT_SPEC_LOAD_GRAPH, false ); + } + + @Test + public void testFetchGraphFind(SessionFactoryScope scope) { + executeTest( scope, AvailableHints.HINT_SPEC_FETCH_GRAPH, true ); + } + + @Test + public void testFetchGraphQuery(SessionFactoryScope scope) { + executeTest( scope, AvailableHints.HINT_SPEC_FETCH_GRAPH, false ); + } + + + private void executeTest(SessionFactoryScope scope, String hint, boolean find) { + scope.inTransaction( session -> { + final RootGraphImplementor graph = session.getEntityGraph( "level1_loadAll" ); + final Level1 root = find ? session.find( Level1.class, 1L, Map.of( hint, graph ) ) : + session.createQuery( "from Level1 where id = :id", Level1.class ) + .setParameter( "id", 1L ) + .setHint( hint, graph ) + .getSingleResult(); + + assertThat( root.getChildren() ).matches( Hibernate::isInitialized ).hasSize( 3 ); + long i = 1; + for ( final Level2 child : root.getChildren() ) { + if ( i == 2 ) { + assertThat( child.getChildren() ).matches( Hibernate::isInitialized ).hasSize( 1 ); + } + assertThat( child.getId() ).as( "Children not in expected order" ).isEqualTo( i++ ); + } + } ); + } + + @BeforeAll + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final Level1 root = new Level1( 1L ); + new Level2( root, 1L ); + final Level2 child2 = new Level2( root, 2L ); + new Level2( root, 3L ); + new Level3( child2, 1L ); + session.persist( root ); + } ); + } + + @AfterAll + public void tearDown(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } + + @Entity( name = "Level1" ) + @NamedEntityGraphs( { + @NamedEntityGraph( + name = "level1_loadAll", + attributeNodes = { + @NamedAttributeNode( value = "children", subgraph = "subgraph.children" ) + }, + subgraphs = { + @NamedSubgraph( + name = "subgraph.children", + attributeNodes = { + @NamedAttributeNode( value = "children" ) + } + ) + } + ) + } ) + static class Level1 { + @Id + private Long id; + + @OneToMany( fetch = FetchType.LAZY, + mappedBy = "parent", + cascade = CascadeType.PERSIST ) + @OrderBy( "id" ) + private Set children = new LinkedHashSet<>(); + + public Level1() { + } + + public Level1(Long id) { + this.id = id; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Set getChildren() { + return children; + } + } + + @Entity( name = "Level2" ) + static class Level2 { + @Id + Long id; + + @ManyToOne( fetch = FetchType.LAZY ) + @JoinColumn( name = "parent_id" ) + private Level1 parent; + + @OneToMany( fetch = FetchType.LAZY, + mappedBy = "parent", + cascade = CascadeType.PERSIST ) + @OrderBy( "id" ) + private Set children = new LinkedHashSet<>(); + + public Level2() { + } + + public Level2(Level1 parent, Long id) { + this.parent = parent; + this.id = id; + parent.getChildren().add( this ); + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Level1 getParent() { + return parent; + } + + public void setParent(Level1 parent) { + this.parent = parent; + } + + public Set getChildren() { + return children; + } + } + + @Entity( name = "Level3" ) + static class Level3 { + @Id + Long id; + + @ManyToOne( fetch = FetchType.LAZY ) + @JoinColumn( name = "parent_id" ) + private Level2 parent; + + public Level3() { + } + + public Level3(Level2 parent, Long id) { + this.parent = parent; + this.id = id; + parent.getChildren().add( this ); + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Level2 getParent() { + return parent; + } + + public void setParent(Level2 parent) { + this.parent = parent; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/extralazy/ExtraLazyMapQueuedPersistTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/extralazy/ExtraLazyMapQueuedPersistTest.java new file mode 100644 index 000000000000..154f68a307c4 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/extralazy/ExtraLazyMapQueuedPersistTest.java @@ -0,0 +1,132 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.extralazy; + +import java.time.LocalDate; +import java.util.TreeMap; +import java.util.SortedMap; + + +import org.hibernate.annotations.LazyCollection; +import org.hibernate.annotations.LazyCollectionOption; +import org.hibernate.annotations.SortNatural; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Column; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.MapKeyColumn; + + +/** + * @author Guillaume Toison + */ +@Jira( "https://hibernate.atlassian.net/browse/HHH-18885" ) +@DomainModel( annotatedClasses = { + ExtraLazyMapQueuedPersistTest.Person.class, + ExtraLazyMapQueuedPersistTest.Event.class, +} ) +@SessionFactory( useCollectingStatementInspector = true ) +public class ExtraLazyMapQueuedPersistTest { + @Test + public void testQueuedPersistOperation(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final Person person = new Person( 1L ); + + session.persist ( person ); + } ); + + scope.inTransaction( session -> { + final Person person = session.find( Person.class, 1L ); + + LocalDate date = LocalDate.of( 2024, 1, 1 ); + Event event = new Event( 1L, person, date ); + + person.getEvents().put( date, event ); + } ); + } + + @Entity( name = "Person" ) + public static class Person { + @Id + private Long id; + + @OneToMany(mappedBy = "person") + @MapKeyColumn(name = "date_col") + @SortNatural + @LazyCollection(LazyCollectionOption.EXTRA) + private SortedMap events = new TreeMap<>(); + + public Person() { + } + + public Person(Long id) { + this.id = id; + } + + public SortedMap getEvents() { + return events; + } + + public void setEvents(SortedMap events) { + this.events = events; + } + } + + @Entity( name = "Event" ) + public static class Event { + @Id + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "person_id") + private Person person; + + @Column( name="date_col" ) + private LocalDate date; + + public Event() { + } + + public Event(Long id, Person person, LocalDate date) { + this.id = id; + this.person = person; + this.date = date; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Person getPerson() { + return person; + } + + public void setPerson(Person person) { + this.person = person; + } + + public LocalDate getDate() { + return date; + } + + public void setDate(LocalDate date) { + this.date = date; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/filter/FilterDefinitionOrderTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/filter/FilterDefinitionOrderTest.java new file mode 100644 index 000000000000..9a3b2631fae9 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/filter/FilterDefinitionOrderTest.java @@ -0,0 +1,130 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.orm.test.filter; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import org.hibernate.SharedSessionContract; +import org.hibernate.annotations.Filter; +import org.hibernate.annotations.FilterDef; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.List; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel( + annotatedClasses = { + FilterDefinitionOrderTest.AMyEntity.class, + FilterDefinitionOrderTest.XEntity.class + } +) +@JiraKey("HHH-19036") +// Real test is if it actually starts, as that's where the filter binding happens, +// but let's also check that it was actually processed as well: +public class FilterDefinitionOrderTest extends AbstractStatefulStatelessFilterTest { + + @BeforeEach + void setUp() { + scope.inTransaction(session -> { + AMyEntity entity1 = new AMyEntity(); + entity1.setField("test"); + AMyEntity entity2 = new AMyEntity(); + entity2.setField("Hello"); + session.persist(entity1); + session.persist(entity2); + }); + } + + @AfterEach + void tearDown() { + scope.inTransaction(session -> { + session.createMutationQuery("delete from AMyEntity").executeUpdate(); + }); + } + + @ParameterizedTest + @MethodSource("transactionKind") + void smokeTest(BiConsumer> inTransaction) { + inTransaction.accept(scope, session -> { + session.enableFilter("x_filter"); + List entities = session.createQuery("FROM AMyEntity", AMyEntity.class).getResultList(); + assertThat(entities).hasSize(1) + .allSatisfy(entity -> assertThat(entity.getField()).isEqualTo("Hello")); + + session.disableFilter("x_filter"); + entities = session.createQuery("FROM AMyEntity", AMyEntity.class).getResultList(); + assertThat(entities).hasSize(2); + }); + } + + @Entity(name = "AMyEntity") + @Filter(name = "x_filter") + public static class AMyEntity { + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE) + @Column(nullable = false) + private Long id; + + public String field; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getField() { + return field; + } + + public void setField(String field) { + this.field = field; + } + } + + @Entity(name = "XEntity") + @FilterDef(name = "x_filter", defaultCondition = "field = 'Hello'") + public static class XEntity { + + @Id + @GeneratedValue(strategy = GenerationType.SEQUENCE) + @Column(nullable = false) + private Long id; + + public String field; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getField() { + return field; + } + + public void setField(String field) { + this.field = field; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayAggregateTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayAggregateTest.java index 136338a4d44f..7f2d30803873 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayAggregateTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayAggregateTest.java @@ -1,11 +1,10 @@ /* - * Hibernate, Relational Persistence for Idiomatic Java - * - * License: GNU Lesser General Public License (LGPL), version 2.1 or later. - * See the lgpl.txt file in the root directory or . + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors */ package org.hibernate.orm.test.function.array; +import java.util.Arrays; import java.util.List; import org.hibernate.boot.ResourceStreamLocator; @@ -14,12 +13,14 @@ import org.hibernate.boot.spi.InFlightMetadataCollector; import org.hibernate.boot.spi.MetadataBuildingContext; import org.hibernate.dialect.OracleArrayJdbcType; +import org.hibernate.dialect.PostgreSQLDialect; import org.hibernate.dialect.OracleDialect; import org.hibernate.dialect.SpannerDialect; import org.hibernate.engine.jdbc.Size; import org.hibernate.query.criteria.JpaCriteriaQuery; import org.hibernate.query.criteria.JpaRoot; import org.hibernate.query.sqm.NodeBuilder; +import org.hibernate.testing.orm.junit.RequiresDialect; import org.hibernate.type.SqlTypes; import org.hibernate.type.descriptor.java.ArrayJavaType; import org.hibernate.type.descriptor.java.spi.JavaTypeRegistry; @@ -32,10 +33,12 @@ import org.hibernate.testing.orm.junit.BootstrapServiceRegistry; import org.hibernate.testing.orm.junit.DialectFeatureChecks; import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; import org.hibernate.testing.orm.junit.RequiresDialectFeature; import org.hibernate.testing.orm.junit.SessionFactory; import org.hibernate.testing.orm.junit.SessionFactoryScope; import org.hibernate.testing.orm.junit.SkipForDialect; +import org.hibernate.testing.orm.junit.Jira; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -160,4 +163,32 @@ public void testNodeBuilder(SessionFactoryScope scope) { } ); } + @Test + @Jira("https://hibernate.atlassian.net/browse/HHH-19666") + public void testNonExistingArrayType(SessionFactoryScope scope) { + scope.inSession( em -> { + List results = em.createQuery( "select array_agg(e.id) within group (order by e.id) from EntityOfBasics e", Integer[].class ) + .getResultList(); + assertEquals( 1, results.size() ); + assertArrayEquals( new Integer[]{ 1, 2, 3 }, results.get( 0 ) ); + } ); + } + + @Test + @Jira("https://hibernate.atlassian.net/browse/HHH-19681") + @RequiresDialect(PostgreSQLDialect.class) + public void testJsonBJdbcArray(SessionFactoryScope scope) { + scope.inTransaction( session -> { + String sql = "select groupId, array_agg(json_values) " + + "from (VALUES (1,'[1,2]'::jsonb),(1,'[10,20]'::jsonb)) as row(groupId,json_values) " + + "group by groupId"; + + List result = session.createNativeQuery(sql, Object[].class).getResultList(); + assertEquals(1,result.size()); + assertEquals(2, result.get(0).length); + assertEquals( 1,result.get(0)[0] ); + assertEquals( "[[1, 2], [10, 20]]", Arrays.toString((String[])result.get(0)[1]) ); + } ); + } + } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayConstructorInSelectClauseTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayConstructorInSelectClauseTest.java new file mode 100644 index 000000000000..1c36651739d7 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayConstructorInSelectClauseTest.java @@ -0,0 +1,144 @@ +package org.hibernate.orm.test.function.array; + +import java.util.Arrays; + +import org.hibernate.dialect.OracleDialect; + +import org.hibernate.testing.jdbc.SharedDriverManagerTypeCacheClearingIntegrator; +import org.hibernate.testing.orm.junit.BootstrapServiceRegistry; +import org.hibernate.testing.orm.junit.DialectFeatureChecks; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.RequiresDialectFeature; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +@DomainModel( + annotatedClasses = { + ArrayConstructorInSelectClauseTest.Book.class, + ArrayConstructorInSelectClauseTest.Author.class + }) +@SessionFactory +@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsStructuralArrays.class) +// Clear the type cache, otherwise we might run into ORA-21700: object does not exist or is marked for delete +@BootstrapServiceRegistry(integrators = SharedDriverManagerTypeCacheClearingIntegrator.class) +public class ArrayConstructorInSelectClauseTest { + private static final Long AUTHOR_ID = 1l; + private static final Long CO_AUTHOR_ID = 2l; + private static final Long BOOK_ID = 3l; + + @BeforeEach + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + var dialect = session.getDialect(); + if ( dialect instanceof OracleDialect ) { + session.createNativeQuery( + "create or replace type LongArray as varying array(127) of number(19,0)" ) + .executeUpdate(); + } + var author = new Author( AUTHOR_ID, "D Thomas" ); + var coAuthor = new Author( CO_AUTHOR_ID, "A Hunt" ); + session.persist( author ); + session.persist( coAuthor ); + + var book = new Book( BOOK_ID, "The Pragmatic Programmer", author, coAuthor ); + session.persist( book ); + } + ); + } + + @AfterEach + public void tearDown(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + session.createMutationQuery( "delete from Book" ).executeUpdate(); + session.createMutationQuery( "delete from Author" ).executeUpdate(); + var dialect = session.getDialect(); + if ( dialect instanceof OracleDialect ) { + session.createNativeQuery( "drop type LongArray" ).executeUpdate(); + } + } + ); + } + + @Test + @Jira("HHH-18353") + public void testArraySelectionNoCast(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + var query = session.createQuery( + "SELECT ARRAY(b.author.id, b.coAuthor.id) FROM Book b WHERE b.id = :bookId", + Long[].class + ).setParameter( "bookId", BOOK_ID ); + + assertThat( query.getSingleResult() ).containsAll( Arrays.asList( AUTHOR_ID, CO_AUTHOR_ID ) ); + } + ); + } + + @Test + public void testArraySelectionCast(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + var query = session.createQuery( + "SELECT ARRAY(CAST(b.author.id as Long), CAST(b.coAuthor.id as Long)) FROM Book b WHERE b.id = :bookId", + Long[].class + ).setParameter( "bookId", BOOK_ID ); + + assertThat( query.getSingleResult() ).containsAll( Arrays.asList( AUTHOR_ID, CO_AUTHOR_ID ) ); + } + ); + } + + @Entity(name = "Author") + public static class Author { + + @Id + private Long id; + + private String name; + + public Author() { + } + + public Author(Long id, String name) { + this.id = id; + this.name = name; + } + } + + @Entity(name = "Book") + public static class Book { + + @Id + private Long id; + + private String title; + + @ManyToOne + private Author author; + + @ManyToOne + private Author coAuthor; + + public Book() { + } + + public Book(long id, String title, Author author, Author coAuthor) { + this.id = id; + this.title = title; + this.author = author; + this.coAuthor = coAuthor; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayContainsTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayContainsTest.java index 2167a8ed5ad7..9bfdf9680d72 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayContainsTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayContainsTest.java @@ -17,6 +17,7 @@ import org.hibernate.testing.orm.junit.BootstrapServiceRegistry; import org.hibernate.testing.orm.junit.DialectFeatureChecks; import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; import org.hibernate.testing.orm.junit.RequiresDialectFeature; import org.hibernate.testing.orm.junit.SessionFactory; import org.hibernate.testing.orm.junit.SessionFactoryScope; @@ -157,4 +158,22 @@ public void testInSyntax(SessionFactoryScope scope) { } ); } + @Test + @JiraKey( "HHH-18851" ) + public void testInArray(SessionFactoryScope scope) { + scope.inSession( em -> { + List results = em.createQuery( + "select e.id " + + "from EntityWithArrays e " + + "where :p in e.theArray", + Tuple.class + ) + .setParameter( "p", "abc" ) + .getResultList(); + + assertEquals( 1, results.size() ); + assertEquals( 2L, results.get( 0 ).get( 0 ) ); + } ); + } + } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayPositionTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayPositionTest.java index 65bac8406609..7746d94634a5 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayPositionTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayPositionTest.java @@ -87,6 +87,18 @@ public void testPositionNull(SessionFactoryScope scope) { } ); } + @Test + @Jira("https://hibernate.atlassian.net/browse/HHH-19490") + public void testPositionParam(SessionFactoryScope scope) { + scope.inSession( em -> { + List results = em.createQuery( "from EntityWithArrays e where array_position(e.theArray, ?1) = 1", EntityWithArrays.class ) + .setParameter( 1, "abc" ) + .getResultList(); + assertEquals( 1, results.size() ); + assertEquals( 2L, results.get( 0 ).getId() ); + } ); + } + @Test @Jira("https://hibernate.atlassian.net/browse/HHH-17801") public void testEnumPosition(SessionFactoryScope scope) { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayPositionsTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayPositionsTest.java index 3e387cf23b04..042cc29e47f3 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayPositionsTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayPositionsTest.java @@ -17,6 +17,7 @@ import org.hibernate.testing.orm.junit.BootstrapServiceRegistry; import org.hibernate.testing.orm.junit.DialectFeatureChecks; import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; import org.hibernate.testing.orm.junit.RequiresDialectFeature; import org.hibernate.testing.orm.junit.SessionFactory; import org.hibernate.testing.orm.junit.SessionFactoryScope; @@ -94,6 +95,20 @@ public void testPositionsNull(SessionFactoryScope scope) { } ); } + @Test + @Jira("https://hibernate.atlassian.net/browse/HHH-19490") + public void testPositionsParam(SessionFactoryScope scope) { + scope.inSession( em -> { + List results = em.createQuery( "select array_positions(e.theArray, ?1) from EntityWithArrays e order by e.id", int[].class ) + .setParameter( 1, "abc" ) + .getResultList(); + assertEquals( 3, results.size() ); + assertArrayEquals( new int[0], results.get( 0 ) ); + assertArrayEquals( new int[]{ 1, 4 }, results.get( 1 ) ); + assertNull( results.get( 2 ) ); + } ); + } + @Test public void testPositionsList(SessionFactoryScope scope) { scope.inSession( em -> { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayToStringWithArrayAggregateTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayToStringWithArrayAggregateTest.java new file mode 100644 index 000000000000..95ad7a0502a1 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/function/array/ArrayToStringWithArrayAggregateTest.java @@ -0,0 +1,144 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.function.array; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Tuple; + +import org.hibernate.dialect.OracleDialect; +import org.hibernate.query.criteria.JpaCriteriaQuery; +import org.hibernate.query.criteria.JpaCteCriteria; +import org.hibernate.query.criteria.JpaRoot; +import org.hibernate.query.sqm.NodeBuilder; + +import org.hibernate.testing.orm.junit.DialectFeatureChecks; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.RequiresDialectFeature; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.SkipForDialect; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Kowsar Atazadeh + */ +@DomainModel( + annotatedClasses = {ArrayToStringWithArrayAggregateTest.Book.class, ArrayToStringWithArrayAggregateTest.Dummy.class}) +@SessionFactory +@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsStructuralArrays.class) +@JiraKey("HHH-18981") +public class ArrayToStringWithArrayAggregateTest { + + @BeforeEach + public void prepareData(SessionFactoryScope scope) { + scope.inTransaction( em -> { + em.persist( new Book( 1, "title1" ) ); + em.persist( new Book( 2, "title2" ) ); + } ); + } + + @AfterEach + public void cleanup(SessionFactoryScope scope) { + scope.inTransaction( em -> { + em.createMutationQuery( "delete from Book" ).executeUpdate(); + } ); + } + + @Test + public void test(SessionFactoryScope scope) { + scope.inSession( em -> { + final NodeBuilder cb = (NodeBuilder) em.getCriteriaBuilder(); + final JpaCriteriaQuery q = cb.createTupleQuery(); + JpaRoot root = q.from( Book.class ); + + q.multiselect( + cb.arrayToString( + cb.arrayAgg( cb.asc( root.get( "title" ) ), root.get( "title" ) ), + "," + ).alias( "titles" ) + ); + List list = em.createQuery( q ).getResultList(); + String titles = list.get( 0 ).get( "titles", String.class ); + assertThat( titles ).isEqualTo( "title1,title2" ); + } ); + } + + @Test + @SkipForDialect(dialectClass = OracleDialect.class, majorVersion = 19, reason = "Oracle bug in version 21") + @SkipForDialect(dialectClass = OracleDialect.class, majorVersion = 21, reason = "Oracle bug in version 21") + public void testWithCte(SessionFactoryScope scope) { + scope.inSession( em -> { + final NodeBuilder cb = (NodeBuilder) em.getCriteriaBuilder(); + + JpaCriteriaQuery cteQuery = cb.createTupleQuery(); + JpaRoot cteRoot = cteQuery.from( Book.class ); + cteQuery.multiselect( + cb.arrayAgg( cb.asc( cteRoot.get( "title" ) ), cteRoot.get( "title" ) ) + .alias( "titles_array" ) + ); + + JpaCriteriaQuery query = cb.createTupleQuery(); + JpaCteCriteria titlesCte = query.with( cteQuery ); + JpaRoot root = query.from( titlesCte ); + query.multiselect( + cb.arrayToString( root.get( "titles_array" ), cb.literal( "," ) ) + .alias( "titles" ) + ); + + List list = em.createQuery( query ).getResultList(); + String titles = list.get( 0 ).get( "titles", String.class ); + assertThat( titles ).isEqualTo( "title1,title2" ); + } ); + } + + @Entity(name = "Book") + public static class Book { + @Id + private Integer id; + private String title; + + public Book() { + } + + public Book(Integer id, String title) { + this.id = id; + this.title = title; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + } + + // Needed for Oracle + @Entity + static class Dummy { + @Id + Long id; + String[] theArray; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/hql/ASTParserLoadingTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/hql/ASTParserLoadingTest.java index 62e8bb02c126..2ac157068bf9 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/hql/ASTParserLoadingTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/hql/ASTParserLoadingTest.java @@ -26,12 +26,13 @@ import org.hibernate.Session; import org.hibernate.Transaction; import org.hibernate.TypeMismatchException; -import org.hibernate.cfg.Configuration; import org.hibernate.cfg.Environment; +import org.hibernate.community.dialect.InformixDialect; import org.hibernate.dialect.AbstractHANADialect; import org.hibernate.dialect.CockroachDialect; import org.hibernate.dialect.DB2Dialect; import org.hibernate.dialect.DerbyDialect; +import org.hibernate.dialect.Dialect; import org.hibernate.dialect.H2Dialect; import org.hibernate.dialect.HSQLDialect; import org.hibernate.dialect.MySQLDialect; @@ -49,7 +50,6 @@ import org.hibernate.query.Query; import org.hibernate.query.SyntaxException; import org.hibernate.query.spi.QueryImplementor; -import org.hibernate.query.sqm.ParsingException; import org.hibernate.query.sqm.SqmExpressible; import org.hibernate.query.sqm.internal.QuerySqmImpl; import org.hibernate.query.sqm.tree.domain.SqmPath; @@ -60,13 +60,17 @@ import org.hibernate.transform.ResultTransformer; import org.hibernate.transform.Transformers; -import org.hibernate.testing.DialectChecks; -import org.hibernate.testing.FailureExpected; -import org.hibernate.testing.RequiresDialect; -import org.hibernate.testing.RequiresDialectFeature; -import org.hibernate.testing.SkipForDialect; -import org.hibernate.testing.TestForIssue; -import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase; +import org.hibernate.testing.orm.junit.DialectFeatureChecks; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.FailureExpected; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.RequiresDialect; +import org.hibernate.testing.orm.junit.RequiresDialectFeature; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.Setting; +import org.hibernate.testing.orm.junit.SkipForDialect; import org.hibernate.orm.test.cid.Customer; import org.hibernate.orm.test.cid.LineItem; @@ -74,8 +78,10 @@ import org.hibernate.orm.test.cid.Order; import org.hibernate.orm.test.cid.Product; -import org.junit.After; -import org.junit.Test; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import org.jboss.logging.Logger; import org.hamcrest.CoreMatchers; @@ -86,7 +92,6 @@ import static org.hamcrest.Matchers.notNullValue; import static org.hibernate.testing.junit4.ExtraAssertions.assertClassAssignability; import static org.hibernate.testing.junit4.ExtraAssertions.assertTyping; -import static org.hibernate.testing.transaction.TransactionUtil.doInHibernate; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; @@ -106,15 +111,40 @@ * * @author Steve */ -@RequiresDialectFeature(DialectChecks.SupportsTemporaryTable.class) -public class ASTParserLoadingTest extends BaseCoreFunctionalTestCase { +@RequiresDialectFeature( feature = DialectFeatureChecks.SupportsTemporaryTable.class) +@DomainModel( annotatedClasses = {Department.class, Employee.class, Title.class}, + xmlMappings = {"/org/hibernate/orm/test/hql/Animal.hbm.xml", + "/org/hibernate/orm/test/hql/FooBarCopy.hbm.xml", + "/org/hibernate/orm/test/hql/SimpleEntityWithAssociation.hbm.xml", + "/org/hibernate/orm/test/hql/CrazyIdFieldNames.hbm.xml", + "/org/hibernate/orm/test/hql/Image.hbm.xml", + "/org/hibernate/orm/test/hql/ComponentContainer.hbm.xml", + "/org/hibernate/orm/test/hql/VariousKeywordPropertyEntity.hbm.xml", + "/org/hibernate/orm/test/hql/Constructor.hbm.xml", + "/org/hibernate/orm/test/batchfetch/ProductLine.hbm.xml", + "/org/hibernate/orm/test/cid/Customer.hbm.xml", + "/org/hibernate/orm/test/cid/Order.hbm.xml", + "/org/hibernate/orm/test/cid/LineItem.hbm.xml", + "/org/hibernate/orm/test/cid/Product.hbm.xml", + "/org/hibernate/orm/test/any/hbm/Properties.hbm.xml", + "/org/hibernate/orm/test/legacy/Commento.hbm.xml", + "/org/hibernate/orm/test/legacy/Marelo.hbm.xml"}) +@SessionFactory +@ServiceRegistry( + settings = { + @Setting(name = Environment.USE_QUERY_CACHE, value = "true"), + @Setting(name = Environment.GENERATE_STATISTICS, value = "true") + } +) +@SuppressWarnings("JUnitMalformedDeclaration") +public class ASTParserLoadingTest { + private static final Logger log = Logger.getLogger(ASTParserLoadingTest.class); private final List createdAnimalIds = new ArrayList<>(); - @After - public void cleanUpTestData() { - inTransaction( - (session) -> { + @AfterEach + public void cleanUpTestData(SessionFactoryScope scope) { + scope.inTransaction( (session) -> { session.createQuery( "from Animal" ).list().forEach( (animal) -> session.delete( animal ) ); @@ -193,51 +223,10 @@ public void cleanUpTestData() { } ); } - @Override - protected boolean isCleanupTestDataRequired() { - return false; - } - - @Override - public String[] getMappings() { - return new String[] { - "/org/hibernate/orm/test/hql/Animal.hbm.xml", - "/org/hibernate/orm/test/hql/FooBarCopy.hbm.xml", - "/org/hibernate/orm/test/hql/SimpleEntityWithAssociation.hbm.xml", - "/org/hibernate/orm/test/hql/CrazyIdFieldNames.hbm.xml", - "/org/hibernate/orm/test/hql/Image.hbm.xml", - "/org/hibernate/orm/test/hql/ComponentContainer.hbm.xml", - "/org/hibernate/orm/test/hql/VariousKeywordPropertyEntity.hbm.xml", - "/org/hibernate/orm/test/hql/Constructor.hbm.xml", - "/org/hibernate/orm/test/batchfetch/ProductLine.hbm.xml", - "/org/hibernate/orm/test/cid/Customer.hbm.xml", - "/org/hibernate/orm/test/cid/Order.hbm.xml", - "/org/hibernate/orm/test/cid/LineItem.hbm.xml", - "/org/hibernate/orm/test/cid/Product.hbm.xml", - "/org/hibernate/orm/test/any/hbm/Properties.hbm.xml", - "/org/hibernate/orm/test/legacy/Commento.hbm.xml", - "/org/hibernate/orm/test/legacy/Marelo.hbm.xml" - }; - } - @Override - protected Class[] getAnnotatedClasses() { - return new Class[] { - Department.class, - Employee.class, - Title.class - }; - } - - @Override - public void configure(Configuration cfg) { - super.configure( cfg ); - cfg.setProperty( Environment.USE_QUERY_CACHE, true ); - cfg.setProperty( Environment.GENERATE_STATISTICS, true ); - } @Test - public void testSubSelectAsArithmeticOperand() { - inTransaction( + public void testSubSelectAsArithmeticOperand(SessionFactoryScope scope) { + scope.inTransaction( (s) -> { s.createQuery( "from Zoo z where ( select count(*) from Zoo ) = 0" ).list(); @@ -252,8 +241,8 @@ public void testSubSelectAsArithmeticOperand() { } @Test - @TestForIssue( jiraKey = "HHH-8432" ) - public void testExpandListParameter() { + @JiraKey( "HHH-8432" ) + public void testExpandListParameter(SessionFactoryScope scope) { final Object[] namesArray = new Object[] { "ZOO 1", "ZOO 2", "ZOO 3", "ZOO 4", "ZOO 5", "ZOO 6", "ZOO 7", "ZOO 8", "ZOO 9", "ZOO 10", "ZOO 11", "ZOO 12" @@ -263,7 +252,7 @@ public void testExpandListParameter() { "City 8", "City 9", "City 10", "City 11", "City 12" }; - inTransaction( + scope.inTransaction( (session) -> { Address address = new Address(); Zoo zoo = new Zoo( "ZOO 1", address ); @@ -272,7 +261,7 @@ public void testExpandListParameter() { } ); - inTransaction( + scope.inTransaction( (session) -> { List result = session.createQuery( "FROM Zoo z WHERE z.name IN (?1) and z.address.city IN (?2)" ) .setParameterList( 1, namesArray ) @@ -284,9 +273,9 @@ public void testExpandListParameter() { } @Test - @TestForIssue(jiraKey = "HHH-8699") - public void testBooleanPredicate() { - final Constructor created = fromTransaction( + @JiraKey( "HHH-8699") + public void testBooleanPredicate(SessionFactoryScope scope) { + final Constructor created = scope.fromTransaction( (session) -> { final Constructor constructor = new Constructor(); session.save( constructor ); @@ -296,7 +285,7 @@ public void testBooleanPredicate() { Constructor.resetConstructorExecutionCount(); - inTransaction( + scope.inTransaction( (session) -> { final String qry = "select new Constructor( c.id, c.id is not null, c.id = c.id, c.id + 1, concat( str(c.id), 'foo' ) ) from Constructor c where c.id = :id"; final Constructor result = session.createQuery(qry, Constructor.class).setParameter( "id", created.getId() ).uniqueResult(); @@ -314,8 +303,8 @@ public void testBooleanPredicate() { } @Test - public void testJpaTypeOperator() { - inTransaction( + public void testJpaTypeOperator(SessionFactoryScope scope) { + scope.inTransaction( session -> { // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // where clause @@ -352,8 +341,8 @@ public void testJpaTypeOperator() { } @Test - public void testComponentJoins() { - inTransaction( + public void testComponentJoins(SessionFactoryScope scope) { + scope.inTransaction( (s) -> { ComponentContainer root = new ComponentContainer( new ComponentContainer.Address( @@ -367,7 +356,7 @@ public void testComponentJoins() { } ); - inTransaction( + scope.inTransaction( (s) -> { List result = s.createQuery( "select a from ComponentContainer c join c.address a" ).list(); assertEquals( 1, result.size() ); @@ -389,9 +378,9 @@ public void testComponentJoins() { } @Test - @TestForIssue( jiraKey = "HHH-9642") - public void testLazyAssociationInComponent() { - inTransaction( + @JiraKey( "HHH-9642") + public void testLazyAssociationInComponent(SessionFactoryScope scope) { + scope.inTransaction( (session) -> { Address address = new Address(); Zoo zoo = new Zoo( "ZOO 1", address ); @@ -404,7 +393,7 @@ public void testLazyAssociationInComponent() { } ); - inTransaction( + scope.inTransaction( (session) -> { final Zoo zoo = (Zoo) session.createQuery( "from Zoo z" ).uniqueResult(); assertNotNull( zoo ); @@ -417,7 +406,7 @@ public void testLazyAssociationInComponent() { ); - inTransaction( + scope.inTransaction( (session) -> { final Zoo zoo = (Zoo) session.createQuery( "from Zoo z join fetch z.address.stateProvince" ).uniqueResult(); assertNotNull( zoo ); @@ -428,7 +417,7 @@ public void testLazyAssociationInComponent() { } ); - inTransaction( + scope.inTransaction( (session) -> { final Zoo zoo = (Zoo) session.createQuery( "from Zoo z join fetch z.address a join fetch a.stateProvince" ).uniqueResult(); assertNotNull( zoo ); @@ -441,9 +430,9 @@ public void testLazyAssociationInComponent() { } @Test - public void testJPAQLQualifiedIdentificationVariablesControl() { + public void testJPAQLQualifiedIdentificationVariablesControl(SessionFactoryScope scope) { // just checking syntax here... - Session s = openSession(); + Session s = scope.getSessionFactory().openSession(); s.beginTransaction(); s.createQuery( "from VariousKeywordPropertyEntity where type = 'something'" ).list(); s.createQuery( "from VariousKeywordPropertyEntity where value = 'something'" ).list(); @@ -460,8 +449,8 @@ public void testJPAQLQualifiedIdentificationVariablesControl() { @Test @SuppressWarnings( {"unchecked"}) - public void testJPAQLMapKeyQualifier() { - Session s = openSession(); + public void testJPAQLMapKeyQualifier(SessionFactoryScope scope) { + Session s = scope.getSessionFactory().openSession(); s.beginTransaction(); Human me = new Human(); me.setName( new Name( "Steve", null, "Ebersole" ) ); @@ -477,7 +466,7 @@ public void testJPAQLMapKeyQualifier() { // in SELECT clause { // hibernate-only form - s = openSession(); + s = scope.getSessionFactory().openSession(); s.beginTransaction(); List results = s.createQuery( "select distinct key(h.family) from Human h" ).list(); assertEquals( 1, results.size() ); @@ -489,7 +478,7 @@ public void testJPAQLMapKeyQualifier() { { // jpa form - s = openSession(); + s = scope.getSessionFactory().openSession(); s.beginTransaction(); List results = s.createQuery( "select distinct KEY(f) from Human h join h.family f" ).list(); assertEquals( 1, results.size() ); @@ -502,7 +491,7 @@ public void testJPAQLMapKeyQualifier() { // in WHERE clause { // hibernate-only form - s = openSession(); + s = scope.getSessionFactory().openSession(); s.beginTransaction(); Long count = (Long) s.createQuery( "select count(*) from Human h where KEY(h.family) = 'son'" ).uniqueResult(); assertEquals( (Long)1L, count ); @@ -512,7 +501,7 @@ public void testJPAQLMapKeyQualifier() { { // jpa form - s = openSession(); + s = scope.getSessionFactory().openSession(); s.beginTransaction(); Long count = (Long) s.createQuery( "select count(*) from Human h join h.family f where key(f) = 'son'" ).uniqueResult(); assertEquals( (Long)1L, count ); @@ -520,7 +509,7 @@ public void testJPAQLMapKeyQualifier() { s.close(); } - s = openSession(); + s = scope.getSessionFactory().openSession(); s.beginTransaction(); s.delete( me ); s.delete( joe ); @@ -530,8 +519,8 @@ public void testJPAQLMapKeyQualifier() { @Test @SuppressWarnings( {"unchecked"}) - public void testJPAQLMapEntryQualifier() { - Session s = openSession(); + public void testJPAQLMapEntryQualifier(SessionFactoryScope scope) { + Session s = scope.getSessionFactory().openSession(); s.beginTransaction(); Human me = new Human(); me.setName( new Name( "Steve", null, "Ebersole" ) ); @@ -547,7 +536,7 @@ public void testJPAQLMapEntryQualifier() { // in SELECT clause { // hibernate-only form - s = openSession(); + s = scope.getSessionFactory().openSession(); s.beginTransaction(); List results = s.createQuery( "select entry(h.family) from Human h" ).list(); assertEquals( 1, results.size() ); @@ -562,7 +551,7 @@ public void testJPAQLMapEntryQualifier() { { // jpa form - s = openSession(); + s = scope.getSessionFactory().openSession(); s.beginTransaction(); List results = s.createQuery( "select ENTRY(f) from Human h join h.family f" ).list(); assertEquals( 1, results.size() ); @@ -578,7 +567,7 @@ public void testJPAQLMapEntryQualifier() { // not exactly sure of the syntax of ENTRY in the WHERE clause... - s = openSession(); + s = scope.getSessionFactory().openSession(); s.beginTransaction(); s.delete( me ); s.delete( joe ); @@ -588,8 +577,8 @@ public void testJPAQLMapEntryQualifier() { @Test @SuppressWarnings( {"unchecked"}) - public void testJPAQLMapValueQualifier() { - Session s = openSession(); + public void testJPAQLMapValueQualifier(SessionFactoryScope scope) { + Session s = scope.getSessionFactory().openSession(); s.beginTransaction(); Human me = new Human(); me.setName( new Name( "Steve", null, "Ebersole" ) ); @@ -605,7 +594,7 @@ public void testJPAQLMapValueQualifier() { // in SELECT clause { // hibernate-only form - s = openSession(); + s = scope.getSessionFactory().openSession(); s.beginTransaction(); List results = s.createQuery( "select value(h.family) from Human h" ).list(); assertEquals( 1, results.size() ); @@ -617,7 +606,7 @@ public void testJPAQLMapValueQualifier() { { // jpa form - s = openSession(); + s = scope.getSessionFactory().openSession(); s.beginTransaction(); List results = s.createQuery( "select VALUE(f) from Human h join h.family f" ).list(); assertEquals( 1, results.size() ); @@ -630,7 +619,7 @@ public void testJPAQLMapValueQualifier() { // in WHERE clause { // hibernate-only form - s = openSession(); + s = scope.getSessionFactory().openSession(); s.beginTransaction(); Long count = (Long) s.createQuery( "select count(*) from Human h where VALUE(h.family) = :joe" ).setParameter( "joe", joe ).uniqueResult(); // ACTUALLY EXACTLY THE SAME AS: @@ -642,7 +631,7 @@ public void testJPAQLMapValueQualifier() { { // jpa form - s = openSession(); + s = scope.getSessionFactory().openSession(); s.beginTransaction(); Long count = (Long) s.createQuery( "select count(*) from Human h join h.family f where value(f) = :joe" ).setParameter( "joe", joe ).uniqueResult(); // ACTUALLY EXACTLY THE SAME AS: @@ -652,7 +641,7 @@ public void testJPAQLMapValueQualifier() { s.close(); } - s = openSession(); + s = scope.getSessionFactory().openSession(); s.beginTransaction(); s.delete( me ); s.delete( joe ); @@ -661,9 +650,9 @@ public void testJPAQLMapValueQualifier() { } @Test - @RequiresDialectFeature( DialectChecks.SupportsSubqueryInSelect.class ) - public void testPaginationWithPolymorphicQuery() { - Session s = openSession(); + @RequiresDialectFeature( feature = DialectFeatureChecks.SupportsSubqueryInSelect.class ) + public void testPaginationWithPolymorphicQuery(SessionFactoryScope scope) { + Session s = scope.getSessionFactory().openSession(); s.beginTransaction(); Human h = new Human(); h.setName( new Name( "Steve", null, "Ebersole" ) ); @@ -671,14 +660,14 @@ public void testPaginationWithPolymorphicQuery() { s.getTransaction().commit(); s.close(); - s = openSession(); + s = scope.getSessionFactory().openSession(); s.beginTransaction(); List results = s.createQuery( "from java.lang.Object" ).setMaxResults( 2 ).list(); assertEquals( 1, results.size() ); s.getTransaction().commit(); s.close(); - s = openSession(); + s = scope.getSessionFactory().openSession(); s.beginTransaction(); s.delete( h ); s.getTransaction().commit(); @@ -686,10 +675,10 @@ public void testPaginationWithPolymorphicQuery() { } @Test - @TestForIssue( jiraKey = "HHH-2045" ) + @JiraKey( "HHH-2045" ) @RequiresDialect( H2Dialect.class ) - public void testEmptyInList() { - Session session = openSession(); + public void testEmptyInList(SessionFactoryScope scope) { + Session session = scope.getSessionFactory().openSession(); session.beginTransaction(); Human human = new Human(); human.setName( new Name( "Lukasz", null, "Antoniak" ) ); @@ -698,14 +687,14 @@ public void testEmptyInList() { session.getTransaction().commit(); session.close(); - session = openSession(); + session = scope.getSessionFactory().openSession(); session.beginTransaction(); List results = session.createQuery( "from Human h where h.nickName in ()" ).list(); assertEquals( 0, results.size() ); session.getTransaction().commit(); session.close(); - session = openSession(); + session = scope.getSessionFactory().openSession(); session.beginTransaction(); session.delete( human ); session.getTransaction().commit(); @@ -713,9 +702,9 @@ public void testEmptyInList() { } @Test - @TestForIssue( jiraKey = "HHH-8901" ) - public void testEmptyInListForDialectsNotSupportsEmptyInList() { - Session session = openSession(); + @JiraKey( "HHH-8901" ) + public void testEmptyInListForDialectsNotSupportsEmptyInList(SessionFactoryScope scope) { + Session session = scope.getSessionFactory().openSession(); session.beginTransaction(); Human human = new Human(); human.setName( new Name( "Lukasz", null, "Antoniak" ) ); @@ -724,7 +713,7 @@ public void testEmptyInListForDialectsNotSupportsEmptyInList() { session.getTransaction().commit(); session.close(); - session = openSession(); + session = scope.getSessionFactory().openSession(); session.beginTransaction(); List results = session.createQuery( "from Human h where h.nickName in (:nickNames)" ) .setParameter( "nickNames", Collections.emptySet() ) @@ -733,7 +722,7 @@ public void testEmptyInListForDialectsNotSupportsEmptyInList() { session.getTransaction().commit(); session.close(); - session = openSession(); + session = scope.getSessionFactory().openSession(); session.beginTransaction(); session.delete( human ); session.getTransaction().commit(); @@ -741,9 +730,9 @@ public void testEmptyInListForDialectsNotSupportsEmptyInList() { } @Test - @TestForIssue( jiraKey = "HHH-2851") - public void testMultipleRefsToSameParam() { - Session s = openSession(); + @JiraKey( "HHH-2851") + public void testMultipleRefsToSameParam(SessionFactoryScope scope) { + Session s = scope.getSessionFactory().openSession(); s.beginTransaction(); Human h = new Human(); h.setName( new Name( "Johnny", 'B', "Goode" ) ); @@ -764,7 +753,7 @@ public void testMultipleRefsToSameParam() { s.getTransaction().commit(); s.close(); - s = openSession(); + s = scope.getSessionFactory().openSession(); s.beginTransaction(); List results = s.createQuery( "from Human where name.first = :name or name.last=:name" ) .setParameter( "name", "Johnny" ) @@ -813,7 +802,7 @@ public void testMultipleRefsToSameParam() { s.getTransaction().commit(); s.close(); - s = openSession(); + s = scope.getSessionFactory().openSession(); s.beginTransaction(); s.createQuery( "delete Human" ).executeUpdate(); s.getTransaction().commit(); @@ -821,8 +810,8 @@ public void testMultipleRefsToSameParam() { } @Test - public void testComponentNullnessChecks() { - Session s = openSession(); + public void testComponentNullnessChecks(SessionFactoryScope scope) { + Session s = scope.getSessionFactory().openSession(); s.beginTransaction(); Human h = new Human(); h.setName( new Name( "Johnny", 'B', "Goode" ) ); @@ -839,18 +828,19 @@ public void testComponentNullnessChecks() { s.getTransaction().commit(); s.close(); - s = openSession(); + s = scope.getSessionFactory().openSession(); s.beginTransaction(); List results = s.createQuery( "from Human where name is null" ).list(); assertEquals( 1, results.size() ); results = s.createQuery( "from Human where name is not null" ).list(); assertEquals( 3, results.size() ); + Dialect dialect = scope.getSessionFactory().getJdbcServices().getDialect(); String query = - ( getDialect() instanceof DB2Dialect || getDialect() instanceof HSQLDialect ) ? + ( dialect instanceof DB2Dialect || dialect instanceof HSQLDialect ) ? "from Human where cast(?1 as string) is null" : "from Human where ?1 is null" ; - if ( getDialect() instanceof DerbyDialect ) { + if ( dialect instanceof DerbyDialect ) { s.createQuery( query ).setParameter( 1, "null" ).list(); } else { @@ -860,7 +850,7 @@ public void testComponentNullnessChecks() { s.getTransaction().commit(); s.close(); - s = openSession(); + s = scope.getSessionFactory().openSession(); s.beginTransaction(); s.createQuery( "delete Human" ).executeUpdate(); s.getTransaction().commit(); @@ -868,9 +858,9 @@ public void testComponentNullnessChecks() { } @Test - @TestForIssue( jiraKey = "HHH-4150" ) - public void testSelectClauseCaseWithSum() { - Session s = openSession(); + @JiraKey( "HHH-4150" ) + public void testSelectClauseCaseWithSum(SessionFactoryScope scope) { + Session s = scope.getSessionFactory().openSession(); Transaction t = s.beginTransaction(); Human h1 = new Human(); @@ -900,9 +890,10 @@ public void testSelectClauseCaseWithSum() { } @Test - @TestForIssue( jiraKey = "HHH-4150" ) - public void testSelectClauseCaseWithCountDistinct() { - Session s = openSession(); + @JiraKey( "HHH-4150" ) + @SkipForDialect( dialectClass = InformixDialect.class, majorVersion = 11, minorVersion = 70, reason = "Informix does not support case with count distinct") + public void testSelectClauseCaseWithCountDistinct(SessionFactoryScope scope) { + Session s = scope.getSessionFactory().openSession(); Transaction t = s.beginTransaction(); Human h1 = new Human(); @@ -937,12 +928,12 @@ public void testSelectClauseCaseWithCountDistinct() { } @Test - public void testInvalidCollectionDereferencesFail() { + public void testInvalidCollectionDereferencesFail(SessionFactoryScope scope) { - try ( final SessionImplementor s = (SessionImplementor) openSession() ) { + try ( final SessionImplementor s = (SessionImplementor) scope.getSessionFactory().openSession() ) { // control group... - inTransaction( + scope.inTransaction( s, session -> { s.createQuery( "from Animal a join a.offspring o where o.description = 'xyz'" ).list(); @@ -952,7 +943,7 @@ public void testInvalidCollectionDereferencesFail() { } ); - inTransaction( + scope.inTransaction( s, session -> { try { @@ -968,7 +959,7 @@ public void testInvalidCollectionDereferencesFail() { } ); - inTransaction( + scope.inTransaction( s, session -> { try { @@ -984,7 +975,7 @@ public void testInvalidCollectionDereferencesFail() { } ); - inTransaction( + scope.inTransaction( s, session -> { try { @@ -1000,7 +991,7 @@ public void testInvalidCollectionDereferencesFail() { } ); - inTransaction( + scope.inTransaction( s, session -> { try { @@ -1019,9 +1010,9 @@ public void testInvalidCollectionDereferencesFail() { } @Test - public void testConcatenation() { + public void testConcatenation(SessionFactoryScope scope) { // simple syntax checking... - Session s = openSession(); + Session s = scope.getSessionFactory().openSession(); s.beginTransaction(); s.createQuery( "from Human h where h.nickName = '1' || 'ov' || 'tha' || 'few'" ).list(); s.getTransaction().commit(); @@ -1029,13 +1020,14 @@ public void testConcatenation() { } @Test - @SkipForDialect(value = CockroachDialect.class, comment = "https://github.com/cockroachdb/cockroach/issues/41943") - public void testExpressionWithParamInFunction() { - Session s = openSession(); + @SkipForDialect(dialectClass = CockroachDialect.class, matchSubTypes = true ,reason = "https://github.com/cockroachdb/cockroach/issues/41943") + public void testExpressionWithParamInFunction(SessionFactoryScope scope) { + Session s = scope.getSessionFactory().openSession(); s.beginTransaction(); s.createQuery( "from Animal a where abs(a.bodyWeight-:param) < 2.0" ).setParameter( "param", 1 ).list(); s.createQuery( "from Animal a where abs(:param - a.bodyWeight) < 2.0" ).setParameter( "param", 1 ).list(); - if ( getDialect() instanceof HSQLDialect || getDialect() instanceof DB2Dialect || getDialect() instanceof DerbyDialect ) { + Dialect dialect = scope.getSessionFactory().getJdbcServices().getDialect(); + if ( dialect instanceof HSQLDialect || dialect instanceof DB2Dialect || dialect instanceof DerbyDialect ) { // HSQLDB and DB2 don't like the abs(? - ?) syntax. bit work if at least one parameter is typed... s.createQuery( "from Animal where abs(cast(:x as long) - :y) < 2.0" ).setParameter( "x", 1 ).setParameter( "y", 1 ).list(); s.createQuery( "from Animal where abs(:x - cast(:y as long)) < 2.0" ).setParameter( "x", 1 ).setParameter( "y", 1 ).list(); @@ -1045,7 +1037,7 @@ public void testExpressionWithParamInFunction() { s.createQuery( "from Animal where abs(:x - :y) < 2.0" ).setParameter( "x", 1 ).setParameter( "y", 1 ).list(); } - if ( getDialect() instanceof DB2Dialect ) { + if ( dialect instanceof DB2Dialect ) { s.createQuery( "from Animal where lower(upper(cast(:foo as string))) like 'f%'" ).setParameter( "foo", "foo" ).list(); } else { @@ -1054,17 +1046,17 @@ public void testExpressionWithParamInFunction() { s.createQuery( "from Animal a where abs(abs(a.bodyWeight - 1.0 + :param) * abs(length('ffobar')-3)) = 3.0" ).setParameter( "param", 1 ).list(); - if ( getDialect() instanceof DB2Dialect ) { + if ( dialect instanceof DB2Dialect ) { s.createQuery( "from Animal where lower(upper('foo') || upper(cast(:bar as string))) like 'f%'" ).setParameter( "bar", "xyz" ).list(); } else { s.createQuery( "from Animal where lower(upper('foo') || upper(:bar)) like 'f%'" ).setParameter( "bar", "xyz" ).list(); } - if ( getDialect() instanceof AbstractHANADialect ) { + if ( dialect instanceof AbstractHANADialect ) { s.createQuery( "from Animal where abs(cast(1 as double) - cast(:param as double)) = 1.0" ).setParameter( "param", 1 ).list(); } - else if ( !( getDialect() instanceof PostgreSQLDialect || getDialect() instanceof MySQLDialect ) ) { + else if ( !( dialect instanceof PostgreSQLDialect || dialect instanceof MySQLDialect ) ) { s.createQuery( "from Animal where abs(cast(1 as float) - cast(:param as float)) = 1.0" ).setParameter( "param", 1 ).list(); } @@ -1073,12 +1065,12 @@ else if ( !( getDialect() instanceof PostgreSQLDialect || getDialect() instanceo } @Test - public void testCrazyIdFieldNames() { + public void testCrazyIdFieldNames(SessionFactoryScope scope) { MoreCrazyIdFieldNameStuffEntity top = new MoreCrazyIdFieldNameStuffEntity( "top" ); HeresAnotherCrazyIdFieldName next = new HeresAnotherCrazyIdFieldName( "next" ); top.setHeresAnotherCrazyIdFieldName( next ); MoreCrazyIdFieldNameStuffEntity other = new MoreCrazyIdFieldNameStuffEntity( "other" ); - Session s = openSession(); + Session s = scope.getSessionFactory().openSession(); s.beginTransaction(); s.save( next ); s.save( top ); @@ -1108,15 +1100,15 @@ public void testCrazyIdFieldNames() { } @Test - @TestForIssue( jiraKey = "HHH-2257" ) - public void testImplicitJoinsInDifferentClauses() { + @JiraKey( "HHH-2257" ) + public void testImplicitJoinsInDifferentClauses(SessionFactoryScope scope) { // both the classic and ast translators output the same syntactically valid sql // for all of these cases; the issue is that shallow (iterate) and // non-shallow (list/scroll) queries return different results because the // shallow skips the inner join which "weeds out" results from the non-shallow queries. // The results were initially different depending upon the clause(s) in which the // implicit join occurred - Session s = openSession(); + Session s = scope.getSessionFactory().openSession(); s.beginTransaction(); SimpleEntityWithAssociation owner = new SimpleEntityWithAssociation( "owner" ); SimpleAssociatedEntity e1 = new SimpleAssociatedEntity( "thing one", owner ); @@ -1127,19 +1119,19 @@ public void testImplicitJoinsInDifferentClauses() { s.getTransaction().commit(); s.close(); - checkCounts( "select e.owner from SimpleAssociatedEntity e", 1, "implicit-join in select clause" ); - checkCounts( "select e.id, e.owner from SimpleAssociatedEntity e", 1, "implicit-join in select clause" ); + checkCounts( scope, "select e.owner from SimpleAssociatedEntity e", 1, "implicit-join in select clause" ); + checkCounts( scope, "select e.id, e.owner from SimpleAssociatedEntity e", 1, "implicit-join in select clause" ); // resolved to a "id short cut" when part of the order by clause -> no inner join = no weeding out... - checkCounts( "from SimpleAssociatedEntity e order by e.owner", 2, "implicit-join in order-by clause" ); + checkCounts( scope, "from SimpleAssociatedEntity e order by e.owner", 2, "implicit-join in order-by clause" ); // resolved to a "id short cut" when part of the group by clause -> no inner join = no weeding out... - checkCounts( + checkCounts( scope, "select e.owner.id, count(*) from SimpleAssociatedEntity e group by e.owner", 2, "implicit-join in select and group-by clauses" ); - s = openSession(); + s = scope.getSessionFactory().openSession(); s.beginTransaction(); s.delete( e1 ); s.delete( e2 ); @@ -1149,8 +1141,8 @@ public void testImplicitJoinsInDifferentClauses() { } @Test - public void testRowValueConstructorSyntaxInInList() { - Session s = openSession(); + public void testRowValueConstructorSyntaxInInList(SessionFactoryScope scope) { + Session s = scope.getSessionFactory().openSession(); s.beginTransaction(); Product product = new Product(); product.setDescription( "My Product" ); @@ -1197,8 +1189,8 @@ public void testRowValueConstructorSyntaxInInList() { } - private void checkCounts(String hql, int expected, String testCondition) { - inTransaction( + private void checkCounts(SessionFactoryScope scope, String hql, int expected, String testCondition) { + scope.inTransaction( session -> { int count = determineCount( session.createQuery( hql ).list().iterator() ); assertEquals( "list() [" + testCondition + "]", expected, count ); @@ -1207,13 +1199,13 @@ private void checkCounts(String hql, int expected, String testCondition) { } @Test - @TestForIssue( jiraKey = "HHH-2257" ) - public void testImplicitSelectEntityAssociationInShallowQuery() { + @JiraKey( "HHH-2257" ) + public void testImplicitSelectEntityAssociationInShallowQuery(SessionFactoryScope scope) { // both the classic and ast translators output the same syntactically valid sql. // the issue is that shallow and non-shallow queries return different // results because the shallow skips the inner join which "weeds out" results // from the non-shallow queries... - Session s = openSession(); + Session s = scope.getSessionFactory().openSession(); s.beginTransaction(); SimpleEntityWithAssociation owner = new SimpleEntityWithAssociation( "owner" ); SimpleAssociatedEntity e1 = new SimpleAssociatedEntity( "thing one", owner ); @@ -1224,7 +1216,7 @@ public void testImplicitSelectEntityAssociationInShallowQuery() { s.getTransaction().commit(); s.close(); - s = openSession(); + s = scope.getSessionFactory().openSession(); s.beginTransaction(); int count = determineCount( s.createQuery( "select e.id, e.owner from SimpleAssociatedEntity e" ).list().iterator() ); // thing two would be removed from the result due to the inner join @@ -1232,7 +1224,7 @@ public void testImplicitSelectEntityAssociationInShallowQuery() { s.getTransaction().commit(); s.close(); - s = openSession(); + s = scope.getSessionFactory().openSession(); s.beginTransaction(); s.delete( e1 ); s.delete( e2 ); @@ -1250,29 +1242,29 @@ private int determineCount(Iterator iterator) { return count; } - @Test - @TestForIssue( jiraKey = "HHH-6714" ) - public void testUnaryMinus(){ - Session s = openSession(); - s.beginTransaction(); - Human stliu = new Human(); - stliu.setIntValue( 26 ); + @Test + @JiraKey( "HHH-6714" ) + public void testUnaryMinus(SessionFactoryScope scope) { + Session s = scope.getSessionFactory().openSession(); + s.beginTransaction(); + Human stliu = new Human(); + stliu.setIntValue( 26 ); - s.persist( stliu ); - s.getTransaction().commit(); - s.clear(); - s.beginTransaction(); - List list =s.createQuery( "from Human h where -(h.intValue - 100)=74" ).list(); - assertEquals( 1, list.size() ); - s.getTransaction().commit(); - s.close(); + s.persist( stliu ); + s.getTransaction().commit(); + s.clear(); + s.beginTransaction(); + List list = s.createQuery( "from Human h where -(h.intValue - 100)=74" ).list(); + assertEquals( 1, list.size() ); + s.getTransaction().commit(); + s.close(); - } + } @Test - public void testEntityAndOneToOneReturnedByQuery() { - Session s = openSession(); + public void testEntityAndOneToOneReturnedByQuery(SessionFactoryScope scope) { + Session s = scope.getSessionFactory().openSession(); s.beginTransaction(); Human h = new Human(); h.setName( new Name( "Gail", null, "Badner" ) ); @@ -1284,7 +1276,7 @@ public void testEntityAndOneToOneReturnedByQuery() { s.getTransaction().commit(); s.close(); - s = openSession(); + s = scope.getSessionFactory().openSession(); s.beginTransaction(); Object [] result = ( Object [] ) s.createQuery( "from User u, Human h where u.human = h" ).uniqueResult(); assertNotNull( result ); @@ -1298,8 +1290,8 @@ public void testEntityAndOneToOneReturnedByQuery() { } @Test - @TestForIssue( jiraKey = "HHH-9305") - public void testExplicitToOneInnerJoin() { + @JiraKey( "HHH-9305") + public void testExplicitToOneInnerJoin(SessionFactoryScope scope) { final Employee employee1 = new Employee(); employee1.setFirstName( "Jane" ); employee1.setLastName( "Doe" ); @@ -1317,7 +1309,7 @@ public void testExplicitToOneInnerJoin() { title2.setDescription( "John's title" ); employee2.setTitle( title2 ); - Session s = openSession(); + Session s = scope.getSessionFactory().openSession(); s.getTransaction().begin(); s.persist( title1 ); s.persist( dept1 ); @@ -1327,7 +1319,7 @@ public void testExplicitToOneInnerJoin() { s.getTransaction().commit(); s.close(); - s = openSession(); + s = scope.getSessionFactory().openSession(); s.getTransaction().begin(); Department department = (Department) s.createQuery( "select e.department from Employee e inner join e.department" ).uniqueResult(); assertEquals( employee1.getDepartment().getDeptName(), department.getDeptName() ); @@ -1341,7 +1333,7 @@ public void testExplicitToOneInnerJoin() { } @Test - public void testExplicitToOneOuterJoin() { + public void testExplicitToOneOuterJoin(SessionFactoryScope scope) { final Employee employee1 = new Employee(); employee1.setFirstName( "Jane" ); employee1.setLastName( "Doe" ); @@ -1359,7 +1351,7 @@ public void testExplicitToOneOuterJoin() { title2.setDescription( "John's title" ); employee2.setTitle( title2 ); - Session s = openSession(); + Session s = scope.getSessionFactory().openSession(); s.getTransaction().begin(); s.persist( title1 ); s.persist( dept1 ); @@ -1368,7 +1360,7 @@ public void testExplicitToOneOuterJoin() { s.persist( employee2 ); s.getTransaction().commit(); s.close(); - s = openSession(); + s = scope.getSessionFactory().openSession(); s.getTransaction().begin(); List list = s.createQuery( "select e.department from Employee e left join e.department" ).list(); assertEquals( 2, list.size() ); @@ -1391,7 +1383,7 @@ public void testExplicitToOneOuterJoin() { } @Test - public void testExplicitToOneInnerJoinAndImplicitToOne() { + public void testExplicitToOneInnerJoinAndImplicitToOne(SessionFactoryScope scope) { final Employee employee1 = new Employee(); employee1.setFirstName( "Jane" ); employee1.setLastName( "Doe" ); @@ -1409,7 +1401,7 @@ public void testExplicitToOneInnerJoinAndImplicitToOne() { title2.setDescription( "John's title" ); employee2.setTitle( title2 ); - Session s = openSession(); + Session s = scope.getSessionFactory().openSession(); s.getTransaction().begin(); s.persist( title1 ); s.persist( dept1 ); @@ -1418,7 +1410,7 @@ public void testExplicitToOneInnerJoinAndImplicitToOne() { s.persist( employee2 ); s.getTransaction().commit(); s.close(); - s = openSession(); + s = scope.getSessionFactory().openSession(); s.getTransaction().begin(); Object[] result = (Object[]) s.createQuery( "select e.firstName, e.lastName, e.title.description, e.department from Employee e inner join e.department" @@ -1437,7 +1429,7 @@ public void testExplicitToOneInnerJoinAndImplicitToOne() { } @Test - public void testNestedComponentIsNull() { + public void testNestedComponentIsNull(SessionFactoryScope scope) { // (1) From MapTest originally... // (2) Was then moved into HQLTest... // (3) However, a bug fix to EntityType#getIdentifierOrUniqueKeyType (HHH-2138) @@ -1447,57 +1439,57 @@ public void testNestedComponentIsNull() { // // fyi... found and fixed the problem in the classic parser; still // leaving here for syntax checking - new SyntaxChecker( "from Commento c where c.marelo.commento.mcompr is null" ).checkAll(); + new SyntaxChecker( scope, "from Commento c where c.marelo.commento.mcompr is null" ).checkAll(); } @Test - @TestForIssue( jiraKey = "HHH-939" ) - public void testSpecialClassPropertyReference() { + @JiraKey( "HHH-939" ) + public void testSpecialClassPropertyReference(SessionFactoryScope scope) { // this is a long standing bug in Hibernate when applied to joined-subclasses; // see HHH-939 for details and history - new SyntaxChecker( "from Zoo zoo where zoo.class = PettingZoo" ).checkAll(); - new SyntaxChecker( "select a.description from Animal a where a.class = Mammal" ).checkAll(); - new SyntaxChecker( "select a.class from Animal a" ).checkAll(); - new SyntaxChecker( "from DomesticAnimal an where an.class = Dog" ).checkAll(); - new SyntaxChecker( "from Animal an where an.class = Dog" ).checkAll(); + new SyntaxChecker( scope, "from Zoo zoo where zoo.class = PettingZoo" ).checkAll(); + new SyntaxChecker( scope, "select a.description from Animal a where a.class = Mammal" ).checkAll(); + new SyntaxChecker( scope, "select a.class from Animal a" ).checkAll(); + new SyntaxChecker( scope, "from DomesticAnimal an where an.class = Dog" ).checkAll(); + new SyntaxChecker( scope, "from Animal an where an.class = Dog" ).checkAll(); } @Test - @TestForIssue( jiraKey = "HHH-2376" ) - public void testSpecialClassPropertyReferenceFQN() { - new SyntaxChecker( "from Zoo zoo where zoo.class = org.hibernate.orm.test.hql.PettingZoo" ).checkAll(); - new SyntaxChecker( "select a.description from Animal a where a.class = org.hibernate.orm.test.hql.Mammal" ).checkAll(); - new SyntaxChecker( "from DomesticAnimal an where an.class = org.hibernate.orm.test.hql.Dog" ).checkAll(); - new SyntaxChecker( "from Animal an where an.class = org.hibernate.orm.test.hql.Dog" ).checkAll(); + @JiraKey( "HHH-2376" ) + public void testSpecialClassPropertyReferenceFQN(SessionFactoryScope scope) { + new SyntaxChecker( scope, "from Zoo zoo where zoo.class = org.hibernate.orm.test.hql.PettingZoo" ).checkAll(); + new SyntaxChecker( scope, "select a.description from Animal a where a.class = org.hibernate.orm.test.hql.Mammal" ).checkAll(); + new SyntaxChecker( scope, "from DomesticAnimal an where an.class = org.hibernate.orm.test.hql.Dog" ).checkAll(); + new SyntaxChecker( scope, "from Animal an where an.class = org.hibernate.orm.test.hql.Dog" ).checkAll(); } @Test - @TestForIssue( jiraKey = "HHH-1631" ) - public void testSubclassOrSuperclassPropertyReferenceInJoinedSubclass() { + @JiraKey( "HHH-1631" ) + public void testSubclassOrSuperclassPropertyReferenceInJoinedSubclass(SessionFactoryScope scope) { // this is a long standing bug in Hibernate; see HHH-1631 for details and history // // (1) pregnant is defined as a property of the class (Mammal) itself // (2) description is defined as a property of the superclass (Animal) // (3) name is defined as a property of a particular subclass (Human) - new SyntaxChecker( "from Zoo z join z.mammals as m where m.name.first = 'John'" ).checkAll(); + new SyntaxChecker( scope, "from Zoo z join z.mammals as m where m.name.first = 'John'" ).checkAll(); - new SyntaxChecker( "from Zoo z join z.mammals as m where m.pregnant = false" ).checkAll(); - new SyntaxChecker( "select m.pregnant from Zoo z join z.mammals as m where m.pregnant = false" ).checkAll(); + new SyntaxChecker( scope, "from Zoo z join z.mammals as m where m.pregnant = false" ).checkAll(); + new SyntaxChecker( scope, "select m.pregnant from Zoo z join z.mammals as m where m.pregnant = false" ).checkAll(); - new SyntaxChecker( "from Zoo z join z.mammals as m where m.description = 'tabby'" ).checkAll(); - new SyntaxChecker( "select m.description from Zoo z join z.mammals as m where m.description = 'tabby'" ).checkAll(); + new SyntaxChecker( scope, "from Zoo z join z.mammals as m where m.description = 'tabby'" ).checkAll(); + new SyntaxChecker( scope, "select m.description from Zoo z join z.mammals as m where m.description = 'tabby'" ).checkAll(); - new SyntaxChecker( "from Zoo z join z.mammals as m where m.name.first = 'John'" ).checkAll(); - new SyntaxChecker( "select m.name from Zoo z join z.mammals as m where m.name.first = 'John'" ).checkAll(); + new SyntaxChecker( scope, "from Zoo z join z.mammals as m where m.name.first = 'John'" ).checkAll(); + new SyntaxChecker( scope, "select m.name from Zoo z join z.mammals as m where m.name.first = 'John'" ).checkAll(); - new SyntaxChecker( "select m.pregnant from Zoo z join z.mammals as m" ).checkAll(); - new SyntaxChecker( "select m.description from Zoo z join z.mammals as m" ).checkAll(); - new SyntaxChecker( "select m.name from Zoo z join z.mammals as m" ).checkAll(); + new SyntaxChecker( scope, "select m.pregnant from Zoo z join z.mammals as m" ).checkAll(); + new SyntaxChecker( scope, "select m.description from Zoo z join z.mammals as m" ).checkAll(); + new SyntaxChecker( scope, "select m.name from Zoo z join z.mammals as m" ).checkAll(); - new SyntaxChecker( "from DomesticAnimal da join da.owner as o where o.nickName = 'Gavin'" ).checkAll(); - new SyntaxChecker( "select da.father from DomesticAnimal da join da.owner as o where o.nickName = 'Gavin'" ).checkAll(); - new SyntaxChecker( "select da.father from DomesticAnimal da where da.owner.nickName = 'Gavin'" ).checkAll(); + new SyntaxChecker( scope, "from DomesticAnimal da join da.owner as o where o.nickName = 'Gavin'" ).checkAll(); + new SyntaxChecker( scope, "select da.father from DomesticAnimal da join da.owner as o where o.nickName = 'Gavin'" ).checkAll(); + new SyntaxChecker( scope, "select da.father from DomesticAnimal da where da.owner.nickName = 'Gavin'" ).checkAll(); } /** @@ -1506,19 +1498,19 @@ public void testSubclassOrSuperclassPropertyReferenceInJoinedSubclass() { * keyword. */ @Test - public void testExplicitEntityCasting() { - new SyntaxChecker( "from Zoo z join treat(z.mammals as Human) as m where m.name.first = 'John'" ).checkAll(); - new SyntaxChecker( "from Zoo z join z.mammals as m where treat(m as Human).name.first = 'John'" ).checkAll(); + public void testExplicitEntityCasting(SessionFactoryScope scope) { + new SyntaxChecker( scope, "from Zoo z join treat(z.mammals as Human) as m where m.name.first = 'John'" ).checkAll(); + new SyntaxChecker( scope, "from Zoo z join z.mammals as m where treat(m as Human).name.first = 'John'" ).checkAll(); } @Test @RequiresDialectFeature( - value = DialectChecks.SupportLimitAndOffsetCheck.class, + feature = DialectFeatureChecks.SupportLimitAndOffsetCheck.class, comment = "dialect does not support offset and limit combo" ) - public void testSimpleSelectWithLimitAndOffset() throws Exception { + public void testSimpleSelectWithLimitAndOffset(SessionFactoryScope scope) throws Exception { // just checking correctness of param binding code... - Session session = openSession(); + Session session = scope.getSessionFactory().openSession(); Transaction t = session.beginTransaction(); session.createQuery( "from Animal" ) .setFirstResult( 2 ) @@ -1529,8 +1521,8 @@ public void testSimpleSelectWithLimitAndOffset() throws Exception { } @Test - public void testJPAPositionalParameterList() { - Session s = openSession(); + public void testJPAPositionalParameterList(SessionFactoryScope scope) { + Session s = scope.getSessionFactory().openSession(); s.beginTransaction(); ArrayList params = new ArrayList(); params.add( "Doe" ); @@ -1572,8 +1564,8 @@ public void testJPAPositionalParameterList() { } @Test - public void testComponentQueries() { - inTransaction( + public void testComponentQueries(SessionFactoryScope scope) { + scope.inTransaction( session -> { final QueryImplementor query = session.createQuery( "select h.name from Human h" ); final SqmSelectStatement sqmStatement = (SqmSelectStatement) query.unwrap( QuerySqmImpl.class ).getSqmStatement(); @@ -1602,10 +1594,10 @@ public void testComponentQueries() { } @Test - @TestForIssue( jiraKey = "HHH-1774" ) - @RequiresDialectFeature( DialectChecks.SupportsSubqueryInSelect.class ) - public void testComponentParameterBinding() { - Session s = openSession(); + @JiraKey( "HHH-1774" ) + @RequiresDialectFeature( feature = DialectFeatureChecks.SupportsSubqueryInSelect.class ) + public void testComponentParameterBinding(SessionFactoryScope scope) { + Session s = scope.getSessionFactory().openSession(); s.beginTransaction(); Order.Id oId = new Order.Id( "1234", 1 ); @@ -1628,8 +1620,8 @@ public void testComponentParameterBinding() { @SuppressWarnings( {"unchecked"}) @Test - public void testAnyMappingReference() { - Session s = openSession(); + public void testAnyMappingReference(SessionFactoryScope scope) { + Session s = scope.getSessionFactory().openSession(); s.beginTransaction(); PropertyValue redValue = new StringPropertyValue( "red" ); @@ -1664,8 +1656,8 @@ public void testAnyMappingReference() { } @Test - public void testJdkEnumStyleEnumConstant() throws Exception { - Session s = openSession(); + public void testJdkEnumStyleEnumConstant(SessionFactoryScope scope) throws Exception { + Session s = scope.getSessionFactory().openSession(); s.beginTransaction(); s.createQuery( "from Zoo z where z.classification = org.hibernate.orm.test.hql.Classification.LAME" ).list(); @@ -1676,9 +1668,9 @@ public void testJdkEnumStyleEnumConstant() throws Exception { @Test @FailureExpected( jiraKey = "unknown" ) - public void testParameterTypeMismatch() { - try ( final SessionImplementor s = (SessionImplementor) openSession() ) { - inTransaction( + public void testParameterTypeMismatch(SessionFactoryScope scope) { + try ( final SessionImplementor s = (SessionImplementor) scope.getSessionFactory().openSession() ) { + scope.inTransaction( s, session -> { try { @@ -1699,9 +1691,9 @@ public void testParameterTypeMismatch() { } @Test - public void testMultipleBagFetchesFail() { - try ( final SessionImplementor s = (SessionImplementor) openSession() ) { - inTransaction( + public void testMultipleBagFetchesFail(SessionFactoryScope scope) { + try ( final SessionImplementor s = (SessionImplementor) scope.getSessionFactory().openSession() ) { + scope.inTransaction( s, session-> { try { @@ -1720,8 +1712,8 @@ public void testMultipleBagFetchesFail() { } @Test - @TestForIssue( jiraKey = "HHH-1248" ) - public void testCollectionJoinsInSubselect() { + @JiraKey( "HHH-1248" ) + public void testCollectionJoinsInSubselect(SessionFactoryScope scope) { // HHH-1248 : initially FromElementFactory treated any explicit join // as an implied join so that theta-style joins would always be used. // This was because correlated subqueries cannot use ANSI-style joins @@ -1729,7 +1721,7 @@ public void testCollectionJoinsInSubselect() { // to only correlated subqueries; it was applied to any subqueries -> // which in-and-of-itself is not necessarily bad. But somewhere later // the choices made there caused joins to be dropped. - Session s = openSession(); + Session s = scope.getSessionFactory().openSession(); Transaction t = s.beginTransaction(); String qryString = "select a.id, a.description" + @@ -1739,11 +1731,11 @@ public void testCollectionJoinsInSubselect() { " select a1 from Animal a1" + " left join a1.offspring o" + " where a1.id=1" + - ")"; + ")"; s.createQuery( qryString ).list(); qryString = "select h.id, h.description" + - " from Human h" + + " from Human h" + " left join h.friends" + " where h in (" + " select h1" + @@ -1754,7 +1746,7 @@ public void testCollectionJoinsInSubselect() { s.createQuery( qryString ).list(); qryString = "select h.id, h.description" + - " from Human h" + + " from Human h" + " left join h.friends f" + " where f in (" + " select h1" + @@ -1768,9 +1760,9 @@ public void testCollectionJoinsInSubselect() { } @Test - public void testCollectionFetchWithDistinctionAndLimit() { + public void testCollectionFetchWithDistinctionAndLimit(SessionFactoryScope scope) { // create some test data... - Session s = openSession(); + Session s = scope.getSessionFactory().openSession(); Transaction t = s.beginTransaction(); int parentCount = 30; for ( int i = 0; i < parentCount; i++ ) { @@ -1790,7 +1782,7 @@ public void testCollectionFetchWithDistinctionAndLimit() { t.commit(); s.close(); - s = openSession(); + s = scope.getSessionFactory().openSession(); t = s.beginTransaction(); // Test simple distinction List results; @@ -1807,7 +1799,7 @@ public void testCollectionFetchWithDistinctionAndLimit() { t.commit(); s.close(); - s = openSession(); + s = scope.getSessionFactory().openSession(); t = s.beginTransaction(); s.createQuery( "delete Animal where mother is not null" ).executeUpdate(); s.createQuery( "delete Animal" ).executeUpdate(); @@ -1816,8 +1808,8 @@ public void testCollectionFetchWithDistinctionAndLimit() { } @Test - public void testFetchInSubqueryFails() { - Session s = openSession(); + public void testFetchInSubqueryFails(SessionFactoryScope scope) { + Session s = scope.getSessionFactory().openSession(); try { s.createQuery( "from Animal a where a.mother in (select m from Animal a1 inner join a1.mother as m join fetch m.mother)" ).list(); fail( "fetch join allowed in subquery" ); @@ -1832,10 +1824,10 @@ public void testFetchInSubqueryFails() { } @Test - @TestForIssue(jiraKey = "HHH-1830") - @SkipForDialect(value = DerbyDialect.class, comment = "Derby doesn't see that the subquery is functionally dependent") - public void testAggregatedJoinAlias() { - Session s = openSession(); + @JiraKey( "HHH-1830") + @SkipForDialect(dialectClass = DerbyDialect.class, matchSubTypes = true, reason = "Derby doesn't see that the subquery is functionally dependent") + public void testAggregatedJoinAlias(SessionFactoryScope scope) { + Session s = scope.getSessionFactory().openSession(); s.getTransaction().begin(); s.createQuery( "select p.id, size( descendants ) " + @@ -1848,11 +1840,11 @@ public void testAggregatedJoinAlias() { } @Test - @TestForIssue( jiraKey = "HHH-1464" ) - public void testQueryMetadataRetrievalWithFetching() { + @JiraKey( "HHH-1464" ) + public void testQueryMetadataRetrievalWithFetching(SessionFactoryScope scope) { // HHH-1464 : there was a problem due to the fact they we polled // the shallow version of the query plan to get the metadata. - inSession( + scope.inSession( session -> { final Query query = session.createQuery( "from Animal a inner join fetch a.mother" ); final SqmSelectStatement sqmStatement = (SqmSelectStatement) query.unwrap( QuerySqmImpl.class ).getSqmStatement(); @@ -1863,16 +1855,16 @@ public void testQueryMetadataRetrievalWithFetching() { assertThat( selectionType.getExpressibleJavaType().getJavaTypeClass(), equalTo( Animal.class ) ); } ); - Session s = openSession(); + Session s = scope.getSessionFactory().openSession(); s.close(); } @Test - @TestForIssue( jiraKey = "HHH-429" ) + @JiraKey( "HHH-429" ) @SuppressWarnings( {"unchecked"}) - public void testSuperclassPropertyReferenceAfterCollectionIndexedAccess() { + public void testSuperclassPropertyReferenceAfterCollectionIndexedAccess(SessionFactoryScope scope) { // note: simply performing syntax checking in the db - Session s = openSession(); + Session s = scope.getSessionFactory().openSession(); s.beginTransaction(); Mammal tiger = new Mammal(); tiger.setDescription( "Tiger" ); @@ -1890,14 +1882,14 @@ public void testSuperclassPropertyReferenceAfterCollectionIndexedAccess() { s.getTransaction().commit(); s.close(); - s = openSession(); + s = scope.getSessionFactory().openSession(); s.beginTransaction(); List results = s.createQuery( "from Zoo zoo where zoo.mammals['tiger'].mother.bodyWeight > 3.0f" ).list(); assertEquals( 1, results.size() ); s.getTransaction().commit(); s.close(); - s = openSession(); + s = scope.getSessionFactory().openSession(); s.beginTransaction(); s.delete( tiger ); s.delete( mother ); @@ -1907,9 +1899,9 @@ public void testSuperclassPropertyReferenceAfterCollectionIndexedAccess() { } @Test - public void testJoinFetchCollectionOfValues() { + public void testJoinFetchCollectionOfValues(SessionFactoryScope scope) { // note: simply performing syntax checking in the db - Session s = openSession(); + Session s = scope.getSessionFactory().openSession(); s.beginTransaction(); s.createQuery( "select h from Human as h join fetch h.nickNames" ).list(); s.getTransaction().commit(); @@ -1917,9 +1909,9 @@ public void testJoinFetchCollectionOfValues() { } @Test - public void testIntegerLiterals() { + public void testIntegerLiterals(SessionFactoryScope scope) { // note: simply performing syntax checking in the db - Session s = openSession(); + Session s = scope.getSessionFactory().openSession(); s.beginTransaction(); s.createQuery( "from Foo where long = 1" ).list(); s.createQuery( "from Foo where long = " + Integer.MIN_VALUE ).list(); @@ -1933,9 +1925,9 @@ public void testIntegerLiterals() { } @Test - public void testDecimalLiterals() { + public void testDecimalLiterals(SessionFactoryScope scope) { // note: simply performing syntax checking in the db - Session s = openSession(); + Session s = scope.getSessionFactory().openSession(); s.beginTransaction(); s.createQuery( "from Animal where bodyWeight > 100.0e-10" ).list(); s.createQuery( "from Animal where bodyWeight > 100.0E-10" ).list(); @@ -1952,9 +1944,9 @@ public void testDecimalLiterals() { } @Test - public void testNakedPropertyRef() { + public void testNakedPropertyRef(SessionFactoryScope scope) { // note: simply performing syntax and column/table resolution checking in the db - Session s = openSession(); + Session s = scope.getSessionFactory().openSession(); s.beginTransaction(); s.createQuery( "from Animal where bodyWeight = bodyWeight" ).list(); s.createQuery( "select bodyWeight from Animal" ).list(); @@ -1964,9 +1956,9 @@ public void testNakedPropertyRef() { } @Test - public void testNakedComponentPropertyRef() { + public void testNakedComponentPropertyRef(SessionFactoryScope scope) { // note: simply performing syntax and column/table resolution checking in the db - Session s = openSession(); + Session s = scope.getSessionFactory().openSession(); s.beginTransaction(); s.createQuery( "from Human where name.first = 'Gavin'" ).list(); s.createQuery( "select name from Human" ).list(); @@ -1977,9 +1969,9 @@ public void testNakedComponentPropertyRef() { } @Test - public void testNakedImplicitJoins() { + public void testNakedImplicitJoins(SessionFactoryScope scope) { // note: simply performing syntax and column/table resolution checking in the db - Session s = openSession(); + Session s = scope.getSessionFactory().openSession(); s.beginTransaction(); s.createQuery( "from Animal where mother.father.id = 1" ).list(); s.getTransaction().commit(); @@ -1987,11 +1979,11 @@ public void testNakedImplicitJoins() { } @Test - public void testNakedEntityAssociationReference() { + public void testNakedEntityAssociationReference(SessionFactoryScope scope) { // note: simply performing syntax and column/table resolution checking in the db - Session s = openSession(); + Session s = scope.getSessionFactory().openSession(); s.beginTransaction(); - if ( getDialect() instanceof AbstractHANADialect ) { + if ( scope.getSessionFactory().getJdbcServices().getDialect() instanceof AbstractHANADialect ) { s.createQuery( "from Animal where mother is null" ).list(); } else { @@ -2003,9 +1995,9 @@ public void testNakedEntityAssociationReference() { } @Test - public void testNakedMapIndex() throws Exception { + public void testNakedMapIndex(SessionFactoryScope scope) throws Exception { // note: simply performing syntax and column/table resolution checking in the db - Session s = openSession(); + Session s = scope.getSessionFactory().openSession(); s.beginTransaction(); s.createQuery( "from Zoo where mammals['dog'].description like '%black%'" ).list(); s.getTransaction().commit(); @@ -2013,10 +2005,10 @@ public void testNakedMapIndex() throws Exception { } @Test - public void testInvalidFetchSemantics() { - try ( final SessionImplementor s = (SessionImplementor) openSession()) { + public void testInvalidFetchSemantics(SessionFactoryScope scope) { + try ( final SessionImplementor s = (SessionImplementor) scope.getSessionFactory().openSession()) { - inTransaction( + scope.inTransaction( s, session -> { try { @@ -2031,7 +2023,7 @@ public void testInvalidFetchSemantics() { } ); - inTransaction( + scope.inTransaction( s, session-> { try { @@ -2050,8 +2042,8 @@ public void testInvalidFetchSemantics() { } @Test - public void testArithmetic() { - Session s = openSession(); + public void testArithmetic(SessionFactoryScope scope) { + Session s = scope.getSessionFactory().openSession(); Transaction t = s.beginTransaction(); Zoo zoo = new Zoo(); zoo.setName("Melbourne Zoo"); @@ -2078,8 +2070,8 @@ public void testArithmetic() { } @Test - public void testNestedCollectionFetch() { - Session s = openSession(); + public void testNestedCollectionFetch(SessionFactoryScope scope) { + Session s = scope.getSessionFactory().openSession(); Transaction t = s.beginTransaction(); s.createQuery("from Animal a left join fetch a.offspring o left join fetch o.offspring where a.mother.id = 1 order by a.description").list(); s.createQuery("from Zoo z left join fetch z.animals a left join fetch a.offspring where z.name ='MZ' order by a.description").list(); @@ -2089,10 +2081,10 @@ public void testNestedCollectionFetch() { } @Test - @RequiresDialectFeature( DialectChecks.SupportsSubqueryInSelect.class ) + @RequiresDialectFeature( feature = DialectFeatureChecks.SupportsSubqueryInSelect.class ) @SuppressWarnings( {"unchecked"}) - public void testSelectClauseSubselect() { - Session s = openSession(); + public void testSelectClauseSubselect(SessionFactoryScope scope) { + Session s = scope.getSessionFactory().openSession(); Transaction t = s.beginTransaction(); Zoo zoo = new Zoo(); zoo.setName("Melbourne Zoo"); @@ -2119,8 +2111,8 @@ public void testSelectClauseSubselect() { } @Test - public void testInitProxy() { - Session s = openSession(); + public void testInitProxy(SessionFactoryScope scope) { + Session s = scope.getSessionFactory().openSession(); Transaction t = s.beginTransaction(); Mammal plat = new Mammal(); plat.setBodyWeight( 11f ); @@ -2140,8 +2132,8 @@ public void testInitProxy() { @Test @SuppressWarnings( {"unchecked"}) - public void testSelectClauseImplicitJoin() { - Session s = openSession(); + public void testSelectClauseImplicitJoin(SessionFactoryScope scope) { + Session s = scope.getSessionFactory().openSession(); Transaction t = s.beginTransaction(); Zoo zoo = new Zoo(); zoo.setName("The Zoo"); @@ -2187,10 +2179,10 @@ private static void verifyAnimalZooSelection(Query q) { } @Test - @TestForIssue( jiraKey = "HHH-9305") + @JiraKey( "HHH-9305") @SuppressWarnings( {"unchecked"}) - public void testSelectClauseImplicitJoinOrderByJoinedProperty() { - Session s = openSession(); + public void testSelectClauseImplicitJoinOrderByJoinedProperty(SessionFactoryScope scope) { + Session s = scope.getSessionFactory().openSession(); Transaction t = s.beginTransaction(); Zoo zoo = new Zoo(); zoo.setName("The Zoo"); @@ -2253,8 +2245,8 @@ public void testSelectClauseImplicitJoinOrderByJoinedProperty() { @Test @SuppressWarnings( {"unchecked"}) - public void testSelectClauseDistinctImplicitJoinOrderByJoinedProperty() { - Session s = openSession(); + public void testSelectClauseDistinctImplicitJoinOrderByJoinedProperty(SessionFactoryScope scope) { + Session s = scope.getSessionFactory().openSession(); Transaction t = s.beginTransaction(); Zoo zoo = new Zoo(); zoo.setName("The Zoo"); @@ -2317,8 +2309,8 @@ public void testSelectClauseDistinctImplicitJoinOrderByJoinedProperty() { @Test @SuppressWarnings( {"unchecked"}) - public void testSelectClauseImplicitJoinWithIterate() { - Session s = openSession(); + public void testSelectClauseImplicitJoinWithIterate(SessionFactoryScope scope) { + Session s = scope.getSessionFactory().openSession(); Transaction t = s.beginTransaction(); Zoo zoo = new Zoo(); zoo.setName("The Zoo"); @@ -2351,8 +2343,8 @@ public void testSelectClauseImplicitJoinWithIterate() { } @Test - public void testComponentOrderBy() { - Session s = openSession(); + public void testComponentOrderBy(SessionFactoryScope scope) { + Session s = scope.getSessionFactory().openSession(); Transaction t = s.beginTransaction(); Long id1 = ( Long ) s.save( genSimpleHuman( "John", "Jacob" ) ); @@ -2378,8 +2370,8 @@ public void testComponentOrderBy() { } @Test - public void testOrderedWithCustomColumnReadAndWrite() { - Session s = openSession(); + public void testOrderedWithCustomColumnReadAndWrite(SessionFactoryScope scope) { + Session s = scope.getSessionFactory().openSession(); Transaction t = s.beginTransaction(); SimpleEntityWithAssociation first = new SimpleEntityWithAssociation(); first.setNegatedNumber( 1 ); @@ -2408,8 +2400,8 @@ public void testOrderedWithCustomColumnReadAndWrite() { } @Test - public void testHavingWithCustomColumnReadAndWrite() { - Session s = openSession(); + public void testHavingWithCustomColumnReadAndWrite(SessionFactoryScope scope) { + Session s = scope.getSessionFactory().openSession(); Transaction t = s.beginTransaction(); SimpleEntityWithAssociation first = new SimpleEntityWithAssociation(); first.setNegatedNumber(5); @@ -2439,9 +2431,9 @@ public void testHavingWithCustomColumnReadAndWrite() { } @Test - public void testLoadSnapshotWithCustomColumnReadAndWrite() { + public void testLoadSnapshotWithCustomColumnReadAndWrite(SessionFactoryScope scope) { // Exercises entity snapshot load when select-before-update is true. - Session s = openSession(); + Session s = scope.getSessionFactory().openSession(); Transaction t = s.beginTransaction(); final double SIZE_IN_KB = 1536d; final double SIZE_IN_MB = SIZE_IN_KB / 1024d; @@ -2458,7 +2450,7 @@ public void testLoadSnapshotWithCustomColumnReadAndWrite() { t.commit(); s.close(); - s = openSession(); + s = scope.getSessionFactory().openSession(); t = s.beginTransaction(); final double NEW_SIZE_IN_KB = 2048d; final double NEW_SIZE_IN_MB = NEW_SIZE_IN_KB / 1024d; @@ -2482,8 +2474,8 @@ private Human genSimpleHuman(String fName, String lName) { } @Test - public void testCastInSelect() { - Session s = openSession(); + public void testCastInSelect(SessionFactoryScope scope) { + Session s = scope.getSessionFactory().openSession(); Transaction t = s.beginTransaction(); Animal a = new Animal(); a.setBodyWeight(12.4f); @@ -2506,8 +2498,8 @@ public void testCastInSelect() { } @Test - public void testNumericExpressionReturnTypes() { - Session s = openSession(); + public void testNumericExpressionReturnTypes(SessionFactoryScope scope) { + Session s = scope.getSessionFactory().openSession(); Transaction t = s.beginTransaction(); Animal a = new Animal(); a.setBodyWeight(12.4f); @@ -2613,8 +2605,8 @@ public void testNumericExpressionReturnTypes() { } @Test - public void testAliases() { - Session s = openSession(); + public void testAliases(SessionFactoryScope scope) { + Session s = scope.getSessionFactory().openSession(); Transaction t = s.beginTransaction(); Animal a = new Animal(); a.setBodyWeight(12.4f); @@ -2650,9 +2642,9 @@ public void testAliases() { } @Test - @RequiresDialectFeature(DialectChecks.SupportsTemporaryTable.class) - public void testParameterMixing() { - Session s = openSession(); + @RequiresDialectFeature( feature = DialectFeatureChecks.SupportsTemporaryTable.class) + public void testParameterMixing(SessionFactoryScope scope) { + Session s = scope.getSessionFactory().openSession(); Transaction t = s.beginTransaction(); s.createQuery( "from Animal a where a.description = ?1 and a.bodyWeight = ?2 or a.bodyWeight = :bw" ) .setParameter( 1, "something" ) @@ -2664,8 +2656,8 @@ public void testParameterMixing() { } @Test - public void testOrdinalParameters() { - Session s = openSession(); + public void testOrdinalParameters(SessionFactoryScope scope) { + Session s = scope.getSessionFactory().openSession(); Transaction t = s.beginTransaction(); s.createQuery( "from Animal a where a.description = ?1 and a.bodyWeight = ?2" ) .setParameter( 1, "something" ) @@ -2680,8 +2672,8 @@ public void testOrdinalParameters() { } @Test - public void testIndexParams() { - Session s = openSession(); + public void testIndexParams(SessionFactoryScope scope) { + Session s = scope.getSessionFactory().openSession(); Transaction t = s.beginTransaction(); s.createQuery( "from Zoo zoo where zoo.mammals[:name].id = :id" ) .setParameter( "name", "Walrus" ) @@ -2711,8 +2703,8 @@ public void testIndexParams() { } @Test - public void testAggregation() { - Session s = openSession(); + public void testAggregation(SessionFactoryScope scope) { + Session s = scope.getSessionFactory().openSession(); s.beginTransaction(); Human h = new Human(); h.setBodyWeight( (float) 74.0 ); @@ -2731,7 +2723,7 @@ public void testAggregation() { s.getTransaction().commit(); s.close(); - s = openSession(); + s = scope.getSessionFactory().openSession(); s.beginTransaction(); h = new Human(); h.setFloatValue( 2.5F ); @@ -2762,8 +2754,8 @@ public void testAggregation() { } @Test - public void testSelectClauseCase() { - Session s = openSession(); + public void testSelectClauseCase(SessionFactoryScope scope) { + Session s = scope.getSessionFactory().openSession(); Transaction t = s.beginTransaction(); Human h = new Human(); h.setBodyWeight( (float) 74.0 ); @@ -2782,9 +2774,9 @@ public void testSelectClauseCase() { } @Test - @RequiresDialectFeature( DialectChecks.SupportsSubqueryInSelect.class ) - public void testImplicitPolymorphism() { - Session s = openSession(); + @RequiresDialectFeature( feature = DialectFeatureChecks.SupportsSubqueryInSelect.class ) + public void testImplicitPolymorphism(SessionFactoryScope scope) { + Session s = scope.getSessionFactory().openSession(); Transaction t = s.beginTransaction(); Product product = new Product(); @@ -2807,8 +2799,8 @@ public void testImplicitPolymorphism() { } @Test - public void testCoalesce() { - Session session = openSession(); + public void testCoalesce(SessionFactoryScope scope) { + Session session = scope.getSessionFactory().openSession(); Transaction txn = session.beginTransaction(); session.createQuery("from Human h where coalesce(h.nickName, h.name.first, h.name.last) = 'max'").list(); session.createQuery("select nullif(nickName, '1e1') from Human").list(); @@ -2817,14 +2809,14 @@ public void testCoalesce() { } @Test - public void testStr() { - Session session = openSession(); + public void testStr(SessionFactoryScope scope) { + Session session = scope.getSessionFactory().openSession(); Transaction txn = session.beginTransaction(); Animal an = new Animal(); an.setBodyWeight(123.45f); session.persist( an ); String str = (String) session.createQuery("select str(an.bodyWeight) from Animal an where str(an.bodyWeight) like '%1%'").uniqueResult(); - if ( getDialect() instanceof DB2Dialect ) { + if ( scope.getSessionFactory().getJdbcServices().getDialect() instanceof DB2Dialect ) { assertTrue( str.startsWith( "1.234" ) ); } else { @@ -2847,10 +2839,10 @@ public void testStr() { } @Test - @SkipForDialect( MySQLDialect.class ) - @SkipForDialect( DB2Dialect.class ) - public void testCast() { - Session session = openSession(); + @SkipForDialect( dialectClass = MySQLDialect.class, matchSubTypes = true ) + @SkipForDialect( dialectClass = DB2Dialect.class, matchSubTypes = true ) + public void testCast(SessionFactoryScope scope) { + Session session = scope.getSessionFactory().openSession(); Transaction txn = session.beginTransaction(); session.createQuery("from Human h where h.nickName like 'G%'").list(); session.createQuery("from Animal a where cast(a.bodyWeight as string) like '1.%'").list(); @@ -2860,12 +2852,12 @@ public void testCast() { } @Test - public void testExtract() { - Session session = openSession(); + public void testExtract(SessionFactoryScope scope) { + Session session = scope.getSessionFactory().openSession(); Transaction txn = session.beginTransaction(); session.createQuery("select second(current_timestamp()), minute(current_timestamp()), hour(current_timestamp()) from Mammal m").list(); session.createQuery("select day(m.birthdate), month(m.birthdate), year(m.birthdate) from Mammal m").list(); - if ( !(getDialect() instanceof DB2Dialect) ) { //no ANSI extract + if ( !(scope.getSessionFactory().getJdbcServices().getDialect() instanceof DB2Dialect) ) { //no ANSI extract session.createQuery("select extract(second from current_timestamp()), extract(minute from current_timestamp()), extract(hour from current_timestamp()) from Mammal m").list(); session.createQuery("select extract(day from m.birthdate), extract(month from m.birthdate), extract(year from m.birthdate) from Mammal m").list(); } @@ -2874,11 +2866,11 @@ public void testExtract() { } @Test - @SkipForDialect(value = CockroachDialect.class, comment = "https://github.com/cockroachdb/cockroach/issues/41943") + @SkipForDialect(dialectClass = CockroachDialect.class, matchSubTypes = true, reason = "https://github.com/cockroachdb/cockroach/issues/41943") @SuppressWarnings( {"UnusedAssignment", "UnusedDeclaration"}) - public void testSelectExpressions() { - createTestBaseData(); - Session session = openSession(); + public void testSelectExpressions(SessionFactoryScope scope) { + createTestBaseData( scope ); + Session session = scope.getSessionFactory().openSession(); Transaction txn = session.beginTransaction(); Human h = new Human(); h.setName( new Name( "Gavin", 'A', "King" ) ); @@ -2906,11 +2898,11 @@ public void testSelectExpressions() { session.delete(h); txn.commit(); session.close(); - destroyTestBaseData(); + destroyTestBaseData( scope ); } - private void createTestBaseData() { - Session session = openSession(); + private void createTestBaseData(SessionFactoryScope scope) { + Session session = scope.getSessionFactory().openSession(); Transaction txn = session.beginTransaction(); Mammal m1 = new Mammal(); @@ -2933,8 +2925,8 @@ private void createTestBaseData() { createdAnimalIds.add( m2.getId() ); } - private void destroyTestBaseData() { - Session session = openSession(); + private void destroyTestBaseData(SessionFactoryScope scope) { + Session session = scope.getSessionFactory().openSession(); Transaction txn = session.beginTransaction(); for ( Long createdAnimalId : createdAnimalIds ) { @@ -2949,8 +2941,8 @@ private void destroyTestBaseData() { } @Test - public void testImplicitJoin() throws Exception { - Session session = openSession(); + public void testImplicitJoin(SessionFactoryScope scope) throws Exception { + Session session = scope.getSessionFactory().openSession(); Transaction t = session.beginTransaction(); Animal a = new Animal(); a.setBodyWeight(0.5f); @@ -2975,48 +2967,48 @@ public void testImplicitJoin() throws Exception { } @Test - public void testFromOnly() throws Exception { - createTestBaseData(); - Session session = openSession(); + public void testFromOnly(SessionFactoryScope scope) throws Exception { + createTestBaseData( scope ); + Session session = scope.getSessionFactory().openSession(); Transaction t = session.beginTransaction(); List results = session.createQuery( "from Animal" ).list(); assertEquals( "Incorrect result size", 2, results.size() ); assertTrue( "Incorrect result return type", results.get( 0 ) instanceof Animal ); t.commit(); session.close(); - destroyTestBaseData(); + destroyTestBaseData( scope ); } @Test - public void testSimpleSelect() throws Exception { - createTestBaseData(); - Session session = openSession(); + public void testSimpleSelect(SessionFactoryScope scope) throws Exception { + createTestBaseData( scope ); + Session session = scope.getSessionFactory().openSession(); Transaction t = session.beginTransaction(); List results = session.createQuery( "select a from Animal as a" ).list(); assertEquals( "Incorrect result size", 2, results.size() ); assertTrue( "Incorrect result return type", results.get( 0 ) instanceof Animal ); t.commit(); session.close(); - destroyTestBaseData(); + destroyTestBaseData( scope ); } @Test - public void testEntityPropertySelect() throws Exception { - createTestBaseData(); - Session session = openSession(); + public void testEntityPropertySelect(SessionFactoryScope scope) throws Exception { + createTestBaseData( scope ); + Session session = scope.getSessionFactory().openSession(); Transaction t = session.beginTransaction(); List results = session.createQuery( "select a.mother from Animal as a" ).list(); assertTrue( "Incorrect result return type", results.get( 0 ) instanceof Animal ); t.commit(); session.close(); - destroyTestBaseData(); + destroyTestBaseData( scope ); } @Test - public void testWhere() throws Exception { - createTestBaseData(); + public void testWhere(SessionFactoryScope scope) throws Exception { + createTestBaseData( scope ); - Session session = openSession(); + Session session = scope.getSessionFactory().openSession(); Transaction t = session.beginTransaction(); List results = session.createQuery( "from Animal an where an.bodyWeight > 10" ).list(); assertEquals( "Incorrect result size", 1, results.size() ); @@ -3039,14 +3031,14 @@ public void testWhere() throws Exception { t.commit(); session.close(); - destroyTestBaseData(); + destroyTestBaseData( scope ); } @Test - public void testEntityFetching() throws Exception { - createTestBaseData(); + public void testEntityFetching(SessionFactoryScope scope) throws Exception { + createTestBaseData( scope ); - Session session = openSession(); + Session session = scope.getSessionFactory().openSession(); Transaction t = session.beginTransaction(); List results = session.createQuery( "from Animal an join fetch an.mother" ).list(); @@ -3064,14 +3056,14 @@ public void testEntityFetching() throws Exception { t.commit(); session.close(); - destroyTestBaseData(); + destroyTestBaseData( scope ); } @Test - public void testCollectionFetching() throws Exception { - createTestBaseData(); + public void testCollectionFetching(SessionFactoryScope scope) throws Exception { + createTestBaseData( scope ); - Session session = openSession(); + Session session = scope.getSessionFactory().openSession(); Transaction t = session.beginTransaction(); List results = session.createQuery( "from Animal an join fetch an.offspring" ).list(); assertEquals( "Incorrect result size", 1, results.size() ); @@ -3088,12 +3080,12 @@ public void testCollectionFetching() throws Exception { t.commit(); session.close(); - destroyTestBaseData(); + destroyTestBaseData( scope ); } @Test @SuppressWarnings( {"unchecked"}) - public void testJoinFetchedCollectionOfJoinedSubclass() throws Exception { + public void testJoinFetchedCollectionOfJoinedSubclass(SessionFactoryScope scope) throws Exception { Mammal mammal = new Mammal(); mammal.setDescription( "A Zebra" ); Zoo zoo = new Zoo(); @@ -3101,14 +3093,14 @@ public void testJoinFetchedCollectionOfJoinedSubclass() throws Exception { zoo.getMammals().put( "zebra", mammal ); mammal.setZoo( zoo ); - Session session = openSession(); + Session session = scope.getSessionFactory().openSession(); Transaction txn = session.beginTransaction(); session.save( mammal ); session.save( zoo ); txn.commit(); session.close(); - session = openSession(); + session = scope.getSessionFactory().openSession(); txn = session.beginTransaction(); List results = session.createQuery( "from Zoo z join fetch z.mammals" ).list(); assertEquals( "Incorrect result size", 1, results.size() ); @@ -3126,7 +3118,7 @@ public void testJoinFetchedCollectionOfJoinedSubclass() throws Exception { @Test @SuppressWarnings( {"unchecked"}) - public void testJoinedCollectionOfJoinedSubclass() throws Exception { + public void testJoinedCollectionOfJoinedSubclass(SessionFactoryScope scope) throws Exception { Mammal mammal = new Mammal(); mammal.setDescription( "A Zebra" ); Zoo zoo = new Zoo(); @@ -3134,14 +3126,14 @@ public void testJoinedCollectionOfJoinedSubclass() throws Exception { zoo.getMammals().put( "zebra", mammal ); mammal.setZoo( zoo ); - Session session = openSession(); + Session session = scope.getSessionFactory().openSession(); Transaction txn = session.beginTransaction(); session.save( mammal ); session.save( zoo ); txn.commit(); session.close(); - session = openSession(); + session = scope.getSessionFactory().openSession(); txn = session.beginTransaction(); List results = session.createQuery( "select z, m from Zoo z join z.mammals m" ).list(); assertEquals( "Incorrect result size", 1, results.size() ); @@ -3159,7 +3151,7 @@ public void testJoinedCollectionOfJoinedSubclass() throws Exception { @Test @SuppressWarnings( {"unchecked"}) - public void testJoinedCollectionOfJoinedSubclassProjection() throws Exception { + public void testJoinedCollectionOfJoinedSubclassProjection(SessionFactoryScope scope) throws Exception { Mammal mammal = new Mammal(); mammal.setDescription( "A Zebra" ); Zoo zoo = new Zoo(); @@ -3167,14 +3159,14 @@ public void testJoinedCollectionOfJoinedSubclassProjection() throws Exception { zoo.getMammals().put( "zebra", mammal ); mammal.setZoo( zoo ); - Session session = openSession(); + Session session = scope.getSessionFactory().openSession(); Transaction txn = session.beginTransaction(); session.save( mammal ); session.save( zoo ); txn.commit(); session.close(); - session = openSession(); + session = scope.getSessionFactory().openSession(); txn = session.beginTransaction(); List results = session.createQuery( "select z, m from Zoo z join z.mammals m" ).list(); assertEquals( "Incorrect result size", 1, results.size() ); @@ -3191,9 +3183,9 @@ public void testJoinedCollectionOfJoinedSubclassProjection() throws Exception { } @Test - public void testProjectionQueries() throws Exception { - createTestBaseData(); - Session session = openSession(); + public void testProjectionQueries(SessionFactoryScope scope) throws Exception { + createTestBaseData( scope ); + Session session = scope.getSessionFactory().openSession(); Transaction t = session.beginTransaction(); List results = session.createQuery( "select an.mother.id, max(an.bodyWeight) from Animal an group by an.mother.id" ).list(); @@ -3204,13 +3196,13 @@ public void testProjectionQueries() throws Exception { t.commit(); session.close(); - destroyTestBaseData(); + destroyTestBaseData( scope ); } @Test - @SkipForDialect(value = CockroachDialect.class, strictMatching = true) - public void testStandardFunctions() { - Session session = openSession(); + @SkipForDialect(dialectClass = CockroachDialect.class) + public void testStandardFunctions(SessionFactoryScope scope) { + Session session = scope.getSessionFactory().openSession(); Transaction t = session.beginTransaction(); Product p = new Product(); p.setDescription( "a product" ); @@ -3232,10 +3224,10 @@ public void testStandardFunctions() { } @Test - public void testDynamicInstantiationQueries() throws Exception { - createTestBaseData(); + public void testDynamicInstantiationQueries(SessionFactoryScope scope) throws Exception { + createTestBaseData( scope ); - Session session = openSession(); + Session session = scope.getSessionFactory().openSession(); Transaction t = session.beginTransaction(); List results = session.createQuery( "select new Animal(an.description, an.bodyWeight) from Animal an" ).list(); @@ -3281,7 +3273,7 @@ public void testDynamicInstantiationQueries() throws Exception { } // caching... - QueryStatistics stats = sessionFactory().getStatistics().getQueryStatistics( "select new Animal(an.description, an.bodyWeight) from Animal an" ); + QueryStatistics stats = scope.getSessionFactory().getStatistics().getQueryStatistics( "select new Animal(an.description, an.bodyWeight) from Animal an" ); results = session.createQuery( "select new Animal(an.description, an.bodyWeight) from Animal an" ) .setCacheable( true ) .list(); @@ -3298,12 +3290,12 @@ public void testDynamicInstantiationQueries() throws Exception { t.commit(); session.close(); - destroyTestBaseData(); + destroyTestBaseData( scope ); } @Test - @TestForIssue( jiraKey = "HHH-9305") - public void testDynamicInstantiationWithToOneQueries() throws Exception { + @JiraKey( "HHH-9305") + public void testDynamicInstantiationWithToOneQueries(SessionFactoryScope scope) throws Exception { final Employee employee1 = new Employee(); employee1.setFirstName( "Jane" ); employee1.setLastName( "Doe" ); @@ -3321,7 +3313,7 @@ public void testDynamicInstantiationWithToOneQueries() throws Exception { title2.setDescription( "John's title" ); employee2.setTitle( title2 ); - Session s = openSession(); + Session s = scope.getSessionFactory().openSession(); s.getTransaction().begin(); s.persist( title1 ); s.persist( dept1 ); @@ -3336,64 +3328,64 @@ public void testDynamicInstantiationWithToOneQueries() throws Exception { // at the beginning of the FROM clause, avoiding failures on DBs that cannot handle cross joins // interleaved with ANSI joins (e.g., PostgreSql). - s = openSession(); + s = scope.getSessionFactory().openSession(); s.getTransaction().begin(); - List results = session.createQuery( + List results = s.createQuery( "select new Employee(e.id, e.lastName, e.title.id, e.title.description, e.department, e.firstName) from Employee e inner join e.title" ).list(); assertEquals( "Incorrect result size", 1, results.size() ); assertClassAssignability( results.get( 0 ).getClass(), Employee.class ); - results = session.createQuery( + results = s.createQuery( "select new Employee(e.id, e.lastName, t.id, t.description, e.department, e.firstName) from Employee e inner join e.title t" ).list(); assertEquals( "Incorrect result size", 1, results.size() ); assertClassAssignability( results.get( 0 ).getClass(), Employee.class ); - results = session.createQuery( + results = s.createQuery( "select new Employee(e.id, e.lastName, e.title.id, e.title.description, e.department, e.firstName) from Employee e inner join e.department" ).list(); assertEquals( "Incorrect result size", 1, results.size() ); assertClassAssignability( results.get( 0 ).getClass(), Employee.class ); - results = session.createQuery( + results = s.createQuery( "select new Employee(e.id, e.lastName, e.title.id, e.title.description, d, e.firstName) from Employee e inner join e.department d" ).list(); assertEquals( "Incorrect result size", 1, results.size() ); assertClassAssignability( results.get( 0 ).getClass(), Employee.class ); - results = session.createQuery( + results = s.createQuery( "select new Employee(e.id, e.lastName, e.title.id, e.title.description, e.department, e.firstName) from Employee e left outer join e.department" ).list(); assertEquals( "Incorrect result size", 2, results.size() ); assertClassAssignability( results.get( 0 ).getClass(), Employee.class ); - results = session.createQuery( + results = s.createQuery( "select new Employee(e.id, e.lastName, e.title.id, e.title.description, d, e.firstName) from Employee e left outer join e.department d" ).list(); assertEquals( "Incorrect result size", 2, results.size() ); assertClassAssignability( results.get( 0 ).getClass(), Employee.class ); - results = session.createQuery( + results = s.createQuery( "select new Employee(e.id, e.lastName, e.title.id, e.title.description, e.department, e.firstName) from Employee e left outer join e.department inner join e.title" ).list(); assertEquals( "Incorrect result size", 2, results.size() ); assertClassAssignability( results.get( 0 ).getClass(), Employee.class ); - results = session.createQuery( + results = s.createQuery( "select new Employee(e.id, e.lastName, t.id, t.description, d, e.firstName) from Employee e left outer join e.department d inner join e.title t" ).list(); assertEquals( "Incorrect result size", 2, results.size() ); assertClassAssignability( results.get( 0 ).getClass(), Employee.class ); - results = session.createQuery( + results = s.createQuery( "select new Employee(e.id, e.lastName, e.title.id, e.title.description, e.department, e.firstName) from Employee e left outer join e.department left outer join e.title" ).list(); assertEquals( "Incorrect result size", 2, results.size() ); assertClassAssignability( results.get( 0 ).getClass(), Employee.class ); - results = session.createQuery( + results = s.createQuery( "select new Employee(e.id, e.lastName, t.id, t.description, d, e.firstName) from Employee e left outer join e.department d left outer join e.title t" ).list(); assertEquals( "Incorrect result size", 2, results.size() ); assertClassAssignability( results.get( 0 ).getClass(), Employee.class ); - results = session.createQuery( + results = s.createQuery( "select new Employee(e.id, e.lastName, e.title.id, e.title.description, e.department, e.firstName) from Employee e left outer join e.department order by e.title.description" ).list(); assertEquals( "Incorrect result size", 2, results.size() ); assertClassAssignability( results.get( 0 ).getClass(), Employee.class ); - results = session.createQuery( + results = s.createQuery( "select new Employee(e.id, e.lastName, e.title.id, e.title.description, e.department, e.firstName) from Employee e left outer join e.department d order by e.title.description" ).list(); assertEquals( "Incorrect result size", 2, results.size() ); @@ -3402,7 +3394,7 @@ public void testDynamicInstantiationWithToOneQueries() throws Exception { s.close(); - s = openSession(); + s = scope.getSessionFactory().openSession(); s.getTransaction().begin(); s.delete( employee1 ); s.delete( title1 ); @@ -3415,7 +3407,7 @@ public void testDynamicInstantiationWithToOneQueries() throws Exception { @Test @SuppressWarnings( {"UnusedAssignment"}) - public void testCachedJoinedAndJoinFetchedManyToOne() throws Exception { + public void testCachedJoinedAndJoinFetchedManyToOne(SessionFactoryScope scope) throws Exception { Animal a = new Animal(); a.setDescription( "an animal" ); @@ -3434,7 +3426,7 @@ public void testCachedJoinedAndJoinFetchedManyToOne() throws Exception { a.addOffspring( offspring2 ); offspring2.setMother( a ); - Session s = openSession(); + Session s = scope.getSessionFactory().openSession(); Transaction t = s.beginTransaction(); s.save( mother ); s.save( a ); @@ -3443,20 +3435,20 @@ public void testCachedJoinedAndJoinFetchedManyToOne() throws Exception { t.commit(); s.close(); - sessionFactory().getCache().evictQueryRegions(); - sessionFactory().getStatistics().clear(); + scope.getSessionFactory().getCache().evictQueryRegions(); + scope.getSessionFactory().getStatistics().clear(); - s = openSession(); + s = scope.getSessionFactory().openSession(); t = s.beginTransaction(); List list = s.createQuery( "from Animal a left join fetch a.mother" ).setCacheable( true ).list(); - assertEquals( 0, sessionFactory().getStatistics().getQueryCacheHitCount() ); - assertEquals( 1, sessionFactory().getStatistics().getQueryCachePutCount() ); + assertEquals( 0, scope.getSessionFactory().getStatistics().getQueryCacheHitCount() ); + assertEquals( 1, scope.getSessionFactory().getStatistics().getQueryCachePutCount() ); list = s.createQuery( "select a from Animal a left join fetch a.mother" ).setCacheable( true ).list(); - assertEquals( 1, sessionFactory().getStatistics().getQueryCacheHitCount() ); - assertEquals( 1, sessionFactory().getStatistics().getQueryCachePutCount() ); + assertEquals( 1, scope.getSessionFactory().getStatistics().getQueryCacheHitCount() ); + assertEquals( 1, scope.getSessionFactory().getStatistics().getQueryCachePutCount() ); list = s.createQuery( "select a, m from Animal a left join a.mother m" ).setCacheable( true ).list(); - assertEquals( 1, sessionFactory().getStatistics().getQueryCacheHitCount() ); - assertEquals( 2, sessionFactory().getStatistics().getQueryCachePutCount() ); + assertEquals( 1, scope.getSessionFactory().getStatistics().getQueryCacheHitCount() ); + assertEquals( 2, scope.getSessionFactory().getStatistics().getQueryCachePutCount() ); list = s.createQuery( "from Animal" ).list(); for(Object obj : list){ s.delete( obj ); @@ -3467,7 +3459,7 @@ public void testCachedJoinedAndJoinFetchedManyToOne() throws Exception { @Test @SuppressWarnings( {"UnusedAssignment", "UnusedDeclaration"}) - public void testCachedJoinedAndJoinFetchedOneToMany() throws Exception { + public void testCachedJoinedAndJoinFetchedOneToMany(SessionFactoryScope scope) throws Exception { Animal a = new Animal(); a.setDescription( "an animal" ); Animal mother = new Animal(); @@ -3483,10 +3475,10 @@ public void testCachedJoinedAndJoinFetchedOneToMany() throws Exception { a.addOffspring( offspring2 ); offspring2.setMother( a ); - sessionFactory().getCache().evictQueryRegions(); - sessionFactory().getStatistics().clear(); + scope.getSessionFactory().getCache().evictQueryRegions(); + scope.getSessionFactory().getStatistics().clear(); - Session s = openSession(); + Session s = scope.getSessionFactory().openSession(); Transaction t = s.beginTransaction(); s.save( mother ); s.save( a ); @@ -3495,17 +3487,17 @@ public void testCachedJoinedAndJoinFetchedOneToMany() throws Exception { t.commit(); s.close(); - s = openSession(); + s = scope.getSessionFactory().openSession(); t = s.beginTransaction(); List list = s.createQuery( "from Animal a left join fetch a.offspring" ).setCacheable( true ).list(); - assertEquals( 0, sessionFactory().getStatistics().getQueryCacheHitCount() ); - assertEquals( 1, sessionFactory().getStatistics().getQueryCachePutCount() ); + assertEquals( 0, scope.getSessionFactory().getStatistics().getQueryCacheHitCount() ); + assertEquals( 1, scope.getSessionFactory().getStatistics().getQueryCachePutCount() ); list = s.createQuery( "select a from Animal a left join fetch a.offspring" ).setCacheable( true ).list(); - assertEquals( 1, sessionFactory().getStatistics().getQueryCacheHitCount() ); - assertEquals( 1, sessionFactory().getStatistics().getQueryCachePutCount() ); + assertEquals( 1, scope.getSessionFactory().getStatistics().getQueryCacheHitCount() ); + assertEquals( 1, scope.getSessionFactory().getStatistics().getQueryCachePutCount() ); list = s.createQuery( "select a, o from Animal a left join a.offspring o" ).setCacheable( true ).list(); - assertEquals( 1, sessionFactory().getStatistics().getQueryCacheHitCount() ); - assertEquals( 2, sessionFactory().getStatistics().getQueryCachePutCount() ); + assertEquals( 1, scope.getSessionFactory().getStatistics().getQueryCacheHitCount() ); + assertEquals( 2, scope.getSessionFactory().getStatistics().getQueryCachePutCount() ); list = s.createQuery( "from Animal" ).list(); for ( Object obj : list ) { s.delete( obj ); @@ -3515,9 +3507,9 @@ public void testCachedJoinedAndJoinFetchedOneToMany() throws Exception { } @Test - public void testSelectNewTransformerQueries() { - createTestBaseData(); - Session session = openSession(); + public void testSelectNewTransformerQueries(SessionFactoryScope scope) { + createTestBaseData( scope ); + Session session = scope.getSessionFactory().openSession(); Transaction t = session.beginTransaction(); List list = session.createQuery( "select new Animal(an.description, an.bodyWeight) as animal from Animal an order by an.description" ) .setResultTransformer( Transformers.ALIAS_TO_ENTITY_MAP ) @@ -3531,16 +3523,16 @@ public void testSelectNewTransformerQueries() { assertEquals( "Mammal #2", m2.get( "animal" ).getDescription() ); t.commit(); session.close(); - destroyTestBaseData(); + destroyTestBaseData( scope ); } @Test - public void testResultTransformerScalarQueries() throws Exception { - createTestBaseData(); + public void testResultTransformerScalarQueries(SessionFactoryScope scope) throws Exception { + createTestBaseData( scope ); String query = "select an.description as description, an.bodyWeight as bodyWeight from Animal an order by bodyWeight desc"; - Session session = openSession(); + Session session = scope.getSessionFactory().openSession(); Transaction t = session.beginTransaction(); List results = session.createQuery( query ) @@ -3556,11 +3548,11 @@ public void testResultTransformerScalarQueries() throws Exception { t.commit(); session.close(); - session = openSession(); + session = scope.getSessionFactory().openSession(); t = session.beginTransaction(); try (ScrollableResults sr = session.createQuery( query ) - .setResultTransformer(Transformers.aliasToBean(Animal.class)).scroll()) { + .setResultTransformer( Transformers.aliasToBean( Animal.class ) ).scroll()) { assertTrue( "Incorrect result size", sr.next() ); assertTrue( "Incorrect return type", sr.get() instanceof Animal ); assertFalse( session.contains( sr.get() ) ); @@ -3569,7 +3561,7 @@ public void testResultTransformerScalarQueries() throws Exception { t.commit(); session.close(); - session = openSession(); + session = scope.getSessionFactory().openSession(); t = session.beginTransaction(); results = session.createQuery( "select a from Animal a, Animal b order by a.id" ) @@ -3590,16 +3582,16 @@ public Object transformTuple(Object[] tuple, String[] aliases) { t.commit(); session.close(); - destroyTestBaseData(); + destroyTestBaseData( scope ); } @Test - public void testResultTransformerEntityQueries() throws Exception { - createTestBaseData(); + public void testResultTransformerEntityQueries(SessionFactoryScope scope) throws Exception { + createTestBaseData( scope ); String query = "select an as an from Animal an order by bodyWeight desc"; - Session session = openSession(); + Session session = scope.getSessionFactory().openSession(); Transaction t = session.beginTransaction(); List results = session.createQuery( query ) @@ -3618,7 +3610,7 @@ public void testResultTransformerEntityQueries() throws Exception { t.commit(); session.close(); - session = openSession(); + session = scope.getSessionFactory().openSession(); t = session.beginTransaction(); try (ScrollableResults sr = session.createQuery( query ) @@ -3630,12 +3622,12 @@ public void testResultTransformerEntityQueries() throws Exception { t.commit(); session.close(); - destroyTestBaseData(); + destroyTestBaseData( scope ); } @Test - public void testEJBQLFunctions() throws Exception { - Session session = openSession(); + public void testEJBQLFunctions(SessionFactoryScope scope) throws Exception { + Session session = scope.getSessionFactory().openSession(); Transaction t = session.beginTransaction(); String hql = "from Animal a where a.description = concat('1', concat('2','3'), '4'||'5')||'0'"; @@ -3665,17 +3657,21 @@ public void testEJBQLFunctions() throws Exception { hql = "select length(a.description) from Animal a"; session.createQuery(hql).list(); - //note: postgres and db2 don't have a 3-arg form, it gets transformed to 2-args - hql = "from Animal a where locate('abc', a.description, 2) = 2"; - session.createQuery(hql).list(); + Dialect dialect = scope.getSessionFactory().getJdbcServices().getDialect(); + // Informix before version 12 didn't support finding the index of substrings + if ( !( dialect instanceof InformixDialect && dialect.getVersion().isBefore( 12 ) ) ) { + //note: postgres and db2 don't have a 3-arg form, it gets transformed to 2-args + hql = "from Animal a where locate('abc', a.description, 2) = 2"; + session.createQuery( hql ).list(); - hql = "from Animal a where locate('abc', a.description) = 2"; - session.createQuery(hql).list(); + hql = "from Animal a where locate('abc', a.description) = 2"; + session.createQuery( hql ).list(); - hql = "select locate('cat', a.description, 2) from Animal a"; - session.createQuery(hql).list(); + hql = "select locate('cat', a.description, 2) from Animal a"; + session.createQuery( hql ).list(); + } - if ( !( getDialect() instanceof DB2Dialect ) ) { + if ( !( dialect instanceof DB2Dialect ) ) { hql = "from Animal a where trim(trailing '_' from a.description) = 'cat'"; session.createQuery(hql).list(); @@ -3689,7 +3685,7 @@ public void testEJBQLFunctions() throws Exception { session.createQuery(hql).list(); } - if ( !(getDialect() instanceof HSQLDialect) ) { //HSQL doesn't like trim() without specification + if ( !(dialect instanceof HSQLDialect) ) { //HSQL doesn't like trim() without specification hql = "from Animal a where trim(a.description) = 'cat'"; session.createQuery(hql).list(); } @@ -3728,10 +3724,10 @@ public void testEJBQLFunctions() throws Exception { } @Test - @TestForIssue( jiraKey = "HHH-11942" ) - public void testOrderByExtraParenthesis() throws Exception { + @JiraKey( "HHH-11942" ) + public void testOrderByExtraParenthesis(SessionFactoryScope scope) throws Exception { try { - doInHibernate( this::sessionFactory, session -> { + scope.inTransaction( session -> { session.createQuery( "select a from Product a " + "where " + @@ -3750,22 +3746,22 @@ public void testOrderByExtraParenthesis() throws Exception { } @RequiresDialectFeature( - value = DialectChecks.SupportSubqueryAsLeftHandSideInPredicate.class, + feature = DialectFeatureChecks.SupportsSubqueryAsLeftHandSideInPredicate.class, comment = "Database does not support using subquery as singular value expression" ) - public void testSubqueryAsSingularValueExpression() { - assertResultSize( "from Animal x where (select max(a.bodyWeight) from Animal a) in (1,2,3)", 0 ); - assertResultSize( "from Animal x where (select max(a.bodyWeight) from Animal a) between 0 and 100", 0 ); - assertResultSize( "from Animal x where (select max(a.description) from Animal a) like 'big%'", 0 ); - assertResultSize( "from Animal x where (select max(a.bodyWeight) from Animal a) is not null", 0 ); + public void testSubqueryAsSingularValueExpression(SessionFactoryScope scope) { + assertResultSize( scope, "from Animal x where (select max(a.bodyWeight) from Animal a) in (1,2,3)", 0 ); + assertResultSize( scope,"from Animal x where (select max(a.bodyWeight) from Animal a) between 0 and 100", 0 ); + assertResultSize( scope,"from Animal x where (select max(a.description) from Animal a) like 'big%'", 0 ); + assertResultSize( scope,"from Animal x where (select max(a.bodyWeight) from Animal a) is not null", 0 ); } - public void testExistsSubquery() { - assertResultSize( "from Animal x where exists (select max(a.bodyWeight) from Animal a)", 0 ); + public void testExistsSubquery(SessionFactoryScope scope) { + assertResultSize( scope, "from Animal x where exists (select max(a.bodyWeight) from Animal a)", 0 ); } - private void assertResultSize(String hql, int size) { - Session session = openSession(); + private void assertResultSize(SessionFactoryScope scope, String hql, int size) { + Session session = scope.getSessionFactory().openSession(); Transaction txn = session.beginTransaction(); assertEquals( size, session.createQuery(hql).list().size() ); txn.commit(); @@ -3782,14 +3778,16 @@ public void prepare(Query query) { }; private class SyntaxChecker { + private final SessionFactoryScope scope; private final String hql; private final QueryPreparer preparer; - public SyntaxChecker(String hql) { - this( hql, DEFAULT_PREPARER ); + public SyntaxChecker(SessionFactoryScope scope, String hql) { + this( scope, hql, DEFAULT_PREPARER ); } - public SyntaxChecker(String hql, QueryPreparer preparer) { + public SyntaxChecker(SessionFactoryScope scope, String hql, QueryPreparer preparer) { + this.scope = scope; this.hql = hql; this.preparer = preparer; } @@ -3800,7 +3798,7 @@ public void checkAll() { } public SyntaxChecker checkList() { - Session s = openSession(); + Session s = scope.getSessionFactory().openSession(); s.beginTransaction(); Query query = s.createQuery( hql ); preparer.prepare( query ); @@ -3811,7 +3809,7 @@ public SyntaxChecker checkList() { } public SyntaxChecker checkScroll() { - Session s = openSession(); + Session s = scope.getSessionFactory().openSession(); s.beginTransaction(); Query query = s.createQuery( hql ); preparer.prepare( query ); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/hql/HQLTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/hql/HQLTest.java index aa71355e8678..752f6118f586 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/hql/HQLTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/hql/HQLTest.java @@ -25,6 +25,7 @@ import org.hibernate.ScrollableResults; import org.hibernate.Session; import org.hibernate.community.dialect.FirebirdDialect; +import org.hibernate.community.dialect.InformixDialect; import org.hibernate.dialect.CockroachDialect; import org.hibernate.dialect.DerbyDialect; import org.hibernate.dialect.H2Dialect; @@ -1523,6 +1524,7 @@ public void test_hql_aggregate_functions_filter_example() { @SkipForDialect(dialectClass = DerbyDialect.class) @SkipForDialect(dialectClass = SybaseASEDialect.class) @SkipForDialect(dialectClass = FirebirdDialect.class, reason = "order by not supported in list") + @SkipForDialect(dialectClass = InformixDialect.class) public void test_hql_aggregate_functions_within_group_example() { doInJPA(this::entityManagerFactory, entityManager -> { //tag::hql-aggregate-functions-within-group-example[] diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/hql/HqlParserMemoryUsageTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/hql/HqlParserMemoryUsageTest.java new file mode 100644 index 000000000000..ec511f1471fd --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/hql/HqlParserMemoryUsageTest.java @@ -0,0 +1,164 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.hql; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import org.hibernate.cfg.QuerySettings; +import org.hibernate.query.hql.HqlTranslator; +import org.hibernate.testing.memory.MemoryUsageUtil; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.Setting; +import org.junit.jupiter.api.Test; + +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +@DomainModel( + annotatedClasses = { + HqlParserMemoryUsageTest.Address.class, + HqlParserMemoryUsageTest.AppUser.class, + HqlParserMemoryUsageTest.Category.class, + HqlParserMemoryUsageTest.Discount.class, + HqlParserMemoryUsageTest.Order.class, + HqlParserMemoryUsageTest.OrderItem.class, + HqlParserMemoryUsageTest.Product.class + } +) +@SessionFactory +@ServiceRegistry(settings = @Setting(name = QuerySettings.QUERY_PLAN_CACHE_ENABLED, value = "false")) +@Jira("https://hibernate.atlassian.net/browse/HHH-19240") +public class HqlParserMemoryUsageTest { + + private static final String HQL = "SELECT DISTINCT u.id\n" + + "FROM AppUser u\n" + + "LEFT JOIN u.addresses a\n" + + "LEFT JOIN u.orders o\n" + + "LEFT JOIN o.orderItems oi\n" + + "LEFT JOIN oi.product p\n" + + "LEFT JOIN p.discounts d\n" + + "WHERE u.id = :userId\n" + + "AND (\n" + + " CASE\n" + + " WHEN u.name = 'SPECIAL_USER' THEN TRUE\n" + + " ELSE (\n" + + " CASE\n" + + " WHEN a.city = 'New York' THEN TRUE\n" + + " ELSE (\n" + + " p.category.name = 'Electronics'\n" + + " OR d.code LIKE '%DISC%'\n" + + " OR u.id IN (\n" + + " SELECT u2.id\n" + + " FROM AppUser u2\n" + + " JOIN u2.orders o2\n" + + " JOIN o2.orderItems oi2\n" + + " JOIN oi2.product p2\n" + + " WHERE p2.price > (\n" + + " SELECT AVG(p3.price) FROM Product p3\n" + + " )\n" + + " )\n" + + " )\n" + + " END\n" + + " )\n" + + " END\n" + + ")\n"; + + + @Test + public void testParserMemoryUsage(SessionFactoryScope scope) { + final HqlTranslator hqlTranslator = scope.getSessionFactory().getQueryEngine().getHqlTranslator(); + + // Ensure classes and basic stuff is initialized in case this is the first test run + hqlTranslator.translate( "from AppUser", AppUser.class ); + + // During testing, before the fix for HHH-19240, the allocation was around 500+ MB, + // and after the fix it dropped to 170 - 250 MB + final long memoryUsage = MemoryUsageUtil.estimateMemoryUsage( () -> hqlTranslator.translate( HQL, Long.class ) ); + System.out.println( "Memory Consumption: " + (memoryUsage / 1024) + " KB" ); + assertTrue( memoryUsage < 256_000_000, "Parsing of queries consumes too much memory (" + ( memoryUsage / 1024 ) + " KB), when at most 256 MB are expected" ); + } + + @Entity(name = "Address") + @Table(name = "addresses") + public static class Address { + @Id + private Long id; + private String city; + @ManyToOne(fetch = FetchType.LAZY) + private AppUser user; + } + @Entity(name = "AppUser") + @Table(name = "app_users") + public static class AppUser { + @Id + private Long id; + private String name; + @OneToMany(mappedBy = "user") + private Set

    addresses; + @OneToMany(mappedBy = "user") + private Set orders; + } + + @Entity(name = "Category") + @Table(name = "categories") + public static class Category { + @Id + private Long id; + private String name; + } + + @Entity(name = "Discount") + @Table(name = "discounts") + public static class Discount { + @Id + private Long id; + private String code; + @ManyToOne(fetch = FetchType.LAZY) + private Product product; + } + + @Entity(name = "Order") + @Table(name = "orders") + public static class Order { + @Id + private Long id; + @ManyToOne(fetch = FetchType.LAZY) + private AppUser user; + @OneToMany(mappedBy = "order") + private Set orderItems; + } + @Entity(name = "OrderItem") + @Table(name = "order_items") + public static class OrderItem { + @Id + private Long id; + @ManyToOne(fetch = FetchType.LAZY) + private Order order; + @ManyToOne(fetch = FetchType.LAZY) + private Product product; + } + + @Entity(name = "Product") + @Table(name = "products") + public static class Product { + @Id + private Long id; + private String name; + private Double price; + @ManyToOne(fetch = FetchType.LAZY) + private Category category; + @OneToMany(mappedBy = "product") + private Set discounts; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/hql/ManyToOneJoinReuseTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/hql/ManyToOneJoinReuseTest.java index 4722974bc7e9..b62d05aea280 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/hql/ManyToOneJoinReuseTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/hql/ManyToOneJoinReuseTest.java @@ -78,7 +78,7 @@ public void joinAndImplicitPath(SessionFactoryScope scope) { query.where( cb.and( root.get( "book" ).isNotNull(), - join.isNotNull() + cb.fk( root.get( "book" ) ).isNotNull() ) ); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/hql/SingleSelectionArrayResultTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/hql/SingleSelectionArrayResultTest.java new file mode 100644 index 000000000000..fc03650e1fc4 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/hql/SingleSelectionArrayResultTest.java @@ -0,0 +1,157 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.hql; + +import java.util.stream.Stream; + +import org.hibernate.dialect.H2Dialect; +import org.hibernate.dialect.PostgreSQLDialect; +import org.hibernate.query.NativeQuery; +import org.hibernate.query.Query; +import org.hibernate.query.SelectionQuery; + +import org.hibernate.testing.orm.domain.gambit.BasicEntity; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.RequiresDialect; +import org.hibernate.testing.orm.junit.RequiresDialects; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.extension.ExtensionContext; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.ArgumentsProvider; +import org.junit.jupiter.params.provider.ArgumentsSource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Marco Belladelli + */ +@DomainModel(annotatedClasses = BasicEntity.class) +@SessionFactory +@Jira("https://hibernate.atlassian.net/browse/HHH-18450") +@Jira("https://hibernate.atlassian.net/browse/HHH-19472") +@RequiresDialects({@RequiresDialect(H2Dialect.class), @RequiresDialect(PostgreSQLDialect.class)}) +public class SingleSelectionArrayResultTest { + + static class TestArguments implements ArgumentsProvider { + @Override + public Stream provideArguments(ExtensionContext extensionContext) { + return Stream.of( + Arguments.of( "select 1", null, null ), + Arguments.of( "select cast(1 as integer)", null, null ), + Arguments.of( "select id from BasicEntity", null, null ), + Arguments.of( "select cast(id as integer) from BasicEntity", null, null ), + Arguments.of( "select ?1", 1, 1 ), + Arguments.of( "select :p1", "p1", 1 ), + Arguments.of( "select cast(?1 as integer)", 1, 1 ), + Arguments.of( "select cast(:p1 as integer)", "p1", 1 ) + ); + } + } + + @ParameterizedTest + @ArgumentsSource(TestArguments.class) + public void testQueryObjectResult(String ql, Object arg1, Object arg2, SessionFactoryScope scope) { + scope.inTransaction( session -> { + Query query = session.createQuery( ql, Object.class ); + if ( arg1 instanceof Integer ) { + query.setParameter( (Integer) arg1, arg2 ); + } + if ( arg1 instanceof String ) { + query.setParameter( (String) arg1, arg2 ); + } + assertThat( query.getSingleResult() ).isInstanceOf( Integer.class ).isEqualTo( 1 ); + } ); + } + + @ParameterizedTest + @ArgumentsSource(TestArguments.class) + public void testNativeQueryObjectResult(String ql, Object arg1, Object arg2, SessionFactoryScope scope) { + scope.inTransaction( session -> { + NativeQuery query = session.createNativeQuery( ql, Object.class ); + if ( arg1 instanceof Integer ) { + query.setParameter( (Integer) arg1, arg2 ); + } + if ( arg1 instanceof String ) { + query.setParameter( (String) arg1, arg2 ); + } + assertThat( query.getSingleResult() ).isInstanceOf( Integer.class ).isEqualTo( 1 ); + } ); + } + + @ParameterizedTest + @ArgumentsSource(TestArguments.class) + public void testSelectionQueryObjectResult(String ql, Object arg1, Object arg2, SessionFactoryScope scope) { + scope.inTransaction( session -> { + SelectionQuery query = session.createSelectionQuery( ql, Object.class ); + if ( arg1 instanceof Integer ) { + query.setParameter( (Integer) arg1, arg2 ); + } + if ( arg1 instanceof String ) { + query.setParameter( (String) arg1, arg2 ); + } + assertThat( query.getSingleResult() ).isInstanceOf( Integer.class ).isEqualTo( 1 ); + } ); + } + + @ParameterizedTest + @ArgumentsSource(TestArguments.class) + public void testQueryArrayResult(String ql, Object arg1, Object arg2, SessionFactoryScope scope) { + scope.inTransaction( session -> { + Query query = session.createQuery( ql, Object[].class ); + if ( arg1 instanceof Integer ) { + query.setParameter( (Integer) arg1, arg2 ); + } + if ( arg1 instanceof String ) { + query.setParameter( (String) arg1, arg2 ); + } + assertThat( query.getSingleResult() ).containsExactly( 1 ); + } ); + } + + @ParameterizedTest + @ArgumentsSource(TestArguments.class) + public void testNativeQueryArrayResult(String ql, Object arg1, Object arg2, SessionFactoryScope scope) { + scope.inTransaction( session -> { + NativeQuery query = session.createNativeQuery( ql, Object[].class ); + if ( arg1 instanceof Integer ) { + query.setParameter( (Integer) arg1, arg2 ); + } + if ( arg1 instanceof String ) { + query.setParameter( (String) arg1, arg2 ); + } + assertThat( query.getSingleResult() ).containsExactly( 1 ); + } ); + } + + @ParameterizedTest + @ArgumentsSource(TestArguments.class) + public void testSelectionQueryArrayResult(String ql, Object arg1, Object arg2, SessionFactoryScope scope) { + scope.inTransaction( session -> { + SelectionQuery query = session.createSelectionQuery( ql, Object[].class ); + if ( arg1 instanceof Integer ) { + query.setParameter( (Integer) arg1, arg2 ); + } + if ( arg1 instanceof String ) { + query.setParameter( (String) arg1, arg2 ); + } + assertThat( query.getSingleResult() ).containsExactly( 1 ); + } ); + } + + @BeforeAll + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> session.persist( new BasicEntity( 1, "entity_1" ) ) ); + } + + @AfterAll + public void tearDown(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/hql/joinedSubclass/JoinedSubclassNativeQueryTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/hql/joinedSubclass/JoinedSubclassNativeQueryTest.java index 0adbb4ba858c..6ee9c2eab947 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/hql/joinedSubclass/JoinedSubclassNativeQueryTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/hql/joinedSubclass/JoinedSubclassNativeQueryTest.java @@ -8,12 +8,13 @@ import org.hibernate.cfg.AvailableSettings; import org.hibernate.engine.spi.SessionFactoryImplementor; -import org.hibernate.type.SqlTypes; +import org.hibernate.metamodel.mapping.internal.SqlTypedMappingImpl; import org.hibernate.testing.TestForIssue; import org.hibernate.testing.orm.junit.DomainModel; import org.hibernate.testing.orm.junit.SessionFactory; import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.type.spi.TypeConfiguration; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; @@ -63,10 +64,14 @@ public void testJoinedInheritanceNativeQuery(SessionFactoryScope scope) { scope.inTransaction( session -> { final SessionFactoryImplementor sessionFactory = scope.getSessionFactory(); + final TypeConfiguration typeConfiguration = sessionFactory.getTypeConfiguration(); final String nullColumnString = sessionFactory .getJdbcServices() .getDialect() - .getSelectClauseNullString( SqlTypes.VARCHAR, sessionFactory.getTypeConfiguration() ); + .getSelectClauseNullString( + new SqlTypedMappingImpl( typeConfiguration.getBasicTypeForJavaType( String.class ) ), + typeConfiguration + ); // PostgreSQLDialect#getSelectClauseNullString produces e.g. `null::text` which we interpret as parameter, // so workaround this problem by configuring to ignore JDBC parameters session.setProperty( AvailableSettings.NATIVE_IGNORE_JDBC_PARAMETERS, true ); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/id/array/PrimitiveByteArrayIdTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/id/array/PrimitiveByteArrayIdTest.java index fdc58d6266b5..6acf9d91f470 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/id/array/PrimitiveByteArrayIdTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/id/array/PrimitiveByteArrayIdTest.java @@ -13,6 +13,7 @@ import jakarta.persistence.Id; import jakarta.persistence.Table; +import org.hibernate.community.dialect.InformixDialect; import org.hibernate.dialect.MySQLDialect; import org.hibernate.dialect.OracleDialect; import org.hibernate.query.Query; @@ -35,6 +36,7 @@ */ @SkipForDialect(dialectClass = MySQLDialect.class, majorVersion = 5, reason = "BLOB/TEXT column 'id' used in key specification without a key length") @SkipForDialect(dialectClass = OracleDialect.class, matchSubTypes = true, reason = "ORA-02329: column of datatype LOB cannot be unique or a primary key") +@SkipForDialect(dialectClass = InformixDialect.class, reason = "Informix does not support unique / primary constraints on binary columns") @DomainModel( annotatedClasses = PrimitiveByteArrayIdTest.DemoEntity.class ) diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/id/enhanced/SequenceStyleConfigUnitTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/id/enhanced/SequenceStyleConfigUnitTest.java index 37a91093698a..34c39525ec05 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/id/enhanced/SequenceStyleConfigUnitTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/id/enhanced/SequenceStyleConfigUnitTest.java @@ -20,6 +20,7 @@ import org.hibernate.dialect.sequence.ANSISequenceSupport; import org.hibernate.dialect.sequence.SequenceSupport; import org.hibernate.engine.jdbc.spi.JdbcServices; +import org.hibernate.generator.GeneratorCreationContext; import org.hibernate.id.OptimizableGenerator; import org.hibernate.id.PersistentIdentifierGenerator; import org.hibernate.id.enhanced.DatabaseStructure; @@ -33,6 +34,9 @@ import org.hibernate.id.enhanced.SequenceStyleGenerator; import org.hibernate.id.enhanced.StandardOptimizerDescriptor; import org.hibernate.id.enhanced.TableStructure; +import org.hibernate.mapping.PersistentClass; +import org.hibernate.mapping.Property; +import org.hibernate.service.ServiceRegistry; import org.hibernate.type.StandardBasicTypes; import org.hibernate.type.spi.TypeConfiguration; @@ -70,8 +74,11 @@ public void testDefaultedSequenceBackedConfiguration() { () -> buildingContext.getBootstrapContext().getTypeConfiguration(), serviceRegistry ); + Database database = new Database( buildingContext.getBuildingOptions() ); + GeneratorCreationContextImpl generatorCreationContext = new GeneratorCreationContextImpl( database ); Properties props = buildGeneratorPropertiesBase( buildingContext ); SequenceStyleGenerator generator = new SequenceStyleGenerator(); + generator.create( generatorCreationContext ); generator.configure( new TypeConfiguration().getBasicTypeRegistry() .resolve( StandardBasicTypes.LONG ), @@ -79,7 +86,6 @@ public void testDefaultedSequenceBackedConfiguration() { serviceRegistry ); - Database database = new Database( buildingContext.getBuildingOptions() ); generator.registerExportables( database ); generator.initialize( SqlStringGenerationContextImpl.forTests( database.getJdbcEnvironment() ) ); @@ -120,8 +126,11 @@ public void testDefaultedTableBackedConfiguration() { () -> buildingContext.getBootstrapContext().getTypeConfiguration(), serviceRegistry ); + Database database = new Database( buildingContext.getBuildingOptions() ); + GeneratorCreationContextImpl generatorCreationContext = new GeneratorCreationContextImpl( database ); Properties props = buildGeneratorPropertiesBase( buildingContext ); SequenceStyleGenerator generator = new SequenceStyleGenerator(); + generator.create( generatorCreationContext ); generator.configure( new TypeConfiguration().getBasicTypeRegistry() .resolve( StandardBasicTypes.LONG ), @@ -129,7 +138,6 @@ public void testDefaultedTableBackedConfiguration() { serviceRegistry ); - Database database = new Database( buildingContext.getBuildingOptions() ); generator.registerExportables( database ); generator.initialize( SqlStringGenerationContextImpl.forTests( database.getJdbcEnvironment() ) ); @@ -160,10 +168,15 @@ public void testDefaultOptimizerBasedOnIncrementBackedBySequence() { () -> buildingContext.getBootstrapContext().getTypeConfiguration(), serviceRegistry ); + + Database database = new Database( buildingContext.getBuildingOptions() ); + GeneratorCreationContextImpl generatorCreationContext = new GeneratorCreationContextImpl( database ); + Properties props = buildGeneratorPropertiesBase( buildingContext ); props.setProperty( SequenceStyleGenerator.INCREMENT_PARAM, "10" ); SequenceStyleGenerator generator = new SequenceStyleGenerator(); + generator.create( generatorCreationContext ); generator.configure( new TypeConfiguration().getBasicTypeRegistry() .resolve( StandardBasicTypes.LONG ), @@ -171,7 +184,6 @@ public void testDefaultOptimizerBasedOnIncrementBackedBySequence() { serviceRegistry ); - Database database = new Database( buildingContext.getBuildingOptions() ); generator.registerExportables( database ); generator.initialize( SqlStringGenerationContextImpl.forTests( database.getJdbcEnvironment() ) ); @@ -190,17 +202,20 @@ public void testDefaultOptimizerBasedOnIncrementBackedBySequence() { () -> buildingContext.getBootstrapContext().getTypeConfiguration(), serviceRegistry ); + Database database = new Database( buildingContext.getBuildingOptions() ); + GeneratorCreationContextImpl generatorCreationContext = new GeneratorCreationContextImpl( database ); + Properties props = buildGeneratorPropertiesBase( buildingContext ); props.setProperty( SequenceStyleGenerator.INCREMENT_PARAM, "10" ); SequenceStyleGenerator generator = new SequenceStyleGenerator(); + generator.create( generatorCreationContext ); generator.configure( new TypeConfiguration().getBasicTypeRegistry() .resolve( StandardBasicTypes.LONG ), props, serviceRegistry ); - Database database = new Database( buildingContext.getBuildingOptions() ); generator.registerExportables( database ); generator.initialize( SqlStringGenerationContextImpl.forTests( database.getJdbcEnvironment() ) ); @@ -226,17 +241,20 @@ public void testDefaultOptimizerBasedOnIncrementBackedByTable() { () -> buildingContext.getBootstrapContext().getTypeConfiguration(), serviceRegistry ); + Database database = new Database( buildingContext.getBuildingOptions() ); + GeneratorCreationContextImpl generatorCreationContext = new GeneratorCreationContextImpl( database ); + Properties props = buildGeneratorPropertiesBase( buildingContext ); props.setProperty( SequenceStyleGenerator.INCREMENT_PARAM, "10" ); SequenceStyleGenerator generator = new SequenceStyleGenerator(); + generator.create( generatorCreationContext ); generator.configure( new TypeConfiguration().getBasicTypeRegistry() .resolve( StandardBasicTypes.LONG ), props, serviceRegistry ); - Database database = new Database( buildingContext.getBuildingOptions() ); generator.registerExportables( database ); generator.initialize( SqlStringGenerationContextImpl.forTests( database.getJdbcEnvironment() ) ); @@ -260,17 +278,19 @@ public void testForceTableUse() { () -> buildingContext.getBootstrapContext().getTypeConfiguration(), serviceRegistry ); + Database database = new Database( buildingContext.getBuildingOptions() ); + GeneratorCreationContextImpl generatorCreationContext = new GeneratorCreationContextImpl( database ); Properties props = buildGeneratorPropertiesBase( buildingContext ); props.setProperty( SequenceStyleGenerator.FORCE_TBL_PARAM, "true" ); SequenceStyleGenerator generator = new SequenceStyleGenerator(); + generator.create( generatorCreationContext ); generator.configure( new TypeConfiguration().getBasicTypeRegistry() .resolve( StandardBasicTypes.LONG ), props, serviceRegistry ); - Database database = new Database( buildingContext.getBuildingOptions() ); generator.registerExportables( database ); generator.initialize( SqlStringGenerationContextImpl.forTests( database.getJdbcEnvironment() ) ); @@ -285,6 +305,44 @@ public void testForceTableUse() { } } + private static class GeneratorCreationContextImpl implements GeneratorCreationContext { + private final Database database; + + public GeneratorCreationContextImpl(Database database) { + this.database = database; + } + + @Override + public Database getDatabase() { + return database; + } + + @Override + public ServiceRegistry getServiceRegistry() { + return database.getServiceRegistry(); + } + + @Override + public String getDefaultCatalog() { + throw new UnsupportedOperationException(); + } + + @Override + public String getDefaultSchema() { + throw new UnsupportedOperationException(); + } + + @Override + public PersistentClass getPersistentClass() { + throw new UnsupportedOperationException(); + } + + @Override + public Property getProperty() { + throw new UnsupportedOperationException(); + } + } + /** * Test explicitly specifying both optimizer and increment */ @@ -302,14 +360,18 @@ public void testExplicitOptimizerWithExplicitIncrementSize() { Properties props = buildGeneratorPropertiesBase( buildingContext ); props.setProperty( SequenceStyleGenerator.OPT_PARAM, StandardOptimizerDescriptor.NONE.getExternalName() ); props.setProperty( SequenceStyleGenerator.INCREMENT_PARAM, "20" ); + + Database database = new Database( buildingContext.getBuildingOptions() ); + GeneratorCreationContextImpl generatorCreationContext = new GeneratorCreationContextImpl( database ); + SequenceStyleGenerator generator = new SequenceStyleGenerator(); + generator.create( generatorCreationContext ); generator.configure( new TypeConfiguration().getBasicTypeRegistry() .resolve( StandardBasicTypes.LONG ), props, serviceRegistry ); - Database database = new Database( buildingContext.getBuildingOptions() ); generator.registerExportables( database ); generator.initialize( SqlStringGenerationContextImpl.forTests( database.getJdbcEnvironment() ) ); @@ -323,6 +385,7 @@ public void testExplicitOptimizerWithExplicitIncrementSize() { props.setProperty( SequenceStyleGenerator.OPT_PARAM, StandardOptimizerDescriptor.HILO.getExternalName() ); props.setProperty( SequenceStyleGenerator.INCREMENT_PARAM, "20" ); generator = new SequenceStyleGenerator(); + generator.create( generatorCreationContext ); generator.configure( new TypeConfiguration().getBasicTypeRegistry() .resolve( StandardBasicTypes.LONG ), @@ -341,6 +404,7 @@ public void testExplicitOptimizerWithExplicitIncrementSize() { props.setProperty( SequenceStyleGenerator.OPT_PARAM, StandardOptimizerDescriptor.POOLED.getExternalName() ); props.setProperty( SequenceStyleGenerator.INCREMENT_PARAM, "20" ); generator = new SequenceStyleGenerator(); + generator.create( generatorCreationContext ); generator.configure( new TypeConfiguration().getBasicTypeRegistry() .resolve( StandardBasicTypes.LONG ), @@ -368,16 +432,19 @@ public void testPreferredPooledOptimizerSetting() { () -> buildingContext.getBootstrapContext().getTypeConfiguration(), serviceRegistry ); + Database database = new Database( buildingContext.getBuildingOptions() ); + GeneratorCreationContextImpl generatorCreationContext = new GeneratorCreationContextImpl( database ); + Properties props = buildGeneratorPropertiesBase( buildingContext ); props.setProperty( SequenceStyleGenerator.INCREMENT_PARAM, "20" ); SequenceStyleGenerator generator = new SequenceStyleGenerator(); + generator.create( generatorCreationContext ); generator.configure( new TypeConfiguration().getBasicTypeRegistry() .resolve( StandardBasicTypes.LONG ), props, serviceRegistry ); - Database database = new Database( buildingContext.getBuildingOptions() ); generator.registerExportables( database ); generator.initialize( SqlStringGenerationContextImpl.forTests( database.getJdbcEnvironment() ) ); assertClassAssignability( SequenceStructure.class, generator.getDatabaseStructure().getClass() ); @@ -385,6 +452,7 @@ public void testPreferredPooledOptimizerSetting() { props.setProperty( Environment.PREFERRED_POOLED_OPTIMIZER, StandardOptimizerDescriptor.POOLED_LO.getExternalName() ); generator = new SequenceStyleGenerator(); + generator.create( generatorCreationContext ); generator.configure( new TypeConfiguration().getBasicTypeRegistry() .resolve( StandardBasicTypes.LONG ), @@ -398,6 +466,7 @@ public void testPreferredPooledOptimizerSetting() { props.setProperty( Environment.PREFERRED_POOLED_OPTIMIZER, StandardOptimizerDescriptor.POOLED_LOTL.getExternalName() ); generator = new SequenceStyleGenerator(); + generator.create( generatorCreationContext ); generator.configure( new TypeConfiguration().getBasicTypeRegistry() .resolve( StandardBasicTypes.LONG ), diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/id/idClass/IdClassSyntheticAttributesTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/id/idClass/IdClassSyntheticAttributesTest.java new file mode 100644 index 000000000000..22fb93f9149b --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/id/idClass/IdClassSyntheticAttributesTest.java @@ -0,0 +1,36 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.id.idClass; + +import org.hibernate.mapping.PersistentClass; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.DomainModelScope; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel( + annotatedClasses = MyEntity.class +) +@SessionFactory +public class IdClassSyntheticAttributesTest { + + @Jira("https://hibernate.atlassian.net/browse/HHH-18841") + @Test + public void test(DomainModelScope scope) { + final PersistentClass entityBinding = scope.getDomainModel().getEntityBinding(MyEntity.class.getName()); + assertThat(entityBinding.getProperties()).hasSize(2) + .anySatisfy(p -> { + assertThat(p.isSynthetic()).isTrue(); + assertThat(p.getName()).isEqualTo("_identifierMapper"); + }) + .anySatisfy(p -> { + assertThat(p.isSynthetic()).isFalse(); + assertThat(p.getName()).isEqualTo("notes"); + }); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/id/sequence/PooledWithCustomNamingStrategyTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/id/sequence/PooledWithCustomNamingStrategyTest.java new file mode 100644 index 000000000000..9d1cb8c7c19a --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/id/sequence/PooledWithCustomNamingStrategyTest.java @@ -0,0 +1,167 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.id.sequence; + +import java.sql.Connection; +import java.sql.SQLException; +import java.sql.Statement; + +import org.hibernate.SessionFactory; +import org.hibernate.boot.MetadataSources; +import org.hibernate.boot.model.naming.Identifier; +import org.hibernate.boot.model.naming.PhysicalNamingStrategy; +import org.hibernate.boot.registry.StandardServiceRegistryBuilder; +import org.hibernate.boot.spi.MetadataImplementor; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.cfg.Environment; +import org.hibernate.dialect.H2Dialect; +import org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl; +import org.hibernate.engine.jdbc.connections.spi.ConnectionProvider; +import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.internal.util.PropertiesHelper; + +import org.hibernate.testing.orm.junit.BaseUnitTest; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.RequiresDialect; +import org.hibernate.testing.util.ServiceRegistryUtil; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.SequenceGenerator; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.hibernate.testing.transaction.TransactionUtil2.inTransaction; +import static org.junit.jupiter.api.Assertions.fail; + +/** + * @author Marco Belladelli + */ +@BaseUnitTest +@RequiresDialect( H2Dialect.class ) +@Jira( "https://hibernate.atlassian.net/browse/HHH-18337" ) +public class PooledWithCustomNamingStrategyTest { + @Test + public void testWrongIncrementSize() { + final StandardServiceRegistryBuilder registryBuilder = ServiceRegistryUtil.serviceRegistryBuilder(); + registryBuilder.applySetting( AvailableSettings.PHYSICAL_NAMING_STRATEGY, MyPhysicalNamingStrategy.INSTANCE ); + final MetadataImplementor metadata = (MetadataImplementor) new MetadataSources( registryBuilder.build() ) + .addAnnotatedClass( WrongEntity.class ) + .buildMetadata(); + try (final SessionFactory sf = metadata.buildSessionFactory()) { + fail( "Default increment size of [50] should not work with the database sequence increment size [1]." ); + } + catch (Exception e) { + assertThat( e.getCause().getMessage() ).isEqualTo( + "The increment size of the [MY_SEQ] sequence is set to [50] in the entity " + + "mapping while the associated database sequence increment size is [1]." + ); + } + } + + @Test + public void testRightIncrementSize() { + final StandardServiceRegistryBuilder registryBuilder = ServiceRegistryUtil.serviceRegistryBuilder(); + registryBuilder.applySetting( AvailableSettings.PHYSICAL_NAMING_STRATEGY, MyPhysicalNamingStrategy.INSTANCE ); + final MetadataImplementor metadata = (MetadataImplementor) new MetadataSources( registryBuilder.build() ) + .addAnnotatedClass( RightEntity.class ) + .buildMetadata(); + try (final SessionFactoryImplementor sf = (SessionFactoryImplementor) metadata.buildSessionFactory()) { + // session factory should be created correctly + inTransaction( sf, session -> session.persist( new RightEntity() ) ); + inTransaction( sf, session -> { + final RightEntity result = session.createQuery( "from RightEntity", RightEntity.class ) + .getSingleResult(); + assertThat( result.id ).isNotNull(); + } ); + } + catch (Exception e) { + fail( "Expected configured increment size of [1] to be compatible with the existing sequence" ); + } + } + + ConnectionProvider connectionProvider; + + @BeforeAll + public void setUp() { + final DriverManagerConnectionProviderImpl provider = new DriverManagerConnectionProviderImpl(); + provider.configure( PropertiesHelper.map( Environment.getProperties() ) ); + connectionProvider = provider; + try (final Connection connection = connectionProvider.getConnection(); + final Statement statement = connection.createStatement()) { + statement.execute( "create sequence MY_SEQ start with 1 increment by 1" ); + statement.execute( "create table RightEntity(id bigint not null, primary key (id))" ); + } + catch (SQLException e) { + throw new RuntimeException( "Failed to setup the test", e ); + } + } + + @AfterAll + public void tearDown() { + try (final Connection connection = connectionProvider.getConnection(); + final Statement statement = connection.createStatement()) { + statement.execute( "drop sequence MY_SEQ" ); + statement.execute( "drop table RightEntity" ); + } + catch (SQLException e) { + } + } + + @Entity( name = "WrongEntity" ) + static class WrongEntity { + @Id + @GeneratedValue( strategy = GenerationType.SEQUENCE, generator = "my-sequence-generator" ) + @SequenceGenerator( name = "my-sequence-generator", sequenceName = "REPLACE_SEQ" ) + private Long id; + } + + @Entity( name = "RightEntity" ) + static class RightEntity { + @Id + @GeneratedValue( strategy = GenerationType.SEQUENCE, generator = "my-sequence-generator" ) + @SequenceGenerator( name = "my-sequence-generator", sequenceName = "REPLACE_SEQ", allocationSize = 1 ) + private Long id; + } + + static class MyPhysicalNamingStrategy implements PhysicalNamingStrategy { + static MyPhysicalNamingStrategy INSTANCE = new MyPhysicalNamingStrategy(); + + @Override + public Identifier toPhysicalCatalogName(Identifier logicalName, JdbcEnvironment jdbcEnvironment) { + return logicalName; + } + + @Override + public Identifier toPhysicalSchemaName(Identifier logicalName, JdbcEnvironment jdbcEnvironment) { + return logicalName; + } + + @Override + public Identifier toPhysicalTableName(Identifier logicalName, JdbcEnvironment jdbcEnvironment) { + return logicalName; + } + + @Override + public Identifier toPhysicalSequenceName(Identifier logicalName, JdbcEnvironment jdbcEnvironment) { + return Identifier.toIdentifier( + logicalName.getText().replaceAll( "REPLACE_", "MY_" ), + logicalName.isQuoted() + ); + } + + @Override + public Identifier toPhysicalColumnName(Identifier logicalName, JdbcEnvironment jdbcEnvironment) { + return logicalName; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/id/usertype/PostgreSQLMultipleTypesOtherContributorTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/id/usertype/PostgreSQLMultipleTypesOtherContributorTest.java index ae0599b36ec3..f6fe9cd4f60b 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/id/usertype/PostgreSQLMultipleTypesOtherContributorTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/id/usertype/PostgreSQLMultipleTypesOtherContributorTest.java @@ -18,15 +18,14 @@ import org.hibernate.annotations.JdbcTypeCode; import org.hibernate.boot.spi.MetadataBuilderContributor; import org.hibernate.boot.spi.MetadataBuilderImplementor; -import org.hibernate.dialect.PostgreSQLDialect; import org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl; import org.hibernate.orm.test.jpa.BaseEntityManagerFunctionalTestCase; import org.hibernate.query.NativeQuery; +import org.hibernate.testing.orm.junit.DialectFeatureChecks; +import org.hibernate.testing.orm.junit.RequiresDialectFeature; import org.hibernate.type.SqlTypes; import org.hibernate.type.spi.TypeConfiguration; -import org.hibernate.testing.RequiresDialect; - import org.hibernate.orm.test.id.usertype.inet.Inet; import org.hibernate.orm.test.id.usertype.inet.InetJavaType; import org.hibernate.orm.test.id.usertype.inet.InetJdbcType; @@ -43,7 +42,7 @@ /** * @author Vlad Mihalcea */ -@RequiresDialect(PostgreSQLDialect.class) +@RequiresDialectFeature(feature = DialectFeatureChecks.IsPgJdbc.class) public class PostgreSQLMultipleTypesOtherContributorTest extends BaseEntityManagerFunctionalTestCase { @Override diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/id/usertype/inet/PostgreSQLInetTypesOtherContributorTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/id/usertype/inet/PostgreSQLInetTypesOtherContributorTest.java index 3f66872a1129..1a6d507105e7 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/id/usertype/inet/PostgreSQLInetTypesOtherContributorTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/id/usertype/inet/PostgreSQLInetTypesOtherContributorTest.java @@ -12,11 +12,11 @@ import org.hibernate.boot.MetadataBuilder; import org.hibernate.boot.spi.MetadataBuilderContributor; import org.hibernate.boot.spi.MetadataBuilderImplementor; -import org.hibernate.dialect.PostgreSQLDialect; import org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl; +import org.hibernate.testing.orm.junit.DialectFeatureChecks; +import org.hibernate.testing.orm.junit.RequiresDialectFeature; import org.hibernate.type.spi.TypeConfiguration; -import org.hibernate.testing.RequiresDialect; import org.junit.Test; import static org.hibernate.testing.transaction.TransactionUtil.doInJPA; @@ -25,7 +25,7 @@ /** * @author Vlad Mihalcea */ -@RequiresDialect(PostgreSQLDialect.class) +@RequiresDialectFeature(feature = DialectFeatureChecks.IsPgJdbc.class) public class PostgreSQLInetTypesOtherContributorTest extends PostgreSQLInetTypesOtherTest { @Override diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/id/usertype/inet/PostgreSQLInetTypesOtherTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/id/usertype/inet/PostgreSQLInetTypesOtherTest.java index c1fc58cec3d4..b25f9dffd85e 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/id/usertype/inet/PostgreSQLInetTypesOtherTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/id/usertype/inet/PostgreSQLInetTypesOtherTest.java @@ -11,13 +11,13 @@ import org.hibernate.boot.spi.MetadataBuilderContributor; import org.hibernate.boot.spi.MetadataBuilderImplementor; -import org.hibernate.dialect.PostgreSQLDialect; import org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl; import org.hibernate.orm.test.jpa.BaseEntityManagerFunctionalTestCase; import org.hibernate.query.NativeQuery; +import org.hibernate.testing.orm.junit.DialectFeatureChecks; +import org.hibernate.testing.orm.junit.RequiresDialectFeature; import org.hibernate.type.spi.TypeConfiguration; -import org.hibernate.testing.RequiresDialect; import org.junit.Test; import static org.hibernate.testing.transaction.TransactionUtil.doInJPA; @@ -26,7 +26,7 @@ /** * @author Vlad Mihalcea */ -@RequiresDialect(PostgreSQLDialect.class) +@RequiresDialectFeature(feature = DialectFeatureChecks.IsPgJdbc.class) public class PostgreSQLInetTypesOtherTest extends BaseEntityManagerFunctionalTestCase { @Override diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/id/uuid/sqlrep/sqlbinary/UUIDBinaryTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/id/uuid/sqlrep/sqlbinary/UUIDBinaryTest.java index e59b38b77e4a..9f12fa719818 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/id/uuid/sqlrep/sqlbinary/UUIDBinaryTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/id/uuid/sqlrep/sqlbinary/UUIDBinaryTest.java @@ -10,6 +10,7 @@ import java.util.UUID; import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.community.dialect.InformixDialect; import org.hibernate.dialect.PostgreSQLDialect; import org.hibernate.dialect.SybaseDialect; import org.hibernate.metamodel.MappingMetamodel; @@ -39,6 +40,7 @@ @SkipForDialect(dialectClass = PostgreSQLDialect.class, reason = "Postgres has its own UUID type") @SkipForDialect( dialectClass = SybaseDialect.class, matchSubTypes = true, reason = "Skipped for Sybase to avoid problems with UUIDs potentially ending with a trailing 0 byte") +@SkipForDialect(dialectClass = InformixDialect.class, reason = "Informix does not support unique / primary constraints on binary columns") public class UUIDBinaryTest { private static class UUIDPair { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/idclass/mappedsuperclass/BaseSummary.java b/hibernate-core/src/test/java/org/hibernate/orm/test/idclass/mappedsuperclass/BaseSummary.java new file mode 100644 index 000000000000..822bd83011df --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/idclass/mappedsuperclass/BaseSummary.java @@ -0,0 +1,62 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.idclass.mappedsuperclass; + +import jakarta.persistence.Id; +import jakarta.persistence.IdClass; +import jakarta.persistence.MappedSuperclass; + +import java.io.Serializable; +import java.math.BigDecimal; +import java.util.Objects; + +@IdClass(PKey.class) +@MappedSuperclass +public class BaseSummary implements Serializable { + + @Id + private Integer year; + @Id + private Integer month; + private BigDecimal value; + + public Integer getYear() { + return year; + } + + public void setYear(Integer year) { + this.year = year; + } + + public Integer getMonth() { + return month; + } + + public void setMonth(Integer month) { + this.month = month; + } + + public BigDecimal getValue() { + return value; + } + + public void setValue(BigDecimal value) { + this.value = value; + } + + @Override + public boolean equals(Object o) { + if ( o == null || getClass() != o.getClass() ) { + return false; + } + BaseSummary that = (BaseSummary) o; + return Objects.equals( year, that.year ) && Objects.equals( month, that.month ); + } + + @Override + public int hashCode() { + return Objects.hash( year, month ); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/idclass/mappedsuperclass/MappedSuperclassIdClassAttributesTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/idclass/mappedsuperclass/MappedSuperclassIdClassAttributesTest.java new file mode 100644 index 000000000000..9782f919a4d3 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/idclass/mappedsuperclass/MappedSuperclassIdClassAttributesTest.java @@ -0,0 +1,32 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.idclass.mappedsuperclass; + +import jakarta.persistence.metamodel.SingularAttribute; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel(annotatedClasses = {Summary.class, BaseSummary.class}) +@SessionFactory +@JiraKey("HHH-18858") +public class MappedSuperclassIdClassAttributesTest { + @Test + public void test(SessionFactoryScope scope) { + scope.inSession( entityManager -> { + final var yearAttribute = Summary_.year.getDeclaringType().getAttribute( "year" ); + assertThat( yearAttribute ).isEqualTo( Summary_.year ); + assertThat( ((SingularAttribute) yearAttribute).isId() ).isTrue(); + + final var monthAttribute = Summary_.month.getDeclaringType().getAttribute( "month" ); + assertThat( monthAttribute ).isEqualTo( Summary_.month ); + assertThat( ((SingularAttribute) monthAttribute).isId() ).isTrue(); + } ); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/idclass/mappedsuperclass/PKey.java b/hibernate-core/src/test/java/org/hibernate/orm/test/idclass/mappedsuperclass/PKey.java new file mode 100644 index 000000000000..6757d23edcc4 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/idclass/mappedsuperclass/PKey.java @@ -0,0 +1,44 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.idclass.mappedsuperclass; + +import java.io.Serializable; +import java.util.Objects; + +public class PKey implements Serializable { + + private Integer year; + private Integer month; + + public Integer getYear() { + return year; + } + + public void setYear(Integer year) { + this.year = year; + } + + public Integer getMonth() { + return month; + } + + public void setMonth(Integer month) { + this.month = month; + } + + @Override + public boolean equals(Object o) { + if ( o == null || getClass() != o.getClass() ) { + return false; + } + PKey pKey = (PKey) o; + return Objects.equals( year, pKey.year ) && Objects.equals( month, pKey.month ); + } + + @Override + public int hashCode() { + return Objects.hash( year, month ); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/idclass/mappedsuperclass/Summary.java b/hibernate-core/src/test/java/org/hibernate/orm/test/idclass/mappedsuperclass/Summary.java new file mode 100644 index 000000000000..108602a2ce16 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/idclass/mappedsuperclass/Summary.java @@ -0,0 +1,11 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.idclass.mappedsuperclass; + +import jakarta.persistence.Entity; + +@Entity +public class Summary extends BaseSummary { +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/idgen/userdefined/BeforeExecutionAssignedValuesTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/idgen/userdefined/BeforeExecutionAssignedValuesTest.java new file mode 100644 index 000000000000..04f95fd62585 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/idgen/userdefined/BeforeExecutionAssignedValuesTest.java @@ -0,0 +1,258 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.idgen.userdefined; + +import jakarta.persistence.Embeddable; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import org.hibernate.annotations.IdGeneratorType; +import org.hibernate.annotations.ValueGenerationType; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.generator.BeforeExecutionGenerator; +import org.hibernate.generator.EventType; +import org.hibernate.generator.EventTypeSets; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; + +import java.lang.annotation.Retention; +import java.lang.annotation.Target; +import java.util.EnumSet; +import java.util.UUID; + +import static java.lang.annotation.ElementType.FIELD; +import static java.lang.annotation.ElementType.METHOD; +import static java.lang.annotation.RetentionPolicy.RUNTIME; +import static org.assertj.core.api.Assertions.assertThat; + +@SessionFactory +@DomainModel(annotatedClasses = { + BeforeExecutionAssignedValuesTest.EntityWithGeneratedId.class, + BeforeExecutionAssignedValuesTest.GeneratedCompositeId.class, + BeforeExecutionAssignedValuesTest.EntityWithGeneratedEmbeddedId.class, + BeforeExecutionAssignedValuesTest.EntityWithGeneratedProperty.class, +}) +@Jira("https://hibernate.atlassian.net/browse/HHH-19320") +class BeforeExecutionAssignedValuesTest { + @Test + void testAssignedId(SessionFactoryScope scope) { + final EntityWithGeneratedId entity1 = new EntityWithGeneratedId( "assigned-id", "assigned-entity" ); + scope.inTransaction( session -> session.persist( entity1 ) ); + assertThat( entity1.getGeneratedId() ).isEqualTo( "assigned-id" ); + + final EntityWithGeneratedId entity2 = new EntityWithGeneratedId( "stateless-id", "stateless-entity" ); + scope.inStatelessTransaction( session -> session.insert( entity2 ) ); + assertThat( entity2.getGeneratedId() ).isEqualTo( "stateless-id" ); + } + + @Test + void testGeneratedId(SessionFactoryScope scope) { + final EntityWithGeneratedId entity = new EntityWithGeneratedId( null, "assigned-entity" ); + scope.inTransaction( session -> session.persist( entity ) ); + assertThat( entity.getGeneratedId() ).isNotNull(); + } + + @Test + void testAssignedEmbeddedId(SessionFactoryScope scope) { + final EntityWithGeneratedEmbeddedId entity1 = new EntityWithGeneratedEmbeddedId( + new GeneratedCompositeId( "assigned-1", null), + "generated-entity" + ); + scope.inTransaction( session -> session.persist( entity1 ) ); + assertThat( entity1.getId().getId1() ).isEqualTo( "assigned-1" ); + assertThat( entity1.getId().getId2() ).isNotNull(); + + final EntityWithGeneratedEmbeddedId entity2 = new EntityWithGeneratedEmbeddedId( + new GeneratedCompositeId( "new-assigned-1", "assigned-2"), + "generated-entity" + ); + scope.inTransaction( session -> session.persist( entity2 ) ); + assertThat( entity2.getId().getId1() ).isEqualTo( "new-assigned-1" ); + assertThat( entity2.getId().getId2() ).isEqualTo( "assigned-2" ); + } + + @Test + void testGeneratedEmbeddedId(SessionFactoryScope scope) { + final EntityWithGeneratedEmbeddedId entity = new EntityWithGeneratedEmbeddedId( + new GeneratedCompositeId(), + "generated-entity" + ); + scope.inTransaction( session -> session.persist( entity ) ); + assertThat( entity.getId().getId1() ).isNotNull(); + assertThat( entity.getId().getId2() ).isNotNull(); + } + + @Test + void testInsertAssignedProperty(SessionFactoryScope scope) { + final String assigned = "assigned-property"; + final EntityWithGeneratedProperty entity = new EntityWithGeneratedProperty( 1L, assigned ); + scope.inTransaction( session -> session.persist( entity ) ); + assertThat( entity.getGeneratedProperty() ).isEqualTo( assigned ); + } + + @Test + void testGeneratedPropertyAndUpdate(SessionFactoryScope scope) { + final EntityWithGeneratedProperty entity = new EntityWithGeneratedProperty( 2L, null ); + scope.inTransaction( session -> { + session.persist( entity ); + session.flush(); + + assertThat( entity.getGeneratedProperty() ).isNotNull(); + + // test update + entity.setGeneratedProperty( "new-assigned-property" ); + } ); + + assertThat( entity.getGeneratedProperty() ).isEqualTo( "new-assigned-property" ); + } + + @AfterAll + public void tearDown(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } + + @Entity(name = "EntityWithGeneratedId") + static class EntityWithGeneratedId { + @Id + @GeneratedValue + @AssignableGenerator + private String generatedId; + + private String name; + + public EntityWithGeneratedId() { + } + + public EntityWithGeneratedId(String generatedId, String name) { + this.generatedId = generatedId; + this.name = name; + } + + public String getGeneratedId() { + return generatedId; + } + + public String getName() { + return name; + } + } + + @Embeddable + static class GeneratedCompositeId { + @AssignableGenerator + private String id1; + + @AssignableGenerator + private String id2; + + public GeneratedCompositeId() { + } + + public GeneratedCompositeId(String id1, String id2) { + this.id1 = id1; + this.id2 = id2; + } + + public String getId1() { + return id1; + } + + public String getId2() { + return id2; + } + } + + @Entity(name = "EntityWithGeneratedEmbeddedId") + static class EntityWithGeneratedEmbeddedId { + @EmbeddedId + private GeneratedCompositeId id; + + private String name; + + public EntityWithGeneratedEmbeddedId() { + } + + public EntityWithGeneratedEmbeddedId(GeneratedCompositeId id, String name) { + this.id = id; + this.name = name; + } + + public GeneratedCompositeId getId() { + return id; + } + } + + @Entity(name = "EntityWithGeneratedProperty") + static class EntityWithGeneratedProperty { + @Id + private Long id; + + @AssignableGenerator + private String generatedProperty; + + public EntityWithGeneratedProperty() { + } + + public EntityWithGeneratedProperty(Long id, String generatedProperty) { + this.id = id; + this.generatedProperty = generatedProperty; + } + + public Long getId() { + return id; + } + + public String getGeneratedProperty() { + return generatedProperty; + } + + public void setGeneratedProperty(String generatedProperty) { + this.generatedProperty = generatedProperty; + } + } + + @IdGeneratorType(AssignedIdGenerator.class) + @ValueGenerationType(generatedBy = AssignedGenerator.class) + @Retention(RUNTIME) + @Target({FIELD, METHOD}) + @interface AssignableGenerator { + } + + public static class AssignedGenerator implements BeforeExecutionGenerator { + @Override + public Object generate( + SharedSessionContractImplementor session, + Object owner, + Object currentValue, + EventType eventType) { + if ( currentValue != null ) { + return currentValue; + } + return UUID.randomUUID().toString(); + } + + @Override + public EnumSet getEventTypes() { + return EventTypeSets.INSERT_AND_UPDATE; + } + } + + public static class AssignedIdGenerator extends AssignedGenerator { + @Override + public EnumSet getEventTypes() { + return EventTypeSets.INSERT_ONLY; + } + + @Override + public boolean allowAssignedIdentifiers() { + return true; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/idgen/userdefined/NativeGenerator.java b/hibernate-core/src/test/java/org/hibernate/orm/test/idgen/userdefined/NativeGenerator.java index 6fb8a192a3f8..f2b4e9190a5d 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/idgen/userdefined/NativeGenerator.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/idgen/userdefined/NativeGenerator.java @@ -8,12 +8,15 @@ import org.hibernate.generator.BeforeExecutionGenerator; import org.hibernate.generator.EventType; import org.hibernate.generator.Generator; +import org.hibernate.generator.GeneratorCreationContext; import org.hibernate.generator.OnExecutionGenerator; import org.hibernate.id.Configurable; import org.hibernate.id.PostInsertIdentityPersister; import org.hibernate.id.factory.IdentifierGeneratorFactory; import org.hibernate.id.factory.spi.CustomIdGeneratorCreationContext; import org.hibernate.id.insert.InsertGeneratedIdentifierDelegate; +import org.hibernate.mapping.PersistentClass; +import org.hibernate.mapping.Property; import org.hibernate.service.ServiceRegistry; import org.hibernate.type.Type; @@ -26,10 +29,12 @@ public class NativeGenerator private final IdentifierGeneratorFactory factory; private final String strategy; + private final CustomIdGeneratorCreationContext creationContext; private Generator generator; public NativeGenerator(NativeId nativeId, Member member, CustomIdGeneratorCreationContext creationContext) { + this.creationContext = creationContext; factory = creationContext.getIdentifierGeneratorFactory(); strategy = creationContext.getDatabase().getDialect().getNativeIdentifierGeneratorStrategy(); if ( "identity".equals(strategy) ) { @@ -49,7 +54,12 @@ public boolean generatedOnExecution() { @Override public void configure(Type type, Properties parameters, ServiceRegistry serviceRegistry) { - generator = factory.createIdentifierGenerator(strategy, type, parameters); + generator = factory.createIdentifierGenerator( + strategy, + type, + creationContext, + parameters + ); //TODO: should use this instead of the deprecated method, but see HHH-18135 // GenerationType generationType; // switch (strategy) { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/DenormalizedTableForeignKeyGeneratorTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/DenormalizedTableForeignKeyGeneratorTest.java new file mode 100644 index 000000000000..68913d2ecc1e --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/DenormalizedTableForeignKeyGeneratorTest.java @@ -0,0 +1,95 @@ +package org.hibernate.orm.test.inheritance; + +import jakarta.persistence.*; +import org.hibernate.dialect.H2Dialect; +import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.Jpa; +import org.hibernate.testing.orm.junit.RequiresDialect; +import org.junit.jupiter.api.Test; + +import java.io.Serializable; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Paria Hosseiny + */ +@JiraKey("HHH-18470") +@Jpa( + annotatedClasses = { + DenormalizedTableForeignKeyGeneratorTest.Employee.class, + DenormalizedTableForeignKeyGeneratorTest.Manager.class, + DenormalizedTableForeignKeyGeneratorTest.Address.class, + DenormalizedTableForeignKeyGeneratorTest.Territory.class + } +) + +@RequiresDialect(H2Dialect.class) +public class DenormalizedTableForeignKeyGeneratorTest { + + @Test + public void shouldCreateForeignKeyForSubclasses(EntityManagerFactoryScope scope) { + scope.inTransaction( + entityManager -> { + String managerQuery = "select CONSTRAINT_NAME FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS " + + "WHERE TABLE_NAME='MANAGER' " + + "AND CONSTRAINT_TYPE='FOREIGN KEY'"; + List managerForeignKeyNames = entityManager.createNativeQuery(managerQuery).getResultList(); + + String employeeQuery = "select CONSTRAINT_NAME FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS " + + "WHERE TABLE_NAME='EMPLOYEE' " + + "AND CONSTRAINT_TYPE='FOREIGN KEY'"; + String employeeForeignKeyName = entityManager.createNativeQuery(employeeQuery).getSingleResult().toString(); + + assertThat(employeeForeignKeyName).isNotNull(); + assertThat(managerForeignKeyNames).isNotNull().hasSize(2); + assertThat(managerForeignKeyNames).doesNotContain(employeeForeignKeyName); + } + ); + } + + @Entity(name = "Employee") + @Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) + static class Employee implements Serializable { + + @Id + @GeneratedValue + Long id; + + @OneToOne(cascade = CascadeType.ALL, orphanRemoval = true) + private Address address; + + } + + @Entity(name = "Manager") + static class Manager extends Employee { + + @OneToOne(fetch = FetchType.EAGER, orphanRemoval = true, cascade = CascadeType.ALL) + private Territory territory; + } + + @Entity(name = "Address") + static class Address { + + @Id + @GeneratedValue + Long id; + + @Column(nullable = false, columnDefinition = "TEXT") + private String address = ""; + } + + @Entity(name = "Territory") + static class Territory { + + @Id + @GeneratedValue + Long id; + + @Column + private String location = ""; + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/EmbeddableInheritanceHierarchyOrderTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/EmbeddableInheritanceHierarchyOrderTest.java new file mode 100644 index 000000000000..bb04a425bdbf --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/EmbeddableInheritanceHierarchyOrderTest.java @@ -0,0 +1,243 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.orm.test.inheritance; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.DiscriminatorColumn; +import jakarta.persistence.Embeddable; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +@DomainModel( + annotatedClasses = { + EmbeddableInheritanceHierarchyOrderTest.Animal.class, + EmbeddableInheritanceHierarchyOrderTest.Cat.class, + EmbeddableInheritanceHierarchyOrderTest.Dog.class, + EmbeddableInheritanceHierarchyOrderTest.Fish.class, + EmbeddableInheritanceHierarchyOrderTest.Mammal.class, + // If Mammal is moved right under Animal (before Dog and Cat), test will pass + EmbeddableInheritanceHierarchyOrderTest.Owner.class + } +) +@SessionFactory +public class EmbeddableInheritanceHierarchyOrderTest { + + @AfterAll + static void clean(SessionFactoryScope scope) { + scope.inTransaction( session -> session.createMutationQuery( "delete from Owner" ).executeUpdate() ); + } + + @Test + public void test(SessionFactoryScope scope) { + scope.inTransaction( session -> { + session.persist( new Owner( 1L, new Animal( 2, "Agapius" ) ) ); + session.persist( new Owner( 2L, new Cat( 3, "Bercharius", "Blaesilla" ) ) ); + session.persist( new Owner( 3L, new Dog( 4, "Censurius", "Caesarea" ) ) ); + session.persist( new Owner( 4L, new Fish( 5, "Dionysius", 3 ) ) ); + session.persist( new Owner( 5L, new Mammal( 6, "Epagraphas", "Eanswida" ) ) ); + } ); + scope.inSession( session -> { + final Owner animalOwner = session.find( Owner.class, 1L ); + assertEquals( 2, animalOwner.getPet().getAge() ); + assertEquals( "Agapius", animalOwner.getPet().getName() ); + + final Owner fishOwner = session.find( Owner.class, 4L ); + if ( fishOwner.getPet() instanceof Fish ) { + final Fish fish = (Fish) fishOwner.getPet(); + assertEquals( 5, fish.getAge() ); + assertEquals( "Dionysius", fish.getName() ); + assertEquals( 3, fish.getFins() ); + } + else { + fail( "Not fish owner" ); + } + + final Owner mammalOwner = session.find( Owner.class, 5L ); + if ( mammalOwner.getPet() instanceof Mammal ) { + final Mammal mammal = (Mammal) mammalOwner.getPet(); + assertEquals( 6, mammal.getAge() ); + assertEquals( "Epagraphas", mammal.getName() ); + assertEquals( "Eanswida", mammal.getMother() ); + } + else { + fail( "Not mammal owner" ); + } + + final Owner catOwner = session.find( Owner.class, 2L ); + if ( catOwner.getPet() instanceof Cat ) { + final Cat cat = (Cat) catOwner.getPet(); + assertEquals( 3, cat.getAge() ); + assertEquals( "Bercharius", cat.getName() ); + assertEquals( "Blaesilla", cat.getMother() ); + } + else { + fail( "Not cat owner" ); + } + + final Owner dogOwner = session.find( Owner.class, 3L ); + if ( dogOwner.getPet() instanceof Dog ) { + final Dog dog = (Dog) dogOwner.getPet(); + assertEquals( 4, dog.getAge() ); + assertEquals( "Censurius", dog.getName() ); + assertEquals( "Caesarea", dog.getMother() ); + } + else { + fail( "Not dog owner" ); + } + } ); + } + + @Embeddable + @DiscriminatorColumn(name = "animal_type", length = 64) + static + class Animal { + private int age; + + private String name; + + public Animal() { + } + + public Animal(int age, String name) { + this.age = age; + this.name = name; + } + + public int getAge() { + return age; + } + + public void setAge(int age) { + this.age = age; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + + @Embeddable + static + class Cat extends Mammal { + //private int mouse; + // [...] + + + public Cat() { + super(); + } + + public Cat(int age, String name, String mother) { + super( age, name, mother ); + } + } + + @Embeddable + static + class Dog extends Mammal { + //private int bone; + // [...] + + public Dog() { + } + + public Dog(int age, String name, String mother) { + super( age, name, mother ); + } + } + + @Embeddable + static + class Fish extends Animal { + private int fins; + + public Fish() { + } + + public Fish(int age, String name, int fins) { + super( age, name ); + this.fins = fins; + } + + public int getFins() { + return fins; + } + + public void setFins(int fins) { + this.fins = fins; + } + } + + @Embeddable + static + class Mammal extends Animal { + private String mother; + + public Mammal() { + } + + public Mammal(int age, String name, String mother) { + super( age, name ); + this.mother = mother; + } + + public String getMother() { + return mother; + } + + public void setMother(String mother) { + this.mother = mother; + } + } + + @Entity(name = "Owner") + static + class Owner { + @Id + private Long id; + + @Embedded + private Animal pet; + + public Owner() { + } + + public Owner(Long id, Animal pet) { + this.id = id; + this.pet = pet; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Animal getPet() { + return pet; + } + + public void setPet(Animal pet) { + this.pet = pet; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/EmbeddableInheritanceReplaceTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/EmbeddableInheritanceReplaceTest.java new file mode 100644 index 000000000000..466e04462636 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/EmbeddableInheritanceReplaceTest.java @@ -0,0 +1,119 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.orm.test.inheritance; + +import jakarta.persistence.*; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Test; + +@DomainModel(annotatedClasses = { + EmbeddableInheritanceReplaceTest.Emb.class, + EmbeddableInheritanceReplaceTest.Base.class, + EmbeddableInheritanceReplaceTest.Next.class, + EmbeddableInheritanceReplaceTest.Ent.class +}) +@SessionFactory +@JiraKey("HHH-19079") +public class EmbeddableInheritanceReplaceTest { + + @Test + void merge(SessionFactoryScope scope) { + scope.inTransaction(session -> { + final var history = new Ent(); + history.setBase(new Emb(42, "Hello, World!")); + + session.merge(history); + }); + } + + @Embeddable + @DiscriminatorValue("E") + public static final class Emb extends Next { + + Emb() { + } + + public Emb(int num, String str) { + super(num, str); + } + } + + @Embeddable + @DiscriminatorColumn(discriminatorType = DiscriminatorType.CHAR) + @DiscriminatorValue("b") + public static class Base { + + protected int num; + + protected Base() { + } + + public Base(int num, String str) { + this.num = num; + } + + public int getNum() { + return num; + } + + public void setNum(int num) { + this.num = num; + } + } + + @Embeddable + @DiscriminatorValue("n") + public static class Next extends Base { + + private String str; + + public Next(int num, String str) { + super(num, str); + this.str = str; + } + + public Next() { + } + } + + @Entity(name = "Ent") + public static class Ent { + + @Id + @GeneratedValue + private Integer id; + + @Embedded + private Base base; + + public Ent() { + } + + public Ent(final Integer id) { + this.id = id; + } + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public Base getBase() { + return base; + } + + public void setBase(Base base) { + this.base = base; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/HierarchyOrderTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/HierarchyOrderTest.java new file mode 100644 index 000000000000..5913429d224a --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/HierarchyOrderTest.java @@ -0,0 +1,195 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.orm.test.inheritance; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Embeddable; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityManagerFactory; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.Inheritance; +import jakarta.persistence.InheritanceType; +import jakarta.persistence.TypedQuery; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +@DomainModel( + annotatedClasses = { + HierarchyOrderTest.DerOA.class, + HierarchyOrderTest.DerDA.class, + HierarchyOrderTest.DerDB.class, + HierarchyOrderTest.DerOB.class, + HierarchyOrderTest.BaseD.class, + HierarchyOrderTest.BaseO.class + } +) +@SessionFactory +class HierarchyOrderTest { + + private EntityManagerFactory emf; + private DerOA deroa; + private DerOB derob; + + @BeforeEach + void setUp() { + DerDB derba1 = new DerDB( 5 ); + DerDA derda1 = new DerDA( "1", "abase" ); + deroa = new DerOA( derda1 ); + derob = new DerOB( derba1 ); +// emf = buildEntityManagerFactory(); + } + + @Test + void testBaseProperty(SessionFactoryScope scope) { + scope.inSession( em -> { + em.getTransaction().begin(); + em.persist( deroa ); + em.persist( derob ); + em.getTransaction().commit(); + Integer ida = deroa.getId(); + Integer idb = derob.getId(); + em.clear(); + TypedQuery qa = em.createQuery( "select o from DerOA o where o.id =:id", DerOA.class ); + qa.setParameter( "id", ida ); + DerOA deroain = qa.getSingleResult(); + assertEquals( "abase", deroain.derda.baseprop ); + } ); + } + + @Test + void testDerivedProperty(SessionFactoryScope scope) { + scope.inSession( em -> { + em.getTransaction().begin(); + em.persist( deroa ); + em.persist( derob ); + em.getTransaction().commit(); + Integer idb = derob.getId(); + em.clear(); + + TypedQuery qb = em.createQuery( "select o from DerOB o where o.id =:id", DerOB.class ); + qb.setParameter( "id", idb ); + DerOB derobin = qb.getSingleResult(); + assertNotNull( derobin ); + assertEquals( 5, derobin.derdb().b ); + } ); + } + + /* + * Created on 03/12/2024 by Paul Harrison (paul.harrison@manchester.ac.uk). + */ + @Entity(name = "DerOA") + public static class DerOA extends BaseO { + public DerOA(DerDA derda) { + this.derda = derda; + } + + @Embedded + // @AttributeOverrides({ + // @AttributeOverride(name="a",column = @Column(name = "da")) + // }) + public BaseD derda; + + public DerOA() { + + } + } + + /* + * Created on 03/12/2024 by Paul Harrison (paul.harrison@manchester.ac.uk). + */ + @Embeddable + public static class DerDB extends BaseD { + public DerDB(int b) { + this.b = b; + } + + public int b; + + public DerDB() { + + } + } + + /* + * Created on 03/12/2024 by Paul Harrison (paul.harrison@manchester.ac.uk). + */ + @Embeddable + public static class DerDA extends BaseD { + public DerDA(String a, String bprop) { + super( bprop ); + this.a = a; + } + + public String a; + + public DerDA() { + + } + } + + /* + * Created on 03/12/2024 by Paul Harrison (paul.harrison@manchester.ac.uk). + */ + @Embeddable + public abstract static class BaseD { //TODO would really like this to be abstract + public String baseprop; + + public BaseD(String baseprop) { + this.baseprop = baseprop; + } + + public BaseD() { + + } + + public String getBaseprop() { + return baseprop; + } + + } + + @Entity(name = "BaseO") + @Inheritance(strategy = InheritanceType.JOINED) + public abstract static class BaseO { + @Id + @GeneratedValue + private Integer id; + + public Integer getId() { + return id; + } + } + + /* + * Created on 03/12/2024 by Paul Harrison (paul.harrison@manchester.ac.uk). + */ + @Entity(name = "DerOB") + public static class DerOB extends BaseO { + public DerOB(DerDB derdb) { + this.derdb = derdb; + } + + @Embedded + BaseD derdb; + + public DerOB() { + + } + + public DerDB derdb() { + return (DerDB) derdb; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/InheritanceJunctionExistsPredicateTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/InheritanceJunctionExistsPredicateTest.java new file mode 100644 index 000000000000..d88eec91cec0 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/InheritanceJunctionExistsPredicateTest.java @@ -0,0 +1,139 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.inheritance; + +import java.util.ArrayList; +import java.util.List; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.DiscriminatorColumn; +import jakarta.persistence.DiscriminatorType; +import jakarta.persistence.DiscriminatorValue; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Marco Belladelli + */ +@DomainModel( annotatedClasses = { + InheritanceJunctionExistsPredicateTest.AbstractEntity.class, + InheritanceJunctionExistsPredicateTest.EntityA.class, + InheritanceJunctionExistsPredicateTest.EntityB.class, + InheritanceJunctionExistsPredicateTest.EntityAContainer.class, + InheritanceJunctionExistsPredicateTest.EntityBContainer.class, +} ) +@SessionFactory( useCollectingStatementInspector = true ) +@Jira( "https://hibernate.atlassian.net/browse/HHH-18174" ) +public class InheritanceJunctionExistsPredicateTest { + @Test + public void testExistsDisjunction(SessionFactoryScope scope) { + scope.inTransaction( session -> assertThat( session.createQuery( + "select c from EntityAContainer c " + + "where exists (select 1 from c.entities e where e.identifier like 'child%') " + + "or exists (select 1 from c.entities e)", + EntityAContainer.class + ).getResultList() ).hasSize( 0 ) ); + } + + @Test + public void testExistsConjunction(SessionFactoryScope scope) { + scope.inTransaction( session -> assertThat( session.createQuery( + "select c from EntityBContainer c " + + "where exists (select 1 from c.entities e where e.identifier like 'child%') " + + "and exists (select 1 from c.entities e)", + EntityBContainer.class + ).getResultList() ).hasSize( 1 ) ); + } + + @BeforeAll + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final EntityB entityB = new EntityB(); + entityB.setIdentifier( "child_b" ); + session.persist( entityB ); + final EntityAContainer containerA = new EntityAContainer(); + containerA.id = 1; + session.persist( containerA ); + final EntityBContainer containerB = new EntityBContainer(); + containerB.id = 1; + containerB.entities.add( entityB ); + session.persist( containerB ); + } ); + } + + @AfterAll + public void tearDown(SessionFactoryScope scope) { + scope.inTransaction( session -> { + session.createMutationQuery( "delete from AbstractEntity" ).executeUpdate(); + session.createMutationQuery( "delete from EntityAContainer" ).executeUpdate(); + session.createMutationQuery( "delete from EntityBContainer" ).executeUpdate(); + } ); + } + + @Entity( name = "AbstractEntity" ) + @DiscriminatorColumn( name = "disc_col", discriminatorType = DiscriminatorType.INTEGER ) + static abstract class AbstractEntity { + @Id + @GeneratedValue + private Integer id; + + private String identifier; + + public String getIdentifier() { + return identifier; + } + + public void setIdentifier(String identifier) { + this.identifier = identifier; + } + } + + @Entity( name = "EntityA" ) + @DiscriminatorValue( "1" ) + static class EntityA extends AbstractEntity { + } + + @Entity( name = "EntityB" ) + @DiscriminatorValue( "2" ) + static class EntityB extends AbstractEntity { + } + + @Entity( name = "EntityAContainer" ) + @Table( name = "a_container" ) + static class EntityAContainer { + @Id + private Integer id; + + @OneToMany + @JoinColumn( name = "reference" ) + private List entities = new ArrayList<>(); + } + + @Entity( name = "EntityBContainer" ) + @Table( name = "b_container" ) + static class EntityBContainer { + @Id + private Integer id; + + @OneToMany + @JoinColumn( name = "reference" ) + private List entities = new ArrayList<>(); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/InheritanceQueryGroupByTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/InheritanceQueryGroupByTest.java index 56590e7cc0be..f7a43ba705da 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/InheritanceQueryGroupByTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/InheritanceQueryGroupByTest.java @@ -6,6 +6,7 @@ */ package org.hibernate.orm.test.inheritance; +import org.hibernate.community.dialect.InformixDialect; import org.hibernate.dialect.Dialect; import org.hibernate.dialect.FunctionalDependencyAnalysisSupport; import org.hibernate.metamodel.mapping.EntityMappingType; @@ -16,6 +17,7 @@ import org.hibernate.testing.orm.junit.Jira; import org.hibernate.testing.orm.junit.SessionFactory; import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.SkipForDialect; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; @@ -86,6 +88,7 @@ public void testGroupBySingleTable(SessionFactoryScope scope) { } @Test + @SkipForDialect( dialectClass = InformixDialect.class , reason = "Informix does not support case expressions within the GROUP BY clause") public void testGroupByJoined(SessionFactoryScope scope) { testGroupBy( scope, "joinedParent", JoinedParent.class, "joined_child_one", 1 ); } @@ -149,8 +152,8 @@ private void testGroupByNotSelected( Long.class ).getSingleResult(); assertThat( sum ).isEqualTo( 3L ); - // When not selected, group by should only use the foreign key (parent_id) - statementInspector.assertNumberOfOccurrenceInQueryNoSpace( 0, parentFkName, 2 ); + // Association is joined, so every use of the join alias will make use of target table columns + statementInspector.assertNumberOfOccurrenceInQueryNoSpace( 0, parentFkName, 1 ); statementInspector.assertNumberOfOccurrenceInQueryNoSpace( 0, "child_one_col", childPropCount ); statementInspector.assertNumberOfOccurrenceInQueryNoSpace( 0, "child_two_col", childPropCount ); } ); @@ -162,6 +165,7 @@ public void testGroupByAndOrderBySingleTable(SessionFactoryScope scope) { } @Test + @SkipForDialect( dialectClass = InformixDialect.class , reason = "Informix does not support case expressions within the GROUP BY clause") public void testGroupByAndOrderByJoined(SessionFactoryScope scope) { testGroupByAndOrderBy( scope, "joinedParent", JoinedParent.class, "joined_child_one", 1 ); } @@ -232,8 +236,8 @@ private void testGroupByAndOrderByNotSelected( Long.class ).getSingleResult(); assertThat( sum ).isEqualTo( 3L ); - // When not selected, group by should only use the foreign key (parent_id) - statementInspector.assertNumberOfOccurrenceInQueryNoSpace( 0, parentFkName, 3 ); + // Association is joined, so every use of the join alias will make use of target table columns + statementInspector.assertNumberOfOccurrenceInQueryNoSpace( 0, parentFkName, 1 ); statementInspector.assertNumberOfOccurrenceInQueryNoSpace( 0, "child_one_col", childPropCount ); statementInspector.assertNumberOfOccurrenceInQueryNoSpace( 0, "child_two_col", childPropCount ); } ); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/InheritanceToOneSubtypeJoinGroupByTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/InheritanceToOneSubtypeJoinGroupByTest.java new file mode 100644 index 000000000000..c9c915d9f7b9 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/InheritanceToOneSubtypeJoinGroupByTest.java @@ -0,0 +1,164 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.inheritance; + +import jakarta.persistence.Embeddable; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.Inheritance; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Tuple; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel( annotatedClasses = { + InheritanceToOneSubtypeJoinGroupByTest.Base.class, + InheritanceToOneSubtypeJoinGroupByTest.EntityA.class, + InheritanceToOneSubtypeJoinGroupByTest.EntityB.class, + InheritanceToOneSubtypeJoinGroupByTest.WhitelistEntry.class, +} ) +@SessionFactory +public class InheritanceToOneSubtypeJoinGroupByTest { + @Test + void testGroupByA(SessionFactoryScope scope) { + scope.inSession( session -> { + final EntityA result = session.createQuery( + "SELECT a FROM WhitelistEntry we JOIN we.primaryKey.a a group by a", + EntityA.class + ).getSingleResult(); + assertThat( result.getAName() ).isEqualTo( "a" ); + } ); + } + + @Test + void testGroupByB(SessionFactoryScope scope) { + scope.inSession( session -> { + final Tuple result = session.createQuery( + "SELECT b.id, b.bName FROM WhitelistEntry we JOIN we.primaryKey.b b group by b", + Tuple.class + ).getSingleResult(); + assertThat( result.get( 1, String.class ) ).isEqualTo( "b" ); + } ); + } + + @BeforeAll + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final EntityA a = new EntityA(); + a.setAName( "a" ); + final EntityB b = new EntityB(); + b.setBName( "b" ); + final WhitelistEntry whitelistEntry = new WhitelistEntry(); + whitelistEntry.setName( "whitelistEntry" ); + final WhitelistEntryPK primaryKey = new WhitelistEntryPK(); + primaryKey.setA( a ); + primaryKey.setB( b ); + whitelistEntry.setPrimaryKey( primaryKey ); + session.persist( a ); + session.persist( b ); + session.persist( whitelistEntry ); + } ); + } + + @AfterAll + public void tearDown(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } + + @Entity(name = "Base") + @Inheritance + public static class Base { + @Id + @GeneratedValue + private Long id; + } + + + @Entity(name = "EntityA") + public static class EntityA extends Base { + private String aName; + + public String getAName() { + return aName; + } + + public void setAName(String name) { + this.aName = name; + } + } + + @Entity(name = "EntityB") + public static class EntityB extends Base { + private String bName; + + public String getBName() { + return bName; + } + + public void setBName(String name) { + this.bName = name; + } + } + + @Embeddable + public static class WhitelistEntryPK { + @ManyToOne + private EntityB b; + + @ManyToOne + private EntityA a; + + public WhitelistEntryPK() { + } + + public EntityB getB() { + return b; + } + + public void setB(EntityB b) { + this.b = b; + } + + public EntityA getA() { + return a; + } + + public void setA(EntityA a) { + this.a = a; + } + } + + @Entity(name = "WhitelistEntry") + public static class WhitelistEntry { + @EmbeddedId + private WhitelistEntryPK primaryKey; + + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public WhitelistEntryPK getPrimaryKey() { + return primaryKey; + } + + public void setPrimaryKey(WhitelistEntryPK primaryKey) { + this.primaryKey = primaryKey; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/JoinedInheritanceTreatQueryTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/JoinedInheritanceTreatQueryTest.java index 72aaaed55477..221481f0bc80 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/JoinedInheritanceTreatQueryTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/JoinedInheritanceTreatQueryTest.java @@ -6,6 +6,7 @@ */ package org.hibernate.orm.test.inheritance; +import org.hibernate.testing.jdbc.SQLStatementInspector; import org.hibernate.testing.orm.junit.DomainModel; import org.hibernate.testing.orm.junit.Jira; import org.hibernate.testing.orm.junit.SessionFactory; @@ -23,12 +24,14 @@ import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; +import java.util.List; + import static org.assertj.core.api.Assertions.assertThat; /** * @author Marco Belladelli */ -@SessionFactory +@SessionFactory( useCollectingStatementInspector = true ) @DomainModel( annotatedClasses = { JoinedInheritanceTreatQueryTest.Product.class, JoinedInheritanceTreatQueryTest.ProductOwner.class, @@ -37,6 +40,7 @@ JoinedInheritanceTreatQueryTest.Description.class, } ) @Jira( "https://hibernate.atlassian.net/browse/HHH-16574" ) +@Jira( "https://hibernate.atlassian.net/browse/HHH-18745" ) public class JoinedInheritanceTreatQueryTest { @BeforeAll public void setUp(SessionFactoryScope scope) { @@ -63,6 +67,8 @@ public void tearDown(SessionFactoryScope scope) { @Test public void testTreatedJoin(SessionFactoryScope scope) { + final SQLStatementInspector inspector = scope.getCollectingStatementInspector(); + inspector.clear(); scope.inTransaction( session -> { final Product result = session.createQuery( "from Product p " + @@ -72,11 +78,103 @@ public void testTreatedJoin(SessionFactoryScope scope) { ).getSingleResult(); assertThat( result.getOwner() ).isInstanceOf( ProductOwner1.class ); assertThat( ( (ProductOwner1) result.getOwner() ).getDescription().getText() ).isEqualTo( "description" ); + inspector.assertNumberOfJoins( 0, 3 ); + } ); + } + + @Test + @Jira("https://hibernate.atlassian.net/browse/HHH-19883") + public void testTreatedJoinWithCondition(SessionFactoryScope scope) { + final SQLStatementInspector inspector = scope.getCollectingStatementInspector(); + inspector.clear(); + scope.inTransaction( session -> { + final List result = session.createSelectionQuery( + "from Product p " + + "join treat(p.owner AS ProductOwner2) as own1 on own1.basicProp = 'unknown value'", + Product.class + ).getResultList(); + assertThat( result ).isEmpty(); + inspector.assertNumberOfJoins( 0, 2 ); + } ); + } + + @Test + @Jira("https://hibernate.atlassian.net/browse/HHH-19883") + public void testMultipleTreatedJoinWithCondition(SessionFactoryScope scope) { + final SQLStatementInspector inspector = scope.getCollectingStatementInspector(); + inspector.clear(); + scope.inTransaction( session -> { + final List result = session.createSelectionQuery( + "from Product p " + + "join treat(p.owner AS ProductOwner1) as own1 on own1.description is null " + + "join treat(p.owner AS ProductOwner2) as own2 on own2.basicProp = 'unknown value'", + Product.class + ).getResultList(); + assertThat( result ).isEmpty(); + inspector.assertNumberOfJoins( 0, 4 ); + } ); + } + + @Test + @Jira("https://hibernate.atlassian.net/browse/HHH-19883") + public void testMultipleTreatedJoinSameAttribute(SessionFactoryScope scope) { + final SQLStatementInspector inspector = scope.getCollectingStatementInspector(); + inspector.clear(); + scope.inTransaction( session -> { + final List result = session.createSelectionQuery( + "from Product p " + + "join treat(p.owner AS ProductOwner1) as own1 " + + "join treat(p.owner AS ProductOwner2) as own2", + Product.class + ).getResultList(); + // No rows, because treat joining the same association with disjunct types can't emit results + assertThat( result ).isEmpty(); + inspector.assertNumberOfJoins( 0, 4 ); + } ); + } + + @Test + @Jira("https://hibernate.atlassian.net/browse/HHH-19883") + public void testMultipleTreatedJoinSameAttributeCriteria(SessionFactoryScope scope) { + final SQLStatementInspector inspector = scope.getCollectingStatementInspector(); + inspector.clear(); + scope.inTransaction( session -> { + final var cb = session.getCriteriaBuilder(); + final var query = cb.createQuery(Product.class); + final var p = query.from( Product.class ); + p.join( "owner" ).treatAs( ProductOwner1.class ); + p.join( "owner" ).treatAs( ProductOwner2.class ); + final List result = session.createSelectionQuery( query ).getResultList(); + // No rows, because treat joining the same association with disjunct types can't emit results + assertThat( result ).isEmpty(); + inspector.assertNumberOfJoins( 0, 4 ); + } ); + } + + @Test + @Jira("https://hibernate.atlassian.net/browse/HHH-19883") + public void testMultipleTreatedJoinCriteria(SessionFactoryScope scope) { + final SQLStatementInspector inspector = scope.getCollectingStatementInspector(); + inspector.clear(); + scope.inTransaction( session -> { + final var cb = session.getCriteriaBuilder(); + final var query = cb.createQuery(Product.class); + final var p = query.from( Product.class ); + final var ownerJoin = p.join( "owner" ); + ownerJoin.treatAs( ProductOwner1.class ); + ownerJoin.treatAs( ProductOwner2.class ); + final List result = session.createSelectionQuery( query ).getResultList(); + // The owner attribute is inner joined, but since there are multiple subtype treats, + // the type restriction for the treat usage does not filter rows + assertThat( result ).hasSize( 2 ); + inspector.assertNumberOfJoins( 0, 3 ); } ); } @Test public void testImplicitTreatedJoin(SessionFactoryScope scope) { + final SQLStatementInspector inspector = scope.getCollectingStatementInspector(); + inspector.clear(); scope.inTransaction( session -> { final Product result = session.createQuery( "from Product p " + @@ -85,11 +183,14 @@ public void testImplicitTreatedJoin(SessionFactoryScope scope) { ).getSingleResult(); assertThat( result.getOwner() ).isInstanceOf( ProductOwner1.class ); assertThat( ( (ProductOwner1) result.getOwner() ).getDescription().getText() ).isEqualTo( "description" ); + inspector.assertNumberOfJoins( 0, 3 ); } ); } @Test public void testTreatedRoot(SessionFactoryScope scope) { + final SQLStatementInspector inspector = scope.getCollectingStatementInspector(); + inspector.clear(); scope.inTransaction( session -> { final ProductOwner result = session.createQuery( "from ProductOwner owner " + @@ -98,11 +199,14 @@ public void testTreatedRoot(SessionFactoryScope scope) { ).getSingleResult(); assertThat( result ).isInstanceOf( ProductOwner1.class ); assertThat( ( (ProductOwner1) result ).getDescription().getText() ).isEqualTo( "description" ); + inspector.assertNumberOfJoins( 0, 3 ); } ); } @Test public void testTreatedEntityJoin(SessionFactoryScope scope) { + final SQLStatementInspector inspector = scope.getCollectingStatementInspector(); + inspector.clear(); scope.inTransaction( session -> { final Product result = session.createQuery( "from Product p " + @@ -112,11 +216,14 @@ public void testTreatedEntityJoin(SessionFactoryScope scope) { ).getSingleResult(); assertThat( result.getOwner() ).isInstanceOf( ProductOwner1.class ); assertThat( ( (ProductOwner1) result.getOwner() ).getDescription().getText() ).isEqualTo( "description" ); + inspector.assertNumberOfJoins( 0, 3 ); } ); } @Test public void testBasicProperty(SessionFactoryScope scope) { + final SQLStatementInspector inspector = scope.getCollectingStatementInspector(); + inspector.clear(); scope.inTransaction( session -> { final Product result = session.createQuery( "from Product p " + @@ -126,6 +233,7 @@ public void testBasicProperty(SessionFactoryScope scope) { ).getSingleResult(); assertThat( result.getOwner() ).isInstanceOf( ProductOwner2.class ); assertThat( ( (ProductOwner2) result.getOwner() ).getBasicProp() ).isEqualTo( "basic_prop" ); + inspector.assertNumberOfJoins( 0, 2 ); } ); } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/ManyToManyTreatJoinTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/ManyToManyTreatJoinTest.java index 5e83f66318b1..cf4e8bef8c99 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/ManyToManyTreatJoinTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/ManyToManyTreatJoinTest.java @@ -15,6 +15,7 @@ import org.hibernate.testing.jdbc.SQLStatementInspector; import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; import org.hibernate.testing.orm.junit.SessionFactory; import org.hibernate.testing.orm.junit.SessionFactoryScope; import org.junit.jupiter.api.AfterAll; @@ -96,6 +97,21 @@ public void testSingleTableSelectParent(SessionFactoryScope scope) { } ); } + @Test + @Jira("https://hibernate.atlassian.net/browse/HHH-19883") + public void testSingleTableTreatJoinCondition(SessionFactoryScope scope) { + final SQLStatementInspector inspector = scope.getCollectingStatementInspector(); + inspector.clear(); + scope.inTransaction( session -> { + final Integer result = session.createSelectionQuery( + "select s.subProp from ParentEntity p join treat(p.singleEntities as SingleSub1) s on s.subProp<>2", + Integer.class + ).getSingleResultOrNull(); + assertThat( result ).isNull(); + inspector.assertNumberOfJoins( 0, 2 ); + } ); + } + @Test public void testJoinedSelectChild(SessionFactoryScope scope) { final SQLStatementInspector inspector = scope.getCollectingStatementInspector(); @@ -124,6 +140,21 @@ public void testJoinedSelectParent(SessionFactoryScope scope) { } ); } + @Test + @Jira("https://hibernate.atlassian.net/browse/HHH-19883") + public void testJoinedTreatJoinCondition(SessionFactoryScope scope) { + final SQLStatementInspector inspector = scope.getCollectingStatementInspector(); + inspector.clear(); + scope.inTransaction( session -> { + final Integer result = session.createSelectionQuery( + "select s.subProp from ParentEntity p join treat(p.joinedEntities as JoinedSub1) s on s.subProp<>3", + Integer.class + ).getSingleResultOrNull(); + assertThat( result ).isNull(); + inspector.assertNumberOfJoins( 0, 3 ); + } ); + } + @Test public void testTablePerClassSelectChild(SessionFactoryScope scope) { final SQLStatementInspector inspector = scope.getCollectingStatementInspector(); @@ -152,6 +183,21 @@ public void testTablePerClassSelectParent(SessionFactoryScope scope) { } ); } + @Test + @Jira("https://hibernate.atlassian.net/browse/HHH-19883") + public void testTablePerClassTreatJoinCondition(SessionFactoryScope scope) { + final SQLStatementInspector inspector = scope.getCollectingStatementInspector(); + inspector.clear(); + scope.inTransaction( session -> { + final Integer result = session.createSelectionQuery( + "select s.subProp from ParentEntity p join treat(p.unionEntities as UnionSub1) s on s.subProp<>4", + Integer.class + ).getSingleResultOrNull(); + assertThat( result ).isNull(); + inspector.assertNumberOfJoins( 0, 2 ); + } ); + } + @Entity( name = "ParentEntity" ) public static class ParentEntity { @Id diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/MultipleInheritanceTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/MultipleInheritanceTest.java new file mode 100644 index 000000000000..b0db2ccd65f7 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/MultipleInheritanceTest.java @@ -0,0 +1,134 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.orm.test.inheritance; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Inheritance; +import jakarta.persistence.InheritanceType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.MappedSuperclass; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OneToOne; + +import org.hibernate.annotations.NotFound; +import org.hibernate.annotations.NotFoundAction; + +import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase; +import org.junit.Test; + +import java.io.Serializable; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +public class MultipleInheritanceTest extends BaseCoreFunctionalTestCase { + + @Override + protected Class[] getAnnotatedClasses() { + return new Class[] { + CarOptional.class, + CarPart.class, + BasicCar.class, + SuperCar.class, + Car.class + }; + } + + @Test + public void test() { + inTransaction( session -> { + + Car car = new Car(); + CarPart carPart = new CarPart(); + + + CarPK id = new CarPK(); + id.carId1 = "1"; + carPart.id = id; + session.persist( carPart ); + + car.id = id; + car.parts = carPart; + ((BasicCar) car).parts = carPart; + session.persist( car ); + session.flush(); + session.clear(); + + Car loadedCar = session.find( Car.class, id ); + assertNotNull( loadedCar.parts ); + } ); + } + + @Embeddable + public static class CarPK implements Serializable { + @Column(name = "CAR_ID_1") + protected String carId1; + } + + @Entity(name = "BasicCar") + @Inheritance(strategy = InheritanceType.SINGLE_TABLE) + public static class BasicCar { + @EmbeddedId + protected CarPK id; + + @OneToOne + @JoinColumn(name = "CAR_ID_1", referencedColumnName = "CAR_ID_1", insertable = false, updatable = false) + CarPart parts; + } + + @Entity(name = "SuperCar") + public static class SuperCar extends BasicCar { + @OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true) +// @JoinColumn(name = "CAR_ID_1", referencedColumnName = "CAR_ID_1") + private List optionals; + } + + @MappedSuperclass + public static abstract class AbstractCar extends BasicCar { + @OneToOne + @NotFound(action = NotFoundAction.IGNORE) + @JoinColumn(name = "CAR_ID_1", referencedColumnName = "CAR_ID_1", insertable = false, updatable = false) + CarPart parts ; + } + + @Entity(name = "CarPart") + public static class CarPart { + @EmbeddedId + private CarPK id; + + String name; + } + + @Entity(name = "Car") + public static class Car extends AbstractCar { + + } + + + + @Entity(name = "CarOptional") + public static class CarOptional { + + @EmbeddedId + private CarOptionalPK id; + + private String name; + + @Embeddable + public class CarOptionalPK implements Serializable { + + @Column(name = "OPTIONAL_ID1") + private String id1; + + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/discriminator/JoinedDiscSameAttributeNameTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/discriminator/JoinedDiscSameAttributeNameTest.java new file mode 100644 index 000000000000..6e866e58f0f7 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/discriminator/JoinedDiscSameAttributeNameTest.java @@ -0,0 +1,157 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.inheritance.discriminator; + +import jakarta.persistence.DiscriminatorColumn; +import jakarta.persistence.DiscriminatorValue; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Inheritance; +import jakarta.persistence.InheritanceType; +import jakarta.persistence.Table; +import jakarta.persistence.Tuple; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel(annotatedClasses = { + JoinedDiscSameAttributeNameTest.Ancestor.class, + JoinedDiscSameAttributeNameTest.DescendantA.class, + JoinedDiscSameAttributeNameTest.DescendantB.class, + JoinedDiscSameAttributeNameTest.DescendantTak.class, + JoinedDiscSameAttributeNameTest.DescendantD.class, +}) +@SessionFactory +@Jira( "https://hibernate.atlassian.net/browse/HHH-19756" ) +public class JoinedDiscSameAttributeNameTest { + @Test + void testCoalesceSameType(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var cb = session.getCriteriaBuilder(); + final var query = cb.createTupleQuery(); + final var root = query.from( Ancestor.class ); + final var dscCRoot = cb.treat( root, DescendantTak.class ); + + query.select( cb.tuple( + root.get( "id" ).alias( "id" ), + cb.coalesce( + dscCRoot.get( "subtitle" ), + dscCRoot.get( "title" ) + ).alias( "description" ) + ) ).orderBy( cb.asc( root.get( "id" ) ) ); + + final var resultList = session.createSelectionQuery( query ).getResultList(); + assertResults( resultList, null, "title", null ); + } ); + } + + @Test + void testCoalesceDifferentTypes(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var cb = session.getCriteriaBuilder(); + final var query = cb.createTupleQuery(); + final var root = query.from( Ancestor.class ); + final var dscARoot = cb.treat( root, DescendantA.class ); + final var dscCRoot = cb.treat( root, DescendantTak.class ); + final var dscDRoot = cb.treat( root, DescendantD.class ); + + query.select( cb.tuple( + root.get( "id" ).alias( "id" ), + cb.coalesce( + dscDRoot.get( "subtitle" ), + cb.coalesce( + cb.coalesce( + dscARoot.get( "subtitle" ), + dscARoot.get( "title" ) + ), + cb.coalesce( + dscCRoot.get( "subtitle" ), + dscCRoot.get( "title" ) + ) + ) + ).alias( "description" ) + ) ).orderBy( cb.asc( root.get( "id" ) ) ); + + final var resultList = session.createSelectionQuery( query ).getResultList(); + assertResults( resultList, null, "title", "subtitle" ); + } ); + } + + private static void assertResults(List resultList, String... expected) { + assertThat( resultList ).hasSize( expected.length ); + for ( int i = 0; i < expected.length; i++ ) { + final var r = resultList.get( i ); + assertThat( r.get( 0, Integer.class) ).isEqualTo( i + 1 ); + assertThat( r.get( 1, String.class ) ).isEqualTo( expected[i] ); + } + } + + @BeforeAll + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var descendantA = new DescendantA(); + descendantA.id = 1; + session.persist( descendantA ); + final var descendantTak = new DescendantTak(); + descendantTak.id = 2; + descendantTak.title = "title"; + session.persist( descendantTak ); + final var descendantD = new DescendantD(); + descendantD.id = 3; + descendantD.subtitle = "subtitle"; + session.persist( descendantD ); + } ); + } + + @AfterAll + public void tearDown(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } + + @Entity(name = "Ancestor") + @Table(name = "t_ancestor") + @Inheritance(strategy = InheritanceType.JOINED) + @DiscriminatorColumn(name = "def_type_id") + static abstract class Ancestor { + @Id + Integer id; + } + + @Entity(name = "DescendantA") + @DiscriminatorValue("A") + @Table(name = "t_descendant_a") + static class DescendantA extends Ancestor { + String title; + String subtitle; + } + + @Entity(name = "DescendantB") + @Table(name = "t_descendant_b") + static abstract class DescendantB extends Ancestor { + } + + @Entity(name = "DescendantTak") + @DiscriminatorValue("C") + @Table(name = "t_descendant_c") + static class DescendantTak extends DescendantB { + String title; + String subtitle; + } + + @Entity(name = "DescendantD") + @DiscriminatorValue("D") + @Table(name = "t_descendant_d") + static class DescendantD extends DescendantB { + String subtitle; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/discriminator/JoinedInheritanceDiscriminatorSelectionTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/discriminator/JoinedInheritanceDiscriminatorSelectionTest.java index 932c6ae6ffa9..2b6967e54957 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/discriminator/JoinedInheritanceDiscriminatorSelectionTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/discriminator/JoinedInheritanceDiscriminatorSelectionTest.java @@ -38,6 +38,7 @@ @SessionFactory( useCollectingStatementInspector = true ) @Jira( "https://hibernate.atlassian.net/browse/HHH-17727" ) @Jira( "https://hibernate.atlassian.net/browse/HHH-17806" ) +@Jira( "https://hibernate.atlassian.net/browse/HHH-18583" ) public class JoinedInheritanceDiscriminatorSelectionTest { @BeforeAll public void setUp(SessionFactoryScope scope) { @@ -51,7 +52,7 @@ public void setUp(SessionFactoryScope scope) { @AfterAll public void tearDown(SessionFactoryScope scope) { - scope.inTransaction( session -> session.createMutationQuery( "delete from ParentEntity" ).executeUpdate() ); + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); } @Test @@ -93,6 +94,23 @@ public void testSelectParentAttribute(SessionFactoryScope scope) { String.class ).getResultList() ).containsOnly( "parent", "child_a" ); inspector.assertNumberOfJoins( 0, 0 ); + inspector.clear(); + + // With treat() we preserve the join + + assertThat( session.createQuery( + "select p.name from ParentEntity p where treat(p as ChildA).id is not null", + String.class + ).getResultList() ).containsExactlyInAnyOrder( "child_a", "sub_child_a" ); + inspector.assertNumberOfJoins( 0, 1 ); + inspector.clear(); + + assertThat( session.createQuery( + "select p.name from ParentEntity p where treat(p as ChildB).id is not null", + String.class + ).getSingleResult() ).isEqualTo( "child_b" ); + inspector.assertNumberOfJoins( 0, 1 ); + inspector.clear(); } ); } @@ -123,27 +141,44 @@ public void testSelectInstance(SessionFactoryScope scope) { inspector.clear(); scope.inTransaction( session -> { - // NOTE: we currently always join all subclasses when selecting the entity instance. We could - // maybe avoid this when we have a physical discriminator column and a type filter + // With type filters we still join all subclasses that have properties when selecting the entity instance + // because we are not aware of the type restriction when processing the selection + assertThat( session.createQuery( "from ParentEntity p where type(p) = ParentEntity", ParentEntity.class ).getResultList() ).hasSize( 1 ); - inspector.assertNumberOfJoins( 0, 3 ); + inspector.assertNumberOfJoins( 0, 2 ); inspector.clear(); assertThat( session.createQuery( "from ParentEntity p where type(p) = ChildA", ParentEntity.class ).getResultList() ).hasSize( 1 ); - inspector.assertNumberOfJoins( 0, 3 ); + inspector.assertNumberOfJoins( 0, 2 ); inspector.clear(); assertThat( session.createQuery( "from ParentEntity p where type(p) = SubChildA", ParentEntity.class ).getResultList() ).hasSize( 1 ); - inspector.assertNumberOfJoins( 0, 3 ); + inspector.assertNumberOfJoins( 0, 2 ); + inspector.clear(); + + // With treat() we only join the needed subclasses + + assertThat( session.createQuery( + "select treat(p as ChildA) from ParentEntity p", + ParentEntity.class + ).getResultList() ).hasSize( 2 ); + inspector.assertNumberOfJoins( 0, 2 ); + inspector.clear(); + + assertThat( session.createQuery( + "select treat(p as ChildB) from ParentEntity p", + ParentEntity.class + ).getResultList() ).hasSize( 1 ); + inspector.assertNumberOfJoins( 0, 1 ); } ); } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/embeddable/EmbeddableInheritanceRecursiveTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/embeddable/EmbeddableInheritanceRecursiveTest.java new file mode 100644 index 000000000000..59087b1b4e52 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/embeddable/EmbeddableInheritanceRecursiveTest.java @@ -0,0 +1,232 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.inheritance.embeddable; + +import org.hibernate.MappingException; +import org.hibernate.boot.Metadata; +import org.hibernate.boot.MetadataSources; +import org.hibernate.boot.registry.StandardServiceRegistryBuilder; + +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.util.ServiceRegistryUtil; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Embeddable; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +@Jira("https://hibernate.atlassian.net/browse/HHH-19648") +public class EmbeddableInheritanceRecursiveTest { + @Test + public void testSimpleRecursiveEmbedded() { + final StandardServiceRegistryBuilder registryBuilder = ServiceRegistryUtil.serviceRegistryBuilder(); + final MetadataSources metadataSources = new MetadataSources( registryBuilder.build() ) + .addAnnotatedClass( Root1.class ) + .addAnnotatedClass( Entity1.class ); + try { + final Metadata metadata = metadataSources.buildMetadata(); + fail( "Expected MappingException due to recursive embeddable mapping" ); + } + catch (Exception e) { + assertThat( e ).isInstanceOf( MappingException.class ); + assertThat( e.getMessage() ) + .contains( "Recursive embeddable mapping detected" ) + .contains( Root1.class.getName() ); + } + } + + @Test + public void testChildWithRootProp() { + final StandardServiceRegistryBuilder registryBuilder = ServiceRegistryUtil.serviceRegistryBuilder(); + final MetadataSources metadataSources = new MetadataSources( registryBuilder.build() ) + .addAnnotatedClass( Root2.class ) + .addAnnotatedClass( Child2.class ) + .addAnnotatedClass( Entity2.class ); + try { + final Metadata metadata = metadataSources.buildMetadata(); + fail( "Expected MappingException due to recursive embeddable mapping" ); + } + catch (Exception e) { + assertThat( e ).isInstanceOf( MappingException.class ); + assertThat( e.getMessage() ) + .contains( "Recursive embeddable mapping detected" ) + .contains( Root2.class.getName() ); + } + } + + @Test + public void testRootWithChildProp() { + final StandardServiceRegistryBuilder registryBuilder = ServiceRegistryUtil.serviceRegistryBuilder(); + final MetadataSources metadataSources = new MetadataSources( registryBuilder.build() ) + .addAnnotatedClass( Root3.class ) + .addAnnotatedClass( Child3.class ) + .addAnnotatedClass( Entity3.class ); + try { + final Metadata metadata = metadataSources.buildMetadata(); + fail( "Expected MappingException due to recursive embeddable mapping" ); + } + catch (Exception e) { + assertThat( e ).isInstanceOf( MappingException.class ); + assertThat( e.getMessage() ) + .contains( "Recursive embeddable mapping detected" ) + .contains( Root3.class.getName() ); + } + } + + @Test + public void testMidEmbedded() { + final StandardServiceRegistryBuilder registryBuilder = ServiceRegistryUtil.serviceRegistryBuilder(); + final MetadataSources metadataSources = new MetadataSources( registryBuilder.build() ) + .addAnnotatedClass( Root4.class ) + .addAnnotatedClass( Mid4.class ) + .addAnnotatedClass( Child4.class ) + .addAnnotatedClass( Entity4.class ); + try { + final Metadata metadata = metadataSources.buildMetadata(); + fail( "Expected MappingException due to recursive embeddable mapping" ); + } + catch (Exception e) { + assertThat( e ).isInstanceOf( MappingException.class ); + assertThat( e.getMessage() ) + .contains( "Recursive embeddable mapping detected" ) + .contains( Root4.class.getName() ); + } + } + + @Test + public void testUnrelatedRecursive() { + final StandardServiceRegistryBuilder registryBuilder = ServiceRegistryUtil.serviceRegistryBuilder(); + final MetadataSources metadataSources = new MetadataSources( registryBuilder.build() ) + .addAnnotatedClass( EmbA.class ) + .addAnnotatedClass( EmbB.class ) + .addAnnotatedClass( Entity5.class ); + try { + final Metadata metadata = metadataSources.buildMetadata(); + fail( "Expected MappingException due to recursive embeddable mapping" ); + } + catch (Exception e) { + assertThat( e ).isInstanceOf( MappingException.class ); + assertThat( e.getMessage() ) + .contains( "Recursive embeddable mapping detected" ) + .contains( EmbA.class.getName() ); + } + } + + @Embeddable + static class Root1 { + String root1Prop; + + @Embedded + Root1 nested1; + } + + @Entity(name = "Entity1") + static class Entity1 { + @Id + private Long id; + + @Embedded + private Root1 root1; + } + + @Embeddable + static class Root2 { + String root2Prop; + } + + @Embeddable + static class Child2 extends Root2 { + String child2Prop; + + @Embedded + Root2 nested2; + } + + @Entity(name = "Entity2") + static class Entity2 { + @Id + private Long id; + + @Embedded + private Root2 root2; + } + + @Embeddable + static class Root3 { + String root3Prop; + + @Embedded + Child3 nested3; + } + + @Embeddable + static class Child3 extends Root3 { + String child3Prop; + } + + @Entity(name = "Entity3") + static class Entity3 { + @Id + private Long id; + + @Embedded + private Root3 root3; + } + + @Embeddable + static class Root4 { + String root4Prop; + } + + @Embeddable + static class Mid4 extends Root4 { + } + + @Embeddable + static class Child4 extends Mid4 { + String child4Prop; + + @Embedded + Mid4 nested4; + } + + @Entity(name = "Entity4") + static class Entity4 { + @Id + private Long id; + + @Embedded + private Root4 root4; + } + + @Embeddable + static class EmbA { + String emb1; + + @Embedded + EmbB embB; + } + + @Embeddable + static class EmbB { + String embB; + + @Embedded + EmbA embA; + } + + @Entity(name = "Entity5") + static class Entity5 { + @Id + private Long id; + + @Embedded + private EmbA embA; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/embeddable/InheritedPropertyTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/embeddable/InheritedPropertyTest.java new file mode 100644 index 000000000000..3e0e07a1fa8d --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/embeddable/InheritedPropertyTest.java @@ -0,0 +1,120 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.inheritance.embeddable; + +import jakarta.persistence.DiscriminatorColumn; +import jakarta.persistence.Embeddable; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; + +@DomainModel( + annotatedClasses = { + InheritedPropertyTest.Animal.class, + InheritedPropertyTest.Cat.class, + InheritedPropertyTest.Dog.class, + InheritedPropertyTest.Fish.class, + InheritedPropertyTest.Mammal.class, + InheritedPropertyTest.Owner.class + } +) +@SessionFactory +public class InheritedPropertyTest { + + @BeforeEach + void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var cat = new Cat(); + cat.age = 7; + cat.name = "Jones"; + cat.mother = "Kitty"; + final var owner = new Owner(); + owner.id = 1L; + owner.pet = cat; + session.persist( owner ); + } ); + } + + @AfterEach + void tearDown(SessionFactoryScope scope) { + scope.inTransaction( session -> session.createMutationQuery( "delete from Owner" ).executeUpdate() ); + } + + @Test + void testInheritedProperty(SessionFactoryScope scope) { + assertDoesNotThrow( () -> scope.inSession( + session -> session.createQuery( + "select o from Owner o where treat(o.pet as InheritedPropertyTest$Cat).mother = :mother", + Owner.class ) ) ); + scope.inSession( + session -> { + final var cats = session.createQuery( + "select o from Owner o where treat(o.pet as InheritedPropertyTest$Cat).mother = :mother", + Owner.class ) + .setParameter( "mother", "Kitty" ) + .getResultList(); + assertEquals( 1, cats.size() ); + final var owner = cats.get( 0 ); + assertInstanceOf( Cat.class, owner.pet ); + assertEquals( "Jones", owner.pet.name ); + assertEquals( "Kitty", ((Cat) owner.pet).mother ); + } ); + } + + @Test + void testDeclaredPropertyCreateQuery(SessionFactoryScope scope) { + assertDoesNotThrow( () -> scope.inSession( + session -> session.createQuery( + "select o from Owner o where treat(o.pet as InheritedPropertyTest$Mammal).mother = :mother", + Owner.class ) ) ); + } + + @Entity(name = "Owner") + public static class Owner { + @Id + Long id; + + @Embedded + Animal pet; + } + + @Embeddable + @DiscriminatorColumn(name = "animal_type") + public static class Animal { + int age; + + String name; + } + + @Embeddable + public static class Fish extends Animal { + int fins; + } + + @Embeddable + public static class Mammal extends Animal { + String mother; + } + + @Embeddable + public static class Cat extends Mammal { + // [...] + } + + @Embeddable + public static class Dog extends Mammal { + // [...] + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/AttributeJoinWithJoinedInheritanceTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/AttributeJoinWithJoinedInheritanceTest.java index c5896e34233e..db0ffa80b576 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/AttributeJoinWithJoinedInheritanceTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/AttributeJoinWithJoinedInheritanceTest.java @@ -57,6 +57,33 @@ public void cleanup(SessionFactoryScope scope) { } ); } + @Test + @Jira( "https://hibernate.atlassian.net/browse/HHH-17646" ) + public void testLeftJoinSelectFk(SessionFactoryScope scope) { + scope.inTransaction( s -> { + final ChildEntityA childEntityA = new SubChildEntityA1( 11 ); + s.persist( childEntityA ); + final ChildEntityB childEntityB = new ChildEntityB( 21 ); + s.persist( childEntityB ); + s.persist( new RootOne( 1, childEntityA ) ); + s.persist( new RootOne( 2, null ) ); + } ); + scope.inTransaction( s -> { + // simulate association with ChildEntityB + s.createNativeMutationQuery( "update root_one set child_id = 21 where id = 2" ).executeUpdate(); + } ); + scope.inTransaction( s -> { + final List resultList = s.createQuery( + "select ce.id " + + "from RootOne r left join r.child ce ", + Integer.class + ).getResultList(); + assertEquals( 2, resultList.size() ); + assertEquals( 11, resultList.get( 0 ) ); + assertNull( resultList.get( 1 ) ); + } ); + } + @Test public void testLeftJoin(SessionFactoryScope scope) { scope.inTransaction( s -> { @@ -111,6 +138,29 @@ public void testLeftJoinExplicitTreat(SessionFactoryScope scope) { } ); } + @Test + @Jira("https://hibernate.atlassian.net/browse/HHH-19883") + public void testTreatedJoinWithCondition(SessionFactoryScope scope) { + scope.inTransaction( s -> { + final ChildEntityA childEntityA1 = new SubChildEntityA1( 11 ); + childEntityA1.setName( "childA1" ); + s.persist( childEntityA1 ); + final ChildEntityA childEntityA2 = new SubChildEntityA2( 21 ); + childEntityA2.setName( "childA2" ); + s.persist( childEntityA2 ); + s.persist( new RootOne( 1, childEntityA1 ) ); + s.persist( new RootOne( 2, childEntityA2 ) ); + } ); + scope.inTransaction( s -> { + final Tuple tuple = s.createQuery( + "select r, ce " + + "from RootOne r join treat(r.child as ChildEntityA) ce on ce.name = 'childA1'", + Tuple.class + ).getSingleResult(); + assertResult( tuple, 1, 11, 11, "child_a_1", SubChildEntityA1.class ); + } ); + } + @Test public void testRightJoin(SessionFactoryScope scope) { scope.inTransaction( s -> { @@ -239,6 +289,7 @@ private void assertResult( public static class BaseClass { @Id private Integer id; + private String name; @Column( name = "disc_col", insertable = false, updatable = false ) private String discCol; @@ -257,6 +308,14 @@ public Integer getId() { public String getDiscCol() { return discCol; } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } } @Entity( name = "ChildEntityA" ) diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/AttributeJoinWithNaturalJoinedInheritanceTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/AttributeJoinWithNaturalJoinedInheritanceTest.java new file mode 100644 index 000000000000..c47697a4bfa8 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/AttributeJoinWithNaturalJoinedInheritanceTest.java @@ -0,0 +1,257 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.inheritance.join; + +import java.util.List; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Column; +import jakarta.persistence.DiscriminatorColumn; +import jakarta.persistence.DiscriminatorValue; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Inheritance; +import jakarta.persistence.InheritanceType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.Tuple; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; + +@DomainModel( annotatedClasses = { + AttributeJoinWithNaturalJoinedInheritanceTest.BaseClass.class, + AttributeJoinWithNaturalJoinedInheritanceTest.ChildEntityA.class, + AttributeJoinWithNaturalJoinedInheritanceTest.SubChildEntityA1.class, + AttributeJoinWithNaturalJoinedInheritanceTest.SubChildEntityA2.class, + AttributeJoinWithNaturalJoinedInheritanceTest.ChildEntityB.class, + AttributeJoinWithNaturalJoinedInheritanceTest.RootOne.class +} ) +@SessionFactory +@Jira( "https://hibernate.atlassian.net/browse/HHH-17646" ) +public class AttributeJoinWithNaturalJoinedInheritanceTest { + @AfterEach + public void cleanup(SessionFactoryScope scope) { + scope.inTransaction( s -> { + s.createMutationQuery( "delete from RootOne" ).executeUpdate(); + s.createMutationQuery( "delete from SubChildEntityA1" ).executeUpdate(); + s.createMutationQuery( "delete from SubChildEntityA2" ).executeUpdate(); + s.createMutationQuery( "delete from BaseClass" ).executeUpdate(); + } ); + } + + @Test + public void testLeftJoinWithDiscriminatorFiltering(SessionFactoryScope scope) { + scope.inTransaction( s -> { + final ChildEntityA childEntityA1 = new SubChildEntityA1( 11 ); + s.persist( childEntityA1 ); + final ChildEntityA childEntityA2 = new SubChildEntityA2( 21 ); + s.persist( childEntityA2 ); + s.persist( new RootOne( 1, childEntityA1 ) ); + s.persist( new RootOne( 2, childEntityA2 ) ); + } ); + scope.inTransaction( s -> { + final List resultList = s.createQuery( + "select r, ce, ce.uk " + + "from RootOne r left join treat(r.child as SubChildEntityA1) ce " + + "order by r.id", + Tuple.class + ).getResultList(); + assertEquals( 2, resultList.size() ); + assertResult( resultList.get( 0 ), 1, 11, 11, "child_a_1", SubChildEntityA1.class, 11 ); + assertResult( resultList.get( 1 ), 2, 21, null, null, null, null ); + } ); + } + + @Test + @Jira("https://hibernate.atlassian.net/browse/HHH-19883") + public void testTreatedJoinWithCondition(SessionFactoryScope scope) { + scope.inTransaction( s -> { + final ChildEntityA childEntityA1 = new SubChildEntityA1( 11 ); + childEntityA1.setName( "childA1" ); + s.persist( childEntityA1 ); + final ChildEntityA childEntityA2 = new SubChildEntityA2( 21 ); + childEntityA2.setName( "childA2" ); + s.persist( childEntityA2 ); + s.persist( new RootOne( 1, childEntityA1 ) ); + s.persist( new RootOne( 2, childEntityA2 ) ); + } ); + scope.inTransaction( s -> { + final Tuple tuple = s.createQuery( + "select r, ce, ce.uk " + + "from RootOne r join treat(r.child as ChildEntityA) ce on ce.name = 'childA1'", + Tuple.class + ).getSingleResult(); + assertResult( tuple, 1, 11, 11, "child_a_1", SubChildEntityA1.class, 11 ); + } ); + } + + private void assertResult( + Tuple result, + Integer rootId, + Integer rootChildId, + Integer childId, + String discValue, + Class subClass, + Integer uk) { + if ( rootId != null ) { + final RootOne root = result.get( 0, RootOne.class ); + assertEquals( rootId, root.getId() ); + assertEquals( rootChildId, root.getChildId() ); + } + else { + assertNull( result.get( 0 ) ); + } + if ( subClass != null ) { + assertInstanceOf( subClass, result.get( 1 ) ); + final ChildEntityA sub1 = result.get( 1, subClass ); + assertEquals( childId, sub1.getId() ); + assertEquals( discValue, sub1.getDiscCol() ); + } + else { + assertNull( result.get( 1 ) ); + } + if ( uk != null ) { + assertEquals( uk, result.get( 2 ) ); + } + else { + assertNull( result.get( 2 ) ); + } + } + + /** + * NOTE: We define a {@link DiscriminatorColumn} to allow multiple subclasses + * to share the same table name. This will need additional care when pruning + * the table expression, since we'll have to add the discriminator condition + * before joining with the subclass tables + */ + @Entity( name = "BaseClass" ) + @Inheritance( strategy = InheritanceType.JOINED ) + @DiscriminatorColumn( name = "disc_col" ) + public static class BaseClass { + @Id + private Integer id; + private String name; + + @Column( name = "disc_col", insertable = false, updatable = false ) + private String discCol; + + public BaseClass() { + } + + public BaseClass(Integer id) { + this.id = id; + } + + public Integer getId() { + return id; + } + + public String getDiscCol() { + return discCol; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + + @Entity( name = "ChildEntityA" ) + @Table( name = "child_entity" ) + public static abstract class ChildEntityA extends BaseClass { + @Column(unique = true) + private Integer uk; + + public ChildEntityA() { + } + + public ChildEntityA(Integer id) { + super( id ); + this.uk = id; + } + + public Integer getUk() { + return uk; + } + } + + @Entity( name = "SubChildEntityA1" ) + @DiscriminatorValue( "child_a_1" ) + public static class SubChildEntityA1 extends ChildEntityA { + public SubChildEntityA1() { + } + + public SubChildEntityA1(Integer id) { + super( id ); + } + } + + @Entity( name = "SubChildEntityA2" ) + @DiscriminatorValue( "child_a_2" ) + public static class SubChildEntityA2 extends ChildEntityA { + public SubChildEntityA2() { + } + + public SubChildEntityA2(Integer id) { + super( id ); + } + } + + @Entity( name = "ChildEntityB" ) + @Table( name = "child_entity" ) + public static class ChildEntityB extends BaseClass { + + public ChildEntityB() { + } + + public ChildEntityB(Integer id) { + super( id ); + } + } + + @Entity( name = "RootOne" ) + @Table( name = "root_one" ) + public static class RootOne { + @Id + private Integer id; + + @Column( name = "child_id", insertable = false, updatable = false ) + private Integer childId; + + @ManyToOne + @JoinColumn( name = "child_id", referencedColumnName = "uk") + private ChildEntityA child; + + public RootOne() { + } + + public RootOne(Integer id, ChildEntityA child) { + this.id = id; + this.child = child; + } + + public Integer getId() { + return id; + } + + public Integer getChildId() { + return childId; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/AttributeJoinWithRestrictedJoinedInheritanceTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/AttributeJoinWithRestrictedJoinedInheritanceTest.java new file mode 100644 index 000000000000..22e7232f18ab --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/AttributeJoinWithRestrictedJoinedInheritanceTest.java @@ -0,0 +1,248 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.inheritance.join; + +import java.util.List; + +import org.hibernate.annotations.SQLRestriction; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Column; +import jakarta.persistence.DiscriminatorColumn; +import jakarta.persistence.DiscriminatorValue; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Inheritance; +import jakarta.persistence.InheritanceType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import jakarta.persistence.Tuple; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; +import static org.junit.jupiter.api.Assertions.assertNull; + +@DomainModel( annotatedClasses = { + AttributeJoinWithRestrictedJoinedInheritanceTest.BaseClass.class, + AttributeJoinWithRestrictedJoinedInheritanceTest.ChildEntityA.class, + AttributeJoinWithRestrictedJoinedInheritanceTest.SubChildEntityA1.class, + AttributeJoinWithRestrictedJoinedInheritanceTest.SubChildEntityA2.class, + AttributeJoinWithRestrictedJoinedInheritanceTest.ChildEntityB.class, + AttributeJoinWithRestrictedJoinedInheritanceTest.RootOne.class +} ) +@SessionFactory +@Jira( "https://hibernate.atlassian.net/browse/HHH-17646" ) +public class AttributeJoinWithRestrictedJoinedInheritanceTest { + @AfterEach + public void cleanup(SessionFactoryScope scope) { + scope.inTransaction( s -> { + s.createNativeQuery( "delete from root_one" ).executeUpdate(); + s.createNativeQuery( "delete from SubChildEntityA1" ).executeUpdate(); + s.createNativeQuery( "delete from SubChildEntityA2" ).executeUpdate(); + s.createNativeQuery( "delete from child_entity" ).executeUpdate(); + s.createNativeQuery( "delete from BaseClass" ).executeUpdate(); + } ); + } + + @Test + public void testLeftJoinWithDiscriminatorFiltering(SessionFactoryScope scope) { + scope.inTransaction( s -> { + final ChildEntityA childEntityA1 = new SubChildEntityA1( 11 ); + s.persist( childEntityA1 ); + final ChildEntityA childEntityA2 = new SubChildEntityA2( 21 ); + s.persist( childEntityA2 ); + s.persist( new RootOne( 1, childEntityA1 ) ); + s.persist( new RootOne( 2, childEntityA2 ) ); + } ); + scope.inTransaction( s -> { + final List resultList = s.createQuery( + "select r, ce " + + "from RootOne r left join r.child ce " + + "order by r.id", + Tuple.class + ).getResultList(); + assertEquals( 2, resultList.size() ); + assertResult( resultList.get( 0 ), 1, 11, 11, "child_a_1", SubChildEntityA1.class ); + assertResult( resultList.get( 1 ), 2, 21, null, null, null ); + } ); + } + + @Test + @Jira("https://hibernate.atlassian.net/browse/HHH-19883") + public void testTreatedJoinWithCondition(SessionFactoryScope scope) { + scope.inTransaction( s -> { + final ChildEntityA childEntityA1 = new SubChildEntityA1( 11 ); + childEntityA1.setName( "childA1" ); + s.persist( childEntityA1 ); + final ChildEntityA childEntityA2 = new SubChildEntityA2( 21 ); + childEntityA2.setName( "childA2" ); + s.persist( childEntityA2 ); + s.persist( new RootOne( 1, childEntityA1 ) ); + s.persist( new RootOne( 2, childEntityA2 ) ); + } ); + scope.inTransaction( s -> { + final Tuple tuple = s.createQuery( + "select r, ce " + + "from RootOne r join treat(r.child as ChildEntityA) ce on ce.name = 'childA1'", + Tuple.class + ).getSingleResult(); + assertResult( tuple, 1, 11, 11, "child_a_1", SubChildEntityA1.class ); + } ); + } + + private void assertResult( + Tuple result, + Integer rootId, + Integer rootChildId, + Integer childId, + String discValue, + Class subClass) { + if ( rootId != null ) { + final RootOne root = result.get( 0, RootOne.class ); + assertEquals( rootId, root.getId() ); + assertEquals( rootChildId, root.getChildId() ); + } + else { + assertNull( result.get( 0 ) ); + } + if ( subClass != null ) { + assertInstanceOf( subClass, result.get( 1 ) ); + final ChildEntityA sub1 = result.get( 1, subClass ); + assertEquals( childId, sub1.getId() ); + assertEquals( discValue, sub1.getDiscCol() ); + } + else { + assertNull( result.get( 1 ) ); + } + } + + /** + * NOTE: We define a {@link DiscriminatorColumn} to allow multiple subclasses + * to share the same table name. This will need additional care when pruning + * the table expression, since we'll have to add the discriminator condition + * before joining with the subclass tables + */ + @Entity( name = "BaseClass" ) + @Inheritance( strategy = InheritanceType.JOINED ) + @DiscriminatorColumn( name = "disc_col" ) + @SQLRestriction( "ident < 20" ) + public static class BaseClass { + @Id + @Column(name = "ident") + private Integer id; + private String name; + + @Column( name = "disc_col", insertable = false, updatable = false ) + private String discCol; + + public BaseClass() { + } + + public BaseClass(Integer id) { + this.id = id; + } + + public Integer getId() { + return id; + } + + public String getDiscCol() { + return discCol; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + + @Entity( name = "ChildEntityA" ) + @Table( name = "child_entity" ) + public static abstract class ChildEntityA extends BaseClass { + + public ChildEntityA() { + } + + public ChildEntityA(Integer id) { + super( id ); + } + } + + @Entity( name = "SubChildEntityA1" ) + @DiscriminatorValue( "child_a_1" ) + public static class SubChildEntityA1 extends ChildEntityA { + public SubChildEntityA1() { + } + + public SubChildEntityA1(Integer id) { + super( id ); + } + } + + @Entity( name = "SubChildEntityA2" ) + @DiscriminatorValue( "child_a_2" ) + public static class SubChildEntityA2 extends ChildEntityA { + public SubChildEntityA2() { + } + + public SubChildEntityA2(Integer id) { + super( id ); + } + } + + @Entity( name = "ChildEntityB" ) + @Table( name = "child_entity" ) + public static class ChildEntityB extends BaseClass { + + public ChildEntityB() { + } + + public ChildEntityB(Integer id) { + super( id ); + } + } + + @Entity( name = "RootOne" ) + @Table( name = "root_one" ) + public static class RootOne { + @Id + private Integer id; + + @Column( name = "child_id", insertable = false, updatable = false ) + private Integer childId; + + @ManyToOne + @JoinColumn( name = "child_id") + private ChildEntityA child; + + public RootOne() { + } + + public RootOne(Integer id, ChildEntityA child) { + this.id = id; + this.child = child; + } + + public Integer getId() { + return id; + } + + public Integer getChildId() { + return childId; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/AttributeJoinWithSingleTableInheritanceTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/AttributeJoinWithSingleTableInheritanceTest.java index 522f20baabcb..1ecf1e7b1db4 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/AttributeJoinWithSingleTableInheritanceTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/AttributeJoinWithSingleTableInheritanceTest.java @@ -111,6 +111,29 @@ public void testLeftJoinExplicitTreat(SessionFactoryScope scope) { } ); } + @Test + @Jira("https://hibernate.atlassian.net/browse/HHH-19883") + public void testTreatedJoinWithCondition(SessionFactoryScope scope) { + scope.inTransaction( s -> { + final ChildEntityA childEntityA1 = new SubChildEntityA1( 11 ); + childEntityA1.setName( "childA1" ); + s.persist( childEntityA1 ); + final ChildEntityA childEntityA2 = new SubChildEntityA2( 21 ); + childEntityA2.setName( "childA2" ); + s.persist( childEntityA2 ); + s.persist( new RootOne( 1, childEntityA1 ) ); + s.persist( new RootOne( 2, childEntityA2 ) ); + } ); + scope.inTransaction( s -> { + final Tuple tuple = s.createQuery( + "select r, ce " + + "from RootOne r join treat(r.child as ChildEntityA) ce on ce.name = 'childA1'", + Tuple.class + ).getSingleResult(); + assertResult( tuple, 1, 11, 11, "child_a_1", SubChildEntityA1.class ); + } ); + } + @Test public void testRightJoin(SessionFactoryScope scope) { scope.inTransaction( s -> { @@ -233,6 +256,7 @@ private void assertResult( public static class BaseClass { @Id private Integer id; + private String name; @Column( name = "disc_col", insertable = false, updatable = false ) private String discCol; @@ -251,6 +275,14 @@ public Integer getId() { public String getDiscCol() { return discCol; } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } } @Entity( name = "ChildEntityA" ) diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/AttributeJoinWithTablePerClassInheritanceTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/AttributeJoinWithTablePerClassInheritanceTest.java index 661be8dae292..dcfd5b757730 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/AttributeJoinWithTablePerClassInheritanceTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/AttributeJoinWithTablePerClassInheritanceTest.java @@ -109,6 +109,29 @@ public void testLeftJoinExplicitTreat(SessionFactoryScope scope) { } ); } + @Test + @Jira("https://hibernate.atlassian.net/browse/HHH-19883") + public void testTreatedJoinWithCondition(SessionFactoryScope scope) { + scope.inTransaction( s -> { + final ChildEntityA childEntityA1 = new SubChildEntityA1( 11 ); + childEntityA1.setName( "childA1" ); + s.persist( childEntityA1 ); + final ChildEntityA childEntityA2 = new SubChildEntityA2( 21 ); + childEntityA2.setName( "childA2" ); + s.persist( childEntityA2 ); + s.persist( new RootOne( 1, childEntityA1 ) ); + s.persist( new RootOne( 2, childEntityA2 ) ); + } ); + scope.inTransaction( s -> { + final Tuple tuple = s.createQuery( + "select r, ce " + + "from RootOne r join treat(r.child as ChildEntityA) ce on ce.name = 'childA1'", + Tuple.class + ).getSingleResult(); + assertResult( tuple, 1, 11, 11, SubChildEntityA1.class ); + } ); + } + @Test public void testRightJoin(SessionFactoryScope scope) { scope.inTransaction( s -> { @@ -228,6 +251,7 @@ private void assertResult( public static class BaseClass { @Id private Integer id; + private String name; public BaseClass() { } @@ -239,6 +263,14 @@ public BaseClass(Integer id) { public Integer getId() { return id; } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } } @Entity( name = "ChildEntityA" ) diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/JoinedHierarchyDiscIdSelectTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/JoinedHierarchyDiscIdSelectTest.java new file mode 100644 index 000000000000..d7e7dc3b8afc --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/JoinedHierarchyDiscIdSelectTest.java @@ -0,0 +1,106 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.orm.test.inheritance.join; + +import java.io.Serializable; +import java.util.List; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import jakarta.persistence.DiscriminatorColumn; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Inheritance; +import jakarta.persistence.InheritanceType; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Root; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel( annotatedClasses = { + JoinedHierarchyDiscIdSelectTest.DooredVehicle.class, + JoinedHierarchyDiscIdSelectTest.BaseVehicle.class, + JoinedHierarchyDiscIdSelectTest.BaseEntity.class, +} ) +@SessionFactory +@Jira( "https://hibernate.atlassian.net/browse/HHH-18503" ) +public class JoinedHierarchyDiscIdSelectTest { + @ParameterizedTest + @ValueSource( classes = { + DooredVehicle.class, + BaseVehicle.class, + BaseEntity.class, + } ) + void testSelectCriteriaId(Class klass, SessionFactoryScope scope) { + scope.inTransaction( session -> { + final CriteriaBuilder criteriaBuilder = session.getCriteriaBuilder(); + final CriteriaQuery criteriaQuery = criteriaBuilder.createQuery( Long.class ); + final Root root = criteriaQuery.from( klass ); + criteriaQuery.select( root.get( "id" ) ); + final List resultList = session.createQuery( criteriaQuery ).getResultList(); + assertThat( resultList ).hasSize( 1 ).containsOnly( 1L ); + } ); + } + + @ParameterizedTest + @ValueSource( classes = { + DooredVehicle.class, + BaseVehicle.class, + BaseEntity.class, + } ) + void testSelectId(Class klass, SessionFactoryScope scope) { + scope.inTransaction( session -> { + final Long result = session.createQuery( + String.format( "select id from %s", klass.getSimpleName() ), + Long.class + ).getSingleResult(); + assertThat( result ).isEqualTo( 1L ); + } ); + } + + @BeforeAll + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> session.persist( DooredVehicle.create( 1L ) ) ); + } + + @AfterAll + public void tearDown(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } + + @Entity( name = "DooredVehicle" ) + static class DooredVehicle extends BaseVehicle { + public String doorType; + + public static DooredVehicle create(Long id) { + DooredVehicle vehicle = new DooredVehicle(); + vehicle.id = id; + return vehicle; + } + } + + @Entity( name = "BaseVehicle" ) + static class BaseVehicle extends BaseEntity { + public String bodyType; + } + + @Entity( name = "BaseEntity" ) + @DiscriminatorColumn( name = "type" ) + @Inheritance( strategy = InheritanceType.JOINED ) + static class BaseEntity implements Serializable { + @Id + public Long id; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/ReloadMultipleCollectionElementsTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/ReloadMultipleCollectionElementsTest.java new file mode 100644 index 000000000000..516bf16c9f33 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/inheritance/join/ReloadMultipleCollectionElementsTest.java @@ -0,0 +1,307 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.inheritance.join; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.hibernate.Hibernate; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToMany; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@DomainModel( annotatedClasses = { + ReloadMultipleCollectionElementsTest.Flight.class, + ReloadMultipleCollectionElementsTest.Ticket.class, + ReloadMultipleCollectionElementsTest.Customer.class, + ReloadMultipleCollectionElementsTest.Company.class +} ) +@SessionFactory +@Jira("https://hibernate.atlassian.net/browse/HHH-18493") +public class ReloadMultipleCollectionElementsTest { + + @BeforeEach + public void setup(SessionFactoryScope scope) { + scope.inTransaction( s -> { + Flight f2 = new Flight(); + f2.setId(1L); + f2.setName("Flight two"); + + Company us = new Company(); + us.setName("Airline 2"); + f2.setCompany(us); + + Customer c1 = new Customer(); + c1.setId( 1L ); + c1.setName("Tom"); + + Customer c2 = new Customer(); + c2.setId( 2L ); + c2.setName("Pete"); + + Ticket t1 = new Ticket(); + t1.setId(1L); + t1.setCustomer(c2); + t1.setNumber( "123" ); + + Ticket t2 = new Ticket(); + t2.setId(2L); + t2.setCustomer(c2); + t2.setNumber( "456" ); + + f2.setCustomers(Set.of(c1, c2)); + + s.persist(c1); + s.persist(c2); + s.persist(f2); + s.persist(t1); + s.persist(t2); + } ); + } + + @AfterEach + public void cleanup(SessionFactoryScope scope) { + scope.inTransaction( s -> { + s.createMutationQuery( "delete from Ticket" ).executeUpdate(); + s.createMutationQuery( "delete from Flight" ).executeUpdate(); + s.createMutationQuery( "delete from Customer" ).executeUpdate(); + s.createMutationQuery( "delete from Company" ).executeUpdate(); + } ); + } + + @Test + public void testResolveElementOfInitializedCollection(SessionFactoryScope scope) { + scope.inTransaction( s -> { + // First load all customers with their flights collection and corresponding customers + List customers = s.createQuery( + "from Customer c join fetch c.flights f join fetch f.customers order by c.id", + Customer.class + ).getResultList(); + assertEquals( 2, customers.size() ); + assertFalse( Hibernate.isInitialized( customers.get( 0 ).getTickets() ) ); + assertFalse( Hibernate.isInitialized( customers.get( 1 ).getTickets() ) ); + + // Then load all flights with their customers collection, but in addition, also the customers tickets + // This will trigger resolveInstance(Object, Data) with the existing collection and will + // fetch tickets data into existing customers + s.createQuery( "from Flight f join fetch f.customers c left join fetch c.tickets", Flight.class ).getResultList(); + + assertTrue( Hibernate.isInitialized( customers.get( 0 ).getTickets() ) ); + assertTrue( Hibernate.isInitialized( customers.get( 1 ).getTickets() ) ); + assertEquals( 0, customers.get( 0 ).getTickets().size() ); + assertEquals( 2, customers.get( 1 ).getTickets().size() ); + } ); + } + + @Entity( name = "Flight" ) + public static class Flight { + private Long id; + private String name; + private Company company; + private Set customers; + + public Flight() { + } + + @Id + @Column(name = "flight_id") + public Long getId() { + return id; + } + + public void setId(Long long1) { + id = long1; + } + + @Column(updatable = false, name = "flight_name", nullable = false, length = 50) + public String getName() { + return name; + } + + public void setName(String string) { + name = string; + } + + + @ManyToOne(cascade = {CascadeType.ALL}) + @JoinColumn(name = "comp_id") + public Company getCompany() { + return company; + } + + public void setCompany(Company company) { + this.company = company; + } + + @ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE}, fetch = FetchType.EAGER) + public Set getCustomers() { + return customers; + } + + public void setCustomers(Set customers) { + this.customers = customers; + } + } + + @Entity( name = "Ticket" ) + public static class Ticket { + + Long id; + String number; + Customer customer; + + public Ticket() { + } + + @Id + public Long getId() { + return id; + } + + public void setId(Long long1) { + id = long1; + } + + @Column(name = "nr") + public String getNumber() { + return number; + } + + public void setNumber(String string) { + number = string; + } + + @ManyToOne(cascade = CascadeType.ALL) + @JoinColumn(name = "CUSTOMER_ID") + public Customer getCustomer() { + return customer; + } + + public void setCustomer(Customer customer) { + this.customer = customer; + } + } + + @Entity( name = "Customer" ) + public static class Customer { + private Long id; + private String name; + private String address; + private Set tickets; + private Set flights; + + // Address address; + + public Customer() { + } + + @Id + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getAddress() { + return address; + } + + public void setAddress(String address) { + this.address = address; + } + + @OneToMany(cascade = CascadeType.ALL, mappedBy = "customer") + public Set getTickets() { + return tickets; + } + + public void setTickets(Set tickets) { + this.tickets = tickets; + } + + @ManyToMany(cascade = {CascadeType.PERSIST, CascadeType.MERGE}, mappedBy = "customers") + public Set getFlights() { + return flights; + } + + public void setFlights(Set flights) { + this.flights = flights; + } + } + + @Entity( name = "Company" ) + public static class Company { + + private Long id; + private String name; + private Set flights = new HashSet(); + + public Company() { + } + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + @Column(name = "comp_id") + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + public void setId(Long newId) { + id = newId; + } + + public void setName(String string) { + name = string; + } + + @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER, mappedBy = "company") + @Column(name = "flight_id") + public Set getFlights() { + return flights; + } + + public void setFlights(Set flights) { + this.flights = flights; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jdbc/internal/AggressiveReleaseTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jdbc/internal/AggressiveReleaseTest.java index eac9332b1546..cb9329494976 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/jdbc/internal/AggressiveReleaseTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jdbc/internal/AggressiveReleaseTest.java @@ -20,6 +20,7 @@ import org.hibernate.testing.boot.BasicTestingJdbcServiceImpl; import org.hibernate.testing.orm.junit.BaseSessionFactoryFunctionalTest; +import org.hibernate.testing.orm.junit.Jira; import org.hibernate.testing.orm.junit.RequiresDialect; import org.hibernate.testing.orm.jdbc.PreparedStatementSpyConnectionProvider; import org.junit.jupiter.api.AfterEach; @@ -118,9 +119,9 @@ protected void cleanupTest() throws Exception { @Test public void testBasicRelease() { - connectionProvider.clear(); ResourceRegistry registry = sessionFactoryScope().fromSession( session -> { + connectionProvider.clear(); JdbcCoordinatorImpl jdbcCoord = (JdbcCoordinatorImpl) session.getJdbcCoordinator(); ResourceRegistry resourceRegistry = jdbcCoord.getLogicalConnection().getResourceRegistry(); try { @@ -155,9 +156,9 @@ public void testBasicRelease() { @Test public void testReleaseCircumventedByHeldResources() { - connectionProvider.clear(); ResourceRegistry registry = sessionFactoryScope().fromSession( session -> { + connectionProvider.clear(); JdbcCoordinatorImpl jdbcCoord = (JdbcCoordinatorImpl) session.getJdbcCoordinator(); ResourceRegistry resourceRegistry = jdbcCoord.getLogicalConnection().getResourceRegistry(); @@ -219,10 +220,9 @@ public void testReleaseCircumventedByHeldResources() { @Test public void testReleaseCircumventedManually() { - connectionProvider.clear(); - connectionProvider.clear(); ResourceRegistry registry = sessionFactoryScope().fromSession( session -> { + connectionProvider.clear(); JdbcCoordinatorImpl jdbcCoord = (JdbcCoordinatorImpl) session.getJdbcCoordinator(); ResourceRegistry resourceRegistry = jdbcCoord.getLogicalConnection().getResourceRegistry(); @@ -275,4 +275,20 @@ public void testReleaseCircumventedManually() { assertEquals( 0, connectionProvider.getAcquiredConnections().size() ); assertEquals( 2, connectionProvider.getReleasedConnections().size() ); } + + @Test + @Jira("https://hibernate.atlassian.net/browse/HHH-19477") + public void testHql() { + sessionFactoryScope().inTransaction( session -> { + connectionProvider.clear(); + JdbcCoordinatorImpl jdbcCoord = (JdbcCoordinatorImpl) session.getJdbcCoordinator(); + ResourceRegistry resourceRegistry = jdbcCoord.getLogicalConnection().getResourceRegistry(); + + session.createSelectionQuery( "select 1" ).uniqueResult(); + + assertFalse( resourceRegistry.hasRegisteredResources() ); + assertEquals( 0, connectionProvider.getAcquiredConnections().size() ); + assertEquals( 1, connectionProvider.getReleasedConnections().size() ); + } ); + } } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/join/JoinAndFetchWithCriteriaSelectionQueryTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/join/JoinAndFetchWithCriteriaSelectionQueryTest.java new file mode 100644 index 000000000000..cb85f948df8b --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/join/JoinAndFetchWithCriteriaSelectionQueryTest.java @@ -0,0 +1,295 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.join; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +import org.hibernate.Hibernate; +import org.hibernate.metamodel.model.domain.EntityDomainType; +import org.hibernate.query.Order; + +import org.hibernate.testing.jdbc.SQLStatementInspector; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Fetch; +import jakarta.persistence.criteria.Join; +import jakarta.persistence.criteria.Root; +import jakarta.persistence.metamodel.SingularAttribute; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * When fetching and joining associations using the criteria API, we need to check that we are not affected by the order + * of the operations + */ +@SessionFactory +@DomainModel(annotatedClasses = { JoinAndFetchWithCriteriaSelectionQueryTest.Book.class, JoinAndFetchWithCriteriaSelectionQueryTest.Author.class }) +@Jira("https://hibernate.atlassian.net/browse/HHH-19034") +class JoinAndFetchWithCriteriaSelectionQueryTest { + + static final Author amal = new Author( 1L, "Amal El-Mohtar" ); + static final Author max = new Author( 2L, "Max Gladstone" ); + static final Author ursula = new Author( 3L, "Ursula K. Le Guin" ); + static final Book timeWar = new Book( 1L, "This Is How You Lose the Time War" ); + static final Book leftHand = new Book( 2L, "The Left Hand of Darkness" ); + + @BeforeAll + public static void populateDb(SessionFactoryScope scope) { + timeWar.getAuthors().add( amal ); + timeWar.getAuthors().add( max ); + leftHand.getAuthors().add( ursula ); + scope.inTransaction( session -> { + session.persist( amal ); + session.persist( max ); + session.persist( ursula ); + session.persist( timeWar ); + session.persist( leftHand ); + } ); + } + + @Test + void fetchBeforeJoinWithWhereClauseTest(SessionFactoryScope scope) { + final SQLStatementInspector inspector = scope.getCollectingStatementInspector(); + inspector.clear(); + scope.inTransaction( session -> { + // Find all the books from an author, and load the authors association eagerly + CriteriaBuilder cb = session.getCriteriaBuilder(); + CriteriaQuery query = cb.createQuery( Book.class ); + Root from = query.from( Book.class ); + + // The fetch MUST BE created before the join for this test + Fetch fetch = from.fetch( "authors" ); + Join join = from.join( "authors" ); + query.where( cb.equal( join.get( "id" ), 2L ) ); + + // Because there's a filter on the association, they need to be two distinct joins + assertThat( join ).isNotEqualTo( fetch ); + + Book book = session.createQuery( query ).getSingleResult(); + assertThat( book ).isEqualTo( timeWar ); + assertThat( Hibernate.isInitialized( book.getAuthors() ) ).isTrue(); + assertThat( book.getAuthors() ).containsExactlyInAnyOrder( amal, max ); + inspector.assertExecutedCount( 1 ); + inspector.assertNumberOfOccurrenceInQuery( 0, "join", 3 ); + } ); + } + + @Test + void fetchAfterJoinWithWhereClauseTest(SessionFactoryScope scope) { + final SQLStatementInspector inspector = scope.getCollectingStatementInspector(); + inspector.clear(); + scope.inTransaction( session -> { + // Find all the books from an author, and load the authors association eagerly + CriteriaBuilder cb = session.getCriteriaBuilder(); + CriteriaQuery query = cb.createQuery( Book.class ); + Root from = query.from( Book.class ); + + // The join MUST BE created before the fetch for this test + Join join = from.join( "authors" ); + Fetch fetch = from.fetch( "authors" ); + query.where( cb.equal( join.get( "id" ), 2L ) ); + + // Because there's a filter on the association, they need to be two distinct joins + assertThat( join ).isNotEqualTo( fetch ); + Book book = session.createQuery( query ).getSingleResult(); + + assertThat( book ).isEqualTo( timeWar ); + assertThat( Hibernate.isInitialized( book.getAuthors() ) ).isTrue(); + assertThat( book.getAuthors() ).containsExactlyInAnyOrder( amal, max ); + inspector.assertExecutedCount( 1 ); + inspector.assertNumberOfOccurrenceInQuery( 0, "join", 3 ); + } ); + } + + @Test + void fetchAfterJoinWithoutWhereClauseTest(SessionFactoryScope scope) { + final SQLStatementInspector inspector = scope.getCollectingStatementInspector(); + inspector.clear(); + scope.inTransaction( session -> { + CriteriaBuilder cb = session.getCriteriaBuilder(); + CriteriaQuery query = cb.createQuery( Book.class ); + Root from = query.from( Book.class ); + + // The join MUST BE created before the fetch for this test + Join join = from.join( "authors" ); + Fetch fetch = from.fetch( "authors" ); + + // The current behaviour, but we could reuse the same join in this case + assertThat( join ).isNotEqualTo( fetch ); + + EntityDomainType bookType = scope.getSessionFactory().getJpaMetamodel().findEntityType( Book.class ); + SingularAttribute title = bookType.findSingularAttribute( "title" ); + query.select( from ).distinct( true ); + + List books = session + .createSelectionQuery( query ) + .setOrder( Order.asc( title ) ) + .getResultList(); + assertThat( books ).containsExactly( leftHand, timeWar ); + assertThat( Hibernate.isInitialized( books.get( 0 ).getAuthors() ) ).isTrue(); + assertThat( Hibernate.isInitialized( books.get( 1 ).getAuthors() ) ).isTrue(); + + inspector.assertExecutedCount( 1 ); + // The current behaviour, but we could generate a query with only 2 join + inspector.assertNumberOfOccurrenceInQuery( 0, "join", 3 ); + } ); + } + + @Test + void fetchBeforeJoinWithoutWhereClauseTest(SessionFactoryScope scope) { + final SQLStatementInspector inspector = scope.getCollectingStatementInspector(); + inspector.clear(); + scope.inTransaction( session -> { + CriteriaBuilder cb = session.getCriteriaBuilder(); + CriteriaQuery query = cb.createQuery( Book.class ); + Root from = query.from( Book.class ); + + // The fetch MUST BE created before the join for this test + Fetch fetch = from.fetch( "authors" ); + Join join = from.join( "authors" ); + + // The current behaviour, but we could reuse the same join in this case + assertThat( join ).isNotEqualTo( fetch ); + + EntityDomainType bookType = scope.getSessionFactory().getJpaMetamodel().findEntityType( Book.class ); + SingularAttribute title = bookType.findSingularAttribute( "title" ); + query.select( from ).distinct( true ); + + List books = session + .createSelectionQuery( query ) + .setOrder( Order.asc( title ) ) + .getResultList(); + assertThat( books ).containsExactly( leftHand, timeWar ); + assertThat( Hibernate.isInitialized( books.get( 0 ).getAuthors() ) ).isTrue(); + assertThat( Hibernate.isInitialized( books.get( 1 ).getAuthors() ) ).isTrue(); + + inspector.assertExecutedCount( 1 ); + // The current behaviour, but we could generate a query with only 2 join + inspector.assertNumberOfOccurrenceInQuery( 0, "join", 3 ); + } ); + } + + @Entity(name = "Author") + public static class Author { + @Id + private Long id; + private String name; + + public Author() { + } + + public Author(Long id, String name) { + this.id = id; + this.name = name; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public boolean equals(Object object) { + if ( object == null || getClass() != object.getClass() ) { + return false; + } + Author author = (Author) object; + return Objects.equals( name, author.name ); + } + + @Override + public int hashCode() { + return Objects.hashCode( name ); + } + + @Override + public String toString() { + return id + ":" + name; + } + } + + @Entity(name = "Book") + public static class Book { + @Id + private Long id; + private String title; + @OneToMany + private List authors = new ArrayList<>(); + + public Book() { + } + + public Book(Long id, String title) { + this.id = id; + this.title = title; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public List getAuthors() { + return authors; + } + + public void setAuthors(List authors) { + this.authors = authors; + } + + @Override + public boolean equals(Object object) { + if ( object == null || getClass() != object.getClass() ) { + return false; + } + Book book = (Book) object; + return Objects.equals( title, book.title ); + } + + @Override + public int hashCode() { + return Objects.hashCode( title ); + } + + @Override + public String toString() { + return id + ":" + title; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/join/OuterJoinTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/join/OuterJoinTest.java index b2eabc30a64d..6b2aaf126d7a 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/join/OuterJoinTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/join/OuterJoinTest.java @@ -17,7 +17,6 @@ import org.hibernate.testing.orm.junit.Jpa; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -411,7 +410,6 @@ public void testJoinOrderWithRightJoinWithInnerImplicitJoins(EntityManagerFactor } @Test - @Disabled("Hibernate doesn't support implicit joins") public void testJoinOrderWithRightNormalJoinWithInnerImplicitJoins(EntityManagerFactoryScope scope) { scope.inTransaction( em -> { List resultList = em.createQuery( diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/joinsubquery/JoinSubqueryTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/joinsubquery/JoinSubqueryTest.java new file mode 100644 index 000000000000..d54a7421cbd7 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/joinsubquery/JoinSubqueryTest.java @@ -0,0 +1,129 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.orm.test.joinsubquery; + +import jakarta.persistence.*; +import org.hibernate.annotations.JoinColumnOrFormula; +import org.hibernate.annotations.JoinFormula; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.io.Serializable; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +@DomainModel(annotatedClasses = { + JoinSubqueryTest.RecordItem.class, + JoinSubqueryTest.RecordType.class +}) +@SessionFactory +@JiraKey("HHH-19052") +class JoinSubqueryTest { + + @BeforeAll + static void setUp(SessionFactoryScope scope) throws Exception { + scope.inTransaction(session -> { + final var id = 1L; + final var typeId = 42L; + final var recordType = new RecordType(id, typeId); + session.persist(recordType); + final var item = new RecordItem(id, typeId, recordType); + session.persist(item); + }); + } + + @Test + void test(SessionFactoryScope scope) throws Exception { + scope.inSession(session -> { + final var item = session.get(RecordItem.class, 1L); + assertNotNull(item); + }); + } + + @Entity + @Table(name = "record_items") + public static class RecordItem implements Serializable { + + @Id + protected Long id; + + @Column(name = "type_id", insertable = false, updatable = false) + private Long typeId; + + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumnOrFormula(column = @JoinColumn(name = "type_id", referencedColumnName = "entity_id")) + @JoinColumnOrFormula(formula = @JoinFormula(value = "(SELECT x.id FROM record_types x WHERE x.entity_id = type_id)", referencedColumnName = "id")) + private RecordType type; + + RecordItem() { + } + + public RecordItem(Long id, Long typeId, RecordType type) { + this.id = id; + this.typeId = typeId; + this.type = type; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getId() { + return this.id; + } + + public Long getTypeId() { + return typeId; + } + + public RecordType getType() { + return type; + } + + + } + + @Entity + @Table(name = "record_types") + public static class RecordType implements Serializable { + + @Id + protected Long id; + + @Column(name = "entity_id") + private Long entityId; + + RecordType() { + } + + public RecordType(Long id, Long entityId) { + this.id = id; + this.entityId = entityId; + } + + public void setId(Long id) { + this.id = id; + } + + public Long getId() { + return this.id; + } + + public Long getEntityId() { + return entityId; + } + + public void setEntityId(Long entityId) { + this.entityId = entityId; + } + + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/CriteriaUpdateAndDeleteWithJoinTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/CriteriaUpdateAndDeleteWithJoinTest.java new file mode 100644 index 000000000000..3aa6ad8c21dc --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/CriteriaUpdateAndDeleteWithJoinTest.java @@ -0,0 +1,152 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.jpa; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaDelete; +import jakarta.persistence.criteria.CriteriaUpdate; +import jakarta.persistence.criteria.Join; +import jakarta.persistence.criteria.JoinType; +import jakarta.persistence.criteria.Root; +import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.Jpa; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +@Jpa( + annotatedClasses = { + CriteriaUpdateAndDeleteWithJoinTest.Parent.class, + CriteriaUpdateAndDeleteWithJoinTest.Child.class + } +) +@JiraKey( "HHH-19579" ) +public class CriteriaUpdateAndDeleteWithJoinTest { + private static final String CHILD_CODE = "123"; + + @BeforeEach + public void setup(EntityManagerFactoryScope scope) { + scope.inTransaction( + entityManager -> { + Child child = new Child( 1L, CHILD_CODE ); + Parent parent = new Parent( 2L, "456", child ); + entityManager.persist( parent ); + } + ); + } + + @AfterEach + public void teardown(EntityManagerFactoryScope scope) { + scope.releaseEntityManagerFactory(); + } + + @Test + public void testUpdate(EntityManagerFactoryScope scope) { + scope.inTransaction( + entityManager -> { + CriteriaBuilder cb = entityManager.getCriteriaBuilder(); + CriteriaUpdate update = cb.createCriteriaUpdate(Parent.class); + + Root root = update.from(Parent.class); + Join joinColor = root.join("child", JoinType.INNER); + + update.set(root.get("code"), "l1s2"); + update.where(cb.equal(joinColor.get("code"), cb.parameter(String.class, "code"))); + + int count = entityManager.createQuery(update).setParameter("code", CHILD_CODE).executeUpdate(); + assertThat(count).isEqualTo(1); + } + ); + } + + @Test + public void testDelete(EntityManagerFactoryScope scope) { + scope.inTransaction( + entityManager -> { + CriteriaBuilder cb = entityManager.getCriteriaBuilder(); + CriteriaDelete delete = cb.createCriteriaDelete(Parent.class); + + Root root = delete.from(Parent.class); + Join joinColor = root.join("child", JoinType.INNER); + + delete.where(cb.equal(joinColor.get("code"), cb.parameter(String.class, "code"))); + + int count = entityManager.createQuery(delete).setParameter("code", CHILD_CODE).executeUpdate(); + assertThat(count).isEqualTo(1); + } + ); + } + + @Entity(name = "Parent") + public static class Parent { + + @Id + private Long id; + + @Column(name = "code") + private String code; + + @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST) + @JoinColumn(name = "color_id") + private Child child; + + public Parent() { + } + + public Parent(Long id, String code, Child child) { + this.id = id; + this.code = code; + this.child = child; + } + + public Long getId() { + return id; + } + + public String getCode() { + return code; + } + + public Child getChild() { + return child; + } + } + + @Entity(name = "Child") + public static class Child { + + @Id + private Long id; + + @Column(name = "code") + private String code; + + public Child() { + } + + public Child(Long id, String code) { + this.id = id; + this.code = code; + } + + public Long getId() { + return id; + } + + public String getCode() { + return code; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/DetachedPreviousRowStateTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/DetachedPreviousRowStateTest.java new file mode 100644 index 000000000000..5f1b5ebf2ed0 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/DetachedPreviousRowStateTest.java @@ -0,0 +1,177 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.jpa; + +import jakarta.persistence.Entity; +import jakarta.persistence.EntityGraph; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Subgraph; +import jakarta.persistence.Table; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Root; +import org.hibernate.Hibernate; +import org.hibernate.jpa.SpecHints; +import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.Jpa; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author RÊda Housni Alaoui + */ +@Jpa(annotatedClasses = { + DetachedPreviousRowStateTest.Version.class, + DetachedPreviousRowStateTest.Product.class, + DetachedPreviousRowStateTest.Description.class, + DetachedPreviousRowStateTest.LocalizedDescription.class +}) +class DetachedPreviousRowStateTest { + + @BeforeEach + void setupData(EntityManagerFactoryScope scope) { + scope.inTransaction( em -> { + Product product = new Product(); + em.persist( product ); + + Description description = new Description( product ); + em.persist( description ); + + LocalizedDescription englishDescription = new LocalizedDescription( description ); + em.persist( englishDescription ); + LocalizedDescription frenchDescription = new LocalizedDescription( description ); + em.persist( frenchDescription ); + } ); + } + + @AfterEach + void cleanupData(EntityManagerFactoryScope scope) { + scope.inTransaction( em -> { + em.createQuery( "delete from LocalizedDescription l" ).executeUpdate(); + em.createQuery( "delete from Description d" ).executeUpdate(); + em.createQuery( "delete from Product p" ).executeUpdate(); + } ); + } + + @Test + @JiraKey(value = "HHH-18719") + void test(EntityManagerFactoryScope scope) { + scope.inTransaction( em -> { + CriteriaBuilder cb = em.getCriteriaBuilder(); + CriteriaQuery query = cb.createQuery( LocalizedDescription.class ); + Root root = query.from( LocalizedDescription.class ); + query.select( root ); + + EntityGraph localizedDescriptionGraph = + em.createEntityGraph( LocalizedDescription.class ); + Subgraph descriptionGraph = localizedDescriptionGraph.addSubgraph( "description" ); + Subgraph productGraph = descriptionGraph.addSubgraph( "product" ); + productGraph.addSubgraph( "versions" ); + + AtomicInteger resultCount = new AtomicInteger(); + + em.createQuery( query ) + .setHint( SpecHints.HINT_SPEC_LOAD_GRAPH, localizedDescriptionGraph ) + .getResultStream() + .forEach( localizedDescription -> { + resultCount.incrementAndGet(); + + assertThat( em.contains( localizedDescription.description.product ) ) + .withFailMessage( "'localizedDescription.description.product' is detached" ) + .isTrue(); + assertThat( Hibernate.isInitialized( localizedDescription.description.product ) ) + .withFailMessage( "'localizedDescription.description.product' is not initialized" ) + .isTrue(); + + em.flush(); + em.clear(); + } ); + + assertThat( resultCount.get() ).isEqualTo( 2 ); + } ); + } + + @Entity(name = "Version") + @Table(name = "version_tbl") + public static class Version { + + @Id + @GeneratedValue + private long id; + + private String description; + + @ManyToOne(fetch = FetchType.LAZY) + private Product product; + } + + @Entity(name = "Product") + @Table(name = "product_tbl") + public static class Product { + @Id + @GeneratedValue + private long id; + + private String description; + + @OneToMany(mappedBy = "product") + private final List versions = new ArrayList<>(); + } + + @Entity(name = "Description") + @Table(name = "description_tbl") + public static class Description { + @Id + @GeneratedValue + private long id; + + private String description; + + @OneToOne(fetch = FetchType.LAZY) + private Product product; + + public Description() { + } + + public Description(Product product) { + this.product = product; + } + } + + @Entity(name = "LocalizedDescription") + @Table(name = "localized_description_tbl") + public static class LocalizedDescription { + + @Id + @GeneratedValue + private long id; + + private String localizedDescription; + + @ManyToOne(fetch = FetchType.LAZY) + private Description description; + + public LocalizedDescription() { + } + + public LocalizedDescription(Description description) { + this.description = description; + } + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/EmptyMapTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/EmptyMapTest.java new file mode 100644 index 000000000000..695ade2e13d2 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/EmptyMapTest.java @@ -0,0 +1,104 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.jpa; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.MapKeyJoinColumn; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.Jpa; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author RÊda Housni Alaoui + */ +@Jpa(annotatedClasses = {EmptyMapTest.User.class, EmptyMapTest.Identity.class, EmptyMapTest.UserIdentity.class}) +public class EmptyMapTest { + + @BeforeEach + public void setupData(EntityManagerFactoryScope scope) { + scope.inTransaction( em -> em.persist( new User( 1 ) ) ); + } + + @AfterEach + public void cleanupData(EntityManagerFactoryScope scope) { + scope.inTransaction(em -> em.createQuery( "delete from User u" ).executeUpdate() ); + } + + @Test + @JiraKey(value = "HHH-18658") + public void test(EntityManagerFactoryScope scope) { + scope.inTransaction( em -> { + assertThat( em.find( User.class, 1 ) ).isNotNull(); + } ); + } + + @Entity(name = "User") + @Table(name = "user_tbl") + public static class User { + @Id + private int id; + private String name; + + @OneToMany( + mappedBy = "user", + fetch = FetchType.EAGER, + cascade = CascadeType.ALL, + orphanRemoval = true) + @MapKeyJoinColumn + private final Map userIdentityByIdentity = new HashMap<>(); + + public User() { + } + + public User(int id) { + this.id = id; + } + } + + @Entity(name = "Identity") + @Table(name = "identity_tbl") + public static class Identity { + @Id + @GeneratedValue + private int id; + private String name; + } + + @Entity(name = "UserIdentity") + @Table(name = "user_identity_tbl") + public static class UserIdentity { + @Id + @GeneratedValue + private int id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(nullable = false, updatable = false) + private User user; + + @OneToOne(fetch = FetchType.EAGER) + @JoinColumn(nullable = false, updatable = false) + private Identity identity; + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/JpaProxyComplianceEnabledTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/JpaProxyComplianceEnabledTest.java new file mode 100644 index 000000000000..fb7fb9050e88 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/JpaProxyComplianceEnabledTest.java @@ -0,0 +1,124 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.jpa; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.Jpa; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +@Jpa( + annotatedClasses = { + JpaProxyComplianceEnabledTest.Provider.class, + JpaProxyComplianceEnabledTest.TelephoneNumber.class, + }, + proxyComplianceEnabled = true +) +@JiraKey("HHH-19476") +public class JpaProxyComplianceEnabledTest { + + private static final Integer PROVIDER_ID = 1; + private static final Integer TELEPHONE_NUMBER_ID = 2; + + @BeforeAll + public static void init(EntityManagerFactoryScope scope) { + scope.inTransaction( entityManager -> { + Provider provider = new Provider( PROVIDER_ID, "A Provider" ); + entityManager.persist( provider ); + + TelephoneNumber telephoneNumber1 = new TelephoneNumber( + TELEPHONE_NUMBER_ID, + "123-456-7890", + provider + ); + entityManager.persist( telephoneNumber1 ); + } ); + } + + @Test + public void testJoinFetchAfterFind(EntityManagerFactoryScope scope) { + scope.inTransaction( entityManager -> { + TelephoneNumber telephoneNumber = entityManager.find( TelephoneNumber.class, TELEPHONE_NUMBER_ID ); + List telephoneNumbers = entityManager.createQuery( + "from TelephoneNumber t join fetch t.provider", + TelephoneNumber.class ) + .getResultList(); + assertThat( telephoneNumbers.size() ).isEqualTo( 1 ); + } + ); + } + + @Entity(name = "TelephoneNumber") + public static class TelephoneNumber { + @Id + private Integer id; + + @Column(name = "phone_number") + private String number; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "provider", nullable = false) + private Provider provider; + + public TelephoneNumber() { + } + + public TelephoneNumber(Integer id, String number, Provider provider) { + this.id = id; + this.number = number; + this.provider = provider; + } + + public Integer getId() { + return id; + } + + public String getNumber() { + return number; + } + + public Provider getProvider() { + return provider; + } + } + + @Entity(name = "Provider") + public static class Provider { + + @Id + private Integer id; + + private String name; + + public Provider() { + } + + public Provider(Integer id, String name) { + this.id = id; + this.name = name; + } + + public Integer getId() { + return id; + } + + public String getName() { + return name; + } + + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/compliance/LocalTimeTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/compliance/LocalTimeTest.java index 985f9b0e9e58..18fe576ca7a8 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/compliance/LocalTimeTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/compliance/LocalTimeTest.java @@ -35,7 +35,7 @@ ) public class LocalTimeTest { - private static final LocalTime LOCAL_TIME = DateTimeUtils.roundToDefaultPrecision( + private static final LocalTime LOCAL_TIME = DateTimeUtils.adjustToDefaultPrecision( LocalTime.now(), DialectContext.getDialect() ); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/CoalesceTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/CoalesceTest.java index 1f0c10c1065e..7962db6fd220 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/CoalesceTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/CoalesceTest.java @@ -6,15 +6,24 @@ */ package org.hibernate.orm.test.jpa.criteria; -import org.hibernate.testing.TestForIssue; +import java.math.BigDecimal; +import java.util.List; + +import org.hibernate.testing.orm.junit.JiraKey; import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; +import org.hibernate.testing.orm.junit.Jira; import org.hibernate.testing.orm.junit.Jpa; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.persistence.Embedded; import jakarta.persistence.Entity; import jakarta.persistence.Id; +import jakarta.persistence.Tuple; import jakarta.persistence.TypedQuery; import jakarta.persistence.criteria.CriteriaBuilder; import jakarta.persistence.criteria.CriteriaQuery; @@ -22,13 +31,18 @@ import jakarta.persistence.criteria.ParameterExpression; import jakarta.persistence.criteria.Root; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Will Dazy */ -@Jpa(annotatedClasses = CoalesceTest.HHH15291Entity.class) -@TestForIssue( jiraKey = "HHH-15291") +@Jpa( annotatedClasses = { + CoalesceTest.HHH15291Entity.class, + CoalesceTest.ComponentEntity.class, + CoalesceTest.ComponentA.class, +} ) +@JiraKey( value = "HHH-15291") public class CoalesceTest { - @Test public void hhh15291JPQL1Test(EntityManagerFactoryScope scope) { scope.inEntityManager( @@ -97,9 +111,74 @@ public void hhh15291Criteria2Test(EntityManagerFactoryScope scope) { ); } + @Test + @Jira( "https://hibernate.atlassian.net/browse/HHH-18321" ) + public void testCoalesceInBinaryArithmetic(EntityManagerFactoryScope scope) { + scope.inTransaction( entityManager -> { + final CriteriaBuilder cb = entityManager.getCriteriaBuilder(); + final CriteriaQuery cquery = cb.createTupleQuery(); + final Root root = cquery.from( ComponentEntity.class ); + + cquery.select( cb.tuple( + root.get( "id" ), + cb.diff( + cb.coalesce( root.get( "componentA" ).get( "income" ), BigDecimal.ZERO ), + cb.coalesce( root.get( "componentA" ).get( "expense" ), BigDecimal.ZERO ) + ) + ) ); + + final List resultList = entityManager.createQuery( cquery ).getResultList(); + assertThat( resultList ).hasSize( 2 ); + for ( Tuple result : resultList ) { + final Long id = result.get( 0, Long.class ); + assertThat( result.get( 1, BigDecimal.class ).intValue() ).isEqualTo( id == 1L ? 0 : 1 ); + } + } ); + } + + @Test + @Jira( "https://hibernate.atlassian.net/browse/HHH-18321" ) + public void testCoalesceInBinaryArithmeticParam(EntityManagerFactoryScope scope) { + scope.inTransaction( entityManager -> { + final CriteriaBuilder cb = entityManager.getCriteriaBuilder(); + final CriteriaQuery cquery = cb.createTupleQuery(); + final Root root = cquery.from( ComponentEntity.class ); + + final ParameterExpression defaultValue = cb.parameter( BigDecimal.class, "default-value" ); + + cquery.select( cb.tuple( + root.get( "id" ), + cb.diff( + defaultValue, + cb.coalesce( root.get( "componentA" ).get( "expense" ), defaultValue ) + ) + ) ); + + final List resultList = entityManager.createQuery( cquery ) + .setParameter( "default-value", BigDecimal.ZERO ).getResultList(); + assertThat( resultList ).hasSize( 2 ); + for ( Tuple result : resultList ) { + final Long id = result.get( 0, Long.class ); + assertThat( result.get( 1, BigDecimal.class ).intValue() ).isEqualTo( id == 1L ? -1 : 0 ); + } + } ); + } + + @BeforeAll + public void setUp(EntityManagerFactoryScope scope) { + scope.inTransaction( entityManager -> { + entityManager.persist( new ComponentEntity( 1L, new ComponentA( BigDecimal.ONE, BigDecimal.ONE ) ) ); + entityManager.persist( new ComponentEntity( 2L, new ComponentA( BigDecimal.ONE, null ) ) ); + } ); + } + + @AfterAll + public void tearDown(EntityManagerFactoryScope scope) { + scope.inTransaction( entityManager -> entityManager.createQuery( "delete from ComponentEntity" ).executeUpdate() ); + } + @Entity(name = "HHH15291Entity") public static class HHH15291Entity { - @Id @Column(name = "KEY_CHAR") private String KeyString; @@ -118,53 +197,36 @@ public static class HHH15291Entity { @Column(name = "ITEM_INTEGER1") private Integer itemInteger1; + } - public String getKeyString() { - return KeyString; - } - - public void setKeyString(String keyString) { - KeyString = keyString; - } - - public String getItemString1() { - return itemString1; - } - - public void setItemString1(String itemString1) { - this.itemString1 = itemString1; - } - - public String getItemString2() { - return itemString2; - } - - public void setItemString2(String itemString2) { - this.itemString2 = itemString2; - } + @Entity( name = "ComponentEntity" ) + static class ComponentEntity { + @Id + private Long id; - public String getItemString3() { - return itemString3; - } + @Embedded + private ComponentA componentA; - public void setItemString3(String itemString3) { - this.itemString3 = itemString3; + public ComponentEntity() { } - public String getItemString4() { - return itemString4; + public ComponentEntity(Long id, ComponentA componentA) { + this.id = id; + this.componentA = componentA; } + } - public void setItemString4(String itemString4) { - this.itemString4 = itemString4; - } + @Embeddable + static class ComponentA { + private BigDecimal income; + private BigDecimal expense; - public Integer getItemInteger1() { - return itemInteger1; + public ComponentA() { } - public void setItemInteger1(Integer itemInteger1) { - this.itemInteger1 = itemInteger1; + public ComponentA(BigDecimal income, BigDecimal expense) { + this.income = income; + this.expense = expense; } } } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/CriteriaSelectOneToOneUnownedTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/CriteriaSelectOneToOneUnownedTest.java new file mode 100644 index 000000000000..cf086f3e7b3e --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/CriteriaSelectOneToOneUnownedTest.java @@ -0,0 +1,138 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.jpa.criteria; + +import java.util.List; + +import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.Jpa; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.OneToOne; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Join; +import jakarta.persistence.criteria.JoinType; +import jakarta.persistence.criteria.Root; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +@Jpa( + annotatedClasses = { + CriteriaSelectOneToOneUnownedTest.Parent.class, + CriteriaSelectOneToOneUnownedTest.Child.class, + } +) +@JiraKey("HHH-18628") +public class CriteriaSelectOneToOneUnownedTest { + + @BeforeEach + public void setUp(EntityManagerFactoryScope scope) { + scope.inTransaction( + entityManager -> { + Child child = new Child( 1l, "child" ); + Parent parent = new Parent( 1l, "parent", child ); + entityManager.persist( parent ); + } + ); + } + + @AfterEach + public void tearDown(EntityManagerFactoryScope scope) { + scope.inTransaction( + entityManager -> { + entityManager.createQuery( "delete from Child" ).executeUpdate(); + entityManager.createQuery( "delete from Parent" ).executeUpdate(); + } + ); + } + + @Test + public void testCriteriaInnerJoin(EntityManagerFactoryScope scope) { + scope.inTransaction( + entityManager -> { + CriteriaBuilder cb = entityManager.getCriteriaBuilder(); + + CriteriaQuery query = cb.createQuery( Child.class ); + Root parent = query.from( Parent.class ); + Join child = parent.join( "child", JoinType.INNER ); + query.select( child ); + + List children = entityManager.createQuery( query ).getResultList(); + assertThat( children ).isNotNull(); + assertThat( children.size() ).isEqualTo( 1 ); + Child c = children.get( 0 ); + assertThat( c ).isNotNull(); + } ); + } + + @Test + public void testCriteriaLeftJoin(EntityManagerFactoryScope scope) { + scope.inTransaction( + entityManager -> { + CriteriaBuilder cb = entityManager.getCriteriaBuilder(); + + CriteriaQuery query = cb.createQuery( Child.class ); + Root parent = query.from( Parent.class ); + Join child = parent.join( "child", JoinType.LEFT ); + query.select( child ); + + List children = entityManager.createQuery( query ).getResultList(); + assertThat( children ).isNotNull(); + assertThat( children.size() ).isEqualTo( 1 ); + Child c = children.get( 0 ); + assertThat( c ).isNotNull(); + } ); + } + + @Entity(name = "Parent") + public static class Parent { + @Id + private Long id; + + private String name; + + @OneToOne(mappedBy = "parent", cascade = CascadeType.PERSIST) + private Child child; + + public Parent() { + } + + public Parent(Long id, String name, Child child) { + this.id = id; + this.name = name; + this.child = child; + child.parent = this; + } + } + + @Entity(name = "Child") + public static class Child { + + @Id + private Long id; + + private String name; + + @OneToOne(optional = false, fetch = FetchType.LAZY) + private Parent parent; + + public Child() { + } + + public Child(Long id, String name) { + this.id = id; + this.name = name; + } + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/QueryBuilderTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/QueryBuilderTest.java index 8c0abed81806..5c5996a90fa8 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/QueryBuilderTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/QueryBuilderTest.java @@ -20,6 +20,8 @@ import jakarta.persistence.criteria.SetJoin; import jakarta.persistence.metamodel.EntityType; import jakarta.persistence.metamodel.Metamodel; + +import org.hibernate.community.dialect.InformixDialect; import org.hibernate.dialect.CockroachDialect; import org.hibernate.orm.test.jpa.BaseEntityManagerFunctionalTestCase; import org.hibernate.orm.test.jpa.metamodel.Address; @@ -38,8 +40,8 @@ import org.hibernate.query.sqm.tree.predicate.SqmComparisonPredicate; import org.hibernate.testing.FailureExpected; -import org.hibernate.testing.SkipForDialect; import org.hibernate.testing.TestForIssue; +import org.hibernate.testing.orm.junit.SkipForDialect; import org.junit.Test; import static org.hibernate.testing.transaction.TransactionUtil.doInJPA; @@ -229,7 +231,7 @@ public void testMultiselectWithPredicates() { } @Test - @SkipForDialect(value = CockroachDialect.class, strictMatching = true) + @SkipForDialect(dialectClass = CockroachDialect.class) public void testDateTimeFunctions() { EntityManager em = getOrCreateEntityManager(); em.getTransaction().begin(); @@ -255,6 +257,7 @@ public void testDateTimeFunctions() { } @Test + @SkipForDialect(dialectClass = InformixDialect.class, majorVersion = 11, minorVersion = 70, reason = "Informix does not support count literals") public void testFunctionDialectFunctions() { EntityManager em = getOrCreateEntityManager(); em.getTransaction().begin(); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/basic/Foo.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/basic/Foo.java new file mode 100644 index 000000000000..60174b52b959 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/basic/Foo.java @@ -0,0 +1,55 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.jpa.criteria.basic; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.JoinColumns; +import jakarta.persistence.ManyToOne; + +import java.util.Objects; + +/** + * Entity used in org.hibernate.orm.test.jpa.criteria.basic.NegatedInPredicateTest. + * + * @author Mike Mannion + */ +@Entity +public class Foo { + + @Id + Long id; + + @ManyToOne + @JoinColumns({ + @JoinColumn(name = "FK_CODE", referencedColumnName = "CODE"), + @JoinColumn(name = "FK_CONTEXT", referencedColumnName = "CONTEXT") + }) + FooType fooType; + + public Foo() { + // For JPA + } + + public Foo(Long id, FooType fooType) { + this.id = id; + this.fooType = fooType; + } + + @Override + public boolean equals(Object o) { + if (o == null || getClass() != o.getClass()) return false; + + Foo customer = (Foo) o; + return Objects.equals(id, customer.id) && Objects.equals(fooType, customer.fooType); + } + + @Override + public int hashCode() { + int result = Objects.hashCode(id); + result = 31 * result + Objects.hashCode(fooType); + return result; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/basic/FooType.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/basic/FooType.java new file mode 100644 index 000000000000..991732ec458f --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/basic/FooType.java @@ -0,0 +1,32 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.jpa.criteria.basic; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; + +/** + * Entity used in org.hibernate.orm.test.jpa.criteria.basic.NegatedInPredicateTest. + * + * @author Mike Mannion + */ +@Entity +public class FooType { + @Id + @Column(name = "CODE") + String code; + + @Column(name = "CONTEXT") + String context; + + public FooType() { + // For JPA + } + + FooType(String code, String context) { + this.code = code; + this.context = context; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/basic/NegatedInPredicateTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/basic/NegatedInPredicateTest.java new file mode 100644 index 000000000000..e1d4a2bc3f23 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/basic/NegatedInPredicateTest.java @@ -0,0 +1,111 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.jpa.criteria.basic; + +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Root; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + + +/** + * Add test which composes not with in. Test introduced after discovery the negated in + * fails under dialects without record-level construction, such as DB2. + * + * @author Mike Mannion + */ +@JiraKey(value = "HHH-19497") +@DomainModel( + annotatedClasses = {Foo.class, FooType.class} +) +@SessionFactory +class NegatedInPredicateTest { + + public static final FooType FOO_TYPE1 = new FooType( "ft1", "ctx1" ); + public static final FooType FOO_TYPE2 = new FooType( "ft2", "ctx1" ); + + @BeforeEach + void setup(SessionFactoryScope scope) { + scope.inTransaction( + em -> { + em.persist( FOO_TYPE1 ); + em.persist( FOO_TYPE2 ); + + Foo foo1 = new Foo( 1L, FOO_TYPE1 ); + Foo foo2 = new Foo( 2L, FOO_TYPE1 ); + Foo foo3 = new Foo( 3L, FOO_TYPE2 ); + + em.persist( foo1 ); + em.persist( foo2 ); + em.persist( foo3 ); + } + ); + } + + @AfterEach + void tearDown(SessionFactoryScope scope) { + scope.inTransaction( + em -> { + em.createQuery( "delete from Foo" ).executeUpdate(); + em.createQuery( "delete from FooType" ).executeUpdate(); + } + ); + } + + @Test + void testSanity(SessionFactoryScope scope) { + scope.inTransaction( + em -> { + CriteriaBuilder cb = em.getCriteriaBuilder(); + CriteriaQuery cq = cb.createQuery( Foo.class ); + Root root = cq.from( Foo.class ); + assertThat( em.createQuery( cq.select( root ) ).getResultList() ) + .hasSize( 3 ); + } + ); + } + + @Test + void testNegatedPredicate(SessionFactoryScope scope) { + scope.inTransaction( + em -> { + CriteriaBuilder cb = em.getCriteriaBuilder(); + CriteriaQuery cq = cb.createQuery( Foo.class ); + Root root = cq.from( Foo.class ); + cq.select( root ) + .where( cb.not( root.get( "fooType" ).in( List.of( FOO_TYPE1, FOO_TYPE2 ) ) ) ); + assertThat( em.createQuery( cq ).getResultList() ) + .isEmpty(); + } + ); + } + + @Test + void testNonNegatedInPredicate(SessionFactoryScope scope) { + scope.inTransaction( + em -> { + CriteriaBuilder cb = em.getCriteriaBuilder(); + CriteriaQuery cq = cb.createQuery( Foo.class ); + Root root = cq.from( Foo.class ); + cq.select( root ) + .where( root.get( "fooType" ).in( List.of( FOO_TYPE1, FOO_TYPE2 ) ) ); + assertThat( em.createQuery( cq ).getResultList() ) + .hasSize( 3 ); + + } + ); + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/paths/PluralAttributeExpressionsTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/paths/PluralAttributeExpressionsTest.java index 8d13761af3f7..af33816d2929 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/paths/PluralAttributeExpressionsTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/paths/PluralAttributeExpressionsTest.java @@ -9,6 +9,9 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; +import java.util.Map; + +import jakarta.persistence.criteria.CriteriaBuilder; import jakarta.persistence.criteria.CriteriaQuery; import jakarta.persistence.criteria.Root; @@ -24,10 +27,11 @@ import org.hibernate.query.criteria.HibernateCriteriaBuilder; import org.hibernate.query.criteria.JpaExpression; -import org.hibernate.testing.TestForIssue; +import org.hibernate.testing.orm.junit.Jira; import org.junit.jupiter.api.Test; +import static org.assertj.core.api.Assertions.assertThat; import static org.hibernate.testing.transaction.TransactionUtil.doInJPA; /** @@ -72,7 +76,7 @@ public void testCollectionIsEmptyCriteria() { } @Test - @TestForIssue( jiraKey = "HHH-11225" ) + @Jira("https://hibernate.atlassian.net/browse/HHH-11225") public void testElementMapIsEmptyHql() { doInJPA( this::entityManagerFactory, entityManager -> { entityManager.createQuery( "select m from MapEntity m where m.localized is empty" ).getResultList(); @@ -80,7 +84,7 @@ public void testElementMapIsEmptyHql() { } @Test - @TestForIssue( jiraKey = "HHH-11225" ) + @Jira("https://hibernate.atlassian.net/browse/HHH-11225") public void testElementMapIsEmptyCriteria() { doInJPA( this::entityManagerFactory, entityManager -> { final HibernateCriteriaBuilder cb = (HibernateCriteriaBuilder) entityManager.getCriteriaBuilder(); @@ -96,7 +100,7 @@ public void testElementMapIsEmptyCriteria() { } @Test - @TestForIssue( jiraKey = "HHH-11225" ) + @Jira("https://hibernate.atlassian.net/browse/HHH-11225") public void testEntityMapIsEmptyHql() { doInJPA( this::entityManagerFactory, entityManager -> { entityManager.createQuery( "select a from Article a where a.translations is empty" ).getResultList(); @@ -104,7 +108,7 @@ public void testEntityMapIsEmptyHql() { } @Test - @TestForIssue( jiraKey = "HHH-11225" ) + @Jira("https://hibernate.atlassian.net/browse/HHH-11225") public void testEntityMapIsEmptyCriteria() { doInJPA( this::entityManagerFactory, entityManager -> { final HibernateCriteriaBuilder cb = (HibernateCriteriaBuilder) entityManager.getCriteriaBuilder(); @@ -146,7 +150,7 @@ public void testCollectionSizeCriteria() { } @Test - @TestForIssue( jiraKey = "HHH-11225" ) + @Jira("https://hibernate.atlassian.net/browse/HHH-11225") public void testElementMapSizeHql() { doInJPA( this::entityManagerFactory, entityManager -> { entityManager.createQuery( "select m from MapEntity m where size( m.localized ) > 1" ).getResultList(); @@ -154,7 +158,7 @@ public void testElementMapSizeHql() { } @Test - @TestForIssue( jiraKey = "HHH-11225" ) + @Jira("https://hibernate.atlassian.net/browse/HHH-11225") public void testElementMapSizeCriteria() { doInJPA( this::entityManagerFactory, entityManager -> { final HibernateCriteriaBuilder cb = (HibernateCriteriaBuilder) entityManager.getCriteriaBuilder(); @@ -170,7 +174,7 @@ public void testElementMapSizeCriteria() { } @Test - @TestForIssue( jiraKey = "HHH-11225" ) + @Jira("https://hibernate.atlassian.net/browse/HHH-11225") public void testEntityMapSizeHql() { doInJPA( this::entityManagerFactory, entityManager -> { entityManager.createQuery( "select a from Article a where size(a.translations) > 1" ).getResultList(); @@ -178,7 +182,7 @@ public void testEntityMapSizeHql() { } @Test - @TestForIssue( jiraKey = "HHH-11225" ) + @Jira("https://hibernate.atlassian.net/browse/HHH-11225") public void testEntityMapSizeCriteria() { doInJPA( this::entityManagerFactory, entityManager -> { final HibernateCriteriaBuilder cb = (HibernateCriteriaBuilder) entityManager.getCriteriaBuilder(); @@ -193,4 +197,31 @@ public void testEntityMapSizeCriteria() { }); } + @Test + @Jira("https://hibernate.atlassian.net/browse/HHH-19126") + public void testPluralMapPathJavaType() { + doInJPA( this::entityManagerFactory, entityManager -> { + final CriteriaBuilder cb = entityManager.getCriteriaBuilder(); + + final CriteriaQuery
    criteria = cb.createQuery( Article.class ); + final Root
    root = criteria.from( Article.class ); + + assertThat( root.get( Article_.translations ).getJavaType() ).isEqualTo( Map.class ); + assertThat( root.get( "translations" ).getJavaType() ).isEqualTo( Map.class ); + } ); + } + + @Test + @Jira("https://hibernate.atlassian.net/browse/HHH-19126") + public void testPluralListPathJavaType() { + doInJPA( this::entityManagerFactory, entityManager -> { + final CriteriaBuilder cb = entityManager.getCriteriaBuilder(); + + final CriteriaQuery
    criteria = cb.createQuery( Address.class ); + final Root
    root = criteria.from( Address.class ); + + assertThat( root.get( Address_.phones ).getJavaType() ).isEqualTo( List.class ); + assertThat( root.get( "phones" ).getJavaType() ).isEqualTo( List.class ); + } ); + } } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/subquery/AttributeJoinOnCorrelatedOrderedJoinTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/subquery/AttributeJoinOnCorrelatedOrderedJoinTest.java new file mode 100644 index 000000000000..d0aeb12d3379 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/criteria/subquery/AttributeJoinOnCorrelatedOrderedJoinTest.java @@ -0,0 +1,102 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.jpa.criteria.subquery; + +import jakarta.persistence.*; +import org.hibernate.Session; +import org.hibernate.query.criteria.*; +import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.Jpa; +import org.junit.jupiter.api.Test; + +import java.io.Serializable; + +@Jpa( annotatedClasses = { + AttributeJoinOnCorrelatedOrderedJoinTest.Primary.class, + AttributeJoinOnCorrelatedOrderedJoinTest.Secondary.class, + AttributeJoinOnCorrelatedOrderedJoinTest.Tertiary.class, +} ) +@Jira( "https://hibernate.atlassian.net/browse/HHH-19550" ) +public class AttributeJoinOnCorrelatedOrderedJoinTest { + + @Test + public void test(EntityManagerFactoryScope scope) { + scope.inTransaction( em -> { + final HibernateCriteriaBuilder cb = em.unwrap( Session.class ).getCriteriaBuilder(); + final JpaCriteriaQuery query = cb.createTupleQuery(); + final JpaRoot primary = query.from( Primary.class ); + // setup ordered joins + final JpaEntityJoin entityJoinedPrimary = primary.join( Primary.class ); + entityJoinedPrimary.on( primary.get( "id" ).equalTo( entityJoinedPrimary.get( "id" ) ) ); + // Need an attribute join to correlate + final JpaJoin secondaryJoin = primary.join( "secondary" ); + + final JpaSubQuery subquery = query.subquery( String.class ); + final JpaJoin correlatedSecondaryJoin = subquery.correlate( secondaryJoin ); + // The association join is being added to the result of getLhs().findRoot() + // so if the correlated join returns a wrong node, this is messed up + // and will produce an exception when copying the criteria tree + final JpaJoin tertiary = correlatedSecondaryJoin.join( "tertiary" ); + subquery.select( tertiary.get( "name" ) ).where( cb.equal( + tertiary.get( "secondaryFk" ), + correlatedSecondaryJoin.get( "id" ) + ) ); + query.multiselect( primary.get( "id" ), secondaryJoin.get( "name" ), subquery ); + em.createQuery( query ).getResultList(); + } ); + } + + @Entity( name = "PrimaryEntity" ) + public static class Primary implements Serializable { + @Id + private Integer id; + + @ManyToOne(fetch = FetchType.LAZY) + private Secondary secondary; + + public Primary() { + } + } + + @Entity( name = "SecondaryEntity" ) + public static class Secondary implements Serializable { + @Id + private Integer id; + + private String name; + @ManyToOne(fetch = FetchType.LAZY) + private Tertiary tertiary; + + public Secondary() { + } + + public Secondary(Integer id, String name) { + this.id = id; + this.name = name; + } + } + + @Entity( name = "TertiaryEntity" ) + public static class Tertiary implements Serializable { + @Id + private Integer id; + + private Integer secondaryFk; + + private String name; + + public Tertiary() { + } + + public Tertiary(Integer id, Integer secondaryFk, String name) { + this.id = id; + this.secondaryFk = secondaryFk; + this.name = name; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/lock/RepeatableReadTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/lock/RepeatableReadTest.java index bd3a24d38c76..d52704e299f0 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/lock/RepeatableReadTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/lock/RepeatableReadTest.java @@ -13,6 +13,7 @@ import org.hibernate.boot.registry.StandardServiceRegistryBuilder; import org.hibernate.cfg.AvailableSettings; import org.hibernate.dialect.CockroachDialect; +import org.hibernate.dialect.MariaDBDialect; import org.hibernate.dialect.SQLServerDialect; import org.hibernate.engine.jdbc.connections.spi.ConnectionProvider; import org.hibernate.exception.SQLGrammarException; @@ -106,6 +107,7 @@ public void testStaleVersionedInstanceFoundInQueryResult() { @Test @SkipForDialect(dialectClass = CockroachDialect.class, reason = "Cockroach uses SERIALIZABLE by default and fails to acquire a write lock after a TX in between committed changes to a row") + @SkipForDialect(dialectClass = MariaDBDialect.class, majorVersion = 11, minorVersion = 6, microVersion = 2, reason = "MariaDB will throw an error DB_RECORD_CHANGED when acquiring a lock on a record that have changed") public void testStaleVersionedInstanceFoundOnLock() { if ( !readCommittedIsolationMaintained( "repeatable read tests" ) ) { return; @@ -228,6 +230,7 @@ public void testStaleNonVersionedInstanceFoundInQueryResult() { @Test @SkipForDialect(dialectClass = CockroachDialect.class, reason = "Cockroach uses SERIALIZABLE by default and fails to acquire a write lock after a TX in between committed changes to a row") + @SkipForDialect(dialectClass = MariaDBDialect.class, majorVersion = 11, minorVersion = 6, microVersion = 2, reason = "MariaDB will throw an error DB_RECORD_CHANGED when acquiring a lock on a record that have changed") public void testStaleNonVersionedInstanceFoundOnLock() { if ( !readCommittedIsolationMaintained( "repeatable read tests" ) ) { return; diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/orphan/onetomany/merge/MergeBidirectionalCascadeTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/orphan/onetomany/merge/MergeBidirectionalCascadeTest.java new file mode 100644 index 000000000000..edd9322aac6f --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/orphan/onetomany/merge/MergeBidirectionalCascadeTest.java @@ -0,0 +1,218 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.jpa.orphan.onetomany.merge; + +import java.util.ArrayList; +import java.util.List; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel( + annotatedClasses = { + MergeBidirectionalCascadeTest.Parent.class, + MergeBidirectionalCascadeTest.Child.class, + } +) +@SessionFactory +@JiraKey("HHH-18389") +public class MergeBidirectionalCascadeTest { + + private static final Long ID_PARENT_WITHOUT_CHILDREN = 1L; + private static final Long ID_PARENT_WITH_CHILDREN = 2L; + + @BeforeEach + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Parent parent = new Parent( ID_PARENT_WITHOUT_CHILDREN, "old name" ); + session.persist( parent ); + + Parent parent2 = new Parent( ID_PARENT_WITH_CHILDREN, "old name" ); + Child child = new Child( 2l, "Child" ); + parent2.addChild( child ); + + session.persist( child ); + session.persist( parent2 ); + } + ); + } + + @AfterEach + private void tearDown(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + session.createMutationQuery( "delete from Child" ).executeUpdate(); + session.createMutationQuery( "delete from Parent" ).executeUpdate(); + } + ); + } + + @Test + public void testMergeParentWihoutChildren(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Parent parent = new Parent( ID_PARENT_WITHOUT_CHILDREN, "new name" ); + Parent merged = session.merge( parent ); + assertThat( merged.getName() ).isEqualTo( "new name" ); + + } + ); + } + + @Test + public void testMergeParentWithChildren(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Parent parent = new Parent( ID_PARENT_WITH_CHILDREN, "new name" ); + Child child = new Child( 2l, "Child" ); + parent.addChild( child ); + Parent merged = session.merge( parent ); + assertThat( merged.getName() ).isEqualTo( "new name" ); + + } + ); + scope.inTransaction( + session -> { + Parent parent = session.get( Parent.class, ID_PARENT_WITH_CHILDREN ); + assertThat( parent.getChildren().size() ).isEqualTo( 1 ); + + } + ); + } + + @Test + public void testMergeParentWithChildren2(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Parent parent = new Parent( ID_PARENT_WITH_CHILDREN, "new name" ); + Parent merged = session.merge( parent ); + + } + ); + scope.inTransaction( + session -> { + Parent parent = session.get( Parent.class, ID_PARENT_WITH_CHILDREN ); + assertThat( parent.getName() ).isEqualTo( "new name" ); + assertThat( parent.getChildren().size() ).isEqualTo( 1 ); + + List children = session.createQuery( "Select c from Child c", Child.class ).list(); + assertThat( children.size() ).isEqualTo( 1 ); + } + ); + } + + @Entity(name = "Parent") + public static class Parent { + + @Id + private Long id; + + private String name; + + @OneToMany(cascade = CascadeType.MERGE, mappedBy = "parent") + private List children; + + public Parent() { + } + + public Parent(Long id, String name) { + this.id = id; + this.name = name; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public List getChildren() { + return children; + } + + public void setChildren(List children) { + this.children = children; + } + + public void addChild(Child child) { + if ( children == null ) { + children = new ArrayList(); + } + children.add( child ); + child.setParent( this ); + } + } + + @Entity(name = "Child") + public static class Child { + + @Id + private Long id; + + private String name; + + public Child() { + } + + public Child(Long id, String name) { + this.id = id; + this.name = name; + } + + @ManyToOne + @JoinColumn + private Parent parent; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Parent getParent() { + return parent; + } + + public void setParent(Parent parent) { + this.parent = parent; + } + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/orphan/onetomany/merge/MergeCascadeTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/orphan/onetomany/merge/MergeCascadeTest.java new file mode 100644 index 000000000000..5a3a603dc2df --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/orphan/onetomany/merge/MergeCascadeTest.java @@ -0,0 +1,200 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.jpa.orphan.onetomany.merge; + +import java.util.ArrayList; +import java.util.List; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel( + annotatedClasses = { + MergeCascadeTest.Parent.class, + MergeCascadeTest.Child.class, + } +) +@SessionFactory +@JiraKey("HHH-18389") +public class MergeCascadeTest { + + private static final Long ID_PARENT_WITHOUT_CHILDREN = 1L; + private static final Long ID_PARENT_WITH_CHILDREN = 2L; + + @BeforeEach + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Parent parent = new Parent( ID_PARENT_WITHOUT_CHILDREN, "old name" ); + session.persist( parent ); + + Parent parent2 = new Parent( ID_PARENT_WITH_CHILDREN, "old name" ); + Child child = new Child( 2l, "Child" ); + parent2.addChild( child ); + + session.persist( child ); + session.persist( parent2 ); + } + ); + } + + @AfterEach + private void tearDown(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + session.createMutationQuery( "delete from Parent" ).executeUpdate(); + session.createMutationQuery( "delete from Child" ).executeUpdate(); + } + ); + } + + @Test + public void testMergeParentWihoutChildren(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Parent parent = new Parent( ID_PARENT_WITHOUT_CHILDREN, "new name" ); + Parent merged = session.merge( parent ); + assertThat( merged.getName() ).isEqualTo( "new name" ); + } + ); + } + + @Test + public void testMergeParentWithChildren(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Parent parent = new Parent( ID_PARENT_WITH_CHILDREN, "new name" ); + Child child = new Child( 2l, "Child" ); + parent.addChild( child ); + Parent merged = session.merge( parent ); + assertThat( merged.getName() ).isEqualTo( "new name" ); + } + ); + scope.inTransaction( + session -> { + Parent parent = session.get( Parent.class, ID_PARENT_WITH_CHILDREN ); + assertThat( parent.getChildren().size() ).isEqualTo( 1 ); + } + ); + } + + @Test + public void testMergeParentWithChildren2(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Parent parent = new Parent( ID_PARENT_WITH_CHILDREN, "new name" ); + session.merge( parent ); + } + ); + scope.inTransaction( + session -> { + Parent parent = session.get( Parent.class, ID_PARENT_WITH_CHILDREN ); + assertThat( parent.getName() ).isEqualTo( "new name" ); + assertThat( parent.getChildren().size() ).isEqualTo( 0 ); + + List children = session.createQuery( "Select c from Child c", Child.class ).list(); + assertThat( children.size() ).isEqualTo( 1 ); + } + ); + } + + @Entity(name = "Parent") + public static class Parent { + + @Id + private Long id; + + private String name; + + @OneToMany(cascade = CascadeType.MERGE) + private List children; + + public Parent() { + } + + public Parent(Long id, String name) { + this.id = id; + this.name = name; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public List getChildren() { + return children; + } + + public void setChildren(List children) { + this.children = children; + } + + public void addChild(Child child) { + if ( children == null ) { + children = new ArrayList(); + } + children.add( child ); + } + } + + @Entity(name = "Child") + public static class Child { + + @Id + private Long id; + + private String name; + + public Child() { + } + + public Child(Long id, String name) { + this.id = id; + this.name = name; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/orphan/onetomany/merge/MergeCascadeWithMapCollectionTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/orphan/onetomany/merge/MergeCascadeWithMapCollectionTest.java new file mode 100644 index 000000000000..1675d935f536 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/orphan/onetomany/merge/MergeCascadeWithMapCollectionTest.java @@ -0,0 +1,200 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.jpa.orphan.onetomany.merge; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel( + annotatedClasses = { + MergeCascadeWithMapCollectionTest.Parent.class, + MergeCascadeWithMapCollectionTest.Child.class, + } +) +@SessionFactory +@JiraKey("HHH-18842") +public class MergeCascadeWithMapCollectionTest { + + private static final Long ID_PARENT_WITHOUT_CHILDREN = 1L; + private static final Long ID_PARENT_WITH_CHILDREN = 2L; + + @BeforeEach + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Parent parent = new Parent( ID_PARENT_WITHOUT_CHILDREN, "old name" ); + session.persist( parent ); + + Parent parent2 = new Parent( ID_PARENT_WITH_CHILDREN, "old name" ); + Child child = new Child( 2l, "Child" ); + parent2.addChild( child ); + + session.persist( child ); + session.persist( parent2 ); + } + ); + } + + @AfterEach + private void tearDown(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + session.createMutationQuery( "delete from Parent" ).executeUpdate(); + session.createMutationQuery( "delete from Child" ).executeUpdate(); + } + ); + } + + @Test + public void testMergeParentWihoutChildren(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Parent parent = new Parent( ID_PARENT_WITHOUT_CHILDREN, "new name" ); + Parent merged = session.merge( parent ); + assertThat( merged.getName() ).isEqualTo( "new name" ); + } + ); + } + + @Test + public void testMergeParentWithChildren(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Parent parent = new Parent( ID_PARENT_WITH_CHILDREN, "new name" ); + Child child = new Child( 2l, "Child" ); + parent.addChild( child ); + Parent merged = session.merge( parent ); + assertThat( merged.getName() ).isEqualTo( "new name" ); + } + ); + scope.inTransaction( + session -> { + Parent parent = session.get( Parent.class, ID_PARENT_WITH_CHILDREN ); + assertThat( parent.getChildren().size() ).isEqualTo( 1 ); + } + ); + } + + @Test + public void testMergeParentWithChildren2(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Parent parent = new Parent( ID_PARENT_WITH_CHILDREN, "new name" ); + session.merge( parent ); + } + ); + scope.inTransaction( + session -> { + Parent parent = session.get( Parent.class, ID_PARENT_WITH_CHILDREN ); + assertThat( parent.getName() ).isEqualTo( "new name" ); + assertThat( parent.getChildren().size() ).isEqualTo( 0 ); + + List children = session.createQuery( "Select c from Child c", Child.class ).list(); + assertThat( children.size() ).isEqualTo( 1 ); + } + ); + } + + @Entity(name = "Parent") + public static class Parent { + + @Id + private Long id; + + private String name; + + @OneToMany(cascade = CascadeType.MERGE) + private Map children; + + public Parent() { + } + + public Parent(Long id, String name) { + this.id = id; + this.name = name; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Map getChildren() { + return children; + } + + public void setChildren(Map children) { + this.children = children; + } + + public void addChild(Child child) { + if ( children == null ) { + children = new HashMap<>(); + } + children.put( child.getName(), child ); + } + } + + @Entity(name = "Child") + public static class Child { + + @Id + private Long id; + + private String name; + + public Child() { + } + + public Child(Long id, String name) { + this.id = id; + this.name = name; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/orphan/onetomany/merge/MergeOrphanRemovalArrayBidirectionalTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/orphan/onetomany/merge/MergeOrphanRemovalArrayBidirectionalTest.java new file mode 100644 index 000000000000..9f2cc05f2a15 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/orphan/onetomany/merge/MergeOrphanRemovalArrayBidirectionalTest.java @@ -0,0 +1,217 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.jpa.orphan.onetomany.merge; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel( + annotatedClasses = { + MergeOrphanRemovalArrayBidirectionalTest.Parent.class, + MergeOrphanRemovalArrayBidirectionalTest.Child.class, + } +) +@SessionFactory +@JiraKey("HHH-18842") +public class MergeOrphanRemovalArrayBidirectionalTest { + + private static final Long ID_PARENT_WITHOUT_CHILDREN = 1L; + private static final Long ID_PARENT_WITH_CHILDREN = 2L; + + @BeforeEach + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Parent parent = new Parent( ID_PARENT_WITHOUT_CHILDREN, "old name" ); + session.persist( parent ); + + Parent parent2 = new Parent( ID_PARENT_WITH_CHILDREN, "old name" ); + Child child = new Child( 2l, "Child" ); + Child[] children = new Child[1]; + children[0] = child; + parent2.setChildren( children ); + + session.persist( child ); + session.persist( parent2 ); + } + ); + } + + @AfterEach + private void tearDown(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + session.createMutationQuery( "delete from Child" ).executeUpdate(); + session.createMutationQuery( "delete from Parent" ).executeUpdate(); + } + ); + } + + @Test + public void testMergeParentWihoutChildren(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Parent parent = new Parent( ID_PARENT_WITHOUT_CHILDREN, "new name" ); + Parent merged = session.merge( parent ); + assertThat( merged.getName() ).isEqualTo( "new name" ); + + } + ); + } + + @Test + public void testMergeParentWithChildren(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Parent parent = new Parent( ID_PARENT_WITH_CHILDREN, "new name" ); + Child child = new Child( 2l, "Child" ); + Child[] children = new Child[1]; + children[0] = child; + parent.setChildren( children ); + Parent merged = session.merge( parent ); + assertThat( merged.getName() ).isEqualTo( "new name" ); + + } + ); + scope.inTransaction( + session -> { + Parent parent = session.get( Parent.class, ID_PARENT_WITH_CHILDREN ); + assertThat( parent.getChildren().length ).isEqualTo( 1 ); + + } + ); + } + + @Test + public void testMergeParentWithChildren2(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Parent parent = new Parent( ID_PARENT_WITH_CHILDREN, "new name" ); + Parent merged = session.merge( parent ); + + } + ); + scope.inTransaction( + session -> { + Parent parent = session.get( Parent.class, ID_PARENT_WITH_CHILDREN ); + assertThat( parent.getName() ).isEqualTo( "new name" ); + assertThat( parent.getChildren().length ).isEqualTo( 0 ); + + List children = session.createQuery( "Select c from Child c", Child.class ).list(); + assertThat( children.size() ).isEqualTo( 0 ); + } + ); + + } + + @Entity(name = "Parent") + public static class Parent { + + @Id + private Long id; + + private String name; + + @OneToMany(orphanRemoval = true, mappedBy = "parent") + private Child[] children; + + public Parent() { + } + + public Parent(Long id, String name) { + this.id = id; + this.name = name; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Child[] getChildren() { + return children; + } + + public void setChildren(Child[] children) { + this.children = children; + for ( int i = 0; i < children.length; i++ ) { + children[i].setParent( this ); + } + } + + } + + @Entity(name = "Child") + public static class Child { + + @Id + private Long id; + + private String name; + + public Child() { + } + + public Child(Long id, String name) { + this.id = id; + this.name = name; + } + + @ManyToOne + @JoinColumn + private Parent parent; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Parent getParent() { + return parent; + } + + public void setParent(Parent parent) { + this.parent = parent; + } + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/orphan/onetomany/merge/MergeOrphanRemovalBidirectionalTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/orphan/onetomany/merge/MergeOrphanRemovalBidirectionalTest.java new file mode 100644 index 000000000000..89aac97c0f6a --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/orphan/onetomany/merge/MergeOrphanRemovalBidirectionalTest.java @@ -0,0 +1,217 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.jpa.orphan.onetomany.merge; + +import java.util.ArrayList; +import java.util.List; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel( + annotatedClasses = { + MergeOrphanRemovalBidirectionalTest.Parent.class, + MergeOrphanRemovalBidirectionalTest.Child.class, + } +) +@SessionFactory +@JiraKey("HHH-18389") +public class MergeOrphanRemovalBidirectionalTest { + + private static final Long ID_PARENT_WITHOUT_CHILDREN = 1L; + private static final Long ID_PARENT_WITH_CHILDREN = 2L; + + @BeforeEach + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Parent parent = new Parent( ID_PARENT_WITHOUT_CHILDREN, "old name" ); + session.persist( parent ); + + Parent parent2 = new Parent( ID_PARENT_WITH_CHILDREN, "old name" ); + Child child = new Child( 2l, "Child" ); + parent2.addChild( child ); + + session.persist( child ); + session.persist( parent2 ); + } + ); + } + + @AfterEach + private void tearDown(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + session.createMutationQuery( "delete from Child" ).executeUpdate(); + session.createMutationQuery( "delete from Parent" ).executeUpdate(); + } + ); + } + + @Test + public void testMergeParentWihoutChildren(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Parent parent = new Parent( ID_PARENT_WITHOUT_CHILDREN, "new name" ); + Parent merged = session.merge( parent ); + assertThat( merged.getName() ).isEqualTo( "new name" ); + + } + ); + } + + @Test + public void testMergeParentWithChildren(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Parent parent = new Parent( ID_PARENT_WITH_CHILDREN, "new name" ); + Child child = new Child( 2l, "Child" ); + parent.addChild( child ); + Parent merged = session.merge( parent ); + assertThat( merged.getName() ).isEqualTo( "new name" ); + + } + ); + scope.inTransaction( + session -> { + Parent parent = session.get( Parent.class, ID_PARENT_WITH_CHILDREN ); + assertThat( parent.getChildren().size() ).isEqualTo( 1 ); + + } + ); + } + + @Test + public void testMergeParentWithChildren2(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Parent parent = new Parent( ID_PARENT_WITH_CHILDREN, "new name" ); + Parent merged = session.merge( parent ); + + } + ); + scope.inTransaction( + session -> { + Parent parent = session.get( Parent.class, ID_PARENT_WITH_CHILDREN ); + assertThat( parent.getName() ).isEqualTo( "new name" ); + assertThat( parent.getChildren().size() ).isEqualTo( 0 ); + + List children = session.createQuery( "Select c from Child c", Child.class ).list(); + assertThat( children.size() ).isEqualTo( 0 ); + } + ); + } + + @Entity(name = "Parent") + public static class Parent { + + @Id + private Long id; + + private String name; + + @OneToMany(orphanRemoval = true, mappedBy = "parent") + private List children; + + public Parent() { + } + + public Parent(Long id, String name) { + this.id = id; + this.name = name; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public List getChildren() { + return children; + } + + public void setChildren(List children) { + this.children = children; + } + + public void addChild(Child child) { + if ( children == null ) { + children = new ArrayList(); + } + children.add( child ); + child.setParent( this ); + } + } + + @Entity(name = "Child") + public static class Child { + + @Id + private Long id; + + private String name; + + public Child() { + } + + public Child(Long id, String name) { + this.id = id; + this.name = name; + } + + @ManyToOne + @JoinColumn + private Parent parent; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Parent getParent() { + return parent; + } + + public void setParent(Parent parent) { + this.parent = parent; + } + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/orphan/onetomany/merge/MergeOrphanRemovalTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/orphan/onetomany/merge/MergeOrphanRemovalTest.java new file mode 100644 index 000000000000..88197e74386e --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/orphan/onetomany/merge/MergeOrphanRemovalTest.java @@ -0,0 +1,229 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.jpa.orphan.onetomany.merge; + +import java.util.ArrayList; +import java.util.List; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel( + annotatedClasses = { + MergeOrphanRemovalTest.Parent.class, + MergeOrphanRemovalTest.Child.class, + } +) +@SessionFactory +@JiraKey("HHH-18389") +public class MergeOrphanRemovalTest { + + private static final Long ID_PARENT_WITHOUT_CHILDREN = 1L; + private static final Long ID_PARENT_WITH_CHILDREN = 2L; + + @BeforeEach + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Parent parent = new Parent( ID_PARENT_WITHOUT_CHILDREN, "old name" ); + session.persist( parent ); + + Parent parent2 = new Parent( ID_PARENT_WITH_CHILDREN, "old name" ); + Child child = new Child( 2l, "Child" ); + parent2.addChild( child ); + + session.persist( child ); + session.persist( parent2 ); + } + ); + } + + @AfterEach + private void tearDown(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + session.createMutationQuery( "delete from Parent" ).executeUpdate(); + session.createMutationQuery( "delete from Child" ).executeUpdate(); + } + ); + } + + @Test + public void testMergeParentWihoutChildren(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Parent parent = new Parent( ID_PARENT_WITHOUT_CHILDREN, "new name" ); + Parent merged = session.merge( parent ); + assertThat( merged.getName() ).isEqualTo( "new name" ); + + } + ); + } + + @Test + public void testMergeParentWithChildren(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Parent parent = new Parent( ID_PARENT_WITH_CHILDREN, "new name" ); + Child child = new Child( 2l, "Child" ); + parent.addChild( child ); + Parent merged = session.merge( parent ); + assertThat( merged.getName() ).isEqualTo( "new name" ); + + } + ); + scope.inTransaction( + session -> { + Parent parent = session.get( Parent.class, ID_PARENT_WITH_CHILDREN ); + assertThat( parent.getChildren().size() ).isEqualTo( 1 ); + + } + ); + } + + @Test + public void testMergeParentWithChildren2(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Parent parent = new Parent( ID_PARENT_WITH_CHILDREN, "new name" ); + Parent merged = session.merge( parent ); + + } + ); + scope.inTransaction( + session -> { + Parent parent = session.get( Parent.class, ID_PARENT_WITH_CHILDREN ); + assertThat( parent.getName() ).isEqualTo( "new name" ); + assertThat( parent.getChildren().size() ).isEqualTo( 0 ); + + List children = session.createQuery( "Select c from Child c", Child.class ).list(); + assertThat( children.size() ).isEqualTo( 0 ); + } + ); + } + + @Test + public void testMergeParentWithChildren3(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Parent parent = new Parent( ID_PARENT_WITH_CHILDREN, "new name" ); + Child child = new Child( 3l, "Child2" ); + parent.addChild( child ); + session.persist( child ); + Parent merged = session.merge( parent ); + + } + ); + scope.inTransaction( + session -> { + Parent parent = session.get( Parent.class, ID_PARENT_WITH_CHILDREN ); + assertThat( parent.getName() ).isEqualTo( "new name" ); + assertThat( parent.getChildren().size() ).isEqualTo( 1 ); + assertThat( parent.getChildren().get( 0 ).getName() ).isEqualTo( "Child2" ); + + List children = session.createQuery( "Select c from Child c", Child.class ).list(); + assertThat( children.size() ).isEqualTo( 1 ); + assertThat( children.get( 0 ).getName() ).isEqualTo( "Child2" ); + } + ); + } + + @Entity(name = "Parent") + public static class Parent { + + @Id + private Long id; + + private String name; + + @OneToMany(orphanRemoval = true) + private List children; + + public Parent() { + } + + public Parent(Long id, String name) { + this.id = id; + this.name = name; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public List getChildren() { + return children; + } + + public void setChildren(List children) { + this.children = children; + } + + public void addChild(Child child) { + if ( children == null ) { + children = new ArrayList(); + } + children.add( child ); + } + } + + @Entity(name = "Child") + public static class Child { + + @Id + private Long id; + + private String name; + + public Child() { + } + + public Child(Long id, String name) { + this.id = id; + this.name = name; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/orphan/onetomany/merge/MergeTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/orphan/onetomany/merge/MergeTest.java new file mode 100644 index 000000000000..1e9304804ab0 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/orphan/onetomany/merge/MergeTest.java @@ -0,0 +1,200 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.jpa.orphan.onetomany.merge; + +import java.util.ArrayList; +import java.util.List; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel( + annotatedClasses = { + MergeTest.Parent.class, + MergeTest.Child.class, + } +) +@SessionFactory +@JiraKey("HHH-18389") +public class MergeTest { + + private static final Long ID_PARENT_WITHOUT_CHILDREN = 1L; + private static final Long ID_PARENT_WITH_CHILDREN = 2L; + + @BeforeEach + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Parent parent = new Parent( ID_PARENT_WITHOUT_CHILDREN, "old name" ); + session.persist( parent ); + + Parent parent2 = new Parent( ID_PARENT_WITH_CHILDREN, "old name" ); + Child child = new Child( 2l, "Child" ); + parent2.addChild( child ); + + session.persist( child ); + session.persist( parent2 ); + } + ); + } + + @AfterEach + private void tearDown(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + session.createMutationQuery( "delete from Parent" ).executeUpdate(); + session.createMutationQuery( "delete from Child" ).executeUpdate(); + } + ); + } + + @Test + public void testMergeParentWihoutChildren(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Parent parent = new Parent( ID_PARENT_WITHOUT_CHILDREN, "new name" ); + Parent merged = session.merge( parent ); + assertThat( merged.getName() ).isEqualTo( "new name" ); + } + ); + } + + @Test + public void testMergeParentWithChildren(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Parent parent = new Parent( ID_PARENT_WITH_CHILDREN, "new name" ); + Child child = new Child( 2l, "Child" ); + parent.addChild( child ); + Parent merged = session.merge( parent ); + assertThat( merged.getName() ).isEqualTo( "new name" ); + } + ); + scope.inTransaction( + session -> { + Parent parent = session.get( Parent.class, ID_PARENT_WITH_CHILDREN ); + assertThat( parent.getChildren().size() ).isEqualTo( 1 ); + } + ); + } + + @Test + public void testMergeParentWithChildren2(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Parent parent = new Parent( ID_PARENT_WITH_CHILDREN, "new name" ); + session.merge( parent ); + } + ); + scope.inTransaction( + session -> { + Parent parent = session.get( Parent.class, ID_PARENT_WITH_CHILDREN ); + assertThat( parent.getName() ).isEqualTo( "new name" ); + assertThat( parent.getChildren().size() ).isEqualTo( 0 ); + + List children = session.createQuery( "Select c from Child c", Child.class ).list(); + assertThat( children.size() ).isEqualTo( 1 ); + } + ); + } + + + @Entity(name = "Parent") + public static class Parent { + + @Id + private Long id; + + private String name; + + @OneToMany + private List children; + + public Parent() { + } + + public Parent(Long id, String name) { + this.id = id; + this.name = name; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public List getChildren() { + return children; + } + + public void setChildren(List children) { + this.children = children; + } + + public void addChild(Child child) { + if ( children == null ) { + children = new ArrayList(); + } + children.add( child ); + } + } + + @Entity(name = "Child") + public static class Child { + + @Id + private Long id; + + private String name; + + public Child() { + } + + public Child(Long id, String name) { + this.id = id; + this.name = name; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/ql/JoinTableOptimizationTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/ql/JoinTableOptimizationTest.java index 9f79f5066e00..eb6b87b3abbf 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/ql/JoinTableOptimizationTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/ql/JoinTableOptimizationTest.java @@ -8,7 +8,6 @@ import java.util.Set; -import org.hibernate.testing.TestForIssue; import org.hibernate.testing.jdbc.SQLStatementInspector; import org.hibernate.testing.orm.junit.DomainModel; import org.hibernate.testing.orm.junit.JiraKey; @@ -21,19 +20,28 @@ import jakarta.persistence.ElementCollection; import jakarta.persistence.Embeddable; import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; import jakarta.persistence.Id; +import jakarta.persistence.Inheritance; +import jakarta.persistence.InheritanceType; import jakarta.persistence.JoinTable; +import jakarta.persistence.ManyToMany; +import jakarta.persistence.ManyToOne; import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; -@TestForIssue(jiraKey = "HHH-16691") @DomainModel( annotatedClasses = { - JoinTableOptimizationTest.Document.class, JoinTableOptimizationTest.Person.class + JoinTableOptimizationTest.Document.class, + JoinTableOptimizationTest.Person.class, + JoinTableOptimizationTest.File.class, + JoinTableOptimizationTest.Picture.class }) @SessionFactory(useCollectingStatementInspector = true) public class JoinTableOptimizationTest { @Test + @JiraKey("HHH-16691") public void testOnlyCollectionTableJoined(SessionFactoryScope scope) { SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); statementInspector.clear(); @@ -48,6 +56,7 @@ public void testOnlyCollectionTableJoined(SessionFactoryScope scope) { } @Test + @JiraKey("HHH-16691") public void testInnerJoin(SessionFactoryScope scope) { SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); statementInspector.clear(); @@ -68,6 +77,7 @@ public void testInnerJoin(SessionFactoryScope scope) { } @Test + @JiraKey("HHH-16691") public void testLeftJoin(SessionFactoryScope scope) { SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); statementInspector.clear(); @@ -88,6 +98,7 @@ public void testLeftJoin(SessionFactoryScope scope) { } @Test + @JiraKey("HHH-16691") public void testInnerJoinCustomOnClause(SessionFactoryScope scope) { SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); statementInspector.clear(); @@ -108,6 +119,7 @@ public void testInnerJoinCustomOnClause(SessionFactoryScope scope) { } @Test + @JiraKey("HHH-16691") public void testLeftJoinCustomOnClause(SessionFactoryScope scope) { SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); statementInspector.clear(); @@ -147,6 +159,199 @@ public void testElementCollectionJoinCustomOnClause(SessionFactoryScope scope) { ); } + @Test + @JiraKey("HHH-17646") + public void testLeftJoinFetch(SessionFactoryScope scope) { + SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); + statementInspector.clear(); + scope.inTransaction( + s -> { + s.createQuery( "select d from Document d left join fetch d.people" ).list(); + statementInspector.assertExecutedCount( 1 ); + Assertions.assertEquals( + "select d1_0.id,d1_0.name,p1_0.Document_id,p1_1.id,p1_1.name " + + "from Document d1_0 " + + "left join people p1_0 on d1_0.id=p1_0.Document_id " + + "left join Person p1_1 on p1_1.id=p1_0.people_id", + statementInspector.getSqlQueries().get( 0 ), + "Nested join was not optimized away" + ); + } + ); + } + + @Test + @JiraKey("HHH-17646") + public void testJoinTablePolymorphicJoined(SessionFactoryScope scope) { + SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); + statementInspector.clear(); + scope.inTransaction( + s -> { + s.createQuery( "select f from Document d join d.manyFiles f" ).list(); + statementInspector.assertExecutedCount( 1 ); + Assertions.assertEquals( + "select mf1_1.id,case when mf1_2.id is not null then 1 when mf1_1.id is not null then 0 end,mf1_1.document_id,mf1_1.name,mf1_2.extension " + + "from Document d1_0 " + + "join many_files mf1_0 on d1_0.id=mf1_0.Document_id " + + "join file_tbl mf1_1 on mf1_1.id=mf1_0.manyFiles_id " + + "left join Picture mf1_2 on mf1_1.id=mf1_2.id", + statementInspector.getSqlQueries().get( 0 ), + "Nested join was not optimized away" + ); + } + ); + } + + @Test + @JiraKey("HHH-17646") + public void testJoinTablePolymorphicLeftJoined(SessionFactoryScope scope) { + SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); + statementInspector.clear(); + scope.inTransaction( + s -> { + s.createQuery( "select f from Document d left join d.manyFiles f" ).list(); + statementInspector.assertExecutedCount( 1 ); + Assertions.assertEquals( + "select mf1_1.id,case when mf1_2.id is not null then 1 when mf1_1.id is not null then 0 end,mf1_1.document_id,mf1_1.name,mf1_2.extension " + + "from Document d1_0 " + + "left join many_files mf1_0 on d1_0.id=mf1_0.Document_id " + + "left join file_tbl mf1_1 on mf1_1.id=mf1_0.manyFiles_id " + + "left join Picture mf1_2 on mf1_1.id=mf1_2.id", + statementInspector.getSqlQueries().get( 0 ), + "Nested join was not optimized away" + ); + } + ); + } + + @Test + @JiraKey("HHH-17646") + public void testJoinPolymorphicJoined(SessionFactoryScope scope) { + SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); + statementInspector.clear(); + scope.inTransaction( + s -> { + s.createQuery( "select f from Document d join d.files f" ).list(); + statementInspector.assertExecutedCount( 1 ); + Assertions.assertEquals( + "select f1_0.id,case when f1_1.id is not null then 1 when f1_0.id is not null then 0 end,d1_0.id,d1_0.name,f1_0.name,f1_1.extension " + + "from Document d1_0 " + + "join file_tbl f1_0 on d1_0.id=f1_0.document_id " + + "left join Picture f1_1 on f1_0.id=f1_1.id", + statementInspector.getSqlQueries().get( 0 ), + "Nested join was not optimized away" + ); + } + ); + } + + @Test + @JiraKey("HHH-17646") + public void testJoinPolymorphicLeftJoined(SessionFactoryScope scope) { + SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); + statementInspector.clear(); + scope.inTransaction( + s -> { + s.createQuery( "select f from Document d left join d.files f" ).list(); + statementInspector.assertExecutedCount( 1 ); + Assertions.assertEquals( + "select f1_0.id,case when f1_1.id is not null then 1 when f1_0.id is not null then 0 end,d1_0.id,d1_0.name,f1_0.name,f1_1.extension " + + "from Document d1_0 " + + "left join file_tbl f1_0 on d1_0.id=f1_0.document_id " + + "left join Picture f1_1 on f1_0.id=f1_1.id", + statementInspector.getSqlQueries().get( 0 ), + "Nested join was not optimized away" + ); + } + ); + } + + @Test + @JiraKey("HHH-17646") + public void testJoinTablePolymorphicSubtypeJoined(SessionFactoryScope scope) { + SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); + statementInspector.clear(); + scope.inTransaction( + s -> { + s.createQuery( "select f from Document d join d.manyPictures f" ).list(); + statementInspector.assertExecutedCount( 1 ); + Assertions.assertEquals( + "select mp1_1.id,mp1_2.document_id,mp1_2.name,mp1_1.extension " + + "from Document d1_0 " + + "join many_pictures mp1_0 on d1_0.id=mp1_0.Document_id " + + "join Picture mp1_1 on mp1_1.id=mp1_0.manyPictures_id " + + "join file_tbl mp1_2 on mp1_1.id=mp1_2.id", + statementInspector.getSqlQueries().get( 0 ), + "Nested join was not optimized away" + ); + } + ); + } + + @Test + @JiraKey("HHH-17646") + public void testJoinTablePolymorphicSubtypeLeftJoined(SessionFactoryScope scope) { + SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); + statementInspector.clear(); + scope.inTransaction( + s -> { + s.createQuery( "select f from Document d left join d.manyPictures f" ).list(); + statementInspector.assertExecutedCount( 1 ); + Assertions.assertEquals( + "select mp1_1.id,mp1_2.document_id,mp1_2.name,mp1_1.extension " + + "from Document d1_0 " + + "left join many_pictures mp1_0 on d1_0.id=mp1_0.Document_id " + + "left join Picture mp1_1 on mp1_1.id=mp1_0.manyPictures_id " + + "left join file_tbl mp1_2 on mp1_1.id=mp1_2.id", + statementInspector.getSqlQueries().get( 0 ), + "Nested join was not optimized away" + ); + } + ); + } + + @Test + @JiraKey("HHH-17646") + public void testJoinPolymorphicSubtypeJoined(SessionFactoryScope scope) { + SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); + statementInspector.clear(); + scope.inTransaction( + s -> { + s.createQuery( "select f from Document d join d.pictures f" ).list(); + statementInspector.assertExecutedCount( 1 ); + Assertions.assertEquals( + "select p1_0.id,p1_1.document_id,p1_1.name,p1_0.extension " + + "from Document d1_0 " + + "join file_tbl p1_1 on d1_0.id=p1_1.document_id " + + "join Picture p1_0 on p1_0.id=p1_1.id", + statementInspector.getSqlQueries().get( 0 ), + "Nested join was not optimized away" + ); + } + ); + } + + @Test + @JiraKey("HHH-17646") + public void testJoinPolymorphicSubtypeLeftJoined(SessionFactoryScope scope) { + SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); + statementInspector.clear(); + scope.inTransaction( + s -> { + s.createQuery( "select f from Document d left join d.pictures f" ).list(); + statementInspector.assertExecutedCount( 1 ); + Assertions.assertEquals( + "select p1_0.id,p1_1.document_id,p1_1.name,p1_0.extension " + + "from Document d1_0 " + + "left join file_tbl p1_1 on d1_0.id=p1_1.document_id " + + "left join Picture p1_0 on p1_0.id=p1_1.id", + statementInspector.getSqlQueries().get( 0 ), + "Nested join was not optimized away" + ); + } + ); + } + @Entity(name = "Document") public static class Document { @Id @@ -158,6 +363,16 @@ public static class Document { @ElementCollection @CollectionTable(name = "document_pages") Set pages; + @OneToMany(mappedBy = "document") + Set files; + @ManyToMany + @JoinTable(name = "many_files") + Set manyFiles; + @OneToMany(mappedBy = "document") + Set pictures; + @ManyToMany + @JoinTable(name = "many_pictures") + Set manyPictures; } @Entity(name = "Person") public static class Person { @@ -165,6 +380,20 @@ public static class Person { Long id; String name; } + @Entity(name = "File") + @Table(name = "file_tbl") + @Inheritance(strategy = InheritanceType.JOINED) + public static class File { + @Id + Long id; + String name; + @ManyToOne(fetch = FetchType.LAZY) + Document document; + } + @Entity(name = "Picture") + public static class Picture extends File { + String extension; + } @Embeddable public static class Page { String text; diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/ql/MapIssueTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/ql/MapIssueTest.java index c735fa79af3d..64705998e664 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/ql/MapIssueTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/ql/MapIssueTest.java @@ -52,13 +52,27 @@ public void testOnlyCollectionTableJoined(SessionFactoryScope scope) { } @Test - public void testMapKeyJoinIsOmitted(SessionFactoryScope scope) { + public void testMapKeyJoinIsNotOmitted(SessionFactoryScope scope) { SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); statementInspector.clear(); scope.inTransaction( s -> { s.createQuery( "select c from MapOwner as o join o.contents c join c.relationship r where r.id is not null" ).list(); statementInspector.assertExecutedCount( 1 ); + // Assert 3 joins, collection table, collection element and collection key (relationship) + statementInspector.assertNumberOfJoins( 0, 3 ); + } + ); + } + + @Test + public void testMapKeyJoinIsOmitted2(SessionFactoryScope scope) { + SQLStatementInspector statementInspector = scope.getCollectingStatementInspector(); + statementInspector.clear(); + scope.inTransaction( + s -> { + s.createQuery( "select c from MapOwner as o join o.contents c where c.relationship.id is not null" ).list(); + statementInspector.assertExecutedCount( 1 ); // Assert 2 joins, collection table and collection element. No need to join the relationship because it is not nullable statementInspector.assertNumberOfJoins( 0, 2 ); } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/query/CachedQueryShallowWithDiscriminatorBytecodeEnhancedTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/query/CachedQueryShallowWithDiscriminatorBytecodeEnhancedTest.java new file mode 100644 index 000000000000..d34386608073 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/query/CachedQueryShallowWithDiscriminatorBytecodeEnhancedTest.java @@ -0,0 +1,167 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.jpa.query; + +import jakarta.persistence.Entity; +import jakarta.persistence.EntityManager; +import jakarta.persistence.Id; +import jakarta.persistence.TypedQuery; +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; +import org.hibernate.annotations.CacheLayout; +import org.hibernate.annotations.QueryCacheLayout; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.stat.Statistics; +import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced; +import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.Jpa; +import org.hibernate.testing.orm.junit.Setting; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.hibernate.jpa.HibernateHints.HINT_CACHEABLE; +import static org.junit.jupiter.api.Assertions.assertEquals; + +@JiraKey("HHH-19734") +@Jpa( + annotatedClasses = { + CachedQueryShallowWithDiscriminatorBytecodeEnhancedTest.Person.class + }, + generateStatistics = true, + properties = { + @Setting(name = AvailableSettings.USE_QUERY_CACHE, value = "true"), + @Setting(name = AvailableSettings.USE_SECOND_LEVEL_CACHE, value = "true") + } +) +@BytecodeEnhanced +public class CachedQueryShallowWithDiscriminatorBytecodeEnhancedTest { + + public final static String HQL = "select p from Person p"; + + @BeforeEach + public void setUp(EntityManagerFactoryScope scope) { + scope.inTransaction( + em -> { + Person person = new Person( 1L ); + person.setName( "Bob" ); + em.persist( person ); + } + ); + } + + @AfterEach + public void tearDown(EntityManagerFactoryScope scope) { + scope.inTransaction( em -> { + em.createQuery( "delete from Person" ).executeUpdate(); + } ); + } + + @Test + public void testCacheableQuery(EntityManagerFactoryScope scope) { + + Statistics stats = getStatistics( scope ); + stats.clear(); + + // First time the query is executed, query and results are cached. + scope.inTransaction( + em -> { + loadPersons( em ); + + assertThatAnSQLQueryHasBeenExecuted( stats ); + + assertEquals( 0, stats.getQueryCacheHitCount() ); + assertEquals( 1, stats.getQueryCacheMissCount() ); + assertEquals( 1, stats.getQueryCachePutCount() ); + + assertEquals( 0, stats.getSecondLevelCacheHitCount() ); + assertEquals( 0, stats.getSecondLevelCacheMissCount() ); + assertEquals( 0, stats.getSecondLevelCachePutCount() ); + } + ); + + stats.clear(); + + // Second time the query is executed, list of entities are read from query cache + + scope.inTransaction( + em -> { + // Create a person proxy in the persistence context to trigger the HHH-19734 error + em.getReference( Person.class, 1L ); + + loadPersons( em ); + + assertThatNoSQLQueryHasBeenExecuted( stats ); + + assertEquals( 1, stats.getQueryCacheHitCount() ); + assertEquals( 0, stats.getQueryCacheMissCount() ); + assertEquals( 0, stats.getQueryCachePutCount() ); + + assertEquals( 1, stats.getSecondLevelCacheHitCount() ); + assertEquals( 0, stats.getSecondLevelCacheMissCount() ); + assertEquals( 0, stats.getSecondLevelCachePutCount() ); + } + ); + + } + + private static Statistics getStatistics(EntityManagerFactoryScope scope) { + return ((SessionFactoryImplementor) scope.getEntityManagerFactory()).getStatistics(); + } + + private static void loadPersons(EntityManager em) { + TypedQuery query = em.createQuery( HQL, Person.class ) + .setHint( HINT_CACHEABLE, true ); + Person person = query.getSingleResult(); + assertEquals( 1L, person.getId() ); + assertEquals( "Bob", person.getName() ); + } + + private static void assertThatAnSQLQueryHasBeenExecuted(Statistics stats) { + assertEquals( 1, stats.getQueryStatistics( HQL ).getExecutionCount() ); + } + + private static void assertThatNoSQLQueryHasBeenExecuted(Statistics stats) { + assertEquals( 0, stats.getQueryStatistics( HQL ).getExecutionCount() ); + } + + @Entity(name = "Person") + @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + @QueryCacheLayout(layout = CacheLayout.SHALLOW) + public static class Person { + @Id + private Long id; + private String name; + + public Person() { + super(); + } + + public Person(Long id) { + this.id = id; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/query/CompositeIdRepeatedQueryTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/query/CompositeIdRepeatedQueryTest.java new file mode 100644 index 000000000000..e5fafcbd1055 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/query/CompositeIdRepeatedQueryTest.java @@ -0,0 +1,216 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.jpa.query; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Embeddable; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.JoinColumns; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.Jpa; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +@Jpa( + annotatedClasses = { + CompositeIdRepeatedQueryTest.Corporation.class, + CompositeIdRepeatedQueryTest.CorporationUser.class, + CompositeIdRepeatedQueryTest.Document.class, + CompositeIdRepeatedQueryTest.Person.class, + } +) +@JiraKey( "HHH-19031" ) +public class CompositeIdRepeatedQueryTest { + + @BeforeAll + public void setup(EntityManagerFactoryScope scope) { + scope.inTransaction( + entityManager -> { + Person person = new Person("1", "Andrew"); + Corporation corporation = new Corporation("2", "ibm"); + Document document = new Document(3,"registration", person, corporation); + entityManager.persist( person ); + entityManager.persist( corporation ); + entityManager.persist( document ); + } + ); + } + + @Test + public void testRepeatedQuery(EntityManagerFactoryScope scope){ + scope.inTransaction( + entityManager -> { + String query = "SELECT d FROM Document d"; + + List resultList = entityManager.createQuery( query, Document.class ).getResultList(); + assertThat(resultList.size()).isEqualTo( 1 ); + Document document = resultList.get( 0 ); + + resultList = entityManager.createQuery( query, Document.class ).getResultList(); + assertThat(resultList.size()).isEqualTo( 1 ); + assertThat( resultList.get( 0 ) ).isSameAs( document ); + } + ); + } + + @Entity(name = "Document") + @Table(name = "documents") + public static class Document { + @Id + private Integer id; + + @Embedded + private Owner owner; + + private String name; + + public Document() { + } + + public Document(Integer id, String name, Person person, Corporation corporation) { + this.id = id; + this.name = name; + owner = new Owner(person, corporation); + } + + public Integer getId() { + return id; + } + + public Owner getOwner() { + return owner; + } + } + + @Embeddable + public static class Owner { + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "owner") + private Person person; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "owner_corporation") + private Corporation corporation; + + @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST) + @JoinColumns({ + @JoinColumn(name = "owner_corporation", referencedColumnName = "id_corporation", insertable = false, updatable = false), + @JoinColumn(name = "owner", referencedColumnName = "id_person", insertable = false, updatable = false), + }) + private CorporationUser corporationUser; + + public Owner() { + } + + public Owner(Person person, Corporation corporation) { + this.person = person; + this.corporation = corporation; + this.corporationUser = new CorporationUser(person, corporation) ; + } + + public Person getPerson() { + return person; + } + + public Corporation getCorporation() { + return corporation; + } + + public CorporationUser getCorporationUser() { + return corporationUser; + } + } + + @Entity(name = "Corporation") + @Table(name = "corporation") + public static class Corporation { + @Id + private String id; + + private String corporateName; + + public Corporation() { + } + + public Corporation(String id, String corporateName) { + this.id = id; + this.corporateName = corporateName; + } + + public String getId() { + return id; + } + + public String getCorporateName() { + return corporateName; + } + } + + @Entity(name = "CorporationUser") + @Table(name = "corporation_person_xref") + public static class CorporationUser { + @Id + @ManyToOne + @JoinColumn(name = "id_person", updatable = false) + private Person person; + + @Id + @ManyToOne + @JoinColumn(name = "id_corporation", updatable = false) + private Corporation corporation; + + public CorporationUser() { + } + + public CorporationUser(Person person, Corporation corporation) { + this.person = person; + this.corporation = corporation; + } + + public Person getPerson() { + return person; + } + + public Corporation getCorporation() { + return corporation; + } + } + + @Entity(name = "Person") + @Table(name = "person") + public static class Person { + @Id + private String id; + + private String surname; + + public Person() { + } + + public Person(String id, String surname) { + this.id = id; + this.surname = surname; + } + + public String getId() { + return id; + } + + public String getSurname() { + return surname; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/query/CorrelationCteDerivedJoinTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/query/CorrelationCteDerivedJoinTest.java new file mode 100644 index 000000000000..0d0e3c40d0a5 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/query/CorrelationCteDerivedJoinTest.java @@ -0,0 +1,55 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.jpa.query; + +import org.hibernate.testing.orm.domain.StandardDomainModel; +import org.hibernate.testing.orm.junit.*; +import org.junit.jupiter.api.Test; + +@SessionFactory +@DomainModel(standardModels = StandardDomainModel.GAMBIT) +@JiraKey("HHH-17522") +@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsSubqueryInOnClause.class) +@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsOrderByInCorrelatedSubquery.class) +public class CorrelationCteDerivedJoinTest { + + @Test + public void tesDerivedJoin(SessionFactoryScope scope) { + scope.inTransaction(s -> { + s.createSelectionQuery("select corrSub.id " + + "from EntityOfBasics e " + + "left join ( " + + " select 1 as id " + + " from EntityOfBasics eSub " + + ") sub on true " + + "left join lateral ( " + + " select 1 as id " + + " from EntityOfBasics eSub " + + " where eSub.id = sub.id " + + ") corrSub on true", Integer.class ).getResultList(); + }); + } + + @Test + public void tesCte(SessionFactoryScope scope) { + scope.inTransaction(s -> { + s.createSelectionQuery("with mycte as ( " + + " select 1 as id " + + " from EntityOfBasics eSub " + + ") " + + "select corrSub.id " + + "from EntityOfBasics e " + + "left join mycte sub on true " + + "left join lateral ( " + + " select 1 as id " + + " from EntityOfBasics eSub " + + " where eSub.id = sub.id " + + ") corrSub on true", Integer.class ).getResultList(); + }); + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/query/CorrelationCteDerivedRootTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/query/CorrelationCteDerivedRootTest.java new file mode 100644 index 000000000000..8651ab42a97c --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/query/CorrelationCteDerivedRootTest.java @@ -0,0 +1,53 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.jpa.query; + +import org.hibernate.testing.orm.domain.StandardDomainModel; +import org.hibernate.testing.orm.junit.*; +import org.junit.jupiter.api.Test; + +@SessionFactory +@DomainModel(standardModels = StandardDomainModel.GAMBIT) +@JiraKey("HHH-17522") +@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsSubqueryInOnClause.class) +@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsOrderByInCorrelatedSubquery.class) +public class CorrelationCteDerivedRootTest { + + @Test + public void tesDerivedRoot(SessionFactoryScope scope) { + scope.inTransaction(s -> { + s.createSelectionQuery("select corrSub.id " + + "from ( " + + " select 1 as id " + + " from EntityOfBasics eSub " + + ") sub " + + "left join lateral ( " + + " select 1 as id " + + " from EntityOfBasics eSub " + + " where eSub.id = sub.id " + + ") corrSub on true", Integer.class ).getResultList(); + }); + } + + @Test + public void tesCte(SessionFactoryScope scope) { + scope.inTransaction(s -> { + s.createSelectionQuery("with mycte as ( " + + " select 1 as id " + + " from EntityOfBasics eSub " + + ") " + + "select corrSub.id " + + "from mycte sub " + + "left join lateral ( " + + " select 1 as id " + + " from EntityOfBasics eSub " + + " where eSub.id = sub.id " + + ") corrSub on true", Integer.class ).getResultList(); + }); + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/query/DateTimeParameterTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/query/DateTimeParameterTest.java index e3a523d0039b..e75b668747a5 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/query/DateTimeParameterTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/query/DateTimeParameterTest.java @@ -6,100 +6,118 @@ */ package org.hibernate.orm.test.jpa.query; -import java.util.Date; -import java.util.GregorianCalendar; -import java.util.List; import jakarta.persistence.Entity; -import jakarta.persistence.EntityManager; import jakarta.persistence.Id; +import jakarta.persistence.Parameter; import jakarta.persistence.Query; import jakarta.persistence.Table; import jakarta.persistence.Temporal; import jakarta.persistence.TemporalType; +import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.Jpa; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; -import org.hibernate.orm.test.jpa.BaseEntityManagerFunctionalTestCase; - -import org.junit.Test; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.List; -import static org.junit.Assert.assertEquals; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; /** * @author Steve Ebersole */ -public class DateTimeParameterTest extends BaseEntityManagerFunctionalTestCase { +@Jpa(annotatedClasses = {DateTimeParameterTest.Thing.class}) +public class DateTimeParameterTest { private static GregorianCalendar nowCal = new GregorianCalendar(); private static Date now = new Date( nowCal.getTime().getTime() ); - @Override - protected Class[] getAnnotatedClasses() { - return new Class[] { Thing.class }; - } - @Test - public void testBindingCalendarAsDate() { - createTestData(); - - EntityManager em = getOrCreateEntityManager(); - em.getTransaction().begin(); - - try { - Query query = em.createQuery( "from Thing t where t.someDate = :aDate" ); + public void testBindingCalendarAsDate(EntityManagerFactoryScope scope) { + scope.inTransaction( entityManager -> { + final Query query = entityManager.createQuery( "from Thing t where t.someDate = :aDate" ); query.setParameter( "aDate", nowCal, TemporalType.DATE ); - List list = query.getResultList(); - assertEquals( 1, list.size() ); - } - finally { - em.getTransaction().rollback(); - em.close(); - } - - deleteTestData(); + final List list = query.getResultList(); + assertThat( list.size() ).isEqualTo( 1 ); + } ); } @Test - public void testBindingNulls() { - EntityManager em = getOrCreateEntityManager(); - em.getTransaction().begin(); - - try { - Query query = em.createQuery( "from Thing t where t.someDate = :aDate or t.someTime = :aTime or t.someTimestamp = :aTimestamp" ); + public void testBindingNulls(EntityManagerFactoryScope scope) { + scope.inTransaction( entityManager -> { + final Query query = entityManager.createQuery( + "from Thing t where t.someDate = :aDate or t.someTime = :aTime or t.someTimestamp = :aTimestamp" + ); query.setParameter( "aDate", (Date) null, TemporalType.DATE ); query.setParameter( "aTime", (Date) null, TemporalType.DATE ); query.setParameter( "aTimestamp", (Date) null, TemporalType.DATE ); - } - finally { - em.getTransaction().rollback(); - em.close(); - } + } ); + + } + + @Test + @Jira("https://hibernate.atlassian.net/browse/HHH-17151") + public void testBindingNullNativeQueryPositional(EntityManagerFactoryScope scope) { + scope.inTransaction( entityManager -> { + final Query query = entityManager.createNativeQuery( "update Thing set someDate = ?1 where id = 1" ); + //noinspection deprecation + query.setParameter( 1, (Date) null, TemporalType.DATE ); + assertThat( query.executeUpdate() ).isEqualTo( 1 ); + } ); + scope.inTransaction( entityManager -> assertThat( entityManager.find( Thing.class, 1 ).someDate ).isNull() ); + } + + @Test + @Jira("https://hibernate.atlassian.net/browse/HHH-17151") + public void testBindingNullNativeQueryNamed(EntityManagerFactoryScope scope) { + scope.inTransaction( entityManager -> { + final Query query = entityManager.createNativeQuery( "update Thing set someDate = :me where id = 1" ); + Parameter p = new Parameter<>() { + @Override + public String getName() { + return "me"; + } + + @Override + public Integer getPosition() { + return null; + } + + @Override + public Class getParameterType() { + return Date.class; + } + }; + //noinspection deprecation + query.setParameter( p, null, TemporalType.DATE ); + assertThat( query.executeUpdate() ).isEqualTo( 1 ); + } ); + scope.inTransaction( entityManager -> assertThat( entityManager.find( Thing.class, 1 ).someDate ).isNull() ); } - private void createTestData() { - EntityManager em = getOrCreateEntityManager(); - em.getTransaction().begin(); - em.persist( new Thing( 1, "test", now, now, now ) ); - em.getTransaction().commit(); - em.close(); + @BeforeEach + public void createTestData(EntityManagerFactoryScope scope) { + scope.inTransaction( entityManager -> entityManager.persist( new Thing( 1, "test", now, now, now ) ) ); } - private void deleteTestData() { - EntityManager em = getOrCreateEntityManager(); - em.getTransaction().begin(); - em.createQuery( "delete Thing" ).executeUpdate(); - em.getTransaction().commit(); - em.close(); + @AfterEach + public void deleteTestData(EntityManagerFactoryScope scope) { + scope.inTransaction( entityManager -> entityManager.createQuery( "delete from Thing" ).executeUpdate() ); } - @Entity( name="Thing" ) - @Table( name = "THING" ) + @Entity(name = "Thing") + @Table(name = "Thing") public static class Thing { @Id public Integer id; public String someString; - @Temporal( TemporalType.DATE ) + @Temporal(TemporalType.DATE) public Date someDate; - @Temporal( TemporalType.TIME ) + @Temporal(TemporalType.TIME) public Date someTime; - @Temporal( TemporalType.TIMESTAMP ) + @Temporal(TemporalType.TIMESTAMP) public Date someTimestamp; public Thing() { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/query/IdClassRepeatedQueryTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/query/IdClassRepeatedQueryTest.java new file mode 100644 index 000000000000..b168cbd2af0d --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/query/IdClassRepeatedQueryTest.java @@ -0,0 +1,224 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.jpa.query; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Embeddable; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.IdClass; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.JoinColumns; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.Jpa; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +@Jpa( + annotatedClasses = { + IdClassRepeatedQueryTest.Corporation.class, + IdClassRepeatedQueryTest.CorporationUser.class, + IdClassRepeatedQueryTest.Document.class, + IdClassRepeatedQueryTest.Person.class, + } +) +@JiraKey( "HHH-19031" ) +public class IdClassRepeatedQueryTest { + + @BeforeAll + public void setup(EntityManagerFactoryScope scope) { + scope.inTransaction( + entityManager -> { + Person person = new Person("1", "Andrew"); + Corporation corporation = new Corporation("2", "ibm"); + Document document = new Document(3,"registration", person, corporation); + entityManager.persist( person ); + entityManager.persist( corporation ); + entityManager.persist( document ); + } + ); + } + + @Test + public void testRepeatedQuery(EntityManagerFactoryScope scope){ + scope.inTransaction( + entityManager -> { + String query = "SELECT d FROM Document d"; + + List resultList = entityManager.createQuery( query, Document.class ).getResultList(); + assertThat(resultList.size()).isEqualTo( 1 ); + Document document = resultList.get( 0 ); + + resultList = entityManager.createQuery( query, Document.class ).getResultList(); + assertThat(resultList.size()).isEqualTo( 1 ); + assertThat( resultList.get( 0 ) ).isSameAs( document ); + } + ); + } + + @Entity(name = "Document") + @Table(name = "documents") + public static class Document { + @Id + private Integer id; + + @Embedded + private Owner owner; + + private String name; + + public Document() { + } + + public Document(Integer id, String name, Person person, Corporation corporation) { + this.id = id; + this.name = name; + owner = new Owner(person, corporation); + } + + public Integer getId() { + return id; + } + + public Owner getOwner() { + return owner; + } + } + + @Embeddable + public static class Owner { + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "owner") + private Person person; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "owner_corporation") + private Corporation corporation; + + @ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.PERSIST) + @JoinColumns({ + @JoinColumn(name = "owner_corporation", referencedColumnName = "id_corporation", insertable = false, updatable = false), + @JoinColumn(name = "owner", referencedColumnName = "id_person", insertable = false, updatable = false), + }) + private CorporationUser corporationUser; + + public Owner() { + } + + public Owner(Person person, Corporation corporation) { + this.person = person; + this.corporation = corporation; + this.corporationUser = new CorporationUser(person, corporation) ; + } + + public Person getPerson() { + return person; + } + + public Corporation getCorporation() { + return corporation; + } + + public CorporationUser getCorporationUser() { + return corporationUser; + } + } + + @Entity(name = "Corporation") + @Table(name = "corporation") + public static class Corporation { + @Id + private String id; + + private String corporateName; + + public Corporation() { + } + + public Corporation(String id, String corporateName) { + this.id = id; + this.corporateName = corporateName; + } + + public String getId() { + return id; + } + + public String getCorporateName() { + return corporateName; + } + } + + @Entity(name = "CorporationUser") + @Table(name = "corporation_person_xref") + @IdClass(CorporationUser.CorporationUserPK.class) + public static class CorporationUser { + @Id + @ManyToOne + @JoinColumn(name = "id_person", updatable = false) + private Person person; + + @Id + @ManyToOne + @JoinColumn(name = "id_corporation", updatable = false) + private Corporation corporation; + + public CorporationUser() { + } + + public CorporationUser(Person person, Corporation corporation) { + this.person = person; + this.corporation = corporation; + } + + public Person getPerson() { + return person; + } + + public Corporation getCorporation() { + return corporation; + } + + public static class CorporationUserPK { + public String person; + + public String corporation; + } + } + + @Entity(name = "Person") + @Table(name = "person") + public static class Person { + @Id + private String id; + + private String surname; + + public Person() { + } + + public Person(String id, String surname) { + this.id = id; + this.surname = surname; + } + + public String getId() { + return id; + } + + public String getSurname() { + return surname; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/query/NativeQueryJoinTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/query/NativeQueryJoinTest.java new file mode 100644 index 000000000000..42e293052825 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/query/NativeQueryJoinTest.java @@ -0,0 +1,166 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.jpa.query; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.math.BigDecimal; +import java.util.List; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +@DomainModel( + annotatedClasses = { + NativeQueryJoinTest.Order.class, + NativeQueryJoinTest.OrderInfo.class, + } +) +@SessionFactory +@JiraKey("HHH-19524") +public class NativeQueryJoinTest { + + private static final long ORDER_ID = 1L; + private static final long ORDER_INFO_ID = 2L; + private static final String ORDER_INFO_DESCRIPTION = "first order"; + + @BeforeEach + public void setup(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + OrderInfo additionalInfo = new OrderInfo( + new BigDecimal( ORDER_INFO_ID ), + ORDER_INFO_DESCRIPTION + ); + Order order = new Order( ORDER_ID, 1L, additionalInfo ); + session.persist( order ); + } + ); + } + + @AfterEach + public void teardown(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } + + @Test + public void testFind(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + Order order = session.find( Order.class, ORDER_ID ); + OrderInfo additionalInfo = order.getOrderInfo(); + assertThat( additionalInfo.getDescription() ).isEqualTo( ORDER_INFO_DESCRIPTION ); + } ); + } + + @Test + public void testQuery(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + List ids = session.createQuery("select o.orderId from Order o", Long.class ).list(); + session.createMutationQuery( "update Order set description = :des" ).setParameter( "des", "abc" ).executeUpdate(); + } ); + } + + @Test + public void testNativeQuery(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + String query = "select o.* from ORDER_TABLE o where o.ORDER_ID = ?1"; + List orders = session.createNativeQuery( query, Order.class ) + .setParameter( 1, ORDER_ID ) + .list(); + Order order = orders.get( 0 ); + OrderInfo additionalInfo = order.getOrderInfo(); + assertThat( additionalInfo.getDescription() ).isEqualTo( ORDER_INFO_DESCRIPTION ); + } ); + } + + @Table(name = "ORDER_TABLE") + @Entity(name = "Order") + public static class Order { + + @Id + @Column(name = "ORDER_ID") + private Long orderId; + + private long orderNumber; + + private String description; + + @OneToOne(mappedBy = "order", fetch = FetchType.LAZY, cascade = CascadeType.PERSIST) + private OrderInfo orderInfo; + + public Order() { + } + + public Long getOrderId() { + return orderId; + } + + public long getOrderNumber() { + return orderNumber; + } + + public OrderInfo getOrderInfo() { + return orderInfo; + } + + public Order(Long orderId, long orderNumber, OrderInfo additionalInfo) { + this.orderId = orderId; + this.orderNumber = orderNumber; + this.orderInfo = additionalInfo; + additionalInfo.order = this; + } + } + + @Entity(name = "OrderInfo") + @Table(name = "ORDER_INFO_TABLE") + public static class OrderInfo { + + @Id + @Column(name = "ORDER_INFO_ID") + private BigDecimal orderInfoId; + + private String description; + + @OneToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "ORDER_ID") + private Order order; + + public OrderInfo() { + } + + public OrderInfo(BigDecimal orderInfoId, String description) { + this.orderInfoId = orderInfoId; + this.description = description; + } + + public BigDecimal getOrderInfoId() { + return orderInfoId; + } + + public String getDescription() { + return description; + } + + public Order getOrder() { + return order; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/txn/JtaObtainConnectionExceptionTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/txn/JtaObtainConnectionExceptionTest.java new file mode 100644 index 000000000000..f909dba75995 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/jpa/txn/JtaObtainConnectionExceptionTest.java @@ -0,0 +1,113 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.jpa.txn; + +import jakarta.transaction.HeuristicMixedException; +import jakarta.transaction.HeuristicRollbackException; +import jakarta.transaction.InvalidTransactionException; +import jakarta.transaction.NotSupportedException; +import jakarta.transaction.RollbackException; +import jakarta.transaction.SystemException; +import jakarta.transaction.Transaction; +import jakarta.transaction.TransactionManager; +import org.hibernate.HibernateException; +import org.hibernate.engine.jdbc.connections.spi.JdbcConnectionAccess; +import org.hibernate.jdbc.AbstractReturningWork; +import org.hibernate.resource.transaction.backend.jta.internal.JtaIsolationDelegate; +import org.hibernate.testing.orm.junit.JiraKey; +import org.junit.jupiter.api.Test; + +import java.sql.Connection; +import java.sql.SQLException; + +import static org.junit.jupiter.api.Assertions.assertThrowsExactly; + +@JiraKey("HHH-18961") +public class JtaObtainConnectionExceptionTest { + + @Test + public void testIt() { + final JtaIsolationDelegate isolationDelegate = new JtaIsolationDelegate( + new ObtainConnectionSqlExceptionConnectionAccess(), + null, + new TransactionManagerImpl() ); + + assertThrowsExactly( HibernateException.class, () -> + isolationDelegate.delegateWork( + new AbstractReturningWork<>() { + @Override + public Object execute(Connection connection) { + return null; + } + }, + false + ) + ); + } + + public static class ObtainConnectionSqlExceptionConnectionAccess implements JdbcConnectionAccess { + @Override + public Connection obtainConnection() throws SQLException { + throw new SQLException( "No connection" ); + } + + @Override + public void releaseConnection(Connection connection) throws SQLException { + + } + + @Override + public boolean supportsAggressiveRelease() { + return false; + } + } + + public static class TransactionManagerImpl implements TransactionManager { + @Override + public void begin() throws NotSupportedException, SystemException { + + } + + @Override + public void commit() + throws RollbackException, HeuristicMixedException, HeuristicRollbackException, SecurityException, IllegalStateException, SystemException { + } + + @Override + public int getStatus() throws SystemException { + return 0; + } + + @Override + public Transaction getTransaction() throws SystemException { + return null; + } + + @Override + public void resume(Transaction transaction) + throws InvalidTransactionException, IllegalStateException, SystemException { + } + + @Override + public void rollback() throws IllegalStateException, SecurityException, SystemException { + + } + + @Override + public void setRollbackOnly() throws IllegalStateException, SystemException { + + } + + @Override + public void setTransactionTimeout(int i) throws SystemException { + + } + + @Override + public Transaction suspend() throws SystemException { + return null; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/lazyonetoone/LazyOneToOneWithEntityGraphTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/lazyonetoone/LazyOneToOneWithEntityGraphTest.java new file mode 100644 index 000000000000..817ab91217c7 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/lazyonetoone/LazyOneToOneWithEntityGraphTest.java @@ -0,0 +1,117 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.lazyonetoone; + +import java.util.List; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.NamedAttributeNode; +import jakarta.persistence.NamedEntityGraph; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OneToOne; +import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.hibernate.Hibernate.isInitialized; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@DomainModel( + annotatedClasses = { + LazyOneToOneWithEntityGraphTest.Company.class, + LazyOneToOneWithEntityGraphTest.Employee.class, + LazyOneToOneWithEntityGraphTest.Project.class + } +) +@SessionFactory +@BytecodeEnhanced(runNotEnhancedAsWell = true) +public class LazyOneToOneWithEntityGraphTest { + @BeforeAll + void setUp(SessionFactoryScope scope) { + scope.inTransaction(session -> { + // Create company + Company company = new Company(); + company.id = 1L; + company.name = "Hibernate"; + session.persist(company); + + // Create project + Project project = new Project(); + project.id = 1L; + session.persist(project); + + // Create employee + Employee employee = new Employee(); + employee.id = 1L; + employee.company = company; + employee.projects = List.of(project); + session.persist(employee); + }); + } + + @AfterAll + void tearDown(SessionFactoryScope scope) { + scope.inTransaction(session -> { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + }); + } + + + @Test + void reproducerTest(SessionFactoryScope scope) { + scope.inTransaction(session -> { + // Load employee using entity graph + Employee employee = session.createQuery( + "select e from Employee e where e.id = :id", Employee.class) + .setParameter("id", 1L) + .setHint("javax.persistence.fetchgraph", session.getEntityGraph("employee.projects")) + .getSingleResult(); + + assertTrue(isInitialized(employee.projects)); + assertEquals("Hibernate", employee.company.name); + }); + } + + @Entity(name = "Company") + public static class Company { + @Id + private Long id; + + private String name; + } + + @Entity(name = "Employee") + @NamedEntityGraph( + name = "employee.projects", + attributeNodes = @NamedAttributeNode("projects") + ) + public static class Employee { + @Id + private Long id; + + @OneToOne + @JoinColumn(name = "company_name", referencedColumnName = "name") + private Company company; + + @OneToMany(fetch = FetchType.LAZY) + private List projects; + } + + @Entity(name = "Project") + public static class Project { + @Id + private Long id; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/length/LengthTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/length/LengthTest.java index d1aba0d93c2c..bc1178e65fd9 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/length/LengthTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/length/LengthTest.java @@ -1,6 +1,7 @@ package org.hibernate.orm.test.length; import org.hibernate.Length; +import org.hibernate.community.dialect.InformixDialect; import org.hibernate.dialect.Dialect; import org.hibernate.metamodel.mapping.BasicValuedMapping; import org.hibernate.type.SqlTypes; @@ -8,12 +9,14 @@ import org.hibernate.testing.orm.junit.DomainModel; import org.hibernate.testing.orm.junit.SessionFactory; import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.SkipForDialect; import org.junit.jupiter.api.Test; import static org.junit.Assert.assertEquals; @SessionFactory @DomainModel(annotatedClasses = {WithLongStrings.class,WithLongTypeStrings.class}) +@SkipForDialect(dialectClass = InformixDialect.class, reason = "Informix rowsize to exceed the allowable limit (32767).") public class LengthTest { @Test public void testLength(SessionFactoryScope scope) { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/lob/InflaterInputStreamBlobTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/lob/InflaterInputStreamBlobTest.java new file mode 100644 index 000000000000..aaa95e06b00d --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/lob/InflaterInputStreamBlobTest.java @@ -0,0 +1,119 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.lob; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Lob; +import org.hibernate.engine.jdbc.NonContextualLobCreator; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Test; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.sql.Blob; +import java.sql.SQLException; +import java.util.Random; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; + +@DomainModel(annotatedClasses = InflaterInputStreamBlobTest.TestEntity.class) +@SessionFactory +@JiraKey("HHH-19464") +class InflaterInputStreamBlobTest { + + private static final int RANDOM_SIZE = 32000; + + @Test + void hibernate_blob_streaming(SessionFactoryScope scope) throws Exception { + final var randomBytes = getRandomBytes(); + final var outputStream = new ByteArrayOutputStream(); + try (var zipOutputStream = new GZIPOutputStream( outputStream )) { + zipOutputStream.write( randomBytes ); + } + + long size = randomBytes.length; + scope.inTransaction( entityManager -> { + try { + InputStream is = new GZIPInputStream( new ByteArrayInputStream( outputStream.toByteArray() ) ); + Blob blob = NonContextualLobCreator.INSTANCE.wrap( + NonContextualLobCreator.INSTANCE.createBlob( is, size ) + ); + TestEntity e = new TestEntity(); + e.setId( 1L ); + e.setData( blob ); + + entityManager.persist( e ); + } + catch (IOException e) { + throw new RuntimeException( e ); + } + } + ); + + scope.inStatelessSession( session -> { + final var entity = session.get( TestEntity.class, 1L ); + try { + final var blob = entity.getData(); + assertEquals( size, blob.length() ); + assertArrayEquals( randomBytes, blob.getBytes( 1L, (int) blob.length() ) ); + } + catch (SQLException e) { + throw new RuntimeException( e ); + } + } ); + } + + private static byte[] getRandomBytes() { + final var bytes = new byte[RANDOM_SIZE]; + new Random().nextBytes( bytes ); + return bytes; + } + + @Entity + public static class TestEntity { + + @Id + Long id; + + @Lob + Blob data; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Blob getData() { + return data; + } + + public InputStream getInputStream() { + try { + return data.getBinaryStream(); + } + catch (SQLException e) { + throw new IllegalArgumentException( "Could not obtain requested input stream", e ); + } + } + + public void setData(Blob data) { + this.data = data; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/locking/OptimisticAndPessimisticLockTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/locking/OptimisticAndPessimisticLockTest.java index 62763467a42e..65c7c3a61036 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/locking/OptimisticAndPessimisticLockTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/locking/OptimisticAndPessimisticLockTest.java @@ -6,7 +6,7 @@ import org.hibernate.LockMode; import org.hibernate.dialect.CockroachDialect; - +import org.hibernate.dialect.MariaDBDialect; import org.hibernate.testing.orm.junit.DomainModel; import org.hibernate.testing.orm.junit.JiraKey; import org.hibernate.testing.orm.junit.SessionFactory; @@ -26,6 +26,7 @@ @SessionFactory @JiraKey("HHH-16461") @SkipForDialect(dialectClass = CockroachDialect.class, reason = "CockroachDB uses SERIALIZABLE isolation, and does not support this") +@SkipForDialect(dialectClass = MariaDBDialect.class, majorVersion = 11, minorVersion = 6, microVersion = 2, reason = "MariaDB will throw an error DB_RECORD_CHANGED when acquiring a lock on a record that have changed") public class OptimisticAndPessimisticLockTest { public Stream pessimisticLockModes() { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/locking/OptimisticLockTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/locking/OptimisticLockTest.java index 006865240519..d81da65aa971 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/locking/OptimisticLockTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/locking/OptimisticLockTest.java @@ -12,8 +12,9 @@ import jakarta.persistence.Version; import org.hibernate.annotations.OptimisticLock; import org.hibernate.dialect.CockroachDialect; +import org.hibernate.dialect.MariaDBDialect; import org.hibernate.orm.test.jpa.BaseEntityManagerFunctionalTestCase; -import org.hibernate.testing.SkipForDialect; +import org.hibernate.testing.orm.junit.SkipForDialect; import org.junit.Test; import static org.hibernate.testing.transaction.TransactionUtil.doInJPA; @@ -31,7 +32,8 @@ protected Class[] getAnnotatedClasses() { } @Test - @SkipForDialect(value = CockroachDialect.class, comment = "Fails at SERIALIZABLE isolation") + @SkipForDialect(dialectClass = CockroachDialect.class, reason = "Fails at SERIALIZABLE isolation") + @SkipForDialect(dialectClass = MariaDBDialect.class, majorVersion = 11, minorVersion = 6, microVersion = 2, reason = "MariaDB will throw an error DB_RECORD_CHANGED when acquiring a lock on a record that have changed") public void test() { doInJPA(this::entityManagerFactory, entityManager -> { Phone phone = new Phone(); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/manytomany/generic/ManyToManyGenericTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/manytomany/generic/ManyToManyGenericTest.java new file mode 100644 index 000000000000..d5385faf4d09 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/manytomany/generic/ManyToManyGenericTest.java @@ -0,0 +1,98 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.orm.test.manytomany.generic; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.JoinTable; +import jakarta.persistence.ManyToMany; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; +import org.hibernate.testing.orm.junit.Jpa; +import org.junit.jupiter.api.Test; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +@Jpa( + annotatedClasses = { + ManyToManyGenericTest.NodeTree.class, + ManyToManyGenericTest.GenericNode.class + } +) +public class ManyToManyGenericTest { + + @Test + void testSelfReferencingGeneric(final EntityManagerFactoryScope scope) { + final UUID treeId = scope.fromTransaction(em -> { + final NodeTree tree = new NodeTree(); + final GenericNode root = new GenericNode<>(); + root.tree = tree; + final GenericNode branch = new GenericNode<>(); + branch.tree = tree; + tree.nodes.add(root); + tree.nodes.add(branch); + root.children.add(branch); + em.persist(tree); + return tree.id; + }); + + final NodeTree nodeTree = scope.fromEntityManager(em -> em.find(NodeTree.class, treeId)); + + assertThat(nodeTree, is(notNullValue())); + assertThat(nodeTree.id, is(treeId)); + assertThat(nodeTree.nodes, iterableWithSize(2)); + assertThat(nodeTree.nodes, containsInAnyOrder(List.of( + hasProperty("children", iterableWithSize(1)), + hasProperty("children", emptyIterable()) + ))); + } + + @Entity(name = "tree") + public static class NodeTree { + @Id + @GeneratedValue(strategy = GenerationType.UUID) + public UUID id; + + @OneToMany(mappedBy = "tree", fetch = FetchType.EAGER, cascade = CascadeType.ALL) + public Set> nodes = new HashSet<>(); + } + + @Entity(name = "node") + public static class GenericNode { + + @Id + @GeneratedValue(strategy = GenerationType.UUID) + public UUID id; + + @ManyToOne(optional = false) + @JoinColumn(name = "TREE_ID") + public NodeTree tree; + + @ManyToMany(fetch = FetchType.EAGER, cascade = {CascadeType.PERSIST, CascadeType.DETACH}) + @JoinTable(name = "NODE_CHILDREN", + joinColumns = {@JoinColumn(name = "TREE_ID", referencedColumnName = "TREE_ID"), @JoinColumn(name = "NODE_ID", referencedColumnName = "ID")}, + inverseJoinColumns = {@JoinColumn(name = "CHILD_ID", referencedColumnName = "ID")} + ) + private final Set> children = new HashSet<>(); + + public Set> getChildren() { + return children; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/manytomany/generic/ManyToManyNonGenericTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/manytomany/generic/ManyToManyNonGenericTest.java new file mode 100644 index 000000000000..8a10874c8b92 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/manytomany/generic/ManyToManyNonGenericTest.java @@ -0,0 +1,109 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.orm.test.manytomany.generic; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.JoinTable; +import jakarta.persistence.ManyToMany; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; +import org.hibernate.testing.orm.junit.Jpa; +import org.junit.jupiter.api.Test; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; +import java.util.stream.Collectors; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.emptyIterable; +import static org.hamcrest.Matchers.hasProperty; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.iterableWithSize; +import static org.hamcrest.Matchers.notNullValue; + +@Jpa( + annotatedClasses = { + ManyToManyNonGenericTest.NodeTree.class, + ManyToManyNonGenericTest.Node.class + } +) +public class ManyToManyNonGenericTest { + + @Test + void testSelfReferencingGeneric(final EntityManagerFactoryScope scope) { + final UUID treeId = scope.fromTransaction(em -> { + final NodeTree tree = new NodeTree(); + final Node root = new Node(); + root.tree = tree; + final Node branch = new Node(); + branch.tree = tree; + tree.nodes.add(root); + tree.nodes.add(branch); + root.children.add(branch); + em.persist(tree); + return tree.id; + }); + + final NodeTree nodeTree = scope.fromEntityManager(em -> em.find(NodeTree.class, treeId)); + + assertThat(nodeTree, is(notNullValue())); + assertThat(nodeTree.id, is(treeId)); + assertThat(nodeTree.nodes, iterableWithSize(2)); + assertThat(nodeTree.nodes, containsInAnyOrder(List.of( + hasProperty("children", iterableWithSize(1)), + hasProperty("children", emptyIterable()) + ))); + } + + @Entity(name = "tree") + public static class NodeTree { + @Id + @GeneratedValue(strategy = GenerationType.UUID) + public UUID id; + + @OneToMany(mappedBy = "tree", fetch = FetchType.EAGER, cascade = CascadeType.ALL) + public Set nodes = new HashSet<>(); + } + + @Entity(name = "node") + public static class Node { + + @Id + @GeneratedValue(strategy = GenerationType.UUID) + public UUID id; + + @ManyToOne(optional = false) + @JoinColumn(name = "TREE_ID") + public NodeTree tree; + + @ManyToMany(fetch = FetchType.EAGER, cascade = {CascadeType.PERSIST, CascadeType.DETACH}) + @JoinTable(name = "NODE_CHILDREN", + joinColumns = {@JoinColumn(name = "TREE_ID", referencedColumnName = "TREE_ID"), @JoinColumn(name = "NODE_ID", referencedColumnName = "ID")}, + inverseJoinColumns = {@JoinColumn(name = "CHILD_ID", referencedColumnName = "ID")} + ) + private final Set children = new HashSet<>(); + + public Set getChildren() { + return children; + } + + @Override + public String toString() { + return String.format("node [%s] parent of %s", id, children.stream().map(n -> n.id).collect(Collectors.toList())); + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/array/ArrayOfArraysTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/array/ArrayOfArraysTest.java new file mode 100644 index 000000000000..0f7becd61deb --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/array/ArrayOfArraysTest.java @@ -0,0 +1,98 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.mapping.array; + +import org.hibernate.MappingException; +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.cfg.Configuration; +import org.hibernate.dialect.CockroachDialect; +import org.hibernate.testing.orm.junit.SkipForDialect; +import org.hibernate.type.SqlTypes; + +import org.hibernate.testing.orm.junit.DialectFeatureChecks; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.RequiresDialectFeature; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.Setting; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import org.assertj.core.data.Index; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +/** + * @author Marco Belladelli + */ +@RequiresDialectFeature( feature = DialectFeatureChecks.SupportsStandardArrays.class ) +public class ArrayOfArraysTest { + @DomainModel( annotatedClasses = ArrayOfArraysTest.EntityWithDoubleByteArray.class ) + @SessionFactory + @ServiceRegistry( settings = @Setting( name = AvailableSettings.HBM2DDL_AUTO, value = "create-drop" ) ) + @Test + @SkipForDialect( dialectClass = CockroachDialect.class, reason = "Unable to find server array type for provided name bytes" ) + public void testDoubleByteArrayWorks(SessionFactoryScope scope) { + final Long id = scope.fromTransaction( session -> { + final EntityWithDoubleByteArray entity = new EntityWithDoubleByteArray(); + entity.setByteArray( new byte[][] { new byte[] { 1 } } ); + session.persist( entity ); + return entity.getId(); + } ); + scope.inSession( session -> { + final byte[][] byteArray = session.find( EntityWithDoubleByteArray.class, id ).getByteArray(); + assertThat( byteArray ).hasDimensions( 1, 1 ).contains( new byte[] { 1 }, Index.atIndex( 0 ) ); + } ); + } + + @Test + public void testDoubleIntegerArrayThrows() { + final Configuration cfg = new Configuration(); + cfg.addAnnotatedClass( EntityWithDoubleIntegerArray.class ); + try (final org.hibernate.SessionFactory sf = cfg.buildSessionFactory()) { + fail( "Expecting Integer[][] to trigger exception as non-byte multidimensional arrays are not supported" ); + } + catch (Exception e) { + assertThat( e ).isInstanceOf( MappingException.class ).hasMessage( "Nested arrays (with the exception of byte[][]) are not supported" ); + } + } + + @Entity( name = "EntityWithDoubleByteArray" ) + static class EntityWithDoubleByteArray { + @Id + @GeneratedValue + private Long id; + + @JdbcTypeCode( SqlTypes.ARRAY ) + private byte[][] byteArray; + + public Long getId() { + return id; + } + + public byte[][] getByteArray() { + return byteArray; + } + + public void setByteArray(byte[][] byteArray) { + this.byteArray = byteArray; + } + } + + @Entity( name = "EntityWithDoubleIntegerArray" ) + static class EntityWithDoubleIntegerArray { + @Id + @GeneratedValue + private Long id; + + @JdbcTypeCode( SqlTypes.ARRAY ) + private Integer[][] integers; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/array/MySqlArrayOfTimestampsTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/array/MySqlArrayOfTimestampsTest.java new file mode 100644 index 000000000000..bb2c636e5058 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/array/MySqlArrayOfTimestampsTest.java @@ -0,0 +1,172 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.mapping.array; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; + +import org.hibernate.dialect.MariaDBDialect; +import org.hibernate.dialect.MySQLDialect; +import org.hibernate.sql.ast.spi.SqlAppender; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.RequiresDialect; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.type.descriptor.java.JdbcTimestampJavaType; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; + +import java.sql.Timestamp; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.Month; +import java.util.Calendar; +import java.util.Date; +import java.util.TimeZone; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel(annotatedClasses = MySqlArrayOfTimestampsTest.Foo.class) +@SessionFactory +@JiraKey("HHH-18881") +class MySqlArrayOfTimestampsTest { + + private static final LocalDateTime[] dataArray = { + // Unix epoch start if you're in the UK + LocalDateTime.of( 1970, Month.JANUARY, 1, 0, 0, 0, 0 ), + // pre-Y2K + LocalDateTime.of( 1999, Month.DECEMBER, 31, 23, 59, 59, 0 ), + // We survived! Why was anyone worried? + LocalDateTime.of( 2000, Month.JANUARY, 1, 0, 0, 0, 0 ), + // Silence will fall! + LocalDateTime.of( 2010, Month.JUNE, 26, 20, 4, 0, 0 ), + // 2024 summer time + LocalDateTime.of( 2024, 6, 20, 0, 0, 0 ), + // 2023 winer time + LocalDateTime.of( 2023, 12, 22, 0, 0, 0 ) + }; + + private TimeZone currentDefault; + + @BeforeAll + void setTimeZone() { + currentDefault = TimeZone.getDefault(); + TimeZone.setDefault( TimeZone.getTimeZone( "Europe/Zagreb" ) ); + } + + @AfterAll + void restoreTimeZone() { + TimeZone.setDefault( currentDefault ); + } + + @Test + @Order(1) + @RequiresDialect(MySQLDialect.class) + @RequiresDialect(MariaDBDialect.class) + public void testLocalDateTime(SessionFactoryScope scope) { + + final Integer basicId = scope.fromTransaction( session -> { + Foo basic = new Foo(); + basic.localDateTimeArray = dataArray; + basic.localDateTimeField = dataArray[0]; + session.persist( basic ); + return basic.id; + } ); + + scope.inTransaction( session -> { + Foo found = session.find( Foo.class, basicId ); + assertThat( found.localDateTimeField ).isEqualTo( dataArray[0] ); + assertThat( found.localDateTimeArray ).isEqualTo( dataArray ); + } ); + } + + + @Test + @Order(2) + @RequiresDialect(MySQLDialect.class) + @RequiresDialect(MariaDBDialect.class) + public void testDate(SessionFactoryScope scope) { + Date[] dataArray = {Calendar.getInstance().getTime(), Calendar.getInstance().getTime()}; + + final Integer basicId = scope.fromTransaction( session -> { + Foo basic = new Foo(); + basic.dateArray = dataArray; + basic.dateField = dataArray[0]; + session.persist( basic ); + return basic.id; + } ); + + scope.inTransaction( session -> { + Foo found = session.find( Foo.class, basicId ); + assertThat( found.dateField.getTime() ).isEqualTo( dataArray[0].getTime() ); + for ( int i = 0; i < dataArray.length; i++ ) { + assertThat( found.dateArray[i].getTime() ).isEqualTo( dataArray[i].getTime() ); + } + } ); + } + + private static final LocalDateTime SUMMER = LocalDate.of( 2024, 6, 20 ).atStartOfDay(); + private static final LocalDateTime WINTER = LocalDate.of( 2023, 12, 22 ).atStartOfDay(); + private static final LocalDate EPOCH = LocalDate.of( 1970, Month.JANUARY, 1 ); + + private static final TimeZone[] TEST_TIME_ZONES = Stream.of( + "Africa/Monrovia", + "Europe/Zagreb", + "Asia/Singapore", + "Europe/Tallinn", + "Europe/Minsk", + "America/Anchorage" + ).map( TimeZone::getTimeZone ).toArray( TimeZone[]::new ); + + @Test + void encodeThenDecodeLocalDateTime() { + for ( final TimeZone zone : TEST_TIME_ZONES ) { + final TimeZone currentTimeZone = TimeZone.getDefault(); + TimeZone.setDefault( zone ); + try { + for ( LocalDateTime dateTime : dataArray ) { + final MySqlAppender appender = new MySqlAppender(); + final Timestamp expected = Timestamp.valueOf( dateTime ); + JdbcTimestampJavaType.INSTANCE.appendEncodedString( appender, expected ); + final Date actual = JdbcTimestampJavaType.INSTANCE.fromEncodedString( appender.stringBuilder, 0, + appender.stringBuilder.length() ); + Assertions.assertEquals( expected, actual ); + } + } + finally { + TimeZone.setDefault( currentTimeZone ); + } + } + } + + @Entity(name = "Foo") + public static class Foo { + @Id + @GeneratedValue + public Integer id; + public Date[] dateArray; + public LocalDateTime[] localDateTimeArray; + public Date dateField; + public LocalDateTime localDateTimeField; + } + + private static class MySqlAppender implements SqlAppender { + + private final StringBuilder stringBuilder = new StringBuilder(); + + @Override + public void appendSql(String fragment) { + stringBuilder.append( fragment ); + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/BlobAttributeQueryUpdateTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/BlobAttributeQueryUpdateTest.java new file mode 100644 index 000000000000..05afcf6cf1f8 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/BlobAttributeQueryUpdateTest.java @@ -0,0 +1,172 @@ +package org.hibernate.orm.test.mapping.basic; + +import java.nio.charset.StandardCharsets; +import java.sql.Blob; +import java.sql.SQLException; + +import org.hibernate.LobHelper; +import org.hibernate.engine.jdbc.BlobProxy; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.TypedQuery; + +import static org.assertj.core.api.AssertionsForInterfaceTypes.assertThat; + +@DomainModel( + annotatedClasses = { + BlobAttributeQueryUpdateTest.TestEntity.class + } +) +@SessionFactory +@JiraKey("HHH-18480") +public class BlobAttributeQueryUpdateTest { + + private static final byte[] INITIAL_BYTES = "initial".getBytes( StandardCharsets.UTF_8 ); + private static final byte[] UPDATED_BYTES_1 = "update1".getBytes( StandardCharsets.UTF_8 ); + private static final byte[] UPDATED_BYTES_2 = "update2".getBytes( StandardCharsets.UTF_8 ); + + @BeforeEach + public void setup(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + LobHelper lobHelper = session.getLobHelper(); + TestEntity testEntity = new TestEntity( + 1, + "test", + lobHelper.createBlob( INITIAL_BYTES ) + ); + session.persist( testEntity ); + } + ); + } + + @AfterEach + public void tearDown(SessionFactoryScope scope) { + scope.inTransaction( + session -> + session.createMutationQuery( "delete from TestEntity" ).executeUpdate() + ); + } + + @Test + public void testUpdateUsingBlobProxy(SessionFactoryScope scope) { + scope.inTransaction( session -> { + TestEntity testEntity = session.find( TestEntity.class, 1 ); + Blob blobValue1 = BlobProxy.generateProxy( UPDATED_BYTES_1 ); + testEntity.setBlobValue( blobValue1 ); + } ); + + scope.inTransaction( session -> { + TestEntity testEntity = session.find( TestEntity.class, 1 ); + checkBlobValue( testEntity, UPDATED_BYTES_1 ); + } ); + + scope.inTransaction( + session -> { + TypedQuery query = session.createQuery( + "UPDATE TestEntity b SET b.blobValue = :blobValue WHERE b.id = :id", + null + ); + query.setParameter( "id", 1 ); + Blob value = BlobProxy.generateProxy( UPDATED_BYTES_2 ); + query.setParameter( "blobValue", value ); + query.executeUpdate(); + } + ); + + scope.inTransaction( + session -> { + TestEntity testEntity = session.find( TestEntity.class, 1 ); + checkBlobValue( testEntity, UPDATED_BYTES_2 ); + } + ); + } + + @Test + public void testUpdateUsingLobHelper(SessionFactoryScope scope) { + scope.inTransaction( session -> { + LobHelper lobHelper = session.getLobHelper(); + TestEntity testEntity = session.find( TestEntity.class, 1 ); + Blob blobValue1 = lobHelper.createBlob( UPDATED_BYTES_1 ); + testEntity.setBlobValue( blobValue1 ); + } ); + + scope.inTransaction( session -> { + TestEntity testEntity = session.find( TestEntity.class, 1 ); + checkBlobValue( testEntity, UPDATED_BYTES_1 ); + } ); + + + scope.inTransaction( + session -> { + LobHelper lobHelper = session.getLobHelper(); + TypedQuery query = session.createQuery( + "UPDATE TestEntity b SET b.blobValue = :blobValue WHERE b.id = :id", + null + ); + query.setParameter( "id", 1 ); + Blob value = lobHelper.createBlob( UPDATED_BYTES_2 ); + query.setParameter( "blobValue", value ); + query.executeUpdate(); + } + ); + + scope.inTransaction( + session -> { + TestEntity testEntity = session.find( TestEntity.class, 1 ); + checkBlobValue( testEntity, UPDATED_BYTES_2 ); + } + ); + } + + private static void checkBlobValue(TestEntity testEntity, byte[] expectedValue) { + try { + assertThat( new String( testEntity.getBlobValue() + .getBytes( 1, (int) testEntity.getBlobValue().length() ) ) ) + .isEqualTo( new String( expectedValue ) ); + } + catch (SQLException e) { + throw new RuntimeException( e ); + } + } + + @Entity(name = "TestEntity") + public static class TestEntity { + @Id + private Integer id; + + private String name; + + private Blob blobValue; + + public TestEntity() { + } + + public TestEntity(Integer id, String name, Blob blobValue) { + this.id = id; + this.name = name; + this.blobValue = blobValue; + } + + public Integer getId() { + return id; + } + + public Blob getBlobValue() { + return blobValue; + } + + public void setBlobValue(Blob blobValue) { + this.blobValue = blobValue; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/ByteArrayMappingTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/ByteArrayMappingTests.java index e6f7aa3dbc6e..ccd85cba4158 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/ByteArrayMappingTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/ByteArrayMappingTests.java @@ -65,7 +65,7 @@ public void verifyMappings(SessionFactoryScope scope) { } { - final BasicAttributeMapping primitive = (BasicAttributeMapping) entityDescriptor.findAttributeMapping("wrapper"); + final BasicAttributeMapping primitive = (BasicAttributeMapping) entityDescriptor.findAttributeMapping("boxed"); final JdbcMapping jdbcMapping = primitive.getJdbcMapping(); assertThat(jdbcMapping.getJavaTypeDescriptor().getJavaTypeClass(), equalTo(Byte[].class)); if ( dialect.supportsStandardArrays() ) { @@ -134,7 +134,7 @@ public static class EntityOfByteArrays { //tag::basic-bytearray-example[] // mapped as VARBINARY private byte[] primitive; - private Byte[] wrapper; + private Byte[] boxed; @JavaType( ByteArrayJavaType.class ) private Byte[] wrapperOld; @@ -151,7 +151,7 @@ public EntityOfByteArrays() { public EntityOfByteArrays(Integer id, byte[] primitive, Byte[] wrapper) { this.id = id; this.primitive = primitive; - this.wrapper = wrapper; + this.boxed = wrapper; this.primitiveLob = primitive; this.wrapperLob = wrapper; } @@ -159,7 +159,7 @@ public EntityOfByteArrays(Integer id, byte[] primitive, Byte[] wrapper) { public EntityOfByteArrays(Integer id, byte[] primitive, Byte[] wrapper, byte[] primitiveLob, Byte[] wrapperLob) { this.id = id; this.primitive = primitive; - this.wrapper = wrapper; + this.boxed = wrapper; this.primitiveLob = primitiveLob; this.wrapperLob = wrapperLob; } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/JsonMappingTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/JsonMappingTests.java index f3187767f214..6195e5556d1d 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/JsonMappingTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/basic/JsonMappingTests.java @@ -12,25 +12,24 @@ import java.util.List; import java.util.Map; -import com.fasterxml.jackson.databind.JsonNode; -import jakarta.json.JsonValue; import org.hibernate.annotations.JdbcTypeCode; import org.hibernate.cfg.AvailableSettings; import org.hibernate.community.dialect.AltibaseDialect; import org.hibernate.dialect.AbstractHANADialect; import org.hibernate.dialect.DerbyDialect; import org.hibernate.dialect.OracleDialect; -import org.hibernate.dialect.PostgreSQLDialect; import org.hibernate.dialect.SybaseDialect; import org.hibernate.metamodel.mapping.internal.BasicAttributeMapping; import org.hibernate.metamodel.spi.MappingMetamodelImplementor; import org.hibernate.persister.entity.EntityPersister; -import org.hibernate.testing.orm.junit.RequiresDialect; +import org.hibernate.query.MutationQuery; +import org.hibernate.query.criteria.HibernateCriteriaBuilder; import org.hibernate.type.SqlTypes; import org.hibernate.type.descriptor.jdbc.JdbcType; import org.hibernate.type.descriptor.jdbc.spi.JdbcTypeRegistry; import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; import org.hibernate.testing.orm.junit.JiraKey; import org.hibernate.testing.orm.junit.ServiceRegistry; import org.hibernate.testing.orm.junit.SessionFactory; @@ -41,15 +40,19 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import com.fasterxml.jackson.databind.JsonNode; +import jakarta.json.JsonValue; import jakarta.persistence.Entity; import jakarta.persistence.Id; import jakarta.persistence.Table; +import jakarta.persistence.criteria.CriteriaUpdate; +import jakarta.persistence.criteria.Root; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.is; -import static org.hamcrest.Matchers.isOneOf; import static org.hamcrest.Matchers.isA; +import static org.hamcrest.Matchers.isOneOf; import static org.hamcrest.Matchers.notNullValue; import static org.hamcrest.Matchers.nullValue; @@ -124,7 +127,8 @@ public void verifyMappings(SessionFactoryScope scope) { "objectMap" ); final BasicAttributeMapping listAttribute = (BasicAttributeMapping) entityDescriptor.findAttributeMapping( "list" ); - final BasicAttributeMapping jsonAttribute = (BasicAttributeMapping) entityDescriptor.findAttributeMapping( "jsonString" ); + final BasicAttributeMapping jsonAttribute = (BasicAttributeMapping) entityDescriptor.findAttributeMapping( + "jsonString" ); assertThat( stringMapAttribute.getJavaType().getJavaTypeClass(), equalTo( Map.class ) ); assertThat( objectMapAttribute.getJavaType().getJavaTypeClass(), equalTo( Map.class ) ); @@ -146,8 +150,8 @@ public void verifyReadWorks(SessionFactoryScope scope) { assertThat( entityWithJson.stringMap, is( stringMap ) ); assertThat( entityWithJson.objectMap, is( objectMap ) ); assertThat( entityWithJson.list, is( list ) ); - assertThat( entityWithJson.jsonNode, is( nullValue() )); - assertThat( entityWithJson.jsonValue, is( nullValue() )); + assertThat( entityWithJson.jsonNode, is( nullValue() ) ); + assertThat( entityWithJson.jsonValue, is( nullValue() ) ); } ); } @@ -167,14 +171,14 @@ public void verifyMergeWorks(SessionFactoryScope scope) { assertThat( entityWithJson.objectMap, is( nullValue() ) ); assertThat( entityWithJson.list, is( nullValue() ) ); assertThat( entityWithJson.jsonString, is( nullValue() ) ); - assertThat( entityWithJson.jsonNode, is( nullValue() )); - assertThat( entityWithJson.jsonValue, is( nullValue() )); + assertThat( entityWithJson.jsonNode, is( nullValue() ) ); + assertThat( entityWithJson.jsonValue, is( nullValue() ) ); } ); } @Test - @JiraKey( "HHH-16682" ) + @JiraKey("HHH-16682") public void verifyDirtyChecking(SessionFactoryScope scope) { scope.inTransaction( (session) -> { @@ -198,7 +202,7 @@ public void verifyDirtyChecking(SessionFactoryScope scope) { @SkipForDialect(dialectClass = AltibaseDialect.class, reason = "Altibase doesn't support comparing CLOBs with the = operator") public void verifyComparisonWorks(SessionFactoryScope scope) { scope.inTransaction( - (session) -> { + (session) -> { // PostgreSQL returns the JSON slightly formatted String alternativeJson = "{\"name\": \"abc\"}"; EntityWithJson entityWithJson = session.createQuery( @@ -243,6 +247,32 @@ else if ( nativeJson instanceof Clob ) { ); } + @Test + @Jira("https://hibernate.atlassian.net/browse/HHH-18709") + public void verifyCriteriaUpdateQueryWorks(SessionFactoryScope scope) { + final Map newMap = Map.of( "name", "ABC" ); + final List newList = List.of( new StringNode( "ABC" ) ); + final String newJson = "{\"count\":123}"; + scope.inTransaction( session -> { + final HibernateCriteriaBuilder builder = session.getCriteriaBuilder(); + final CriteriaUpdate criteria = builder.createCriteriaUpdate( EntityWithJson.class ); + final Root root = criteria.from( EntityWithJson.class ); + criteria.set( root.get( "stringMap" ), newMap ); + criteria.set( "list", newList ); + criteria.set( root.get( "jsonString" ), newJson ); + criteria.where( builder.equal( root.get( "id" ), 1 ) ); + final MutationQuery query = session.createMutationQuery( criteria ); + final int count = query.executeUpdate(); + assertThat( count, is( 1 ) ); + } ); + scope.inSession( session -> { + final EntityWithJson entityWithJson = session.find( EntityWithJson.class, 1 ); + assertThat( entityWithJson.stringMap, is( newMap ) ); + assertThat( entityWithJson.list, is( newList ) ); + assertThat( entityWithJson.jsonString.replaceAll( "\\s", "" ), is( newJson ) ); + } ); + } + @Entity(name = "EntityWithJson") @Table(name = "EntityWithJson") public static class EntityWithJson { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/collections/BidirectionalOneToManyTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/collections/BidirectionalOneToManyTest.java new file mode 100644 index 000000000000..ada1ff55898a --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/collections/BidirectionalOneToManyTest.java @@ -0,0 +1,124 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.mapping.collections; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.JoinTable; +import jakarta.persistence.ManyToMany; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Table; +import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.Jpa; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; + +@Jpa( annotatedClasses = {BidirectionalOneToManyTest.Organization.class, BidirectionalOneToManyTest.User.class} ) +@Jira("https://hibernate.atlassian.net/browse/HHH-19963") +public class BidirectionalOneToManyTest { + + @Test + public void testParentNotTreatedAsBidirectional(EntityManagerFactoryScope scope) { + scope.inTransaction( entityManager -> { + Organization o3 = new Organization( 3L, "o3", null, new ArrayList<>() ); + Organization o1 = new Organization( 1L, "o1", null, new ArrayList<>( Arrays.asList( o3 )) ); + Organization o2 = new Organization( 2L, "o2", o1, new ArrayList<>() ); + entityManager.persist(o3); + entityManager.persist(o1); + entityManager.persist(o2); + + User u1 = new User( 1L, o2 ); + User u2 = new User( 2L, o2 ); + entityManager.persist(u1); + entityManager.persist(u2); + }); + + scope.inTransaction( entityManager -> { + User user1 = entityManager.find(User.class, 1L); + Organization ou3 = entityManager.find(Organization.class, 3L); + assertNull( ou3.getParentOrganization(), "Parent of o3 is null"); + assertEquals(0, ou3.getPredecessorOrganizations().size(), "Predecessors of o3 is empty"); + }); + } + + @Entity(name = "Organization") + public static class Organization { + + @Id + private Long id; + private String name; + + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "parentorganization_objectId") + private Organization parentOrganization; + + @ManyToMany(fetch = FetchType.EAGER) + @JoinTable(name = "organization_predecessor") + private List predecessorOrganizations = new ArrayList<>(); + + public Organization() { + } + + public Organization(Long id, String name, Organization parentOrganization, List predecessorOrganizations) { + this.id = id; + this.name = name; + this.parentOrganization = parentOrganization; + this.predecessorOrganizations = predecessorOrganizations; + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + public Organization getParentOrganization() { + return parentOrganization; + } + + public List getPredecessorOrganizations() { + return predecessorOrganizations; + } + } + + @Entity(name = "User") + @Table(name = "usr_tbl") + public static class User { + + @Id + private Long id; + @ManyToOne(fetch = FetchType.EAGER) + private Organization organization; + + public User() { + } + + public User(Long id, Organization organization) { + this.id = id; + this.organization = organization; + } + + public Long getId() { + return id; + } + + public Organization getOrganization() { + return organization; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/collections/OrderColumnListIndexArrayInitializerTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/collections/OrderColumnListIndexArrayInitializerTest.java new file mode 100644 index 000000000000..c35b57f615aa --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/collections/OrderColumnListIndexArrayInitializerTest.java @@ -0,0 +1,110 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.mapping.collections; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.JoinTable; +import jakarta.persistence.ManyToMany; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OrderColumn; +import org.hibernate.annotations.ListIndexBase; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Test; + + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +@JiraKey("HHH-18876") +@DomainModel( + annotatedClasses = { + OrderColumnListIndexArrayInitializerTest.Person.class, + OrderColumnListIndexArrayInitializerTest.Phone.class, + } +) +@SessionFactory +class OrderColumnListIndexArrayInitializerTest { + + @Test + void hhh18876Test(SessionFactoryScope scope) { + + // prepare data + scope.inTransaction( session -> { + + // person + Person person = new Person(); + person.id = 1L; + session.persist( person ); + + // add phone + Phone phone = new Phone(); + phone.id = 1L; + phone.person = person; + + person.phones = new Phone[1]; + person.phones[0] = phone; + + // add children + Person children = new Person(); + children.id = 2L; + children.mother = person; + + person.children = new Person[1]; + person.children[0] = children; + } ); + + // load and assert + scope.inTransaction( session -> { + Person person = session.createSelectionQuery( "select p from Person p where id=1", Person.class ) + .getSingleResult(); + assertNotNull( person ); + assertEquals( 1, person.phones.length ); + assertNotNull( person.phones[0] ); + assertEquals( 1, person.children.length ); + assertEquals( person, person.children[0].mother ); + } ); + } + + @Entity(name = "Person") + public static class Person { + + @Id + Long id; + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "person", cascade = CascadeType.ALL) + @OrderColumn(name = "order_id") + @ListIndexBase(100) + Phone[] phones; + + @ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL) + @JoinTable(name = "parent_child_relationships", joinColumns = @JoinColumn(name = "parent_id"), + inverseJoinColumns = @JoinColumn(name = "child_id")) + @OrderColumn(name = "pos") + @ListIndexBase(200) + Person[] children; + + @ManyToOne + @JoinColumn(name = "mother_id") + Person mother; + } + + @Entity(name = "Phone") + public static class Phone { + + @Id + Long id; + + @ManyToOne + Person person; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/collections/OrderColumnListIndexHHH18771ListInitializerTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/collections/OrderColumnListIndexHHH18771ListInitializerTest.java new file mode 100644 index 000000000000..93a3e88d1cf7 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/collections/OrderColumnListIndexHHH18771ListInitializerTest.java @@ -0,0 +1,148 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.mapping.collections; + +import java.util.ArrayList; +import java.util.List; + +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; +import org.hibernate.annotations.ListIndexBase; +import org.hibernate.orm.test.jpa.BaseEntityManagerFunctionalTestCase; + +import org.junit.Test; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.JoinTable; +import jakarta.persistence.ManyToMany; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OrderColumn; + +import static org.hibernate.testing.transaction.TransactionUtil.doInJPA; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +/** + * @author Selaron + */ +public class OrderColumnListIndexHHH18771ListInitializerTest extends BaseEntityManagerFunctionalTestCase { + + @Override + protected Class[] getAnnotatedClasses() { + return new Class[] { + Person.class, + Phone.class, + }; + } + + @Test + public void testLifecycle() { + doInJPA( this::entityManagerFactory, entityManager -> { + Person person = new Person( 1L ); + entityManager.persist( person ); + person.addPhone( new Phone( 1L ) ); + person.getChildren().add( new Person( 2L ) ); + person.getChildren().get( 0 ).setMother( person ); + } ); + doInJPA( this::entityManagerFactory, entityManager -> { + Person person = entityManager.find( Person.class, 1L ); + assertNotNull( person ); + assertEquals( 1, person.getPhones().size() ); + assertNotNull( person.getPhones().get( 0 ) ); + assertEquals( 1, person.getChildren().size() ); + assertEquals( person, person.getChildren().get( 0 ).getMother() ); + } ); + } + + @Entity(name = "Person") + @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + public static class Person { + + @Id + private Long id; + + @OneToMany(fetch = FetchType.EAGER, mappedBy = "person", cascade = CascadeType.ALL) + @OrderColumn(name = "order_id") + @ListIndexBase(100) + private List phones = new ArrayList<>(); + + @ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL) + @JoinTable(name = "parent_child_relationships", joinColumns = @JoinColumn(name = "parent_id"), inverseJoinColumns = @JoinColumn(name = "child_id")) + @OrderColumn(name = "pos") + @ListIndexBase(200) + private List children = new ArrayList<>(); + + @ManyToOne + @JoinColumn(name = "mother_id") + private Person mother; + + public Person() { + } + + public Person(Long id) { + this.id = id; + } + + public List getPhones() { + return phones; + } + + public void addPhone(Phone phone) { + phones.add( phone ); + phone.setPerson( this ); + } + + public List getChildren() { + return children; + } + + public Person getMother() { + return mother; + } + + public void setMother(Person mother) { + this.mother = mother; + } + + public void removePhone(Phone phone) { + phones.remove( phone ); + phone.setPerson( null ); + } + } + + @Entity(name = "Phone") + public static class Phone { + + @Id + private Long id; + + @ManyToOne + private Person person; + + public Phone() { + } + + public Phone(Long id) { + this.id = id; + } + + public Long getId() { + return id; + } + + public Person getPerson() { + return person; + } + + public void setPerson(Person person) { + this.person = person; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/converted/converter/object/ConvertedClassAttributeTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/converted/converter/object/ConvertedClassAttributeTest.java new file mode 100644 index 000000000000..ae2190ed1d0a --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/converted/converter/object/ConvertedClassAttributeTest.java @@ -0,0 +1,119 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.mapping.converted.converter.object; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.AttributeConverter; +import jakarta.persistence.Convert; +import jakarta.persistence.Converter; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Marco Belladelli + */ +@DomainModel( annotatedClasses = { + ConvertedClassAttributeTest.EntityWithStatus.class, + Status.class, +} ) +@SessionFactory +@Jira( "https://hibernate.atlassian.net/browse/HHH-18564" ) +public class ConvertedClassAttributeTest { + @Test + public void testConvertedAttributeSelection(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var resultList = session.createQuery( + "select t.status from EntityWithStatus t", + Status.class + ).getResultList(); + assertThat( resultList ).hasSize( 2 ).containsExactlyInAnyOrder( Status.ONE, Status.TWO ); + } ); + } + + @Test + public void testLiteralPredicateDomainForm(SessionFactoryScope scope) { + scope.inTransaction( session -> { + var result = session.createQuery( + String.format( "select t from EntityWithStatus t where t.status > %s.ONE", Status.class.getName() ), + EntityWithStatus.class + ).getSingleResult(); + assertThat( result.getStatus().getValue() ).isEqualTo( 2 ); + } ); + } + + @Test + public void testLiteralPredicateRelationalForm(SessionFactoryScope scope) { + scope.inTransaction( session -> { + var result = session.createQuery( + "select t from EntityWithStatus t where t.status > 1", + EntityWithStatus.class + ).getSingleResult(); + assertThat( result.getStatus().getValue() ).isEqualTo( 2 ); + } ); + } + + @BeforeAll + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final EntityWithStatus entity1 = new EntityWithStatus(); + entity1.setStatus( Status.ONE ); + session.persist( entity1 ); + final EntityWithStatus entity2 = new EntityWithStatus(); + entity2.setStatus( Status.TWO ); + session.persist( entity2 ); + } ); + } + + @AfterAll + public void tearDown(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } + + @Entity( name = "EntityWithStatus" ) + static class EntityWithStatus { + @Id + @GeneratedValue + private Long id; + + @Convert( converter = StatusConverter.class ) + private Status status; + + public Long getId() { + return id; + } + + public Status getStatus() { + return status; + } + + public void setStatus(Status status) { + this.status = status; + } + } + + @Converter + static class StatusConverter implements AttributeConverter { + @Override + public Integer convertToDatabaseColumn(Status attribute) { + return attribute == null ? null : attribute.getValue(); + } + + @Override + public Status convertToEntityAttribute(Integer dbData) { + return dbData == null ? null : Status.from( dbData ); + } + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/converted/converter/object/Status.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/converted/converter/object/Status.java new file mode 100644 index 000000000000..c862e197d1c6 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/converted/converter/object/Status.java @@ -0,0 +1,53 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.mapping.converted.converter.object; + +import org.hibernate.annotations.Imported; + +/** + * @author Marco Belladelli + */ +@Imported +public class Status { + public static Status ONE = new Status( 1 ); + public static Status TWO = new Status( 2 ); + + private final int value; + + public Status(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + + public static Status from(int value) { + return new Status( value ); + } + + @Override + public boolean equals(Object o) { + if ( this == o ) { + return true; + } + if ( o == null || getClass() != o.getClass() ) { + return false; + } + + final Status status = (Status) o; + return value == status.value; + } + + @Override + public int hashCode() { + return value; + } + + @Override + public String toString() { + return "Status{" + "value=" + value + '}'; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/embeddable/JsonWithArrayEmbeddableTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/embeddable/JsonWithArrayEmbeddableTest.java index 49a691f4bfdc..3bd899f85e6e 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/embeddable/JsonWithArrayEmbeddableTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/embeddable/JsonWithArrayEmbeddableTest.java @@ -240,6 +240,7 @@ public void testUpdateAggregateMember(SessionFactoryScope scope) { @Test @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsJsonComponentUpdate.class) + @SkipForDialect(dialectClass = OracleDialect.class, majorVersion = 19, reason = "Oracle bug: ORA-03113 database connection closed by peer") public void testUpdateMultipleAggregateMembers(SessionFactoryScope scope) { scope.inTransaction( entityManager -> { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/embeddable/NestedEmbeddedObjectWithASecondaryTableTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/embeddable/NestedEmbeddedObjectWithASecondaryTableTest.java new file mode 100644 index 000000000000..b2ccded06b15 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/embeddable/NestedEmbeddedObjectWithASecondaryTableTest.java @@ -0,0 +1,71 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.orm.test.mapping.embeddable; + +import jakarta.persistence.AttributeOverride; +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.SecondaryTable; +import jakarta.persistence.Table; +import org.hibernate.boot.MetadataSources; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.ServiceRegistryScope; +import org.junit.jupiter.api.Test; + +/** + * Test passes if Author#name is renamed to Author#aname because it will be read before house (alphabetical order). + * The issue occurs if the nested embedded is read first, due to the table calculation (ComponentPropertyHolder#addProperty) used by the embedded, which retrieves the table from the first property. + * + * @author Vincent Bouthinon + */ +@SuppressWarnings("JUnitMalformedDeclaration") +@ServiceRegistry() +@JiraKey("HHH-19272") +class NestedEmbeddedObjectWithASecondaryTableTest { + + @Test + void testNestedEmbeddedAndSecondaryTables(ServiceRegistryScope registryScope) { + final MetadataSources metadataSources = new MetadataSources( registryScope.getRegistry() ) + .addAnnotatedClasses( Book.class, Author.class, House.class ); + metadataSources.buildMetadata(); + } + + @Entity(name = "book") + @Table(name = "TBOOK") + @SecondaryTable(name = "TSECONDARYTABLE") + public static class Book { + + @Id + @GeneratedValue + private Long id; + + @AttributeOverride(name = "name", column = @Column(name = "authorName", table = "TSECONDARYTABLE")) + @Embedded + private Author author; + + } + + @Embeddable + public static class Author { + + @AttributeOverride(name = "name", column = @Column(name = "houseName", table = "TSECONDARYTABLE")) + @Embedded + private House house; + + private String name; + } + + @Embeddable + public static class House { + private String name; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/embeddable/StructEmbeddableArrayEmptyTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/embeddable/StructEmbeddableArrayEmptyTest.java new file mode 100644 index 000000000000..90ee1468f64c --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/embeddable/StructEmbeddableArrayEmptyTest.java @@ -0,0 +1,88 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.orm.test.mapping.embeddable; + +import jakarta.persistence.Embeddable; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import org.hibernate.annotations.Struct; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.dialect.OracleDialect; +import org.hibernate.dialect.PostgreSQLDialect; +import org.hibernate.testing.jdbc.SharedDriverManagerTypeCacheClearingIntegrator; +import org.hibernate.testing.orm.junit.BootstrapServiceRegistry; +import org.hibernate.testing.orm.junit.DomainModel; +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.SettingProvider; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +@BootstrapServiceRegistry( + // Clear the type cache, otherwise we might run into ORA-21700: object does not exist or is marked for delete + integrators = SharedDriverManagerTypeCacheClearingIntegrator.class +) +@ServiceRegistry( + settingProviders = @SettingProvider( + settingName = AvailableSettings.PREFERRED_ARRAY_JDBC_TYPE, + provider = OracleNestedTableSettingProvider.class + ) +) +@DomainModel(annotatedClasses = StructEmbeddableArrayEmptyTest.StructHolder.class) +@SessionFactory +@RequiresDialect( PostgreSQLDialect.class ) +@RequiresDialect( OracleDialect.class ) +public class StructEmbeddableArrayEmptyTest { + + @BeforeEach + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + var entity = new StructHolder(); + entity.id = 1L; + entity.myStruct = new MyStruct(); + entity.myStructs = new MyStruct[] { new MyStruct() }; + session.persist( entity ); + } + ); + } + + @AfterEach + protected void cleanupTest(SessionFactoryScope scope) { + scope.inTransaction( session -> { + session.createQuery( "delete StructHolder" ).executeUpdate(); + } ); + } + + @Test + void testEmptyStructBehavesDifferently(SessionFactoryScope scope) { + var loadedEntity = scope.fromTransaction(session -> session.find(StructHolder.class, 1)); + assertThat(loadedEntity.myStruct).isNotNull(); + assertThat(loadedEntity.myStructs).usingRecursiveComparison() + .isEqualTo(new MyStruct[] { new MyStruct() }) + .isNotEqualTo(new MyStruct[] { null }); + } + + @Entity(name = "StructHolder") + public static class StructHolder { + @Id + Long id; + MyStruct myStruct; + MyStruct[] myStructs; + } + + @Embeddable + @Struct(name = "MyStruct") + public static class MyStruct { + String field; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/generated/GeneratedNoOpUpdateTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/generated/GeneratedNoOpUpdateTest.java new file mode 100644 index 000000000000..bdd8489b9676 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/generated/GeneratedNoOpUpdateTest.java @@ -0,0 +1,182 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.mapping.generated; + +import java.time.ZonedDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +import org.hibernate.annotations.CurrentTimestamp; + +import org.hibernate.testing.jdbc.SQLStatementInspector; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Marco Belladelli + */ +@DomainModel( annotatedClasses = { + GeneratedNoOpUpdateTest.Pizza.class, + GeneratedNoOpUpdateTest.Topping.class, +} ) +@SessionFactory(useCollectingStatementInspector = true) +@Jira( "https://hibernate.atlassian.net/browse/HHH-18484" ) +public class GeneratedNoOpUpdateTest { + @Test + public void testUpdate(SessionFactoryScope scope) { + final SQLStatementInspector inspector = scope.getCollectingStatementInspector(); + inspector.clear(); + + final ZonedDateTime updatedTime = scope.fromTransaction( session -> { + final Pizza pizza = session.find( Pizza.class, 1L ); + final ZonedDateTime initialTime = pizza.getLastUpdated(); + // Create a new topping + final Topping newTopping1 = new Topping(); + newTopping1.setName( "Cheese" ); + newTopping1.setPizza( pizza ); + // Let's mutate the existing list + pizza.getToppings().add( newTopping1 ); + session.flush(); + // pizza was not dirty so no update is executed + inspector.assertNoUpdate(); + assertThat( pizza.getLastUpdated() ).isEqualTo( initialTime ); + return pizza.getLastUpdated(); + } ); + + inspector.clear(); + scope.inTransaction( session -> { + // Now let's try adding a new topping via a new list + final Pizza pizza = session.find( Pizza.class, 1L ); + // Create a new topping + final Topping newTopping2 = new Topping(); + newTopping2.setName( "Mushroom" ); + newTopping2.setPizza( pizza ); + // This time, instead of mutating the existing list, we're creating a new list + pizza.setToppings( List.of( pizza.getToppings().get( 0 ), newTopping2 ) ); + session.flush(); + // pizza this time was dirty, but still no update is executed because + // only the unowned one-to-many association has changed + inspector.assertNoUpdate(); + assertThat( pizza.getLastUpdated() ).isEqualTo( updatedTime ); + } ); + + scope.inTransaction( session -> { + final Pizza pizza = session.find( Pizza.class, 1L ); + assertThat( pizza.getToppings() ).hasSize( 3 ) + .extracting( Topping::getName ) + .containsExactlyInAnyOrder( "Pepperoni", "Cheese", "Mushroom" ); + // This time we mutate the pizza to trigger a real update + pizza.setName( "Salamino e funghi" ); + session.flush(); + assertThat( inspector.getSqlQueries() ).anyMatch( sql -> sql.toLowerCase( Locale.ROOT ).contains( "update" ) ); + assertThat( pizza.getLastUpdated() ).isAfter( updatedTime ); + } ); + } + + @BeforeAll + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final Pizza pizza = new Pizza( 1L, "Salamino" ); + session.persist( pizza ); + final Topping topping = new Topping(); + topping.setName( "Pepperoni" ); + topping.setPizza( pizza ); + pizza.getToppings().add( topping ); + } ); + } + + @AfterAll + public void tearDown(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } + + @Entity( name = "Pizza" ) + static class Pizza { + @Id + private Long id; + + @OneToMany( mappedBy = "pizza", cascade = CascadeType.ALL ) + private List toppings = new ArrayList<>(); + + @CurrentTimestamp + private ZonedDateTime lastUpdated; + + private String name; + + public Pizza() { + } + + public Pizza(Long id, String name) { + this.id = id; + this.name = name; + } + + public Long getId() { + return id; + } + + public List getToppings() { + return toppings; + } + + public void setToppings(final List toppings) { + this.toppings = toppings; + } + + public ZonedDateTime getLastUpdated() { + return lastUpdated; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + + @Entity( name = "Topping" ) + static class Topping { + @Id + @GeneratedValue + private Long id; + + @ManyToOne + private Pizza pizza; + + private String name; + + public void setName(final String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public void setPizza(final Pizza pizza) { + this.pizza = pizza; + } + + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/generated/always/GeneratedAlwaysTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/generated/always/GeneratedAlwaysTest.java index ec6ac851b948..0fd10efb51d4 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/generated/always/GeneratedAlwaysTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/generated/always/GeneratedAlwaysTest.java @@ -6,6 +6,7 @@ import org.hibernate.annotations.GeneratedColumn; import org.hibernate.community.dialect.AltibaseDialect; import org.hibernate.dialect.DerbyDialect; +import org.hibernate.community.dialect.InformixDialect; import org.hibernate.dialect.H2Dialect; import org.hibernate.dialect.HSQLDialect; import org.hibernate.dialect.PostgreSQLDialect; @@ -33,6 +34,7 @@ @SkipForDialect(dialectClass = PostgreSQLDialect.class, majorVersion = 10, matchSubTypes = true) @SkipForDialect(dialectClass = PostgreSQLDialect.class, majorVersion = 11, matchSubTypes = true) // 'generated always' was added in 12 @SkipForDialect(dialectClass = AltibaseDialect.class, reason = "generated always is not supported in Altibase") +@SkipForDialect(dialectClass = InformixDialect.class) public class GeneratedAlwaysTest { @Test diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/identifier/CompositeIdGenerationTypeTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/identifier/CompositeIdGenerationTypeTest.java new file mode 100644 index 000000000000..a5e4d4099fb1 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/identifier/CompositeIdGenerationTypeTest.java @@ -0,0 +1,270 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.mapping.identifier; + +import java.util.UUID; + +import org.hibernate.annotations.UuidGenerator; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Embeddable; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.IdClass; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +/** + * @author Marco Belladelli + */ +@DomainModel( annotatedClasses = { + CompositeIdGenerationTypeTest.SingleIdClass.class, + CompositeIdGenerationTypeTest.MultipleIdClass.class, + CompositeIdGenerationTypeTest.IdClassPK.class, + CompositeIdGenerationTypeTest.SingleEmbeddedId.class, + CompositeIdGenerationTypeTest.SingleEmbeddedPK.class, + CompositeIdGenerationTypeTest.MultipleEmbeddedId.class, + CompositeIdGenerationTypeTest.MultipleEmbeddedPK.class, +} ) +@SessionFactory +@Jira( "https://hibernate.atlassian.net/browse/HHH-18524" ) +public class CompositeIdGenerationTypeTest { + @Test + public void testSingleIdClass(SessionFactoryScope scope) { + scope.inTransaction( session -> session.persist( new SingleIdClass( 1L, "id_class_1" ) ) ); + scope.inSession( session -> { + final SingleIdClass result = session.createQuery( "from SingleIdClass", SingleIdClass.class ) + .getSingleResult(); + assertThat( result.getId() ).isEqualTo( 1L ); + assertDoesNotThrow( () -> UUID.fromString( result.getUuid() ) ); + } ); + } + + @Test + public void testMultipleIdClass(SessionFactoryScope scope) { + scope.inTransaction( session -> session.persist( new MultipleIdClass( "id_class_2" ) ) ); + scope.inSession( session -> { + final MultipleIdClass result = session.createQuery( "from MultipleIdClass", MultipleIdClass.class ) + .getSingleResult(); + assertThat( result.getId() ).isGreaterThan( 0 ); + assertDoesNotThrow( () -> UUID.fromString( result.getUuid() ) ); + } ); + } + + @Test + public void testSingleEmbeddedId(SessionFactoryScope scope) { + scope.inTransaction( session -> session.persist( new SingleEmbeddedId( + new SingleEmbeddedPK( 1L ), + "embedded_id_1" + ) ) ); + scope.inSession( session -> { + final SingleEmbeddedId result = session.createQuery( "from SingleEmbeddedId", SingleEmbeddedId.class ) + .getSingleResult(); + final SingleEmbeddedPK embeddedId = result.getEmbeddedId(); + assertThat( embeddedId.getId() ).isEqualTo( 1L ); + assertDoesNotThrow( () -> UUID.fromString( embeddedId.getUuid() ) ); + } ); + } + + @Test + public void testMultipleEmbeddedId(SessionFactoryScope scope) { + scope.inTransaction( session -> session.persist( new MultipleEmbeddedId( + new MultipleEmbeddedPK(), + "embedded_id_2" + ) ) ); + + scope.inTransaction( session -> { + final MultipleEmbeddedId result = session.createQuery( "from MultipleEmbeddedId", MultipleEmbeddedId.class ) + .getSingleResult(); + final MultipleEmbeddedPK embeddedId = result.getEmbeddedId(); + assertThat( embeddedId.getId() ).isGreaterThan( 0 ); + assertDoesNotThrow( () -> UUID.fromString( embeddedId.getUuid() ) ); + } ); + } + + @AfterAll + public void tearDown(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } + + @Entity( name = "SingleIdClass" ) + @IdClass( IdClassPK.class ) + static class SingleIdClass { + @Id + private Long id; + + @Id + @UuidGenerator + private String uuid; + + private String name; + + public SingleIdClass() { + } + + public SingleIdClass(Long id, String name) { + this.id = id; + this.name = name; + } + + public Long getId() { + return id; + } + + public String getUuid() { + return uuid; + } + + public String getName() { + return name; + } + } + + @Embeddable + static class IdClassPK { + private Long id; + private String uuid; + + public IdClassPK() { + } + + public IdClassPK(Long id, String uuid) { + this.id = id; + this.uuid = uuid; + } + } + + @Entity( name = "MultipleIdClass" ) + @IdClass( IdClassPK.class ) + static class MultipleIdClass { + @Id + @GeneratedValue + private Long id; + + @Id + @UuidGenerator + private String uuid; + + private String name; + + public MultipleIdClass() { + } + + public MultipleIdClass(String name) { + this.name = name; + } + + public Long getId() { + return id; + } + + public String getUuid() { + return uuid; + } + + public String getName() { + return name; + } + } + + @Entity( name = "SingleEmbeddedId" ) + static class SingleEmbeddedId { + @EmbeddedId + private SingleEmbeddedPK embeddedId; + + private String name; + + public SingleEmbeddedId() { + } + + public SingleEmbeddedId(SingleEmbeddedPK embeddedId, String name) { + this.embeddedId = embeddedId; + this.name = name; + } + + public SingleEmbeddedPK getEmbeddedId() { + return embeddedId; + } + + public String getName() { + return name; + } + } + + @Embeddable + static class SingleEmbeddedPK { + private Long id; + + @UuidGenerator + private String uuid; + + public SingleEmbeddedPK() { + } + + public SingleEmbeddedPK(Long id) { + this.id = id; + } + + public Long getId() { + return id; + } + + public String getUuid() { + return uuid; + } + } + + @Entity( name = "MultipleEmbeddedId" ) + static class MultipleEmbeddedId { + @EmbeddedId + private MultipleEmbeddedPK embeddedId; + + private String name; + + public MultipleEmbeddedId() { + } + + public MultipleEmbeddedId(MultipleEmbeddedPK embeddedId, String name) { + this.embeddedId = embeddedId; + this.name = name; + } + + public MultipleEmbeddedPK getEmbeddedId() { + return embeddedId; + } + + public String getName() { + return name; + } + } + + @Embeddable + static class MultipleEmbeddedPK { + @GeneratedValue + private Long id; + + @UuidGenerator + private String uuid; + + public Long getId() { + return id; + } + + public String getUuid() { + return uuid; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/identifier/composite/CompositeInheritanceFailTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/identifier/composite/CompositeInheritanceFailTest.java new file mode 100644 index 000000000000..ede98a79273b --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/identifier/composite/CompositeInheritanceFailTest.java @@ -0,0 +1,199 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.mapping.identifier.composite; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.IdClass; +import jakarta.persistence.MappedSuperclass; +import jakarta.persistence.Version; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.Setting; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +/** + * This test Fails + */ +@DomainModel( + annotatedClasses = { + CompositeInheritanceFailTest.TupAbstractEntity.class, + CompositeInheritanceFailTest.DummyEntity.class, + CompositeInheritanceFailTest.TestEntity.class, // Here the class is called TestEntity + CompositeInheritanceFailTest.Test2Entity.class, + } +) +@ServiceRegistry( + settings = { + // For your own convenience to see generated queries: + @Setting(name = AvailableSettings.SHOW_SQL, value = "true"), + @Setting(name = AvailableSettings.FORMAT_SQL, value = "true"), + // @Setting( name = AvailableSettings.GENERATE_STATISTICS, value = "true" ), + } +) +@SessionFactory +@Jira("HHH-19076") +public class CompositeInheritanceFailTest { + + @Test + void hhh19076FailingTest(SessionFactoryScope scope) { + scope.inTransaction( em -> { + TestEntity e1 = new TestEntity("foo", "bar"); + em.persist(e1); + + CompositeIdClass key = e1.getCompositeId(); + TestEntity e2 = em.find(TestEntity.class, key); + assertNotNull(e2); + } ); + } + + @Test + void hhh19076FailingTest2(SessionFactoryScope scope) { + scope.inTransaction( em -> { + Test2Entity e1 = new Test2Entity("foo", "xxxxxx"); + em.persist(e1); + + CompositeId2Class key = e1.getCompositeId(); + Test2Entity e2 = em.find(Test2Entity.class, key); + assertNotNull(e2); + } ); + } + + @MappedSuperclass + public static abstract class TupAbstractEntity { + @Id + private String oid = null; + + + @SuppressWarnings("this-escape") + protected TupAbstractEntity() { + } + + protected TupAbstractEntity(String oid) { + this.oid = oid; + } + + public String getOid() { + return oid; + } + + } + + @Entity + public static class DummyEntity extends TupAbstractEntity { + } + + @Entity + @IdClass(CompositeIdClass.class) + public static class TestEntity extends TupAbstractEntity { + + @Id + private String myId; + + protected TestEntity() { + // for JPA + } + + public TestEntity(String oid, String myId) { + super(oid); + this.myId = myId; + } + + public String myId() { + return myId; + } + + public CompositeIdClass getCompositeId() { + return new CompositeIdClass(getOid(), myId); + } + + } + + @Entity + @IdClass(CompositeId2Class.class) + public static class Test2Entity extends TupAbstractEntity { + + @Id + private String otherId; + + @Version + private long tanum = 0; + + protected Test2Entity() { + // for JPA + } + + public Test2Entity(String oid, String otherId) { + super(oid); + this.otherId = otherId; + } + + public String myId() { + return otherId; + } + + public long tanum() { + return tanum; + } + + public CompositeId2Class getCompositeId() { + return new CompositeId2Class(getOid(), otherId); + } + + } + + public static class CompositeIdClass { + + private String oid; + private String myId; + + public CompositeIdClass(String oid, String myId) { + this.oid = oid; + this.myId = myId; + } + + public CompositeIdClass() { + } + + public String oid() { + return oid; + } + + public String myId() { + return myId; + } + + } + + public static class CompositeId2Class { + + private String oid; + private String otherId; + + public CompositeId2Class(String oid, String otherId) { + this.oid = oid; + this.otherId = otherId; + } + + public CompositeId2Class() { + } + + public String oid() { + return oid; + } + + public String otherId() { + return otherId; + } + + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/identifier/composite/CompositeInheritanceWorkingTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/identifier/composite/CompositeInheritanceWorkingTest.java new file mode 100644 index 000000000000..2a3c81603785 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/identifier/composite/CompositeInheritanceWorkingTest.java @@ -0,0 +1,199 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.mapping.identifier.composite; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.IdClass; +import jakarta.persistence.MappedSuperclass; +import jakarta.persistence.Version; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.Setting; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +/** + * This test works for some reason... + */ +@DomainModel( + annotatedClasses = { + CompositeInheritanceWorkingTest.TupAbstractEntity.class, + CompositeInheritanceWorkingTest.DummyEntity.class, + CompositeInheritanceWorkingTest.FooEntity.class, // And here the class is called FooEntity and this works for some reason + CompositeInheritanceWorkingTest.Test2Entity.class, + } +) +@ServiceRegistry( + settings = { + // For your own convenience to see generated queries: + @Setting(name = AvailableSettings.SHOW_SQL, value = "true"), + @Setting(name = AvailableSettings.FORMAT_SQL, value = "true"), + // @Setting( name = AvailableSettings.GENERATE_STATISTICS, value = "true" ), + } +) +@SessionFactory +@Jira("HHH-19076") +public class CompositeInheritanceWorkingTest { + + @Test + void hhh19076WorkingTest(SessionFactoryScope scope) { + scope.inTransaction( em -> { + FooEntity e1 = new FooEntity("foo", "bar"); + em.persist(e1); + + CompositeIdClass key = e1.getCompositeId(); + FooEntity e2 = em.find( FooEntity.class, key); + assertNotNull(e2); + } ); + } + + @Test + void hhh19076FailingTest2(SessionFactoryScope scope) { + scope.inTransaction( em -> { + Test2Entity e1 = new Test2Entity("foo", "xxxxxx"); + em.persist(e1); + + CompositeId2Class key = e1.getCompositeId(); + Test2Entity e2 = em.find( Test2Entity.class, key); + assertNotNull(e2); + } ); + } + + @MappedSuperclass + public static abstract class TupAbstractEntity { + @Id + private String oid = null; + + @Version + private long tanum = 0; + + + @SuppressWarnings("this-escape") + protected TupAbstractEntity() { + } + + protected TupAbstractEntity(String oid) { + this.oid = oid; + } + + public String getOid() { + return oid; + } + + public long getTanum() { + return tanum; + } + } + + @Entity + public static class DummyEntity extends TupAbstractEntity { + } + + @Entity + @IdClass(CompositeIdClass.class) + public static class FooEntity extends TupAbstractEntity { + + @Id + private String myId; + + protected FooEntity() { + // for JPA + } + + public FooEntity(String oid, String myId) { + super(oid); + this.myId = myId; + } + + public String myId() { + return myId; + } + + public CompositeIdClass getCompositeId() { + return new CompositeIdClass(getOid(), myId); + } + + } + + @Entity + @IdClass(CompositeId2Class.class) + public static class Test2Entity extends TupAbstractEntity { + + @Id + private String otherId; + + protected Test2Entity() { + // for JPA + } + + public Test2Entity(String oid, String otherId) { + super(oid); + this.otherId = otherId; + } + + public String myId() { + return otherId; + } + + public CompositeId2Class getCompositeId() { + return new CompositeId2Class(getOid(), otherId); + } + + } + + public static class CompositeIdClass { + + private String oid; + private String myId; + + public CompositeIdClass(String oid, String myId) { + this.oid = oid; + this.myId = myId; + } + + public CompositeIdClass() { + } + + public String oid() { + return oid; + } + + public String myId() { + return myId; + } + + } + + + public static class CompositeId2Class { + + private String oid; + private String otherId; + + public CompositeId2Class(String oid, String otherId) { + this.oid = oid; + this.otherId = otherId; + } + + public CompositeId2Class() { + } + + public String oid() { + return oid; + } + + public String otherId() { + return otherId; + } + + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/inheritance/joined/JoinedDiscriminatorSameChildTableTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/inheritance/joined/JoinedDiscriminatorSameChildTableTest.java new file mode 100644 index 000000000000..9679f00be5bb --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/inheritance/joined/JoinedDiscriminatorSameChildTableTest.java @@ -0,0 +1,163 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.mapping.inheritance.joined; + +import jakarta.persistence.Column; +import jakarta.persistence.DiscriminatorColumn; +import jakarta.persistence.DiscriminatorValue; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.Inheritance; +import jakarta.persistence.InheritanceType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import org.hibernate.testing.jdbc.SQLStatementInspector; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel(annotatedClasses = { + JoinedDiscriminatorSameChildTableTest.EntityParent.class, + JoinedDiscriminatorSameChildTableTest.EntityChildOne.class, + JoinedDiscriminatorSameChildTableTest.EntityChildTwo.class, + JoinedDiscriminatorSameChildTableTest.EntityRelation.class, +}) +@SessionFactory(useCollectingStatementInspector = true) +@Jira("https://hibernate.atlassian.net/browse/HHH-19457") +public class JoinedDiscriminatorSameChildTableTest { + @Test + public void testParents(SessionFactoryScope scope) { + final SQLStatementInspector inspector = scope.getCollectingStatementInspector(); + + scope.inSession( session -> { + final EntityRelation relation = session.find( EntityRelation.class, "relation_1" ); + inspector.clear(); + assertThat( relation.getParents() ).hasSize( 2 ); + } ); + // no need to filter by discriminator column, as we're selecting all subtypes of EntityParent + inspector.assertNumberOfOccurrenceInQueryNoSpace( 0, "disc_col", 1 ); + + scope.inSession( session -> { + final EntityRelation relation = session.createQuery( + "from EntityRelation where id = 'relation_1'", + EntityRelation.class + ).getSingleResult(); + inspector.clear(); + assertThat( relation.getParents() ).hasSize( 2 ); + } ); + inspector.assertNumberOfOccurrenceInQueryNoSpace( 0, "disc_col", 1 ); + } + + @BeforeAll + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final EntityRelation relation = new EntityRelation(); + relation.setId( "relation_1" ); + session.persist( relation ); + + final EntityChildOne c1 = new EntityChildOne(); + c1.setId( "child_1" ); + c1.setIdRelation( "relation_1" ); + session.persist( c1 ); + + final EntityChildTwo c2 = new EntityChildTwo(); + c2.setId( "child_2" ); + c2.setIdRelation( "relation_1" ); + session.persist( c2 ); + } ); + } + + @AfterAll + public void tearDown(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } + + @Entity(name = "EntityParent") + @Table(name = "parent_table") + @Inheritance(strategy = InheritanceType.JOINED) + @DiscriminatorColumn(name = "disc_col") + static abstract class EntityParent { + @Id + @Column(name = "id") + private String id; + + @Column(name = "id_relation") + private String idRelation; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "id_relation", referencedColumnName = "id", insertable = false, updatable = false) + private EntityRelation relation; + + public EntityRelation getRelation() { + return relation; + } + + public void setRelation(EntityRelation requisition) { + this.relation = requisition; + } + + public String getIdRelation() { + return idRelation; + } + + public void setIdRelation(String idRelation) { + this.idRelation = idRelation; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + } + + @Entity(name = "EntityRelation") + static class EntityRelation { + @Id + private String id; + + @OneToMany(fetch = FetchType.LAZY, mappedBy = "relation") + private List parents; + + public void setId(String id) { + this.id = id; + } + + public String getId() { + return id; + } + + public List getParents() { + return parents; + } + } + + @Entity(name = "EntityChildOne") + @Table(name = "child_table") + @DiscriminatorValue("child-one") + static class EntityChildOne extends EntityParent { + private String name; + } + + @Entity(name = "EntityChildTwo") + @Table(name = "child_table") + @DiscriminatorValue("child-two") + static class EntityChildTwo extends EntityParent { + private Integer age; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/javatime/GlobalJavaTimeJdbcTypeTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/javatime/GlobalJavaTimeJdbcTypeTests.java index 26060e9de7bf..48876351a4e5 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/javatime/GlobalJavaTimeJdbcTypeTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/javatime/GlobalJavaTimeJdbcTypeTests.java @@ -10,6 +10,7 @@ import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; +import java.time.OffsetDateTime; import java.time.temporal.ChronoUnit; import org.hibernate.cfg.MappingSettings; @@ -19,6 +20,7 @@ import org.hibernate.dialect.Dialect; import org.hibernate.dialect.HANADialect; import org.hibernate.dialect.OracleDialect; +import org.hibernate.dialect.PostgreSQLDialect; import org.hibernate.dialect.SybaseDialect; import org.hibernate.mapping.BasicValue; import org.hibernate.mapping.PersistentClass; @@ -31,6 +33,7 @@ import org.hibernate.testing.orm.junit.DomainModel; import org.hibernate.testing.orm.junit.DomainModelScope; +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; @@ -44,7 +47,7 @@ import jakarta.persistence.Table; import static org.assertj.core.api.Assertions.assertThat; -import static org.hibernate.type.descriptor.DateTimeUtils.roundToDefaultPrecision; +import static org.hibernate.type.descriptor.DateTimeUtils.adjustToDefaultPrecision; /** * Tests for "direct" JDBC handling of {@linkplain java.time Java Time} types. @@ -84,7 +87,7 @@ private void checkAttribute( @Test void testInstant(SessionFactoryScope scope) { final Dialect dialect = scope.getSessionFactory().getJdbcServices().getDialect(); - final Instant start = roundToDefaultPrecision( Instant.EPOCH, dialect ); + final Instant start = adjustToDefaultPrecision( Instant.EPOCH, dialect ); scope.inTransaction( (session) -> { final EntityWithJavaTimeValues entity = new EntityWithJavaTimeValues(); @@ -114,7 +117,7 @@ void testInstant(SessionFactoryScope scope) { @Test void testLocalDateTime(SessionFactoryScope scope) { final Dialect dialect = scope.getSessionFactory().getJdbcServices().getDialect(); - final LocalDateTime start = roundToDefaultPrecision( LocalDateTime.now(), dialect ); + final LocalDateTime start = adjustToDefaultPrecision( LocalDateTime.now(), dialect ); scope.inTransaction( (session) -> { final EntityWithJavaTimeValues entity = new EntityWithJavaTimeValues(); @@ -144,7 +147,7 @@ void testLocalDateTime(SessionFactoryScope scope) { @Test void testLocalDate(SessionFactoryScope scope) { final Dialect dialect = scope.getSessionFactory().getJdbcServices().getDialect(); - final LocalDate startTime = roundToDefaultPrecision( LocalDate.now(), dialect ); + final LocalDate startTime = adjustToDefaultPrecision( LocalDate.now(), dialect ); scope.inTransaction( (session) -> { final EntityWithJavaTimeValues entity = new EntityWithJavaTimeValues(); @@ -177,7 +180,7 @@ void testLocalDate(SessionFactoryScope scope) { @SkipForDialect(dialectClass = AltibaseDialect.class, reason = "Altibase drivers truncate fractional seconds from the LocalTime") void testLocalTime(SessionFactoryScope scope) { final Dialect dialect = scope.getSessionFactory().getJdbcServices().getDialect(); - final LocalTime startTime = roundToDefaultPrecision( LocalTime.now(), dialect ); + final LocalTime startTime = adjustToDefaultPrecision( LocalTime.now(), dialect ); scope.inTransaction( (session) -> { final EntityWithJavaTimeValues entity = new EntityWithJavaTimeValues(); @@ -204,6 +207,23 @@ void testLocalTime(SessionFactoryScope scope) { } ); } + @Test + @RequiresDialect(value = PostgreSQLDialect.class) + void testArray(SessionFactoryScope scope) { + final var offsetDateTime = OffsetDateTime.parse("1977-07-24T12:34:56+02:00"); + scope.inTransaction( session -> { + final var nativeQuery = session.createNativeQuery( + "WITH data AS (SELECT unnest(?) AS id, unnest(?) AS offset_date_time)" + + " INSERT INTO EntityWithJavaTimeValues (id, theOffsetDateTime) SELECT * FROM data" + ); + nativeQuery.setParameter( 1, new int[] { 1 } ); + nativeQuery.setParameter( 2, new OffsetDateTime[] { offsetDateTime } ); + assertThat( nativeQuery.executeUpdate() ).isEqualTo( 1 ); + final var found = session.find( EntityWithJavaTimeValues.class, 1 ); + assertThat( found.theOffsetDateTime.toInstant() ).isEqualTo( offsetDateTime.toInstant() ); + } ); + } + @AfterEach void dropTestData(SessionFactoryScope scope) { scope.inTransaction( (session) -> { @@ -218,6 +238,8 @@ public static class EntityWithJavaTimeValues { private Integer id; private String name; + private OffsetDateTime theOffsetDateTime; + private Instant theInstant; private LocalDateTime theLocalDateTime; diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/javatime/JavaTimeJdbcTypeTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/javatime/JavaTimeJdbcTypeTests.java index 91085418acbc..481d8f8f2517 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/javatime/JavaTimeJdbcTypeTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/mapping/javatime/JavaTimeJdbcTypeTests.java @@ -45,7 +45,7 @@ import jakarta.persistence.Table; import static org.assertj.core.api.Assertions.assertThat; -import static org.hibernate.type.descriptor.DateTimeUtils.roundToDefaultPrecision; +import static org.hibernate.type.descriptor.DateTimeUtils.adjustToDefaultPrecision; /** * Tests for "direct" JDBC handling of {@linkplain java.time Java Time} types. @@ -85,7 +85,7 @@ private void checkAttribute( @Test void testInstant(SessionFactoryScope scope) { final Dialect dialect = scope.getSessionFactory().getJdbcServices().getDialect(); - final Instant start = roundToDefaultPrecision( Instant.EPOCH, dialect ); + final Instant start = adjustToDefaultPrecision( Instant.EPOCH, dialect ); scope.inTransaction( (session) -> { final EntityWithJavaTimeValues entity = new EntityWithJavaTimeValues(); @@ -115,7 +115,7 @@ void testInstant(SessionFactoryScope scope) { @Test void testLocalDateTime(SessionFactoryScope scope) { final Dialect dialect = scope.getSessionFactory().getJdbcServices().getDialect(); - final LocalDateTime start = roundToDefaultPrecision( LocalDateTime.now(), dialect ); + final LocalDateTime start = adjustToDefaultPrecision( LocalDateTime.now(), dialect ); scope.inTransaction( (session) -> { final EntityWithJavaTimeValues entity = new EntityWithJavaTimeValues(); @@ -145,7 +145,7 @@ void testLocalDateTime(SessionFactoryScope scope) { @Test void testLocalDate(SessionFactoryScope scope) { final Dialect dialect = scope.getSessionFactory().getJdbcServices().getDialect(); - final LocalDate startTime = roundToDefaultPrecision( LocalDate.now(), dialect ); + final LocalDate startTime = adjustToDefaultPrecision( LocalDate.now(), dialect ); scope.inTransaction( (session) -> { final EntityWithJavaTimeValues entity = new EntityWithJavaTimeValues(); @@ -178,7 +178,7 @@ void testLocalDate(SessionFactoryScope scope) { @SkipForDialect(dialectClass = AltibaseDialect.class, reason = "Altibase drivers truncate fractional seconds from the LocalTime") void testLocalTime(SessionFactoryScope scope) { final Dialect dialect = scope.getSessionFactory().getJdbcServices().getDialect(); - final LocalTime startTime = roundToDefaultPrecision( LocalTime.now(), dialect ); + final LocalTime startTime = adjustToDefaultPrecision( LocalTime.now(), dialect ); scope.inTransaction( (session) -> { final EntityWithJavaTimeValues entity = new EntityWithJavaTimeValues(); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/merge/BidirectionalOneToManyMergeTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/merge/BidirectionalOneToManyMergeTest.java new file mode 100644 index 000000000000..d9261f6be747 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/merge/BidirectionalOneToManyMergeTest.java @@ -0,0 +1,178 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.merge; + +import org.hibernate.testing.orm.junit.JiraKey; +import org.junit.Before; +import org.junit.Test; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import java.util.ArrayList; +import java.util.List; + +import static org.hibernate.testing.transaction.TransactionUtil.doInJPA; + +/** + * @author Lisandro Fernandez (kelechul at gmail dot com) + */ +@JiraKey("HHH-13815") +public class BidirectionalOneToManyMergeTest extends org.hibernate.orm.test.jpa.BaseEntityManagerFunctionalTestCase { + + @Override + protected Class[] getAnnotatedClasses() { + return new Class[]{ + Post.class, + PostComment.class, + }; + } + + @Before + public void setUp() { + doInJPA(this::entityManagerFactory, entityManager -> { + entityManager.persist( + new Post("High-Performance Java Persistence").setId(1L) + ); + }); + } + + @Test + public void testMerge() { + doInJPA(this::entityManagerFactory, entityManager -> { + Post post = entityManager.find(Post.class, 1L); + post.addComment(new PostComment("This post rocks!", post)); + post.getComments().isEmpty(); + entityManager.merge(post); + }); + } + + @Entity + public static class Post { + + @Id + private Long id; + + private String title; + + @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true) + private List comments = new ArrayList<>(); + + public Post() { + } + + public Post(String title) { + this.title = title; + } + + public Long getId() { + return id; + } + + public Post setId(Long id) { + this.id = id; + return this; + } + + public String getTitle() { + return title; + } + + public Post setTitle(String title) { + this.title = title; + return this; + } + + public List getComments() { + return comments; + } + + private Post setComments(List comments) { + this.comments = comments; + return this; + } + + public Post addComment(PostComment comment) { + comments.add(comment); + comment.setPost(this); + + return this; + } + + public Post removeComment(PostComment comment) { + comments.remove(comment); + comment.setPost(null); + + return this; + } + } + + @Entity + public static class PostComment { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String review; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "post_id") + private Post post; + + public PostComment() { + } + + public PostComment(String review, Post post) { + this.review = review; + this.post = post; + } + + public Long getId() { + return id; + } + + public PostComment setId(Long id) { + this.id = id; + return this; + } + + public String getReview() { + return review; + } + + public PostComment setReview(String review) { + this.review = review; + return this; + } + + public Post getPost() { + return post; + } + + public PostComment setPost(Post post) { + this.post = post; + return this; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (!(o instanceof PostComment)) return false; + return id != null && id.equals(((PostComment) o).getId()); + } + + @Override + public int hashCode() { + return 31; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/EmbeddableMetaModelTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/EmbeddableMetaModelTest.java index 7ae21c5c877a..3f64f5457ee2 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/EmbeddableMetaModelTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/EmbeddableMetaModelTest.java @@ -28,6 +28,8 @@ Person.class, Measurement.class, Height.class, + WeightClass.class, + Weight.class, } ) public class EmbeddableMetaModelTest { @Test @@ -62,4 +64,18 @@ public void test(EntityManagerFactoryScope scope) { assertNotNull( Measurement_.unit ); } ); } + + @Test + @Jira( "https://hibernate.atlassian.net/browse/HHH-18819" ) + public void testIdClass(EntityManagerFactoryScope scope) { + scope.inTransaction( entityManager -> { + final EmbeddableDomainType embeddable = (EmbeddableDomainType) entityManager.getMetamodel() + .embeddable( Weight.class ); + assertNotNull( embeddable.getSuperType() ); + assertEquals( MAPPED_SUPERCLASS, embeddable.getSuperType().getPersistenceType() ); + assertEquals( Measurement.class, embeddable.getSuperType().getJavaType() ); + assertNotNull( Weight_.weight ); + assertNotNull( Measurement_.unit ); + } ); + } } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/Weight.java b/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/Weight.java new file mode 100644 index 000000000000..c5557314bbfc --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/Weight.java @@ -0,0 +1,15 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.metamodel; + +import jakarta.persistence.Embeddable; + +/** + * @author Marco Belladelli + */ +@Embeddable +public class Weight extends Measurement { + private float weight; +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/WeightClass.java b/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/WeightClass.java new file mode 100644 index 000000000000..18d313b405a1 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/WeightClass.java @@ -0,0 +1,24 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.metamodel; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.IdClass; + +/** + * @author Marco Belladelli + */ +@Entity +@IdClass(Weight.class) +public class WeightClass { + @Id + private String unit; + + @Id + private float weight; + + private String description; +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/generics/embeddable/AbstractValueObject.java b/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/generics/embeddable/AbstractValueObject.java new file mode 100644 index 000000000000..3b9e40980920 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/generics/embeddable/AbstractValueObject.java @@ -0,0 +1,34 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.metamodel.generics.embeddable; + +import java.io.Serializable; + +import jakarta.persistence.Column; +import jakarta.persistence.MappedSuperclass; + +@MappedSuperclass +public abstract class AbstractValueObject> implements Serializable, + Comparable> { + public static final String VALUE = "value"; + + @Column( name = "value_col" ) + private V value; + + protected AbstractValueObject() { + super(); + } + + protected AbstractValueObject(final V value) { + this.value = value; + } + + @Override + public int compareTo(final AbstractValueObject object) { + return value.compareTo( object.value ); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/generics/embeddable/CreationDate.java b/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/generics/embeddable/CreationDate.java new file mode 100644 index 000000000000..21700a1cb159 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/generics/embeddable/CreationDate.java @@ -0,0 +1,22 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.metamodel.generics.embeddable; + +import java.util.Date; + +import jakarta.persistence.Embeddable; + +@Embeddable +public class CreationDate extends AbstractValueObject { + protected CreationDate() { + super(); + } + + public CreationDate(final Date value) { + super( value ); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/generics/embeddable/GenericEmbeddableSuperclassMetamodelTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/generics/embeddable/GenericEmbeddableSuperclassMetamodelTest.java new file mode 100644 index 000000000000..474085add4cd --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/generics/embeddable/GenericEmbeddableSuperclassMetamodelTest.java @@ -0,0 +1,80 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.metamodel.generics.embeddable; + +import java.util.Date; + +import org.hibernate.SessionFactory; + +import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.Jpa; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Tuple; +import jakarta.persistence.TypedQuery; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Expression; +import jakarta.persistence.criteria.Path; +import jakarta.persistence.criteria.Root; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Marco Belladelli + */ +@Jpa( annotatedClasses = { + Parent.class, + AbstractValueObject.class, + CreationDate.class, + SomeNumber.class, + SomeString.class, +} ) +@Jira( "https://hibernate.atlassian.net/browse/HHH-18490" ) +public class GenericEmbeddableSuperclassMetamodelTest { + @SuppressWarnings( { "rawtypes", "unchecked" } ) + @Test + public void test(EntityManagerFactoryScope scope) { + scope.inTransaction( entityManager -> { + final CriteriaBuilder cb = entityManager.getCriteriaBuilder(); + final CriteriaQuery cq = cb.createTupleQuery(); + final Root from = cq.from( Parent.class ); + + final Path someStringPath = from.get( Parent_.someString ).get( SomeString_.value ); + final Path someNumberPath = from.get( Parent_.someNumber ).get( SomeNumber_.value ); + final Path timestampPath = from.get( Parent_.date ).get( CreationDate_.value ); + final Expression maxNumber = cb.max( someNumberPath ); + final Expression maxTimestamp = cb.function( "max", Date.class, timestampPath ); + cq.select( cb.tuple( someStringPath, maxTimestamp, maxNumber ) ); + cq.groupBy( someStringPath ); + + final TypedQuery query = entityManager.createQuery( cq ); + final Tuple result = query.getSingleResult(); + + assertThat( result.get( 0, String.class ) ).isEqualTo( "something" ); + assertThat( result.get( 1, Date.class ) ).isNotNull(); + assertThat( result.get( 2, Object.class ) ).isEqualTo( 42 ); + } ); + } + + @BeforeAll + public void setUp(EntityManagerFactoryScope scope) { + scope.inTransaction( entityManager -> entityManager.persist( new Parent( + new SomeString( "something" ), + new CreationDate( new Date() ), + new SomeNumber( 42 ) + ) ) ); + } + + @AfterAll + public void tearDown(EntityManagerFactoryScope scope) { + scope.getEntityManagerFactory().unwrap( SessionFactory.class ).getSchemaManager().truncateMappedObjects(); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/generics/embeddable/Parent.java b/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/generics/embeddable/Parent.java new file mode 100644 index 000000000000..17898c5b48a4 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/generics/embeddable/Parent.java @@ -0,0 +1,44 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.metamodel.generics.embeddable; + +import jakarta.persistence.AttributeOverride; +import jakarta.persistence.Column; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; + +import static org.hibernate.orm.test.metamodel.generics.embeddable.AbstractValueObject.VALUE; + +@Entity +public class Parent { + @Id + @GeneratedValue + private Long id; + + @Embedded + @AttributeOverride( name = VALUE, column = @Column( name = "some_string" ) ) + private SomeString someString; + + @Embedded + @AttributeOverride( name = VALUE, column = @Column( name = "some_date" ) ) + private CreationDate date; + + @Embedded + @AttributeOverride( name = VALUE, column = @Column( name = "some_number" ) ) + private SomeNumber someNumber; + + public Parent() { + } + + public Parent(final SomeString someString, final CreationDate date, final SomeNumber someNumber) { + this.someString = someString; + this.date = date; + this.someNumber = someNumber; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/generics/embeddable/SomeNumber.java b/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/generics/embeddable/SomeNumber.java new file mode 100644 index 000000000000..3f618556ed33 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/generics/embeddable/SomeNumber.java @@ -0,0 +1,19 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.metamodel.generics.embeddable; + +import jakarta.persistence.Embeddable; + +@Embeddable +public class SomeNumber extends AbstractValueObject { + protected SomeNumber() { + } + + public SomeNumber(final Integer value) { + super( value ); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/generics/embeddable/SomeString.java b/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/generics/embeddable/SomeString.java new file mode 100644 index 000000000000..825a743d3801 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/generics/embeddable/SomeString.java @@ -0,0 +1,13 @@ +package org.hibernate.orm.test.metamodel.generics.embeddable; + +import jakarta.persistence.Embeddable; + +@Embeddable +public class SomeString extends AbstractValueObject { + protected SomeString() { + } + + public SomeString(final String value) { + super( value ); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/generics/embeddedid/BaseEntity.java b/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/generics/embeddedid/BaseEntity.java new file mode 100644 index 000000000000..7c6e8226e841 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/generics/embeddedid/BaseEntity.java @@ -0,0 +1,32 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.metamodel.generics.embeddedid; + +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.MappedSuperclass; + +@MappedSuperclass +public abstract class BaseEntity { + @EmbeddedId + private ID id; + + private String name; + + public BaseEntity() { + } + + public BaseEntity(ID id, String name) { + this.id = id; + this.name = name; + } + + public ID getId() { + return id; + } + + public String getName() { + return name; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/generics/embeddedid/EmployeeEntity.java b/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/generics/embeddedid/EmployeeEntity.java new file mode 100644 index 000000000000..7038062ff059 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/generics/embeddedid/EmployeeEntity.java @@ -0,0 +1,20 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.metamodel.generics.embeddedid; + +import jakarta.persistence.Entity; + +@Entity +public class EmployeeEntity extends BaseEntity { + private Integer employeeNumber; + + public EmployeeEntity() { + } + + public EmployeeEntity(EmployeeId id, String name, Integer employeeNumber) { + super( id, name ); + this.employeeNumber = employeeNumber; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/generics/embeddedid/EmployeeId.java b/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/generics/embeddedid/EmployeeId.java new file mode 100644 index 000000000000..763f5ff8d8ce --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/generics/embeddedid/EmployeeId.java @@ -0,0 +1,29 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.metamodel.generics.embeddedid; + +import jakarta.persistence.Embeddable; + +@Embeddable +public class EmployeeId { + private String employeeCode; + private PersonId personId; + + public EmployeeId() { + } + + public EmployeeId(String employeeCode, PersonId personId) { + this.employeeCode = employeeCode; + this.personId = personId; + } + + public String getEmployeeCode() { + return employeeCode; + } + + public PersonId getPersonId() { + return personId; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/generics/embeddedid/GenericEmbeddedIdMetamodelTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/generics/embeddedid/GenericEmbeddedIdMetamodelTest.java new file mode 100644 index 000000000000..e4725a52d15c --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/generics/embeddedid/GenericEmbeddedIdMetamodelTest.java @@ -0,0 +1,79 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.metamodel.generics.embeddedid; + +import jakarta.persistence.criteria.Path; +import org.hibernate.SessionFactory; +import org.hibernate.query.sqm.tree.domain.SqmPath; +import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; +import org.hibernate.testing.orm.junit.Jpa; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + + +/** + * @author Marco Belladelli + */ +@Jpa(annotatedClasses = { + BaseEntity.class, + PersonEntity.class, + PersonId.class, + EmployeeEntity.class, + EmployeeId.class, +}) +public class GenericEmbeddedIdMetamodelTest { + @SuppressWarnings({"unchecked", "rawtypes"}) + @Test + public void test(EntityManagerFactoryScope scope) { + scope.inTransaction( entityManager -> { + var criteriaBuilder = entityManager.getCriteriaBuilder(); + var query = criteriaBuilder.createQuery( EmployeeEntity.class ); + + var employee = query.from( EmployeeEntity.class ); + var person = query.from( PersonEntity.class ); + + final Path employeeId = employee.get( EmployeeEntity_.id ); + // The Path.getModel() method returns the generic (Object) type, whereas our getResolvedModel() + // returns the correct EmployeeId type. + assertThat( employeeId.getModel().getBindableJavaType() ).isEqualTo( Object.class ); + assertThat( ((SqmPath) employeeId).getResolvedModel().getBindableJavaType() ).isEqualTo( EmployeeId.class ); + final Path employeePersonId = employeeId.get( EmployeeId_.personId ); + assertThat( employeePersonId.getModel().getBindableJavaType() ).isEqualTo( PersonId.class ); + final var personId = person.get( PersonEntity_.id ); + // Same as before: generic vs resolved type + assertThat( personId.getModel().getBindableJavaType() ).isEqualTo( Object.class ); + assertThat( ((SqmPath) employeeId).getResolvedModel().getBindableJavaType() ).isEqualTo( EmployeeId.class ); + + var equal = criteriaBuilder.equal( + employeePersonId, + personId + ); + + query.select( employee ).where( equal ); + + final var result = entityManager.createQuery( query ).getSingleResult(); + assertThat( result.getName() ).isEqualTo( "Employee One" ); + assertThat( result.getId().getPersonId().getIdentifier() ).isEqualTo( 1L ); + } ); + } + + @BeforeAll + public void setUp(EntityManagerFactoryScope scope) { + scope.inTransaction( entityManager -> { + entityManager.persist( new PersonEntity( new PersonId( 1L ), "Person One" ) ); + entityManager.persist( new PersonEntity( new PersonId( 2L ), "Person Two" ) ); + entityManager.persist( + new EmployeeEntity( new EmployeeId( "E001", new PersonId( 1L ) ), "Employee One", 1001 ) ); + } ); + } + + @AfterAll + public void tearDown(EntityManagerFactoryScope scope) { + scope.getEntityManagerFactory().unwrap( SessionFactory.class ).getSchemaManager().truncateMappedObjects(); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/generics/embeddedid/PersonEntity.java b/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/generics/embeddedid/PersonEntity.java new file mode 100644 index 000000000000..1990aa9996a6 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/generics/embeddedid/PersonEntity.java @@ -0,0 +1,17 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.metamodel.generics.embeddedid; + +import jakarta.persistence.Entity; + +@Entity +public class PersonEntity extends BaseEntity { + public PersonEntity() { + } + + public PersonEntity(PersonId id, String name) { + super( id, name ); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/generics/embeddedid/PersonId.java b/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/generics/embeddedid/PersonId.java new file mode 100644 index 000000000000..0420dabe3c9f --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/metamodel/generics/embeddedid/PersonId.java @@ -0,0 +1,23 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.metamodel.generics.embeddedid; + +import jakarta.persistence.Embeddable; + +@Embeddable +public class PersonId { + private Long identifier; + + public PersonId() { + } + + public PersonId(Long identifier) { + this.identifier = identifier; + } + + public Long getIdentifier() { + return identifier; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/notfound/CompositeForeignKeyNotFoundTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/notfound/CompositeForeignKeyNotFoundTest.java new file mode 100644 index 000000000000..27fe7a028481 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/notfound/CompositeForeignKeyNotFoundTest.java @@ -0,0 +1,144 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.notfound; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import org.hibernate.FetchNotFoundException; +import org.hibernate.annotations.JoinColumnOrFormula; +import org.hibernate.annotations.JoinFormula; +import org.hibernate.annotations.NotFound; +import org.hibernate.annotations.NotFoundAction; +import org.hibernate.query.Query; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; + +@JiraKey(value = "HHH-18891") +@DomainModel( + annotatedClasses = {CompositeForeignKeyNotFoundTest.Document.class, CompositeForeignKeyNotFoundTest.DocumentIgnore.class, + CompositeForeignKeyNotFoundTest.DocumentException.class, CompositeForeignKeyNotFoundTest.Person.class}) +@SessionFactory +public class CompositeForeignKeyNotFoundTest { + + @Test + void hhh18891TestWithNotFoundIgnore(SessionFactoryScope scope) { + + // prepare document + scope.inTransaction( session -> { + Query nativeQuery = session.createNativeQuery( + "insert into DocumentIgnore (id,owner) values (123,42)" ); + nativeQuery.executeUpdate(); + } ); + + // assert document + scope.inTransaction( session -> { + final DocumentIgnore document = session.find( DocumentIgnore.class, 123 ); + assertNotNull( document ); + assertEquals( 123, document.id ); + assertNull( document.owner ); + } ); + } + + @Test + void hhh18891TestWithNotFoundException(SessionFactoryScope scope) { + + // prepare document + scope.inTransaction( session -> { + Query nativeQuery = session.createNativeQuery( + "insert into DocumentException (id,owner) values (123,42)" ); + nativeQuery.executeUpdate(); + } ); + + // assert document + scope.inTransaction( session -> { + assertThrows( FetchNotFoundException.class, () -> + session.find( DocumentException.class, 123 ) ); + } ); + } + + @Test + void hhh18891TestWithoutNotFoundAnnotation(SessionFactoryScope scope) { + + // prepare document + scope.inTransaction( session -> { + Query nativeQuery = session.createNativeQuery( + "insert into Document (id,owner) values (123,42)" ); + nativeQuery.executeUpdate(); + } ); + + // assert document + scope.inTransaction( session -> { + final Document document = session.find( Document.class, 123 ); + assertNotNull( document ); + assertEquals( 123, document.id ); + assertNull( document.owner ); + } ); + } + + @Entity(name = "DocumentIgnore") + public static class DocumentIgnore { + + @Id + @GeneratedValue + Long id; + + @ManyToOne + @NotFound(action = NotFoundAction.IGNORE) + @JoinColumnOrFormula(column = @JoinColumn(name = "owner", referencedColumnName = "id", insertable = false, + updatable = false)) + @JoinColumnOrFormula(formula = @JoinFormula(value = "'fubar'", referencedColumnName = "name")) + Person owner; + } + + @Entity(name = "DocumentException") + public static class DocumentException { + + @Id + @GeneratedValue + Long id; + + @ManyToOne + @NotFound(action = NotFoundAction.EXCEPTION) + @JoinColumnOrFormula(column = @JoinColumn(name = "owner", referencedColumnName = "id", insertable = false, + updatable = false)) + @JoinColumnOrFormula(formula = @JoinFormula(value = "'fubar'", referencedColumnName = "name")) + Person owner; + } + + @Entity(name = "Document") + public static class Document { + + @Id + @GeneratedValue + Long id; + + @ManyToOne + @JoinColumnOrFormula(column = @JoinColumn(name = "owner", referencedColumnName = "id", insertable = false, + updatable = false)) + @JoinColumnOrFormula(formula = @JoinFormula(value = "'fubar'", referencedColumnName = "name")) + Person owner; + } + + @Entity(name = "Person") + public static class Person { + + @Id + Long id; + + String name; + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/ondeletecascade/OnDeleteCascadeRemoveTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/ondeletecascade/OnDeleteCascadeRemoveTest.java new file mode 100644 index 000000000000..c6b26aa4806c --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/ondeletecascade/OnDeleteCascadeRemoveTest.java @@ -0,0 +1,124 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.orm.test.ondeletecascade; + +import jakarta.persistence.Cacheable; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import org.hibernate.Hibernate; +import org.hibernate.SessionFactory; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; +import org.hibernate.engine.spi.SessionImplementor; +import org.hibernate.stat.EntityStatistics; +import org.hibernate.stat.Statistics; +import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; +import org.hibernate.testing.orm.junit.Jpa; +import org.junit.jupiter.api.Test; + +import java.util.HashSet; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Jpa(annotatedClasses = + {OnDeleteCascadeRemoveTest.Parent.class, + OnDeleteCascadeRemoveTest.Child.class}, + generateStatistics = true, + useCollectingStatementInspector = true) +//@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsCascadeDeleteCheck.class) +class OnDeleteCascadeRemoveTest { + @Test + void testOnDeleteCascadeRemove1(EntityManagerFactoryScope scope) { + Statistics statistics = + scope.getEntityManagerFactory().unwrap( SessionFactory.class ) + .getStatistics(); + statistics.clear(); + var inspector = scope.getCollectingStatementInspector(); + inspector.clear(); + scope.inTransaction( em -> { + Parent parent = new Parent(); + Child child = new Child(); + parent.children.add( child ); + child.parent = parent; + em.persist( parent ); + } ); + scope.inTransaction( em -> { + Parent parent = em.find( Parent.class, 0L ); + assertFalse( Hibernate.isInitialized( parent.children ) ); + em.remove( parent ); + // note: ideally we would skip the initialization here + assertTrue( Hibernate.isInitialized( parent.children ) ); + }); + EntityStatistics entityStatistics = statistics.getEntityStatistics( Child.class.getName() ); + assertEquals( 1L, entityStatistics.getDeleteCount() ); + inspector.assertExecutedCount( scope.getDialect().supportsCascadeDelete() ? 5 : 6 ); + long children = + scope.fromTransaction( em -> em.createQuery( "select count(*) from CascadeChild", Long.class ) + .getSingleResult() ); + assertEquals( 0L, children ); + } + + @Test + void testOnDeleteCascadeRemove2(EntityManagerFactoryScope scope) { + Statistics statistics = + scope.getEntityManagerFactory().unwrap( SessionFactory.class ) + .getStatistics(); + statistics.clear(); + var inspector = scope.getCollectingStatementInspector(); + inspector.clear(); + scope.inTransaction( em -> { + Parent parent = new Parent(); + Child child = new Child(); + parent.children.add( child ); + child.parent = parent; + em.persist( parent ); + } ); + scope.inTransaction( em -> { + Parent parent = em.find( Parent.class, 0L ); + assertEquals(1, parent.children.size()); + assertTrue( Hibernate.isInitialized( parent.children ) ); + em.remove( parent ); + assertTrue( em.unwrap( SessionImplementor.class ) + .getPersistenceContext() + .getEntry( parent.children.iterator().next() ) + .getStatus().isDeletedOrGone() ); + }); + EntityStatistics entityStatistics = statistics.getEntityStatistics( Child.class.getName() ); + assertEquals( 1L, entityStatistics.getDeleteCount() ); + inspector.assertExecutedCount( scope.getDialect().supportsCascadeDelete() ? 5 : 6 ); + long children = + scope.fromTransaction( em -> em.createQuery( "select count(c.id) from CascadeChild c", Long.class ) + .getSingleResult() ); + assertEquals( 0L, children ); + } + + @Entity(name="CascadeParent") + static class Parent { + @Id + long id; + @OneToMany(mappedBy = "parent", + cascade = {CascadeType.PERSIST, CascadeType.REMOVE}) + @OnDelete(action = OnDeleteAction.CASCADE) + Set children = new HashSet<>(); + } + @Entity(name="CascadeChild") + @Cacheable +// @SQLDelete( sql = "should never happen" ) + static class Child { + @Id + long id; + @OnDelete(action = OnDeleteAction.CASCADE) + @ManyToOne + Parent parent; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/ondeletecascade/OnDeleteCollectionTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/ondeletecascade/OnDeleteCollectionTest.java new file mode 100644 index 000000000000..9975c135dbde --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/ondeletecascade/OnDeleteCollectionTest.java @@ -0,0 +1,82 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.orm.test.ondeletecascade; + +import org.hibernate.Hibernate; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; + +import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.Jpa; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.ElementCollection; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import java.util.HashSet; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; + +@Jpa(annotatedClasses = + {OnDeleteCollectionTest.A.class}, + useCollectingStatementInspector = true) +class OnDeleteCollectionTest { + @Test + void test(EntityManagerFactoryScope scope) { + var inspector = scope.getCollectingStatementInspector(); + inspector.clear(); + + scope.inTransaction( em -> { + A a = new A(); + a.id = 2; + a.bs.add( "b" ); + em.persist( a ); + } ); + inspector.assertExecutedCount( 2 ); + inspector.clear(); + + scope.inTransaction( em -> { + A a = em.find( A.class, 2L ); + inspector.assertExecutedCount( 1 ); + assertEquals( 1, a.bs.size() ); + inspector.assertExecutedCount( 2 ); + assertTrue( Hibernate.isInitialized( a.bs ) ); + } ); + inspector.clear(); + + scope.inTransaction( em -> { + A a = em.find( A.class, 2L ); + inspector.assertExecutedCount( 1 ); + em.remove( a ); + assertFalse( Hibernate.isInitialized( a.bs ) ); + } ); + inspector.assertExecutedCount( scope.getDialect().supportsCascadeDelete() ? 2 : 3 ); + + scope.inTransaction( em -> { + assertEquals( 0, + em.createNativeQuery( "select count(*) from A_bs", Integer.class ) + .getSingleResult() ); + assertEquals( 0, + em.createNativeQuery( "select count(*) from A", Integer.class ) + .getSingleResult() ); + }); + } + + @Entity(name = "A") + static class A { + @Id + long id; + boolean a; + @ElementCollection + @OnDelete(action = OnDeleteAction.CASCADE) + Set bs = new HashSet<>(); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/ondeletecascade/OnDeleteJoinedInheritanceTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/ondeletecascade/OnDeleteJoinedInheritanceTest.java new file mode 100644 index 000000000000..68e76bf55ebd --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/ondeletecascade/OnDeleteJoinedInheritanceTest.java @@ -0,0 +1,84 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.orm.test.ondeletecascade; + +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; + +import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; +import org.hibernate.testing.orm.junit.Jpa; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Inheritance; +import jakarta.persistence.InheritanceType; + +import static org.junit.Assert.assertEquals; + +@Jpa(annotatedClasses = + {OnDeleteJoinedInheritanceTest.A.class, + OnDeleteJoinedInheritanceTest.B.class, + OnDeleteJoinedInheritanceTest.C.class}, + useCollectingStatementInspector = true) +//@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsCascadeDeleteCheck.class) +class OnDeleteJoinedInheritanceTest { + @Test void test(EntityManagerFactoryScope scope) { + var inspector = scope.getCollectingStatementInspector(); + scope.inTransaction( em -> { + B b = new B(); + b.id = 1; + em.persist( b ); + C c = new C(); + c.id = 2; + em.persist( c ); + } ); + inspector.assertExecutedCount( 4 ); + inspector.clear(); + + scope.inTransaction( em -> { + A b = em.find( A.class, 1L ); + A c = em.getReference( A.class, 2L ); + inspector.assertExecutedCount( 1 ); + em.remove( b ); + em.remove( c ); + } ); + inspector.assertExecutedCount( scope.getDialect().supportsCascadeDelete() ? 4 : 6 ); + + scope.inTransaction( em -> { + assertEquals( 0, + em.createNativeQuery( "select count(*) from B", Integer.class ) + .getSingleResult() ); + assertEquals( 0, + em.createNativeQuery( "select count(*) from C", Integer.class ) + .getSingleResult() ); + assertEquals( 0, + em.createNativeQuery( "select count(*) from A", Integer.class ) + .getSingleResult() ); + }); + } + + @Entity(name = "A") + @Inheritance(strategy = InheritanceType.JOINED) + static class A { + @Id + long id; + boolean a; + } + + @Entity(name = "B") + @OnDelete(action = OnDeleteAction.CASCADE) + static class B extends A { + long b; + } + + @Entity(name = "C") + @OnDelete(action = OnDeleteAction.CASCADE) + static class C extends A { + int c; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/ondeletecascade/OnDeleteManyToOneTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/ondeletecascade/OnDeleteManyToOneTest.java new file mode 100644 index 000000000000..e3bec0798c70 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/ondeletecascade/OnDeleteManyToOneTest.java @@ -0,0 +1,106 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.orm.test.ondeletecascade; + +import org.hibernate.annotations.OnDelete; +import org.hibernate.internal.SessionFactoryImpl; + +import org.hibernate.testing.orm.junit.DialectFeatureChecks; +import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; +import org.hibernate.testing.orm.junit.Jpa; +import org.hibernate.testing.orm.junit.RequiresDialectFeature; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import java.util.HashSet; +import java.util.Set; + +import static jakarta.persistence.FetchType.EAGER; +import static org.hibernate.annotations.OnDeleteAction.CASCADE; +import static org.junit.jupiter.api.Assertions.assertNull; + +@Jpa(annotatedClasses = {OnDeleteManyToOneTest.Parent.class, OnDeleteManyToOneTest.Child.class}) +@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsCascadeDeleteCheck.class) +public class OnDeleteManyToOneTest { + @Test + public void testOnDelete(EntityManagerFactoryScope scope) { + Parent parent = new Parent(); + Child child = new Child(); + child.parent = parent; + parent.children.add( child ); + scope.inTransaction( em -> { + em.persist( parent ); + em.persist( child ); + } ); + scope.inTransaction( em -> { + Parent p = em.find( Parent.class, parent.id ); + em.remove( p ); + } ); + scope.inTransaction( em -> { + assertNull( em.find( Child.class, child.id ) ); + } ); + } + + @Test + public void testOnDeleteReference(EntityManagerFactoryScope scope) { + Parent parent = new Parent(); + Child child = new Child(); + child.parent = parent; + parent.children.add( child ); + scope.inTransaction( em -> { + em.persist( parent ); + em.persist( child ); + } ); + scope.inTransaction( em -> em.remove( em.getReference( Parent.class, parent.id ) ) ); + scope.inTransaction( em -> assertNull( em.find( Child.class, child.id ) ) ); + } + + @Test + public void testOnDeleteInReverse(EntityManagerFactoryScope scope) { + Parent parent = new Parent(); + Child child = new Child(); + child.parent = parent; + parent.children.add( child ); + scope.inTransaction( em -> { + em.persist( parent ); + em.persist( child ); + } ); + scope.inTransaction( em -> { + Child c = em.find( Child.class, child.id ); + em.remove( c ); + } ); + scope.inTransaction( em -> { + assertNull( em.find( Child.class, child.id ) ); + } ); + } + + @AfterEach + public void tearDown(EntityManagerFactoryScope scope) { + scope.getEntityManagerFactory().unwrap( SessionFactoryImpl.class ).getSchemaManager().truncateMappedObjects(); + } + + @Entity + static class Parent { + @Id + long id; + @OneToMany(mappedBy = "parent", fetch = EAGER) + Set children = new HashSet<>(); + } + + @Entity + static class Child { + @Id + long id; + @ManyToOne + @OnDelete(action = CASCADE) + Parent parent; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/ondeletecascade/OnDeleteOneToManyTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/ondeletecascade/OnDeleteOneToManyTest.java new file mode 100644 index 000000000000..fa75150ca88f --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/ondeletecascade/OnDeleteOneToManyTest.java @@ -0,0 +1,107 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.orm.test.ondeletecascade; + +import org.hibernate.Hibernate; +import org.hibernate.TransientObjectException; +import org.hibernate.annotations.OnDelete; +import org.hibernate.annotations.OnDeleteAction; +import org.hibernate.internal.SessionFactoryImpl; + +import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; +import org.hibernate.testing.orm.junit.Jpa; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToMany; +import jakarta.persistence.RollbackException; +import java.util.HashSet; +import java.util.Set; + +import static jakarta.persistence.FetchType.EAGER; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +@Jpa(annotatedClasses = + {OnDeleteOneToManyTest.Parent.class, OnDeleteOneToManyTest.Child.class}, + useCollectingStatementInspector = true) +//@RequiresDialectFeature(feature = DialectFeatureChecks.SupportsCascadeDeleteCheck.class) +public class OnDeleteOneToManyTest { + @Test + public void testOnDeleteParent(EntityManagerFactoryScope scope) { + var inspector = scope.getCollectingStatementInspector(); + inspector.clear(); + Parent parent = new Parent(); + Child child = new Child(); + parent.children.add( child ); + scope.inTransaction( em -> { + em.persist( parent ); + em.persist( child ); + } ); + inspector.assertExecutedCount( 3 ); + inspector.clear(); + scope.inTransaction( em -> { + Parent p = em.find( Parent.class, parent.id ); + inspector.assertExecutedCount( 1 ); + assertTrue( Hibernate.isInitialized( p.children ) ); + em.remove( p ); + } ); + inspector.assertExecutedCount( 3 ); + scope.inTransaction( em -> { + // since it's an owned collection, the FK gets set to null + assertNotNull( em.find( Child.class, child.id ) ); + } ); + } + + @Test + public void testOnDeleteChildrenFails(EntityManagerFactoryScope scope) { + Parent parent = new Parent(); + Child child = new Child(); + parent.children.add( child ); + scope.inTransaction( em -> { + em.persist( parent ); + em.persist( child ); + } ); + try { + scope.inTransaction( em -> { + Parent p = em.find( Parent.class, parent.id ); + for ( Child c : p.children ) { + em.remove( c ); + } + } ); + fail(); + } + catch (RollbackException re) { + assertTrue(re.getCause().getCause() instanceof TransientObjectException); + } + } + + @AfterEach + public void tearDown(EntityManagerFactoryScope scope) { + scope.getEntityManagerFactory().unwrap( SessionFactoryImpl.class ).getSchemaManager().truncateMappedObjects(); + } + + @Entity + static class Parent { + @Id + long id; + @OneToMany(fetch = EAGER) + @JoinColumn(name = "parent_id") + @OnDelete(action = OnDeleteAction.CASCADE) + Set children = new HashSet<>(); + } + + @Entity + static class Child { + @Id + long id; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/onetoone/embeddedid/OneToOneEmbeddedIdTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/onetoone/embeddedid/OneToOneEmbeddedIdTest.java new file mode 100644 index 000000000000..9f308f40cdfe --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/onetoone/embeddedid/OneToOneEmbeddedIdTest.java @@ -0,0 +1,247 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.onetoone.embeddedid; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +@DomainModel( + annotatedClasses = { + OneToOneEmbeddedIdTest.EntityA.class, + OneToOneEmbeddedIdTest.EntityB.class, + } +) +@SessionFactory +@BytecodeEnhanced(runNotEnhancedAsWell = true) +@JiraKey("HHH-17838") +public class OneToOneEmbeddedIdTest { + + private static final String ENTITY_A_NAME = "a"; + private static final String ENTITY_B_NAME = "B"; + + private static final Integer ENTITY_A_ID1 = 1; + private static final String ENTITY_A_ID2 = "1"; + + private static final Integer ENTITY_B_ID1 = 2; + private static final String ENTITY_B_ID2 = "2"; + + @BeforeEach + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + EntityAKey entityAKey = new EntityAKey( ENTITY_A_ID1, ENTITY_A_ID2 ); + EntityA entityA = new EntityA( entityAKey, ENTITY_A_NAME ); + + EntityBKey entityBKey = new EntityBKey( ENTITY_B_ID1, ENTITY_B_ID2 ); + EntityB entityB = new EntityB( entityBKey, entityA, ENTITY_B_NAME ); + + session.persist( entityA ); + session.persist( entityB ); + } + ); + } + + @AfterEach + public void tearDown(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } + + @Test + public void testFind(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + EntityAKey entityAKey = new EntityAKey( ENTITY_A_ID1, ENTITY_A_ID2 ); + EntityA entityA = session.find( EntityA.class, entityAKey ); + assertThat( entityA ).isNotNull(); + assertThat( entityA.getName() ).isEqualTo( ENTITY_A_NAME ); + + EntityB entityB = entityA.getEntityB(); + assertThat( entityB ).isNotNull(); + + EntityBKey key = entityB.getEntityBKey(); + assertThat( key.id1 ).isEqualTo( ENTITY_B_ID1 ); + assertThat( key.id2 ).isEqualTo( ENTITY_B_ID2 ); + + assertThat( entityB.getName() ).isEqualTo( ENTITY_B_NAME ); + } + ); + } + + @Test + public void testFind2(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + EntityBKey entityBKey = new EntityBKey( ENTITY_B_ID1, ENTITY_B_ID2 ); + EntityB entityB = session.find( EntityB.class, entityBKey ); + assertThat( entityB ).isNotNull(); + assertThat( entityB.getName() ).isEqualTo( ENTITY_B_NAME ); + + EntityA entityA = entityB.getEntityA(); + assertThat( entityA ).isNotNull(); + + EntityAKey entityAKey = entityA.getEntityAKey(); + assertThat( entityAKey.id1 ).isEqualTo( ENTITY_A_ID1 ); + assertThat( entityAKey.id2 ).isEqualTo( ENTITY_A_ID2 ); + + assertThat( entityA.getName() ).isEqualTo( ENTITY_A_NAME ); + } + ); + } + + @Test + public void testNativeQuery(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + String query = "select a.* from ENTITY_A a"; + List entityAS = session.createNativeQuery( query, EntityA.class ).list(); + assertThat( entityAS.size() ).isEqualTo( 1 ); + EntityA entityA = entityAS.get( 0 ); + assertThat( entityA.getName() ).isEqualTo( ENTITY_A_NAME ); + assertThat( entityA.getEntityB().getName() ).isEqualTo( ENTITY_B_NAME ); + } + ); + } + + @Test + public void testNativeQuery2(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + String query = "select b.* from ENTITY_B b"; + List entityBS = session.createNativeQuery( query, EntityB.class ).list(); + assertThat( entityBS.size() ).isEqualTo( 1 ); + + EntityB entityB = entityBS.get( 0 ); + assertThat( entityB.getName()).isEqualTo( ENTITY_B_NAME ); + assertThat( entityB.getEntityA().getName()).isEqualTo( ENTITY_A_NAME ); + } + ); + } + + @Entity(name = "EntityA") + @Table(name= "ENTITY_A") + public static class EntityA { + + @EmbeddedId + private EntityAKey entityAKey; + + private String name; + + @OneToOne(mappedBy = "entityA", fetch = FetchType.LAZY) + private EntityB entityB; + + public EntityA() { + } + + public EntityA(EntityAKey key, String name) { + this.entityAKey = key; + this.name = name; + } + + public EntityAKey getEntityAKey() { + return entityAKey; + } + + public String getName() { + return name; + } + + public EntityB getEntityB() { + return entityB; + } + } + + @Embeddable + public static class EntityAKey { + + @Column(name = "id1") + private Integer id1; + + @Column(name = "id2") + private String id2; + + public EntityAKey() { + } + + public EntityAKey(Integer id1, String id2) { + this.id1 = id1; + this.id2 = id2; + } + } + + @Entity(name = "EntityB") + @Table(name = "ENTITY_B") + public static class EntityB { + @EmbeddedId + private EntityBKey entityBKey; + + private String name; + + @OneToOne(optional = false, fetch = FetchType.LAZY) +// @JoinColumns({ +// @JoinColumn(name = "id1", referencedColumnName = "id1", nullable = false, +// insertable = false, updatable = false), +// @JoinColumn(name = "id2", referencedColumnName = "id2", nullable = false, +// insertable = false, updatable = false) +// }) + private EntityA entityA; + + public EntityB() { + } + + public EntityB(EntityBKey key, EntityA testEntity, String name) { + this.entityBKey = key; + this.entityA = testEntity; + testEntity.entityB = this; + this.name = name; + } + + public EntityBKey getEntityBKey() { + return entityBKey; + } + + public EntityA getEntityA() { + return entityA; + } + + public String getName() { + return name; + } + } + + @Embeddable + public static class EntityBKey { + + @Column(name = "id1") + private Integer id1; + + @Column(name = "id2") + private String id2; + + public EntityBKey() { + } + + public EntityBKey(Integer documentType, String no) { + this.id1 = documentType; + this.id2 = no; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/onetoone/embeddedid/OneToOneJoinColumnsEmbeddedIdTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/onetoone/embeddedid/OneToOneJoinColumnsEmbeddedIdTest.java new file mode 100644 index 000000000000..58575fc6250f --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/onetoone/embeddedid/OneToOneJoinColumnsEmbeddedIdTest.java @@ -0,0 +1,235 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.onetoone.embeddedid; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.JoinColumns; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; +import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +@DomainModel( + annotatedClasses = { + OneToOneJoinColumnsEmbeddedIdTest.EntityA.class, + OneToOneJoinColumnsEmbeddedIdTest.EntityB.class, + } +) +@SessionFactory +@BytecodeEnhanced(runNotEnhancedAsWell = true) +@JiraKey("HHH-17838") +public class OneToOneJoinColumnsEmbeddedIdTest { + + private static final String ENTITY_A_NAME = "a"; + private static final String ENTITY_B_NAME = "B"; + + private static final Integer ENTITY_A_ID1 = 1; + private static final String ENTITY_A_ID2 = "1"; + + private static final Integer ENTITY_B_ID1 = 1; + private static final String ENTITY_B_ID2 = "1"; + + @BeforeEach + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + EntityAKey entityAKey = new EntityAKey( ENTITY_A_ID1, ENTITY_A_ID2 ); + EntityA entityA = new EntityA( entityAKey, ENTITY_A_NAME ); + + EntityBKey entityBKey = new EntityBKey( ENTITY_B_ID1, ENTITY_B_ID2 ); + EntityB entityB = new EntityB( entityBKey, entityA, ENTITY_B_NAME ); + + session.persist( entityA ); + session.persist( entityB ); + } + ); + } + + @AfterEach + public void tearDown(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } + + @Test + public void testFind(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + EntityAKey entityAKey = new EntityAKey( ENTITY_A_ID1, ENTITY_A_ID2 ); + EntityA entityA = session.find( EntityA.class, entityAKey ); + assertThat( entityA ).isNotNull(); + assertThat( entityA.getName() ).isEqualTo( ENTITY_A_NAME ); + + EntityB entityB = entityA.getEntityB(); + assertThat( entityB ).isNotNull(); + + EntityBKey key = entityB.getEntityBKey(); + assertThat( key.id1 ).isEqualTo( ENTITY_B_ID1 ); + assertThat( key.id2 ).isEqualTo( ENTITY_B_ID2 ); + + assertThat( entityB.getName() ).isEqualTo( ENTITY_B_NAME ); + } + ); + } + + @Test + public void testFind2(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + EntityBKey entityBKey = new EntityBKey( ENTITY_B_ID1, ENTITY_B_ID2 ); + EntityB entityB = session.find( EntityB.class, entityBKey ); + assertThat( entityB ).isNotNull(); + assertThat( entityB.getName() ).isEqualTo( ENTITY_B_NAME ); + + EntityA entityA = entityB.getEntityA(); + assertThat( entityA ).isNotNull(); + + EntityAKey entityAKey = entityA.getEntityAKey(); + assertThat( entityAKey.id1 ).isEqualTo( ENTITY_A_ID1 ); + assertThat( entityAKey.id2 ).isEqualTo( ENTITY_A_ID2 ); + + assertThat( entityA.getName() ).isEqualTo( ENTITY_A_NAME ); + } + ); + } + + @Test + public void testNativeQuery2(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + String query = "select b.* from ENTITY_B b"; + List entityBS = session.createNativeQuery( query, EntityB.class ).list(); + assertThat( entityBS.size() ).isEqualTo( 1 ); + + EntityB entityB = entityBS.get( 0 ); + assertThat( entityB.getName()).isEqualTo( ENTITY_B_NAME ); + assertThat( entityB.getEntityA().getName()).isEqualTo( ENTITY_A_NAME ); + } + ); + } + + @Entity(name = "EntityA") + @Table(name= "ENTITY_A") + public static class EntityA { + + @EmbeddedId + private EntityAKey entityAKey; + + private String name; + + @OneToOne(mappedBy = "entityA", fetch = FetchType.LAZY) + private EntityB entityB; + + public EntityA() { + } + + public EntityA(EntityAKey key, String name) { + this.entityAKey = key; + this.name = name; + } + + public EntityAKey getEntityAKey() { + return entityAKey; + } + + public String getName() { + return name; + } + + public EntityB getEntityB() { + return entityB; + } + } + + @Embeddable + public static class EntityAKey { + + @Column(name = "id1") + private Integer id1; + + @Column(name = "id2") + private String id2; + + public EntityAKey() { + } + + public EntityAKey(Integer id1, String id2) { + this.id1 = id1; + this.id2 = id2; + } + } + + @Entity(name = "EntityB") + @Table(name = "ENTITY_B") + public static class EntityB { + @EmbeddedId + private EntityBKey entityBKey; + + private String name; + + @OneToOne(optional = false, fetch = FetchType.LAZY) + @JoinColumns({ + @JoinColumn(name = "id1", referencedColumnName = "id1", nullable = false, + insertable = false, updatable = false), + @JoinColumn(name = "id2", referencedColumnName = "id2", nullable = false, + insertable = false, updatable = false) + }) + private EntityA entityA; + + public EntityB() { + } + + public EntityB(EntityBKey key, EntityA testEntity, String name) { + this.entityBKey = key; + this.entityA = testEntity; + testEntity.entityB = this; + this.name = name; + } + + public EntityBKey getEntityBKey() { + return entityBKey; + } + + public EntityA getEntityA() { + return entityA; + } + + public String getName() { + return name; + } + } + + @Embeddable + public static class EntityBKey { + + @Column(name = "id1") + private Integer id1; + + @Column(name = "id2") + private String id2; + + public EntityBKey() { + } + + public EntityBKey(Integer documentType, String no) { + this.id1 = documentType; + this.id2 = no; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/ops/MergeExplicitInitialVersionTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/ops/MergeExplicitInitialVersionTest.java new file mode 100644 index 000000000000..cb49e2e274b4 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/ops/MergeExplicitInitialVersionTest.java @@ -0,0 +1,71 @@ +package org.hibernate.orm.test.ops; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.Version; +import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; +import org.hibernate.testing.orm.junit.Jpa; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@Jpa(annotatedClasses = {MergeExplicitInitialVersionTest.E.class, + MergeExplicitInitialVersionTest.F.class, + MergeExplicitInitialVersionTest.G.class}) +public class MergeExplicitInitialVersionTest { + @Test public void testGeneratedId(EntityManagerFactoryScope scope) { + E e = new E(); + scope.inTransaction(s->s.persist(e)); + assertEquals(e.version, 1); + e.text = "hello"; + E e2 = scope.fromTransaction(s->s.merge(e)); + assertEquals(e2.version, 2); + } + @Test public void testAssignedId(EntityManagerFactoryScope scope) { + F f = new F(); + scope.inTransaction(s->s.persist(f)); + assertEquals(f.version, 1); + f.text = "hello"; + F f2 = scope.fromTransaction(s->s.merge(f)); + assertEquals(f2.version, 2); + } + @Test public void testNegativeVersion(EntityManagerFactoryScope scope) { + G g = new G(); + scope.inTransaction(s->s.persist(g)); + assertEquals(g.version, 0); + g.text = "hello"; + G g2 = scope.fromTransaction(s->s.merge(g)); + assertEquals(g2.version, 1); + } + + @Entity + static class E { + @Id + @GeneratedValue + long id; + @Version + int version = 1; + String text; + } + + @Entity + static class F { + @Id + long id = 5; + @Version + int version = 1; + String text; + } + + @Entity + static class G { + @Id + @GeneratedValue + long id; + @Version + int version = -1; + String text; + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/optlock/OptimisticLockTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/optlock/OptimisticLockTest.java index 30096e56aaa6..6782f11ad60b 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/optlock/OptimisticLockTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/optlock/OptimisticLockTest.java @@ -15,6 +15,7 @@ import org.hibernate.StaleStateException; import org.hibernate.dialect.CockroachDialect; import org.hibernate.dialect.Dialect; +import org.hibernate.dialect.MariaDBDialect; import org.hibernate.dialect.SQLServerDialect; import org.hibernate.testing.orm.junit.DialectFeatureChecks; @@ -25,6 +26,7 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; +import static org.hibernate.testing.orm.junit.DialectContext.getDialect; import static org.junit.jupiter.api.Assertions.fail; /** @@ -191,8 +193,9 @@ else if ( dialect instanceof CockroachDialect && ( (JDBCException) cause ).getSQ "40001" ) ) { // CockroachDB always runs in SERIALIZABLE isolation, and uses SQL state 40001 to indicate // serialization failure. - } - else { + } else if (dialect instanceof MariaDBDialect && getDialect().getVersion().isAfter( 11, 6, 2 )) { + // Mariadb snapshot_isolation throws error + } else { throw e; } } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/procedure/PostgreSQLFunctionProcedureTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/procedure/PostgreSQLFunctionProcedureTest.java index aa03b34248d7..ff8194f996d6 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/procedure/PostgreSQLFunctionProcedureTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/procedure/PostgreSQLFunctionProcedureTest.java @@ -9,7 +9,6 @@ import java.sql.CallableStatement; import java.sql.ResultSet; import java.sql.SQLException; -import java.sql.SQLFeatureNotSupportedException; import java.sql.Timestamp; import java.sql.Types; import java.time.LocalDateTime; @@ -236,34 +235,6 @@ public void testFunctionWithJDBC() { } ); } - @Test - public void testFunctionWithJDBCByName() { - doInJPA( this::entityManagerFactory, entityManager -> { - try { - Session session = entityManager.unwrap( Session.class ); - Long phoneCount = session.doReturningWork( connection -> { - CallableStatement function = null; - try { - function = connection.prepareCall( "{ ? = call fn_count_phones(?) }" ); - function.registerOutParameter( "phoneCount", Types.BIGINT ); - function.setLong( "personId", 1L ); - function.execute(); - return function.getLong( 1 ); - } - finally { - if ( function != null ) { - function.close(); - } - } - } ); - assertEquals( Long.valueOf( 2 ), phoneCount ); - } - catch (Exception e) { - assertEquals( SQLFeatureNotSupportedException.class, e.getCause().getClass() ); - } - } ); - } - @Test @TestForIssue(jiraKey = "HHH-11863") public void testSysRefCursorAsOutParameter() { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/procedure/PostgreSQLStoredProcedureTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/procedure/PostgreSQLStoredProcedureTest.java index 696125dd75f1..f129cd253475 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/procedure/PostgreSQLStoredProcedureTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/procedure/PostgreSQLStoredProcedureTest.java @@ -9,7 +9,6 @@ import java.sql.CallableStatement; import java.sql.ResultSet; import java.sql.SQLException; -import java.sql.SQLFeatureNotSupportedException; import java.sql.Statement; import java.sql.Timestamp; import java.sql.Types; @@ -120,34 +119,6 @@ public void testStoredProcedureWithJDBC(EntityManagerFactoryScope scope) { } ); } - @Test - public void testProcedureWithJDBCByName(EntityManagerFactoryScope scope) { - scope.inTransaction( entityManager -> { - try { - Session session = entityManager.unwrap( Session.class ); - Long phoneCount = session.doReturningWork( connection -> { - CallableStatement procedure = null; - try { - procedure = connection.prepareCall( "{ call sp_count_phones(?,?) }" ); - procedure.registerOutParameter( "phoneCount", Types.BIGINT ); - procedure.setLong( "personId", 1L ); - procedure.execute(); - return procedure.getLong( 1 ); - } - finally { - if ( procedure != null ) { - procedure.close(); - } - } - } ); - assertEquals( Long.valueOf( 2 ), phoneCount ); - } - catch (Exception e) { - assertEquals( SQLFeatureNotSupportedException.class, e.getCause().getClass() ); - } - } ); - } - @Test @JiraKey("HHH-11863") @RequiresDialect(value = PostgreSQLDialect.class, majorVersion = 14, comment = "Stored procedure OUT parameters are only supported since version 14") diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/proxy/ProxyClassReuseTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/proxy/ProxyClassReuseTest.java new file mode 100644 index 000000000000..5166cd8f554e --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/proxy/ProxyClassReuseTest.java @@ -0,0 +1,193 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.orm.test.proxy; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import org.hibernate.boot.MetadataSources; +import org.hibernate.boot.registry.BootstrapServiceRegistryBuilder; +import org.hibernate.boot.registry.StandardServiceRegistry; +import org.hibernate.boot.registry.StandardServiceRegistryBuilder; +import org.hibernate.boot.registry.classloading.internal.TcclLookupPrecedence; +import org.hibernate.bytecode.internal.bytebuddy.BytecodeProviderImpl; +import org.hibernate.bytecode.spi.ByteCodeHelper; +import org.hibernate.bytecode.spi.BytecodeProvider; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.engine.spi.SessionImplementor; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.util.ServiceRegistryUtil; +import org.junit.jupiter.api.Test; + +import java.io.InputStream; +import java.util.Set; +import java.util.function.Function; + +import static org.junit.jupiter.api.Assertions.assertNotSame; +import static org.junit.jupiter.api.Assertions.assertSame; + +@Jira("https://hibernate.atlassian.net/browse/HHH-14694") +public class ProxyClassReuseTest { + + @Test + void testReuse() { + Function> proxyGetter = sf -> { + try (SessionImplementor s = sf.openSession()) { + return s.getReference( MyEntity.class, "abc" ).getClass(); + } + }; + final BytecodeProvider bytecodeProvider = new BytecodeProviderImpl(); + Class proxyClass1 = withFactory( proxyGetter, bytecodeProvider, null ); + Class proxyClass2 = withFactory( proxyGetter, bytecodeProvider, null ); + assertSame( proxyClass1, proxyClass2 ); + } + + @Test + void testReuseWithDifferentFactories() { + Function> proxyGetter = sf -> { + try (SessionImplementor s = sf.openSession()) { + return s.getReference( MyEntity.class, "abc" ).getClass(); + } + }; + Class proxyClass1 = withFactory( proxyGetter, null, null ); + Class proxyClass2 = withFactory( proxyGetter, null, null ); + assertSame( proxyClass1, proxyClass2 ); + assertSame( proxyClass1.getClassLoader(), MyEntity.class.getClassLoader() ); + assertSame( proxyClass2.getClassLoader(), MyEntity.class.getClassLoader() ); + } + + @Test + void testNoReuse() { + Function> proxyGetter = sf -> { + try { + //noinspection unchecked + Function> getter = (Function>) Thread.currentThread() + .getContextClassLoader() + .loadClass( ProxyClassReuseTest.class.getName() + "$ProxyGetter" ) + .getConstructor() + .newInstance(); + return getter.apply( sf ); + } + catch (Exception ex) { + throw new RuntimeException( ex ); + } + }; + // Create two isolated class loaders that load the entity and proxy classes in isolation + Set isolatedClasses = Set.of( "org.hibernate.orm.test.proxy.*" ); + ClassLoader cl1 = new IsolatingClassLoader( isolatedClasses ); + ClassLoader cl2 = new IsolatingClassLoader( isolatedClasses ); + Class proxyClass1 = withFactory( proxyGetter, null, cl1 ); + Class proxyClass2 = withFactory( proxyGetter, null, cl2 ); + // The two proxy classes shall be defined on the respective isolated class loaders and hence be different + assertNotSame( proxyClass1, proxyClass2 ); + assertSame( proxyClass1.getClassLoader(), cl1 ); + assertSame( proxyClass2.getClassLoader(), cl2 ); + } + + T withFactory(Function consumer, BytecodeProvider bytecodeProvider, ClassLoader classLoader) { + final ClassLoader oldClassLoader = Thread.currentThread().getContextClassLoader(); + try { + if (classLoader != null) { + Thread.currentThread().setContextClassLoader( classLoader ); + } + final BootstrapServiceRegistryBuilder bsr = new BootstrapServiceRegistryBuilder(); + bsr.applyTcclLookupPrecedence( TcclLookupPrecedence.BEFORE ); + final StandardServiceRegistryBuilder builder = ServiceRegistryUtil.serviceRegistryBuilder(bsr.build()); + if ( bytecodeProvider != null ) { + builder.addService( BytecodeProvider.class, bytecodeProvider ); + } + final StandardServiceRegistry ssr = builder.build(); + + try (final SessionFactoryImplementor sf = (SessionFactoryImplementor) new MetadataSources( ssr ) + .addAnnotatedClassName( ProxyClassReuseTest.class.getName() + "$MyEntity" ) + .buildMetadata() + .getSessionFactoryBuilder() + .build()) { + return consumer.apply( sf ); + } + catch (Exception e) { + StandardServiceRegistryBuilder.destroy( ssr ); + throw e; + } + } + finally { + if (classLoader != null) { + Thread.currentThread().setContextClassLoader( oldClassLoader ); + } + } + } + + @Entity(name = "MyEntity") + public static class MyEntity { + @Id + String id; + String name; + } + + public static class ProxyGetter implements Function> { + @Override + public Class apply(SessionFactoryImplementor sf) { + try (SessionImplementor s = sf.openSession()) { + return s.getReference( MyEntity.class, "abc" ).getClass(); + } + } + } + + private static class IsolatingClassLoader extends ClassLoader { + + private final Set isolatedClasses; + + public IsolatingClassLoader(Set isolatedClasses) { + super( IsolatingClassLoader.class.getClassLoader() ); + this.isolatedClasses = isolatedClasses; + } + + @Override + protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { + synchronized (getClassLoadingLock(name)) { + Class c = findLoadedClass(name); + if (c == null) { + if (isIsolatedClass(name)) { + InputStream is = this.getResourceAsStream( name.replace( '.', '/' ) + ".class" ); + if ( is == null ) { + throw new ClassNotFoundException( name + " not found" ); + } + + try { + byte[] bytecode = ByteCodeHelper.readByteCode( is ); + return defineClass( name, bytecode, 0, bytecode.length ); + } + catch( Throwable t ) { + throw new ClassNotFoundException( name + " not found", t ); + } + } else { + // Parent first + c = super.loadClass(name, resolve); + } + } + return c; + } + } + + private boolean isIsolatedClass(String name) { + if (isolatedClasses != null) { + for (String isolated : isolatedClasses) { + if (isolated.endsWith(".*")) { + String isolatedPackage = isolated.substring(0, isolated.length() - 1); + String paramPackage = name.substring(0, name.lastIndexOf('.') + 1); + if (paramPackage.startsWith(isolatedPackage)) { + // Matching package + return true; + } + } else if (isolated.equals(name)) { + return true; + } + } + } + return false; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/proxy/concrete/ConcreteProxyToOneSecondLevelCacheTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/proxy/concrete/ConcreteProxyToOneSecondLevelCacheTest.java new file mode 100644 index 000000000000..e9380cc4bc66 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/proxy/concrete/ConcreteProxyToOneSecondLevelCacheTest.java @@ -0,0 +1,220 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.proxy.concrete; + +import jakarta.persistence.Cacheable; +import jakarta.persistence.DiscriminatorColumn; +import jakarta.persistence.DiscriminatorType; +import jakarta.persistence.DiscriminatorValue; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.Inheritance; +import jakarta.persistence.InheritanceType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import org.hibernate.Hibernate; +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; +import org.hibernate.annotations.ConcreteProxy; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.stat.Statistics; +import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.Setting; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel(annotatedClasses = { + ConcreteProxyToOneSecondLevelCacheTest.TestNode.class, + ConcreteProxyToOneSecondLevelCacheTest.TestCompositeNode.class +}) +@ServiceRegistry(settings = { + @Setting(name = AvailableSettings.GENERATE_STATISTICS, value = "true"), + @Setting(name = AvailableSettings.USE_SECOND_LEVEL_CACHE, value = "true") +}) +@SessionFactory +@BytecodeEnhanced(runNotEnhancedAsWell = true) +@Jira("https://hibernate.atlassian.net/browse/HHH-18872") +public class ConcreteProxyToOneSecondLevelCacheTest { + @Test + public void testToOneInCacheGetReference(SessionFactoryScope scope) { + final Statistics stats = scope.getSessionFactory().getStatistics(); + stats.clear(); + // load only the node with ID 1 into the 2LC + scope.inTransaction( session -> { + assertThat( session.find( TestCompositeNode.class, 1 ) ).isNotNull(); + assertThat( session.find( TestCompositeNode.class, 2 ) ).isNotNull(); + } ); + assertCacheStats( stats, 0, 2, 2 ); + scope.inSession( session -> { + final TestCompositeNode node1 = session.getReference( TestCompositeNode.class, 1 ); + assertThat( Hibernate.isInitialized( node1 ) ).isFalse(); + // this triggers node1 initialization, but should maintain laziness for parent + final TestNode parent = node1.getParent(); + assertThat( Hibernate.isInitialized( node1 ) ).isTrue(); + // node 1 will be loaded from cache + assertCacheStats( stats, 1, 2, 2 ); + assertParent( parent, stats, 2 ); + } ); + } + + @Test + public void testToOneNotInCacheGetReference(SessionFactoryScope scope) { + final Statistics stats = scope.getSessionFactory().getStatistics(); + stats.clear(); + // load only the node with ID 1 into the 2LC + scope.inTransaction( session -> assertThat( session.find( TestCompositeNode.class, 1 ) ).isNotNull() ); + assertCacheStats( stats, 0, 1, 1 ); + scope.inSession( session -> { + final TestCompositeNode node1 = session.getReference( TestCompositeNode.class, 1 ); + assertThat( Hibernate.isInitialized( node1 ) ).isFalse(); + // this triggers node1 initialization, but should maintain laziness for parent + final TestNode parent = node1.getParent(); + assertThat( Hibernate.isInitialized( node1 ) ).isTrue(); + // node 1 will be loaded from cache + assertCacheStats( stats, 1, 1, 1 ); + assertParent( parent, stats, 1 ); + } ); + } + + @Test + public void testToOneInCacheFind(SessionFactoryScope scope) { + final Statistics stats = scope.getSessionFactory().getStatistics(); + stats.clear(); + // load only the node with ID 1 into the 2LC + scope.inTransaction( session -> { + assertThat( session.find( TestCompositeNode.class, 1 ) ).isNotNull(); + assertThat( session.find( TestCompositeNode.class, 2 ) ).isNotNull(); + } ); + assertCacheStats( stats, 0, 2, 2 ); + scope.inSession( session -> { + final TestCompositeNode node1 = session.find( TestCompositeNode.class, 1 ); + assertThat( Hibernate.isInitialized( node1 ) ).isTrue(); + // node 1 will be loaded from cache + assertCacheStats( stats, 1, 2, 2 ); + assertParent( node1.getParent(), stats, 2 ); + } ); + } + + @Test + public void testToOneNotInCacheFind(SessionFactoryScope scope) { + final Statistics stats = scope.getSessionFactory().getStatistics(); + stats.clear(); + // load only the node with ID 1 into the 2LC + scope.inTransaction( session -> assertThat( session.find( TestCompositeNode.class, 1 ) ).isNotNull() ); + assertCacheStats( stats, 0, 1, 1 ); + scope.inSession( session -> { + final TestCompositeNode node1 = session.find( TestCompositeNode.class, 1 ); + assertThat( Hibernate.isInitialized( node1 ) ).isTrue(); + // node 1 will be loaded from cache + assertCacheStats( stats, 1, 1, 1 ); + assertParent( node1.getParent(), stats, 1 ); + } ); + } + + private static void assertParent(final TestNode parent, final Statistics stats, final long hits) { + assertThat( TestCompositeNode.class ).as( "Expecting parent proxy to be narrowed to concrete type" ) + .isAssignableFrom( parent.getClass() ); + final TestCompositeNode parentComposite = (TestCompositeNode) parent; + assertThat( Hibernate.isInitialized( parentComposite ) ).isFalse(); + assertThat( parentComposite.getName() ).isEqualTo( "parent_node" ); + assertThat( Hibernate.isInitialized( parentComposite ) ).isTrue(); + // node 2 will not be found in cache + assertCacheStats( stats, hits, 2, 2 ); + assertThat( parentComposite ).as( String.format( + "Expecting parent to be an instance of TestCompositeNode but was: [%s]", + parent.getClass() + ) ).isInstanceOf( TestCompositeNode.class ); + } + + private static void assertCacheStats(final Statistics stats, final long hits, final long misses, final long puts) { + assertThat( stats.getSecondLevelCacheHitCount() ).isEqualTo( hits ); + assertThat( stats.getSecondLevelCacheMissCount() ).isEqualTo( misses ); + assertThat( stats.getSecondLevelCachePutCount() ).isEqualTo( puts ); + } + + @BeforeEach + public void clearCache(SessionFactoryScope scope) { + scope.getSessionFactory().getCache().evictAllRegions(); + } + + @BeforeAll + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final TestCompositeNode node1 = new TestCompositeNode( 1, "child_node" ); + final TestCompositeNode node2 = new TestCompositeNode( 2, "parent_node" ); + node1.setParent( node2 ); + session.persist( node1 ); + session.persist( node2 ); + } ); + } + + @AfterAll + public void tearDown(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } + + @Entity(name = "TestNode") + @Cacheable + @ConcreteProxy + @Inheritance(strategy = InheritanceType.SINGLE_TABLE) + @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + @DiscriminatorColumn(discriminatorType = DiscriminatorType.STRING, name = "disc_col") + @DiscriminatorValue(value = "simple") + public static class TestNode { + @Id + private Integer id; + + private String name; + + public TestNode() { + } + + public TestNode(Integer id, String name) { + this.id = id; + this.name = name; + } + + public Integer getId() { + return id; + } + + public String getName() { + return name; + } + } + + @Entity + @DiscriminatorValue("composite") + public static class TestCompositeNode extends TestNode { + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "parent_id") + private TestNode parent; + + public TestCompositeNode() { + } + + public TestCompositeNode(Integer id, String name) { + super( id, name ); + } + + public TestNode getParent() { + return parent; + } + + public void setParent(TestNode parent) { + this.parent = parent; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/CriteriaSubqueryInPredicateTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/CriteriaSubqueryInPredicateTest.java new file mode 100644 index 000000000000..cf1863824207 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/CriteriaSubqueryInPredicateTest.java @@ -0,0 +1,84 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.query; + +import java.util.List; + +import org.hibernate.testing.orm.domain.gambit.BasicEntity; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Predicate; +import jakarta.persistence.criteria.Root; +import jakarta.persistence.criteria.Subquery; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Marco Belladelli + */ +@DomainModel( annotatedClasses = BasicEntity.class ) +@SessionFactory +@Jira( "https://hibernate.atlassian.net/browse/HHH-18502" ) +public class CriteriaSubqueryInPredicateTest { + @Test + public void testInCollection(SessionFactoryScope scope) { + executeQuery( scope, 1, sub -> sub.in( List.of( "entity_1", "another_entity" ) ) ); + } + + @Test + public void testInArray(SessionFactoryScope scope) { + executeQuery( scope, 2, sub -> sub.in( "entity_2", "another_entity" ) ); + } + + @Test + public void testInLiteral(SessionFactoryScope scope) { + executeQuery( scope, 3, sub -> sub.in( "entity_3" ) ); + } + + private void executeQuery(SessionFactoryScope scope, Integer expectedId, InPredicateProducer producer) { + scope.inTransaction( session -> { + final CriteriaBuilder cb = session.getCriteriaBuilder(); + final CriteriaQuery cq = cb.createQuery( BasicEntity.class ); + final Root root = cq.from( BasicEntity.class ); + + final Subquery sub = cq.subquery( String.class ); + final Root subRoot = sub.from( BasicEntity.class ); + sub.select( subRoot.get( "data" ) ).where( cb.equal( subRoot.get( "id" ), root.get( "id" ) ) ); + + cq.select( root ).where( producer.accept( sub ) ); + + final List resultList = session.createQuery( cq ).getResultList(); + assertThat( resultList ).hasSize( 1 ).extracting( BasicEntity::getId ).containsOnlyOnce( expectedId ); + } ); + } + + private interface InPredicateProducer { + Predicate accept(Subquery sub); + } + + @BeforeAll + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + session.persist( new BasicEntity( 1, "entity_1" ) ); + session.persist( new BasicEntity( 2, "entity_2" ) ); + session.persist( new BasicEntity( 3, "entity_3" ) ); + } ); + } + + @AfterAll + public void tearDown(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/CteTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/CteTests.java index f52896c7742e..f4ed251a4a5f 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/CteTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/CteTests.java @@ -268,11 +268,11 @@ public void testInSubquery(SessionFactoryScope scope) { final QueryImplementor query = session.createQuery( "select c.name.first from Contact c where c.id in (" + - "with contacts as (" + + "with cte as (" + "select c.id id, c.name.first firstName from Contact c " + "where c.id in (1,2)" + ") " + - "select c.id from contacts c" + + "select c.id from cte c" + ")", String.class ); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/LeftJoinNullnessPredicateQueryTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/LeftJoinNullnessPredicateQueryTest.java index 92b79bc67bdf..45635f3871f3 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/LeftJoinNullnessPredicateQueryTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/LeftJoinNullnessPredicateQueryTest.java @@ -9,6 +9,7 @@ import java.util.List; import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; import org.hibernate.testing.orm.junit.SessionFactory; import org.hibernate.testing.orm.junit.SessionFactoryScope; import org.junit.jupiter.api.AfterAll; @@ -29,7 +30,11 @@ @DomainModel( annotatedClasses = { LeftJoinNullnessPredicateQueryTest.Author.class, LeftJoinNullnessPredicateQueryTest.Book.class -} ) +}) +@Jira( "https://hibernate.atlassian.net/browse/HHH-16505" ) +@Jira( "https://hibernate.atlassian.net/browse/HHH-17379" ) +@Jira( "https://hibernate.atlassian.net/browse/HHH-17397" ) +@Jira( "https://hibernate.atlassian.net/browse/HHH-19116" ) public class LeftJoinNullnessPredicateQueryTest { @BeforeAll public void setUp(SessionFactoryScope scope) { @@ -80,6 +85,19 @@ public void testIsNotNull(SessionFactoryScope scope) { } ); } + @Test + public void testIsNullImplicit(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final List resultList = session.createQuery( + "select book from Book book " + + "where book.author is null", + Book.class + ).getResultList(); + assertThat( resultList ).hasSize( 1 ); + assertThat( resultList.get( 0 ).getTitle() ).isEqualTo( "Unknown Author" ); + } ); + } + @Test public void testDereferenceIsNull(SessionFactoryScope scope) { scope.inTransaction( session -> { @@ -108,6 +126,19 @@ public void testDereferenceIsNotNull(SessionFactoryScope scope) { } ); } + @Test + public void testFkImplicitIsNull(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final List resultList = session.createQuery( + "select book from Book book " + + "where fk(book.author) is null", + Book.class + ).getResultList(); + assertThat( resultList ).hasSize( 1 ); + assertThat( resultList.get( 0 ).getTitle() ).isEqualTo( "Unknown Author" ); + } ); + } + @Test public void testIsNotNullWithCondition(SessionFactoryScope scope) { scope.inTransaction( session -> { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/NativeQueryAndDiscriminatorTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/NativeQueryAndDiscriminatorTest.java new file mode 100644 index 000000000000..c745061fb328 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/NativeQueryAndDiscriminatorTest.java @@ -0,0 +1,112 @@ +package org.hibernate.orm.test.query; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Column; +import jakarta.persistence.DiscriminatorColumn; +import jakarta.persistence.DiscriminatorType; +import jakarta.persistence.DiscriminatorValue; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.Id; +import jakarta.persistence.Inheritance; +import jakarta.persistence.InheritanceType; +import jakarta.persistence.Table; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +@DomainModel( + annotatedClasses = { + NativeQueryAndDiscriminatorTest.BaseEntity.class, + NativeQueryAndDiscriminatorTest.TestEntity.class + } +) +@SessionFactory +@JiraKey("HHH-18515") +public class NativeQueryAndDiscriminatorTest { + + + @BeforeEach + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + TestEntity e = new TestEntity( 1l, "test", EntityDiscriminator.T ); + session.persist( e ); + } + ); + } + + @AfterEach + public void tearDown(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + session.createMutationQuery( "delete TestEntity" ).executeUpdate(); + } + ); + } + + @Test + public void testNativeQuery(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + BaseEntity entity = session.createNativeQuery( + "select * from BASE_ENTITY where id = :id", + BaseEntity.class + ) + .setParameter( "id", 1l ) + .getSingleResult(); + assertThat( entity ).isNotNull(); + } + ); + } + + + @Entity(name = "BaseEntity") + @Table(name = "BASE_ENTITY") + @Inheritance(strategy = InheritanceType.SINGLE_TABLE) + @DiscriminatorColumn(name = "discriminator", discriminatorType = DiscriminatorType.CHAR) + @DiscriminatorValue("B") + public static class BaseEntity { + + @Id + private Long id; + + private String name; + + @Column(insertable = false, updatable = false) + @Enumerated(EnumType.STRING) + private EntityDiscriminator discriminator; + + public BaseEntity() { + } + + public BaseEntity(Long id, String name, EntityDiscriminator discriminator) { + this.id = id; + this.name = name; + this.discriminator = discriminator; + } + } + + @Entity(name = "TestEntity") + @DiscriminatorValue("T") + public static class TestEntity extends BaseEntity { + + public TestEntity() { + } + + public TestEntity(Long id, String name, EntityDiscriminator discriminator) { + super( id, name, discriminator ); + } + } + + enum EntityDiscriminator { + T; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/NativeQueryNestedTreeTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/NativeQueryNestedTreeTest.java new file mode 100644 index 000000000000..36a5b4961606 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/NativeQueryNestedTreeTest.java @@ -0,0 +1,107 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.orm.test.query; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Test; + +import java.util.HashSet; +import java.util.Set; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; + +/** + * This reproduces an issue in Hibernate 6 parsing native queries. + */ +@DomainModel( + annotatedClasses = { + NativeQueryNestedTreeTest.Tree.class, + NativeQueryNestedTreeTest.Forest.class + } +) +@SessionFactory +@Jira("https://hibernate.atlassian.net/browse/HHH-18871") +public class NativeQueryNestedTreeTest { + + @Test + public void test(SessionFactoryScope scope) { + // We want to make sure 'Could not locate TableGroup' no longer is thrown + assertDoesNotThrow( () -> scope.inTransaction( session -> + session.createNativeQuery( + "select {t.*}, {t2.*}, {t3.*}\n" + + "from tree t\n" + + "inner join tree t2 on t2.parent_id = t.id\n" + + "inner join tree t3 on t3.parent_id = t2.id\n", Tree.class ) + .addEntity( "t", Tree.class ) + .addJoin( "t2", "t.children" ) + .addJoin( "t3", "t2.children" ) + .list() + ) ); + + assertDoesNotThrow( () -> scope.inTransaction( session -> + session.createNativeQuery( + "select {t.*}, {t2.*}, {t3.*}, {t4.*}\n" + + "from tree t\n" + + "inner join tree t2 on t2.parent_id = t.id\n" + + "inner join tree t3 on t3.parent_id = t2.id\n" + + "inner join tree t4 on t4.parent_id = t3.id\n", Tree.class ) + .addEntity( "t", Tree.class ) + .addJoin( "t2", "t.children" ) + .addJoin( "t3", "t2.children" ) + .addJoin( "t4", "t3.children" ) + .list() + ) ); + + assertDoesNotThrow( () -> scope.inTransaction( session -> + session.createNativeQuery( + "select {f.*}, {t.*}, {t2.*}\n" + + "from forest f\n" + + "inner join tree t on t.parent_id is null\n" + + "inner join tree t2 on t2.parent_id = t.id\n", Forest.class ) + .addEntity( "f", Forest.class ) + .addJoin( "t", "f.trees" ) + .addJoin( "t2", "t.children" ) + .list() + ) ); + } + + @Entity(name = "Tree") + @Table(name = "tree") + public static class Tree { + @ManyToOne(cascade = CascadeType.ALL, fetch = FetchType.LAZY) + @JoinColumn(name = "parent_id") + private Tree parent; + @OneToMany(mappedBy = "parent", cascade = CascadeType.ALL, fetch = FetchType.LAZY) + private Set children = new HashSet<>(); + @Id + @GeneratedValue + private long id; + } + + @Entity(name = "Forest") + @Table(name = "forest") + public static class Forest { + @Id + @GeneratedValue + private Long id; + @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY) + @JoinColumn(name = "forest_id") + private Set trees = new HashSet<>(); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/QueryTimeOutTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/QueryTimeOutTest.java index 69937a770b76..7cf09d2decec 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/QueryTimeOutTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/QueryTimeOutTest.java @@ -12,6 +12,7 @@ import java.util.Map; import org.hibernate.cfg.AvailableSettings; +import org.hibernate.community.dialect.InformixDialect; import org.hibernate.dialect.AbstractTransactSQLDialect; import org.hibernate.dialect.OracleDialect; import org.hibernate.dialect.SybaseDialect; @@ -80,6 +81,9 @@ else if ( DialectContext.getDialect() instanceof SybaseDialect ) { else if ( DialectContext.getDialect() instanceof AbstractTransactSQLDialect ) { baseQuery = "update ae1_0 set name=? from AnEntity ae1_0"; } + else if (DialectContext.getDialect() instanceof InformixDialect ) { + baseQuery = "update AnEntity set name=?"; + } else { baseQuery = "update AnEntity ae1_0 set name=?"; } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/SelectCaseWhenNullLiteralTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/SelectCaseWhenNullLiteralTest.java index 836e7df34a6e..330e804078c2 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/SelectCaseWhenNullLiteralTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/SelectCaseWhenNullLiteralTest.java @@ -4,6 +4,7 @@ import org.hibernate.testing.TestForIssue; import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; import org.hibernate.testing.orm.junit.SessionFactory; import org.hibernate.testing.orm.junit.SessionFactoryScope; import org.junit.jupiter.api.AfterEach; @@ -55,6 +56,30 @@ public void testSelectCaseWhenNullLiteral(SessionFactoryScope scope) { ); } + @Test + @JiraKey( "HHH-18556" ) + public void testSelectCaseWhenNullLiteralWithParameters(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + List result = session.createQuery( "select case when 1=1 then ?1 else null end from Person p" ) + .setParameter( 1, 2 ) + .list(); + assertThat( result.size(), is( 1 ) ); + assertThat( result.get( 0 ), is( 2 ) ); + } + ); + + scope.inTransaction( + session -> { + List result = session.createQuery( "select count(case when 1=1 then ?1 else null end) from Person p" ) + .setParameter( 1, 2 ) + .list(); + assertThat( result.size(), is( 1 ) ); + assertThat( result.get( 0 ), is( 1L ) ); + } + ); + } + @Entity(name = "Person") @Table(name = "PERSON_TABLE") public static class Person { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/SelectJoinedAssociationMultipleTimesTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/SelectJoinedAssociationMultipleTimesTest.java new file mode 100644 index 000000000000..ff163b37b292 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/SelectJoinedAssociationMultipleTimesTest.java @@ -0,0 +1,60 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.query; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; + +@DomainModel(annotatedClasses = { + SelectJoinedAssociationMultipleTimesTest.Book.class +}) +@SessionFactory +public class SelectJoinedAssociationMultipleTimesTest { + + @Test + public void test(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + // Add a proxy first to trigger the error + session.getReference( Book.class, 1 ); + session.createSelectionQuery( "select b b1, b b2 from Book b", Object[].class ).getResultList(); + } + ); + } + + @BeforeEach + public void prepareTestData(SessionFactoryScope scope) { + scope.inTransaction( (session) -> session.persist( new Book( 1, "First book" ) ) ); + } + + @AfterEach + public void dropTestData(SessionFactoryScope scope) { + scope.inTransaction( (session) -> session.createMutationQuery( "delete Book" ).executeUpdate() ); + } + + @Entity( name = "Book") + public static class Book { + @Id + private Integer id; + private String name; + + public Book() { + } + + public Book(Integer id, String name) { + this.id = id; + this.name = name; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/SelectUnknownEnumLiteralTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/SelectUnknownEnumLiteralTest.java new file mode 100644 index 000000000000..5cb857db1935 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/SelectUnknownEnumLiteralTest.java @@ -0,0 +1,66 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.query; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Tuple; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@DomainModel(annotatedClasses = SelectUnknownEnumLiteralTest.Transaction.class) +@SessionFactory +public class SelectUnknownEnumLiteralTest { + + @BeforeAll + void setup(SessionFactoryScope scope) { + scope.inTransaction( em -> em.persist( new Transaction( 1L, "abc" ) ) ); + } + + @AfterAll + void clean(SessionFactoryScope scope) { + scope.inTransaction( em -> em.createMutationQuery( "delete from Tx" ).executeUpdate() ); + } + + @Test + void test(SessionFactoryScope scope) { + final List tuples = scope.fromSession( em -> + em.createQuery( + "SELECT org.hibernate.orm.test.query.SelectUnknownEnumLiteralTest$Type.TRANSACTION, e.id, e.reference FROM Tx e", + Tuple.class ).getResultList() ); + assertEquals( 1, tuples.size() ); + assertEquals( Type.TRANSACTION, tuples.get( 0 ).get( 0 ) ); + assertEquals( 1L, tuples.get( 0 ).get( 1 ) ); + } + + @Entity(name = "Tx") + static class Transaction { + @Id + Long id; + String reference; + + Transaction() { + } + + Transaction(Long id, String reference) { + this.id = id; + this.reference = reference; + } + } + + enum Type { + TRANSACTION, DIRECT_DEBIT_GROUP, DIRECT_DEBIT + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/SubQuerySelectCaseWhenTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/SubQuerySelectCaseWhenTest.java new file mode 100644 index 000000000000..424c95237c84 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/SubQuerySelectCaseWhenTest.java @@ -0,0 +1,79 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.query; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + + +@DomainModel( + annotatedClasses = { + SubQuerySelectCaseWhenTest.TestEntity.class, + } +) +@SessionFactory +@JiraKey("HHH-18681") +public class SubQuerySelectCaseWhenTest { + + @BeforeEach + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + session.persist( new TestEntity( "A" ) ); + session.persist( new TestEntity( "B" ) ); + session.persist( new TestEntity( "C" ) ); + } + ); + } + + @Test + public void testSelectCase(SessionFactoryScope scope) { + scope.inTransaction( session -> { + List result = session.createQuery( "select " + + " (select " + + " case " + + " when " + + " t.name = ?1 " + + " then 0 " + + " else 1 " + + " end)" + + " from TestEntity t order by t.name", Integer.class ) + .setParameter( 1, "A" ) + .list(); + assertThat( result.size() ).isEqualTo( 3 ); + assertThat( result.get( 0 ) ).isEqualTo( 0 ); + assertThat( result.get( 1 ) ).isEqualTo( 1 ); + assertThat( result.get( 2 ) ).isEqualTo( 1 ); + } ); + } + + @Entity(name = "TestEntity") + public class TestEntity { + + @Id + @GeneratedValue + private Long id; + + private String name; + + public TestEntity() { + } + + public TestEntity(String name) { + this.name = name; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/criteria/CountQueryTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/criteria/CountQueryTests.java index aa5215dfc51d..ad3a721c9a14 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/criteria/CountQueryTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/criteria/CountQueryTests.java @@ -88,6 +88,48 @@ public void testForHHH17967(SessionFactoryScope scope) { ); } + @Test + @JiraKey( "HHH-18850" ) + public void testForHHH18850(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + HibernateCriteriaBuilder cb = session.getCriteriaBuilder(); + JpaCriteriaQuery cq = cb.createQuery( Contract.class ); + cq.distinct( true ); + Root root = cq.from( Contract.class ); + cq.select( root ); + cq.orderBy( cb.asc( root.get( "customerName" ) ) ); + TypedQuery query = session.createQuery( cq.createCountQuery() ); + try { + // Leads to NPE on pre-6.5 versions + query.getSingleResult(); + } + catch (Exception e) { + fail( e ); + } + } + ); + + scope.inTransaction( + session -> { + HibernateCriteriaBuilder cb = session.getCriteriaBuilder(); + JpaCriteriaQuery cq = cb.createQuery( Contract.class ); + cq.distinct( false ); + Root root = cq.from( Contract.class ); + cq.select( root ); + cq.orderBy( cb.desc( root.get( "customerName" ) ) ); + TypedQuery query = session.createQuery( cq.createCountQuery() ); + try { + // Leads to NPE on pre-6.5 versions + query.getSingleResult(); + } + catch (Exception e) { + fail( e ); + } + } + ); + } + @Test @JiraKey("HHH-17410") public void testBasic(SessionFactoryScope scope) { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/criteria/CriteriaMutationQueryFkValuesTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/criteria/CriteriaMutationQueryFkValuesTest.java new file mode 100644 index 000000000000..aa5b6a2c22ed --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/criteria/CriteriaMutationQueryFkValuesTest.java @@ -0,0 +1,172 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.query.criteria; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import org.assertj.core.extractor.Extractors; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Marco Belladelli + */ +@DomainModel( annotatedClasses = { + CriteriaMutationQueryFkValuesTest.DemoEntity.class, + CriteriaMutationQueryFkValuesTest.A.class, + CriteriaMutationQueryFkValuesTest.B.class, + CriteriaMutationQueryFkValuesTest.C.class, +} ) +@SessionFactory( useCollectingStatementInspector = true ) +@Jira( "https://hibernate.atlassian.net/browse/HHH-18647" ) +public class CriteriaMutationQueryFkValuesTest { + @Test + public void testInsertValuesFkColumns(SessionFactoryScope scope) { + final var inspector = scope.getCollectingStatementInspector(); + inspector.clear(); + + scope.inTransaction( session -> { + final var cb = session.getCriteriaBuilder(); + final var criteriaInsert = cb.createCriteriaInsertValues( DemoEntity.class ); + criteriaInsert.setInsertionTargetPaths( + criteriaInsert.getTarget().get( "id" ), + // insert values into foreign key columns + criteriaInsert.getTarget().get( "a" ).get( "id" ), // a_id + criteriaInsert.getTarget().get( "b" ).get( "id" ), // b_id + criteriaInsert.getTarget().get( "c" ).get( "id" ) // c_id + ); + criteriaInsert.values( cb.values( + cb.value( 2L ), + cb.value( 1 ), + cb.value( 2 ), + cb.value( 3 ) + ) ); + final var count = session.createMutationQuery( criteriaInsert ).executeUpdate(); + assertThat( count ).isEqualTo( 1 ); + inspector.assertNumberOfOccurrenceInQueryNoSpace( 0, "join", 0 ); + + assertThat( session.find( DemoEntity.class, 2L ) ).extracting( "a", "b", "c" ).doesNotContainNull(); + } ); + } + + @Test + public void testUpdateFkColumns(SessionFactoryScope scope) { + final var inspector = scope.getCollectingStatementInspector(); + inspector.clear(); + + scope.inTransaction( session -> { + final var cb = session.getCriteriaBuilder(); + final var criteriaUpdate = cb.createCriteriaUpdate( DemoEntity.class ); + criteriaUpdate.set( criteriaUpdate.getTarget().get( "a" ).get( "id" ), cb.value( 4 ) ); + criteriaUpdate.set( + criteriaUpdate.getTarget().get( "b" ).get( "id" ), + cb.nullLiteral( Integer.class ) + ); + criteriaUpdate.where( cb.equal( criteriaUpdate.getTarget().get( "id" ), 1L ) ); + final var count = session.createMutationQuery( criteriaUpdate ).executeUpdate(); + assertThat( count ).isEqualTo( 1 ); + inspector.assertNumberOfOccurrenceInQueryNoSpace( 0, "join", 0 ); + + assertThat( session.find( DemoEntity.class, 1L ) ) + .extracting( "a", "b", "c" ) + .extracting( o -> o == null ? null : Extractors.byName( "id" ).apply( o ) ) + .contains( 4, null, 3 ); + } ); + } + + @BeforeAll + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final var a = new A( 1 ); + final var b = new B( 2 ); + final var c = new C( 3 ); + session.persist( new DemoEntity( 1L, a, b, c ) ); + session.persist( new A( 4 ) ); + } ); + } + + @AfterAll + public void tearDown(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } + + @Entity( name = "DemoEntity" ) + static class DemoEntity { + @Id + private Long id; + + @ManyToOne( cascade = CascadeType.PERSIST ) + @JoinColumn( name = "a_id" ) + private A a; + + @ManyToOne( cascade = CascadeType.PERSIST ) + @JoinColumn( name = "b_id" ) + private B b; + + @ManyToOne( cascade = CascadeType.PERSIST ) + @JoinColumn( name = "c_id" ) + private C c; + + public DemoEntity() { + } + + public DemoEntity(Long id, A a, B b, C c) { + this.id = id; + this.a = a; + this.b = b; + this.c = c; + } + } + + @Entity( name = "AEntity" ) + static class A { + @Id + private Integer id; + + public A() { + } + + public A(Integer id) { + this.id = id; + } + } + + @Entity( name = "BEntity" ) + static class B { + @Id + private Integer id; + + public B() { + } + + public B(Integer id) { + this.id = id; + } + } + + @Entity( name = "CEntity" ) + static class C { + @Id + private Integer id; + + public C() { + } + + public C(Integer id) { + this.id = id; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/ExistsSubqueryForeignKeyTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/ExistsSubqueryForeignKeyTest.java new file mode 100644 index 000000000000..a5f86d55ebd8 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/ExistsSubqueryForeignKeyTest.java @@ -0,0 +1,112 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.query.hql; + +import org.hibernate.dialect.DerbyDialect; +import org.hibernate.dialect.HSQLDialect; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.SkipForDialect; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.Tuple; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Marco Belladelli + */ +@DomainModel(annotatedClasses = { + ExistsSubqueryForeignKeyTest.Person.class, + ExistsSubqueryForeignKeyTest.Document.class, +}) +@SessionFactory +@SkipForDialect(dialectClass = HSQLDialect.class, reason = "HSQLDB doesn't like the case-when selection not being in the group-by") +@SkipForDialect(dialectClass = DerbyDialect.class, reason = "Derby doesn't like the case-when selection not being in the group-by") +@Jira( "https://hibernate.atlassian.net/browse/HHH-18816" ) +public class ExistsSubqueryForeignKeyTest { + @Test + public void testWhereClause(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final Long result = session.createQuery( + "select count(*) from Document d join d.owner o " + + "where exists(select p.id from Person p where p.id = o.id) group by o.id", + Long.class + ).getSingleResult(); + assertThat( result ).isEqualTo( 1L ); + } ); + } + + @Test + public void testSelectCaseWhen(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final Tuple result = session.createQuery( + "select case when exists(select p.id from Person p where p.id = o.id) then 1 else 0 end," + + "count(*) from Document d join d.owner o group by o.id", + Tuple.class + ).getSingleResult(); + assertThat( result.get( 0, Integer.class ) ).isEqualTo( 1 ); + assertThat( result.get( 1, Long.class ) ).isEqualTo( 1L ); + } ); + } + + @BeforeAll + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final Person person1 = new Person( 1L, "person_1" ); + session.persist( person1 ); + session.persist( new Document( 1L, "doc_1", person1 ) ); + } ); + } + + @AfterAll + public void tearDown(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } + + @Entity(name = "Person") + static class Person { + @Id + private Long id; + + private String name; + + public Person() { + } + + public Person(Long id, String name) { + this.id = id; + this.name = name; + } + } + + @Entity(name = "Document") + static class Document { + @Id + private Long id; + + private String title; + + @ManyToOne + private Person owner; + + public Document() { + } + + public Document(Long id, String title, Person owner) { + this.id = id; + this.title = title; + this.owner = owner; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/FunctionTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/FunctionTests.java index af1a148e83f2..70dc3f410cff 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/FunctionTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/FunctionTests.java @@ -7,8 +7,12 @@ package org.hibernate.orm.test.query.hql; import org.hamcrest.Matchers; + +import org.hibernate.HibernateException; +import org.hibernate.JDBCException; import org.hibernate.QueryException; import org.hibernate.community.dialect.AltibaseDialect; +import org.hibernate.community.dialect.InformixDialect; import org.hibernate.dialect.CockroachDialect; import org.hibernate.dialect.DB2Dialect; import org.hibernate.dialect.DerbyDialect; @@ -23,6 +27,8 @@ import org.hibernate.dialect.SybaseDialect; import org.hibernate.dialect.TiDBDialect; import org.hibernate.query.sqm.produce.function.FunctionArgumentException; +import org.hibernate.sql.exec.ExecutionException; + import org.hibernate.testing.TestForIssue; import org.hibernate.testing.orm.domain.StandardDomainModel; import org.hibernate.testing.orm.domain.gambit.EntityOfBasics; @@ -31,6 +37,7 @@ import org.hibernate.testing.orm.domain.gambit.SimpleEntity; import org.hibernate.testing.orm.junit.DialectFeatureChecks; import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; import org.hibernate.testing.orm.junit.JiraKey; import org.hibernate.testing.orm.junit.RequiresDialect; import org.hibernate.testing.orm.junit.RequiresDialectFeature; @@ -50,7 +57,9 @@ import java.time.LocalDateTime; import java.time.LocalTime; import java.time.OffsetDateTime; +import java.time.ZoneId; import java.time.ZoneOffset; +import java.time.ZonedDateTime; import java.time.temporal.ChronoUnit; import java.util.Date; import java.util.List; @@ -66,6 +75,7 @@ import static org.hibernate.testing.orm.domain.gambit.EntityOfBasics.Gender.FEMALE; import static org.junit.jupiter.api.Assertions.assertArrayEquals; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertInstanceOf; import static org.junit.jupiter.api.Assertions.fail; import static org.junit.jupiter.api.Assertions.assertThrows; @@ -599,6 +609,7 @@ public void testDateTruncFunction(SessionFactoryScope scope) { @Test @SkipForDialect(dialectClass = DerbyDialect.class, reason = "Derby doesn't support any form of date truncation") @SkipForDialect(dialectClass = OracleDialect.class, reason = "See HHH-16442, Oracle trunc() throws away the timezone") + @SkipForDialect(dialectClass = InformixDialect.class, reason = "Informix doesn't support any form of date truncation") public void testDateTruncWithOffsetFunction(SessionFactoryScope scope) { scope.inTransaction( session -> { @@ -1030,6 +1041,44 @@ public void testCastFunction(SessionFactoryScope scope) { ); } + @Test + @Jira("https://hibernate.atlassian.net/browse/HHH-18447") + public void testCastStringToBoolean(SessionFactoryScope scope) { + scope.inTransaction( session -> { + assertThat( session.createQuery("select cast('1' as Boolean)", Boolean.class).getSingleResult(), is(true) ); + assertThat( session.createQuery("select cast('0' as Boolean)", Boolean.class).getSingleResult(), is(false) ); + assertThat( session.createQuery("select cast('y' as Boolean)", Boolean.class).getSingleResult(), is(true) ); + assertThat( session.createQuery("select cast('n' as Boolean)", Boolean.class).getSingleResult(), is(false) ); + assertThat( session.createQuery("select cast('Y' as Boolean)", Boolean.class).getSingleResult(), is(true) ); + assertThat( session.createQuery("select cast('N' as Boolean)", Boolean.class).getSingleResult(), is(false) ); + assertThat( session.createQuery("select cast('t' as Boolean)", Boolean.class).getSingleResult(), is(true) ); + assertThat( session.createQuery("select cast('f' as Boolean)", Boolean.class).getSingleResult(), is(false) ); + assertThat( session.createQuery("select cast('T' as Boolean)", Boolean.class).getSingleResult(), is(true) ); + assertThat( session.createQuery("select cast('F' as Boolean)", Boolean.class).getSingleResult(), is(false) ); + assertThat( session.createQuery("select cast('true' as Boolean)", Boolean.class).getSingleResult(), is(true) ); + assertThat( session.createQuery("select cast('false' as Boolean)", Boolean.class).getSingleResult(), is(false) ); + assertThat( session.createQuery("select cast('TRUE' as Boolean)", Boolean.class).getSingleResult(), is(true) ); + assertThat( session.createQuery("select cast('FALSE' as Boolean)", Boolean.class).getSingleResult(), is(false) ); + }); + } + + @Test + @Jira("https://hibernate.atlassian.net/browse/HHH-18447") + public void testCastInvalidStringToBoolean(SessionFactoryScope scope) { + scope.inTransaction( session -> { + try { + session.createQuery( "select cast('bla' as Boolean)", Boolean.class ).getSingleResult(); + fail("Casting invalid boolean string should fail"); + } + catch ( HibernateException e ) { + // Expected + if ( !( e instanceof JDBCException || e instanceof ExecutionException ) ) { + throw e; + } + } + } ); + } + @Test @SkipForDialect(dialectClass = DB2Dialect.class, matchSubTypes = true) @SkipForDialect(dialectClass = DerbyDialect.class) @@ -1063,6 +1112,7 @@ public void testCastDoubleToString(SessionFactoryScope scope) { @SkipForDialect(dialectClass = OracleDialect.class, reason = "Oracle treats the cast value as a hexadecimal literal") @SkipForDialect(dialectClass = HSQLDialect.class, reason = "HSQL treats the cast value as a hexadecimal literal") @SkipForDialect(dialectClass = AltibaseDialect.class, reason = "Altibase doesn't support casting varchar to binary") + @SkipForDialect(dialectClass = InformixDialect.class, reason = "Informix doesn't support casting varchar to byte") public void testCastFunctionBinary(SessionFactoryScope scope) { scope.inTransaction( session -> { @@ -2336,10 +2386,15 @@ public void testIn(SessionFactoryScope scope) { session.createQuery("select 1 where 1 in :list", Integer.class) .setParameterList("list",List.of()) .list().size() ); + assertEquals( 0, + session.createQuery( "select e from EntityWithOneToOne e where e.other in (:list)" ) + .setParameter( "list", null ) + .list().size() ); } ); } + @Test public void testMaxGreatest(SessionFactoryScope scope) { scope.inTransaction( @@ -2524,4 +2579,40 @@ public void testCtidColumnFunction(SessionFactoryScope scope) { .getSingleResultOrNull(); }); } + + @Test + @JiraKey("HHH-18837") + public void testEpochFunction(SessionFactoryScope scope) { + + LocalDate someLocalDate = LocalDate.of( 2013, 7, 5 ); + LocalDateTime someLocalDateTime = someLocalDate.atStartOfDay(); + Date someDate = Date.from( someLocalDateTime.toInstant( ZoneOffset.UTC ) ); + ZonedDateTime someZonedDateTime = ZonedDateTime.of( someLocalDate, LocalTime.MIN, + ZoneId.of( "Europe/Vienna" ) ); + + scope.inTransaction( session -> { + EntityOfBasics entityOfBasics = new EntityOfBasics(); + entityOfBasics.setId( 124 ); + entityOfBasics.setTheDate( someDate ); + entityOfBasics.setTheLocalDate( someLocalDate ); + entityOfBasics.setTheLocalDateTime( someLocalDateTime ); + entityOfBasics.setTheZonedDateTime( someZonedDateTime ); + session.persist( entityOfBasics ); + + assertEquals( someDate.toInstant().toEpochMilli() / 1000, + session.createSelectionQuery( "select epoch(a.theDate) from EntityOfBasics a where id=124", + Long.class ).getSingleResult() ); + assertEquals( someLocalDate.atStartOfDay( ZoneOffset.UTC ).toInstant().toEpochMilli() / 1000, + session.createSelectionQuery( "select epoch(a.theLocalDate) from EntityOfBasics a where id=124", + Long.class ).getSingleResult() ); + assertEquals( someLocalDateTime.toEpochSecond( ZoneOffset.UTC ), session.createSelectionQuery( + "select epoch(a.theLocalDateTime) from EntityOfBasics a where id=124", + Long.class ).getSingleResult() ); + assertEquals( someZonedDateTime.toEpochSecond(), session.createSelectionQuery( + "select epoch(a.theZonedDateTime) from EntityOfBasics a where id=124", + Long.class ).getSingleResult() ); + + session.remove( entityOfBasics ); + } ); + } } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/ImplicitNestedJoinTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/ImplicitNestedJoinTest.java new file mode 100644 index 000000000000..e4b6df300661 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/ImplicitNestedJoinTest.java @@ -0,0 +1,143 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.orm.test.query.hql; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Christian Beikov + */ +@DomainModel(annotatedClasses = { + ImplicitNestedJoinTest.RootEntity.class, + ImplicitNestedJoinTest.FirstLevelReferencedEntity.class, + ImplicitNestedJoinTest.SecondLevelReferencedEntityA.class, + ImplicitNestedJoinTest.SecondLevelReferencedEntityB.class +}) +@SessionFactory +@Jira("https://hibernate.atlassian.net/browse/HHH-19905") +public class ImplicitNestedJoinTest { + + @Test + public void testInnerAndLeftJoin(SessionFactoryScope scope) { + scope.inSession( session -> { + final var resultList = session.createQuery( + "select r.id from RootEntity r" + + " join r.firstLevelReference.secondLevelReferenceA sa " + + " left join r.firstLevelReference.secondLevelReferenceB sb", + Long.class + ).getResultList(); + assertThat( resultList ).hasSize( 2 ).containsExactlyInAnyOrder( 1L, 2L ); + } ); + } + + @Test + public void testLeftAndInnerJoin(SessionFactoryScope scope) { + scope.inSession( session -> { + final var resultList = session.createQuery( + "select r.id from RootEntity r" + + " left join r.firstLevelReference.secondLevelReferenceA sa " + + " join r.firstLevelReference.secondLevelReferenceB sb", + Long.class + ).getResultList(); + assertThat( resultList ).hasSize( 1 ).containsExactly( 1L ); + } ); + } + + @Test + public void testBothInnerJoins(SessionFactoryScope scope) { + scope.inSession( session -> { + final var resultList = session.createQuery( + "select r.id from RootEntity r" + + " join r.firstLevelReference.secondLevelReferenceA sa " + + " join r.firstLevelReference.secondLevelReferenceB sb", + Long.class + ).getResultList(); + assertThat( resultList ).hasSize( 1 ).containsExactly( 1L ); + } ); + } + + @BeforeAll + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + // create some test data : one first level reference with both second level references, and + // another first level reference with only one second level reference + SecondLevelReferencedEntityA secondLevelA = new SecondLevelReferencedEntityA(); + secondLevelA.id = 1L; + secondLevelA.name = "Second Level A"; + session.persist( secondLevelA ); + SecondLevelReferencedEntityB secondLevelB = new SecondLevelReferencedEntityB(); + secondLevelB.id = 1L; + session.persist( secondLevelB ); + FirstLevelReferencedEntity firstLevel1 = new FirstLevelReferencedEntity(); + firstLevel1.id = 1L; + firstLevel1.secondLevelReferenceA = secondLevelA; + firstLevel1.secondLevelReferenceB = secondLevelB; + session.persist( firstLevel1 ); + RootEntity root1 = new RootEntity(); + root1.id = 1L; + root1.firstLevelReference = firstLevel1; + session.persist( root1 ); + FirstLevelReferencedEntity firstLevel2 = new FirstLevelReferencedEntity(); + firstLevel2.id = 2L; + firstLevel2.secondLevelReferenceA = secondLevelA; + session.persist( firstLevel2 ); + RootEntity root2 = new RootEntity(); + root2.id = 2L; + root2.firstLevelReference = firstLevel2; + session.persist( root2 ); + } ); + } + + @AfterAll + public void tearDown(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } + + @Entity(name = "RootEntity") + public static class RootEntity { + @Id + private Long id; + + @ManyToOne + private FirstLevelReferencedEntity firstLevelReference; + } + + @Entity(name = "FirstLevelReferencedEntity") + public static class FirstLevelReferencedEntity { + @Id + private Long id; + @ManyToOne + private SecondLevelReferencedEntityA secondLevelReferenceA; + @ManyToOne + private SecondLevelReferencedEntityB secondLevelReferenceB; + + } + + @Entity(name = "SecondLevelReferencedEntityA") + public static class SecondLevelReferencedEntityA { + @Id + private Long id; + private String name; + } + + @Entity(name = "SecondLevelReferencedEntityB") + public static class SecondLevelReferencedEntityB { + @Id + private Long id; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/InsertConflictWithCriteriaCopyTreeEnabledTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/InsertConflictWithCriteriaCopyTreeEnabledTests.java new file mode 100644 index 000000000000..5df94e23a97d --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/InsertConflictWithCriteriaCopyTreeEnabledTests.java @@ -0,0 +1,92 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.query.hql; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Tuple; +import org.hibernate.cfg.QuerySettings; +import org.hibernate.query.criteria.HibernateCriteriaBuilder; +import org.hibernate.query.criteria.JpaCriteriaInsertSelect; +import org.hibernate.query.criteria.JpaCriteriaInsertValues; +import org.hibernate.query.criteria.JpaCriteriaQuery; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.Setting; +import org.junit.jupiter.api.Test; + + +@DomainModel( + annotatedClasses = { + InsertConflictWithCriteriaCopyTreeEnabledTests.TestEntity.class, + InsertConflictWithCriteriaCopyTreeEnabledTests.AnotherTestEntity.class, + } +) +@ServiceRegistry( + settings = {@Setting(name = QuerySettings.CRITERIA_COPY_TREE, value = "true")} +) +@SessionFactory +@JiraKey("HHH-19314") +public class InsertConflictWithCriteriaCopyTreeEnabledTests { + + @Test + void createCriteriaInsertValuesTest(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + HibernateCriteriaBuilder cb = session.getCriteriaBuilder(); + + JpaCriteriaInsertValues insertIntoItem = cb + .createCriteriaInsertValues( TestEntity.class ); + insertIntoItem.setInsertionTargetPaths( insertIntoItem.getTarget().get( "id" ) ); + insertIntoItem.values( cb.values( cb.value( 1L ) ) ); + insertIntoItem.onConflict().onConflictDoNothing(); + + session.createMutationQuery( insertIntoItem ).executeUpdate(); + } + ); + } + + @Test + void createCriteriaInsertSelectTest(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + HibernateCriteriaBuilder cb = session.getCriteriaBuilder(); + + JpaCriteriaInsertSelect insertIntoItem = cb + .createCriteriaInsertSelect( TestEntity.class ); + insertIntoItem.setInsertionTargetPaths( insertIntoItem.getTarget().get( "id" ) ); + + JpaCriteriaQuery cq = cb.createQuery( Tuple.class ); + cq.select( cb.tuple( cb.literal( 1 ) ) ); + cq.fetch( 1 ); + insertIntoItem.select( cq ); + insertIntoItem.onConflict().onConflictDoNothing(); + + session.createMutationQuery( insertIntoItem ).executeUpdate(); + } + ); + } + + @Entity(name = "TestEntity") + public static class TestEntity { + @Id + private Long id; + + private String name; + + } + + @Entity(name = "AnotherTestEntity") + public static class AnotherTestEntity { + @Id + private Long id; + + private String name; + + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/LiteralTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/LiteralTests.java index 2aef7e6ceddc..ef0a7ad92973 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/LiteralTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/LiteralTests.java @@ -13,6 +13,7 @@ import org.hibernate.testing.orm.junit.ServiceRegistry; import org.hibernate.testing.orm.junit.SessionFactory; import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.SkipForDialect; import org.junit.jupiter.api.Test; import java.math.BigDecimal; @@ -25,6 +26,7 @@ import java.time.LocalDateTime; import java.time.LocalTime; +import org.hibernate.community.dialect.InformixDialect; import org.hibernate.query.spi.QueryImplementor; import org.hibernate.type.descriptor.java.PrimitiveByteArrayJavaType; @@ -45,6 +47,7 @@ public class LiteralTests { @Test + @SkipForDialect(dialectClass = InformixDialect.class, reason = "Informix does not support binary literals") public void testBinaryLiteral(SessionFactoryScope scope) { scope.inTransaction( session -> { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/MultiValuedParameterTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/MultiValuedParameterTest.java index a083fbcd3a9c..569e6bf30020 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/MultiValuedParameterTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/MultiValuedParameterTest.java @@ -6,6 +6,7 @@ */ package org.hibernate.orm.test.query.hql; +import java.math.BigDecimal; import java.math.BigInteger; import java.time.LocalDate; import java.util.ArrayList; @@ -28,6 +29,7 @@ import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; /** * @author Andrea Boriero @@ -102,6 +104,20 @@ public void test() { } ); } + @Test + @Jira( "https://hibernate.atlassian.net/browse/HHH-18575" ) + void testMultiValuedBigDecimals() { + inTransaction( session -> { + assertEquals( + 1, + session.createQuery("SELECT 1 WHERE :value IN (:list)", Integer.class) + .setParameter( "value", BigDecimal.valueOf( 2.0)) + .setParameter("list", List.of(BigDecimal.valueOf(2.0), BigDecimal.valueOf(3.0))) + .getSingleResult() + ); + }); + } + @AfterAll public void cleanupData() { inTransaction( session -> { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/UnionAllSelectNullTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/UnionAllSelectNullTest.java new file mode 100644 index 000000000000..519be3f4c403 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/UnionAllSelectNullTest.java @@ -0,0 +1,140 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.query.hql; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Tuple; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +@DomainModel( + annotatedClasses = { + UnionAllSelectNullTest.TestEntity.class, + UnionAllSelectNullTest.AnotherTestEntity.class + } +) +@SessionFactory +@JiraKey("HHH-18720") +class UnionAllSelectNullTest { + + @BeforeEach + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + session.persist( new TestEntity( 1L, "a" ) ); + session.persist( new AnotherTestEntity( 2L, "b" ) ); + } + ); + } + + @AfterEach + public void tearDown(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + session.createQuery( "delete TestEntity" ).executeUpdate(); + session.createQuery( "delete AnotherTestEntity" ).executeUpdate(); + } + ); + } + + @Test + void testSelect(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + List resultList = session.createQuery( + "SELECT te.id as id from TestEntity te" + + " union all SELECT null as id from AnotherTestEntity ate" + , Tuple.class ) + .getResultList(); + assertThat( resultList.size() ).isEqualTo( 2 ); + assertResultIsCorrect( resultList, 1L, null ); + } + ); + } + + @Test + void testSelect2(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + List resultList = session.createQuery( + "SELECT null as id from TestEntity te" + + " union all SELECT ate.id as id from AnotherTestEntity ate" + , Tuple.class ) + .getResultList(); + assertThat( resultList.size() ).isEqualTo( 2 ); + assertResultIsCorrect( resultList, null, 2L ); + } + ); + } + + @Test + void testSelect3(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + List resultList = session.createQuery( + "SELECT te.id as id from TestEntity te" + + " union all SELECT ate.id as id from AnotherTestEntity ate" + , Tuple.class ) + .getResultList(); + assertThat( resultList.size() ).isEqualTo( 2 ); + assertResultIsCorrect( resultList, 1L, 2L ); + } + ); + } + + private static void assertResultIsCorrect(List resultList, Long id1, Long id2) { + Set ids = new HashSet<>( 2 ); + ids.add( (Long) resultList.get( 0 ).get( "id" ) ); + ids.add( (Long) resultList.get( 1 ).get( "id" ) ); + assertThat( ids.contains( id1 ) ).as( "Result does not contain expected value:" + id1 ).isTrue(); + assertThat( ids.contains( id2 ) ).as( "Result does not contain expected value:" + id2 ).isTrue(); + } + + @Entity(name = "TestEntity") + public static class TestEntity { + + @Id + private Long id; + + private String name; + + public TestEntity() { + } + + public TestEntity(Long id, String name) { + this.id = id; + this.name = name; + } + } + + @Entity(name = "AnotherTestEntity") + public static class AnotherTestEntity { + + @Id + private Long id; + + private String name; + + public AnotherTestEntity() { + } + + public AnotherTestEntity(Long id, String name) { + this.id = id; + this.name = name; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/instantiation/InstantiationWithGenericsExpressionTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/instantiation/InstantiationWithGenericsExpressionTest.java new file mode 100644 index 000000000000..78592a687767 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/instantiation/InstantiationWithGenericsExpressionTest.java @@ -0,0 +1,224 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.query.hql.instantiation; + +import java.io.Serializable; + +import org.hibernate.annotations.Imported; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.MappedSuperclass; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Marco Belladelli + */ +@DomainModel(annotatedClasses = { + InstantiationWithGenericsExpressionTest.AbstractEntity.class, + InstantiationWithGenericsExpressionTest.ConcreteEntity.class, + InstantiationWithGenericsExpressionTest.ConstructorDto.class, + InstantiationWithGenericsExpressionTest.InjectionDto.class, +}) +@SessionFactory +@Jira("https://hibernate.atlassian.net/browse/HHH-18218") +public class InstantiationWithGenericsExpressionTest { + + @BeforeEach + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final ConcreteEntity entity = new ConcreteEntity(); + entity.setId( 1 ); + entity.setGen( 1L ); + entity.setData( "entity_1" ); + session.persist( entity ); + } ); + } + + @AfterEach + public void tearDown(SessionFactoryScope scope) { + scope.inTransaction( session -> session.createMutationQuery( "delete from ConcreteEntity" ).executeUpdate() ); + } + + @Test + public void testConstructorBinaryExpression(SessionFactoryScope scope) { + scope.inTransaction( session -> assertThat( session.createQuery( + "select new ConstructorDto(e.gen+e.gen, e.data) from ConcreteEntity e", + ConstructorDto.class + ).getSingleResult() ).extracting( ConstructorDto::getGen, ConstructorDto::getData ) + .containsExactly( 2L, "entity_1" ) ); + } + + @Test + public void testImplicitConstructorBinaryExpression(SessionFactoryScope scope) { + scope.inTransaction( session -> assertThat( session.createQuery( + "select e.gen+e.gen, e.data from ConcreteEntity e", + ConstructorDto.class + ).getSingleResult() ).extracting( ConstructorDto::getGen, ConstructorDto::getData ) + .containsExactly( 2L, "entity_1" ) ); + } + + @Test + public void testInjectionBinaryExpression(SessionFactoryScope scope) { + scope.inTransaction( session -> assertThat( session.createQuery( + "select new InjectionDto(e.gen+e.gen as gen, e.data as data) from ConcreteEntity e", + InjectionDto.class + ).getSingleResult() ).extracting( InjectionDto::getGen, InjectionDto::getData ) + .containsExactly( 2L, "entity_1" ) ); + } + + @Test + public void testConstructorUnaryExpression(SessionFactoryScope scope) { + scope.inTransaction( session -> assertThat( session.createQuery( + "select new ConstructorDto(-e.gen, e.data) from ConcreteEntity e", + ConstructorDto.class + ).getSingleResult() ).extracting( ConstructorDto::getGen, ConstructorDto::getData ) + .containsExactly( -1L, "entity_1" ) ); + } + + @Test + public void testImplicitConstructorUnaryExpression(SessionFactoryScope scope) { + scope.inTransaction( session -> assertThat( session.createQuery( + "select -e.gen, e.data from ConcreteEntity e", + ConstructorDto.class + ).getSingleResult() ).extracting( ConstructorDto::getGen, ConstructorDto::getData ) + .containsExactly( -1L, "entity_1" ) ); + } + + @Test + public void testInjectionUnaryExpression(SessionFactoryScope scope) { + scope.inTransaction( session -> assertThat( session.createQuery( + "select new InjectionDto(-e.gen as gen, e.data as data) from ConcreteEntity e", + InjectionDto.class + ).getSingleResult() ).extracting( InjectionDto::getGen, InjectionDto::getData ) + .containsExactly( -1L, "entity_1" ) ); + } + + @Test + public void testConstructorFunction(SessionFactoryScope scope) { + scope.inTransaction( session -> { + assertThat( session.createQuery( + "select new ConstructorDto(abs(e.gen), e.data) from ConcreteEntity e", + ConstructorDto.class + ).getSingleResult() ).extracting( ConstructorDto::getGen, ConstructorDto::getData ) + .containsExactly( 1L, "entity_1" ); + } ); + } + + @Test + public void testImplicitFunction(SessionFactoryScope scope) { + scope.inTransaction( session -> { + assertThat( session.createQuery( + "select abs(e.gen), e.data from ConcreteEntity e", + ConstructorDto.class + ).getSingleResult() ).extracting( ConstructorDto::getGen, ConstructorDto::getData ) + .containsExactly( 1L, "entity_1" ); + } ); + } + + @Test + public void testInjectionFunction(SessionFactoryScope scope) { + scope.inTransaction( session -> { + assertThat( session.createQuery( + "select new InjectionDto(abs(e.gen) as gen, e.data as data) from ConcreteEntity e", + InjectionDto.class + ).getSingleResult() ).extracting( InjectionDto::getGen, InjectionDto::getData ) + .containsExactly( 1L, "entity_1" ); + } ); + } + + @MappedSuperclass + static abstract class AbstractEntity { + @Id + protected Integer id; + + protected K gen; + + public Integer getId() { + return id; + } + + public void setId(final Integer id) { + this.id = id; + } + + public K getGen() { + return gen; + } + + public void setGen(final K gen) { + this.gen = gen; + } + } + + @Entity(name = "ConcreteEntity") + static class ConcreteEntity extends AbstractEntity { + protected String data; + + public String getData() { + return data; + } + + public void setData(String data) { + this.data = data; + } + } + + @Imported + public static class ConstructorDto { + private final Long gen; + + private final String data; + + public ConstructorDto(Long gen, String data) { + this.gen = gen; + this.data = data; + } + + public Long getGen() { + return gen; + } + + public String getData() { + return data; + } + } + + @Imported + public static class InjectionDto { + private long gen; + + private String data; + + public long getGen() { + return gen; + } + + public void setGen(final long gen) { + this.gen = gen; + } + + public String getData() { + return data; + } + + public void setData(String data) { + this.data = data; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/set/UnionOfPartitionResultsTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/set/UnionOfPartitionResultsTest.java new file mode 100644 index 000000000000..ff9896246bb6 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/set/UnionOfPartitionResultsTest.java @@ -0,0 +1,215 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.query.hql.set; + +import java.time.LocalDate; + +import org.hibernate.query.Query; + +import org.hibernate.testing.orm.junit.DialectFeatureChecks; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.RequiresDialectFeature; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; + +/** + * @author Jan Schatteman + */ +@DomainModel( + annotatedClasses = {UnionOfPartitionResultsTest.Apple.class, UnionOfPartitionResultsTest.Pie.class} +) +@SessionFactory +@JiraKey( "HHH-18069" ) +public class UnionOfPartitionResultsTest { + + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsUnion.class) + public void testSubqueryWithUnion(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + String q = "SELECT id " + + "FROM " + + "( " + + "( SELECT id id, bakedPie bakedPie " + + "\tFROM Apple c " + + ") " + + "\tUNION ALL " + + "( SELECT id id, bakedPie bakedPie " + + "\tFROM Apple a " + + ") " + + ")"; + + Query query = session.createQuery( q ); + + query.list(); + } + ); + } + + @Test + public void testSubquery(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + String q = "SELECT id " + + "FROM " + + "( " + + "\tSELECT id id, bakedPie bakedPie " + + "\tFROM Apple c " + + ")"; + + Query query = session.createQuery( q ); + + query.list(); + } + ); + } + + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsUnion.class) + public void testUnionQuery(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + String q = "( SELECT id id, bakedPie bakedPie " + + "\tFROM Apple c " + + ") " + + "\tUNION ALL " + + "( SELECT id id, bakedPie bakedPie " + + "\tFROM Apple c " + + ") "; + + Query query = session.createQuery( q ); + + query.list(); + } + ); + } + + + @Test + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportsUnion.class) + @RequiresDialectFeature(feature = DialectFeatureChecks.SupportPartitionBy.class) + public void testUnionOfPartitionResults(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + String q = + "SELECT new CurrentApple(id, bakedPie.id, dir) " + + "FROM (" + + "(" + + "SELECT id id, bakedPie bakedPie, bakedOn bakedOn, MAX(bakedOn) OVER (PARTITION BY bakedPie.id) mbo, -1 dir " + + "FROM Apple c " + + "WHERE bakedPie.id IN (1,2,3,4) AND bakedOn <= :now" + + ") UNION ALL (" + + "SELECT id id, bakedPie bakedPie, bakedOn bakedOn, MIN(bakedOn) OVER (PARTITION BY bakedPie.id) mbo, 1 dir " + + "FROM Apple c " + + "WHERE bakedPie.id IN (1,2,3,4) AND bakedOn > :now" + + ")" + + ") " + + "WHERE bakedOn = mbo ORDER BY dir"; + + Query query = session.createQuery( q, CurrentApple.class ); + query.setParameter( "now", LocalDate.now()); + + query.list(); + } + ); + } + + + public static class CurrentApple { + private final int id; + private final int pieId; + private final int dir; + + public CurrentApple(int id, int pieId, int dir) { + this.id = id; + this.pieId = pieId; + this.dir = dir; + } + + public int getDir() { + return dir; + } + + public int getId() { + return id; + } + + public int getPieId() { + return pieId; + } + } + + @Entity(name = "Apple") + public static class Apple { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + private LocalDate bakedOn; + @ManyToOne + private Pie bakedPie; + + public Integer getId() { + return id; + } + + public Apple setId(Integer id) { + this.id = id; + return this; + } + + public LocalDate getBakedOn() { + return bakedOn; + } + + public Apple setBakedOn(LocalDate bakedOn) { + this.bakedOn = bakedOn; + return this; + } + + public Pie getBakedPie() { + return bakedPie; + } + + public Apple setBakedPie(Pie bakedPie) { + this.bakedPie = bakedPie; + return this; + } + } + + @Entity(name = "Pie") + public static class Pie { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Integer id; + private String taste; + + public Integer getId() { + return id; + } + + public Pie setId(Integer id) { + this.id = id; + return this; + } + + public String getTaste() { + return taste; + } + + public Pie setTaste(String taste) { + this.taste = taste; + return this; + } + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/joinfetch/JoinFetchNestedAssociationsTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/joinfetch/JoinFetchNestedAssociationsTest.java new file mode 100644 index 000000000000..82699cf60641 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/joinfetch/JoinFetchNestedAssociationsTest.java @@ -0,0 +1,181 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.query.joinfetch; + +import java.util.HashSet; +import java.util.Set; + +import org.hibernate.Hibernate; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.MappedSuperclass; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OneToOne; +import jakarta.persistence.Table; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Marco Belladelli + */ +@DomainModel( annotatedClasses = { + JoinFetchNestedAssociationsTest.AEntity.class, + JoinFetchNestedAssociationsTest.BaseBEntity.class, + JoinFetchNestedAssociationsTest.B1Entity.class, + JoinFetchNestedAssociationsTest.B2Entity.class, + JoinFetchNestedAssociationsTest.CEntity.class, +} ) +@SessionFactory +@Jira( "https://hibernate.atlassian.net/browse/HHH-18469" ) +public class JoinFetchNestedAssociationsTest { + @Test + public void test(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final AEntity result = session.createQuery( + "select a from AEntity a " + + "left join fetch a.b_entity b_join " + + "left join fetch b_join.c_entities c", + AEntity.class + ).getSingleResult(); + final B2Entity bEntity = result.getB_entity(); + assertThat( bEntity ).matches( Hibernate::isInitialized ); + assertThat( bEntity.getC_entities() ).matches( Hibernate::isInitialized ) + .hasSize( 1 ) + .allMatch( c -> c.getName().equals( "c_entity" ) ); + } ); + } + + @BeforeAll + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + // B1 or B2 doesn't matter as they map to the same table + final B1Entity b = new B1Entity(); + b.setId( 1L ); + session.persist( b ); + session.flush(); + + final AEntity a = new AEntity(); + a.setB_entity_id( 1L ); + final CEntity c = new CEntity(); + c.setbEntityId( 1L ); + c.setName( "c_entity" ); + session.persist( a ); + session.persist( c ); + } ); + } + + @AfterAll + public void tearDown(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } + + @Entity( name = "AEntity" ) + static class AEntity { + @Id + @GeneratedValue + private Long id; + + @OneToOne + @JoinColumn( name = "b_entity_id", updatable = false, insertable = false ) + private B2Entity b_entity; + + @Column( name = "b_entity_id" ) + private Long b_entity_id; + + public B2Entity getB_entity() { + return b_entity; + } + + public Long getB_entity_id() { + return b_entity_id; + } + + public void setB_entity_id(Long b_entity_id) { + this.b_entity_id = b_entity_id; + } + } + + @MappedSuperclass + static class BaseBEntity { + @Id + private Long id; + + @OneToMany( mappedBy = "b_entity" ) + private Set c_entities = new HashSet<>(); + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public Set getC_entities() { + return c_entities; + } + } + + @Entity( name = "B1Entity" ) + @Table( name = "b_entities" ) + static class B1Entity extends BaseBEntity { + } + + @Entity( name = "B2Entity" ) + @Table( name = "b_entities" ) + static class B2Entity extends BaseBEntity { + } + + @Entity( name = "CEntity" ) + static class CEntity { + @Id + @GeneratedValue + private Long id; + + @Column( name = "b_id" ) + private Long bEntityId; + + @ManyToOne + @JoinColumn( name = "b_id", updatable = false, insertable = false ) + private B1Entity b_entity; + + private String name; + + public Long getbEntityId() { + return bEntityId; + } + + public void setbEntityId(Long bEntityId) { + this.bEntityId = bEntityId; + } + + public B1Entity getB_entity() { + return b_entity; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/mutationquery/MutationQueriesCollectionTableTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/mutationquery/MutationQueriesCollectionTableTest.java new file mode 100644 index 000000000000..467acb159696 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/mutationquery/MutationQueriesCollectionTableTest.java @@ -0,0 +1,60 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.query.mutationquery; + +import java.util.List; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.ElementCollection; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Inheritance; +import jakarta.persistence.InheritanceType; + +@SessionFactory +@DomainModel(annotatedClasses = { + MutationQueriesCollectionTableTest.Base1.class, + MutationQueriesCollectionTableTest.Table1.class +}) +@Jira("https://hibernate.atlassian.net/browse/HHH-19740") +public class MutationQueriesCollectionTableTest { + + @Test + public void testDelete(SessionFactoryScope scope) { + scope.inTransaction( session -> { + session.createQuery( "delete from Table1 where name = :name" ) + .setParameter( "name", "test" ) + .executeUpdate(); + } ); + } + + @Entity(name = "Base1") + @Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) + public abstract static class Base1 { + + @Id + private Long id; + + @SuppressWarnings("unused") + private String name; + + @ElementCollection + private List roles; + + } + + @Entity(name = "Table1") + @Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) + public static class Table1 extends Base1 { + + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/sql/NativeQueryJoinTableTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/sql/NativeQueryJoinTableTest.java new file mode 100644 index 000000000000..ddece2eb3dd0 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/sql/NativeQueryJoinTableTest.java @@ -0,0 +1,205 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.query.sql; + +import java.util.HashSet; +import java.util.Set; + +import org.hibernate.query.NativeQuery; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.JoinTable; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * @author Marco Belladelli + */ +@DomainModel( annotatedClasses = { + NativeQueryJoinTableTest.Shelf.class, NativeQueryJoinTableTest.Book.class, +} ) +@SessionFactory +@Jira( "https://hibernate.atlassian.net/browse/HHH-18494" ) +public class NativeQueryJoinTableTest { + private static final String SHELF_ID = "shelf1"; + private static final String FILE_ID = "file1"; + + @Test + public void testTypedQuery(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final NativeQuery query = session.createNativeQuery( + "select book.*, book_1_.shelfid from BOOK_T book, SHELF_BOOK book_1_ where book.fileid = book_1_.fileid", + Book.class + ); + final Book retrievedBook = query.getSingleResult(); + assertEquals( FILE_ID, retrievedBook.getFileId() ); + assertEquals( "Birdwatchers Guide to Dodos", retrievedBook.getTitle() ); + assertEquals( "nonfiction", retrievedBook.getShelf().getArea() ); + assertEquals( 3, retrievedBook.getShelf().getShelfNumber() ); + assertEquals( SHELF_ID, retrievedBook.getShelf().getShelfid() ); + assertEquals( 5, retrievedBook.getShelf().getPosition() ); + } ); + } + + @Test + public void testAddEntity(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final NativeQuery query = session.createNativeQuery( + "select {book.*}, book_1_.shelfid from BOOK_T book, SHELF_BOOK book_1_ where book.fileid = book_1_.fileid" + ); + query.addEntity( "book", Book.class ); + final Book retrievedBook = (Book) query.getSingleResult(); + assertEquals( FILE_ID, retrievedBook.getFileId() ); + assertEquals( "Birdwatchers Guide to Dodos", retrievedBook.getTitle() ); + assertEquals( "nonfiction", retrievedBook.getShelf().getArea() ); + assertEquals( 3, retrievedBook.getShelf().getShelfNumber() ); + assertEquals( SHELF_ID, retrievedBook.getShelf().getShelfid() ); + assertEquals( 5, retrievedBook.getShelf().getPosition() ); + } ); + } + + @BeforeAll + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final Shelf shelf = new Shelf(); + shelf.setShelfid( SHELF_ID ); + shelf.setArea( "nonfiction" ); + shelf.setPosition( Integer.valueOf( 5 ) ); + shelf.setShelfNumber( Integer.valueOf( 3 ) ); + shelf.setBooks( new HashSet<>() ); + session.persist( shelf ); + final Book book = new Book( FILE_ID ); + book.setTitle( "Birdwatchers Guide to Dodos" ); + book.setShelf( shelf ); + session.persist( book ); + } ); + } + + @AfterAll + public void tearDown(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } + + @Entity( name = "Shelf" ) + @Table( name = "SHELF" ) + public static class Shelf { + @Id + private String shelfid; + + private String area; + + private Integer shelfNumber; + + private Integer position; + + @OneToMany + @JoinTable( name = "SHELF_BOOK", joinColumns = @JoinColumn( name = "shelfid" ), inverseJoinColumns = @JoinColumn( name = "fileid" ) ) + private Set books; + + public Shelf() { + } + + public String getShelfid() { + return shelfid; + } + + public void setShelfid(String shelfid) { + this.shelfid = shelfid; + } + + public String getArea() { + return area; + } + + public void setArea(String area) { + this.area = area; + } + + public Integer getShelfNumber() { + return shelfNumber; + } + + public void setShelfNumber(Integer shelfNumber) { + this.shelfNumber = shelfNumber; + } + + public Integer getPosition() { + return position; + } + + public void setPosition(Integer position) { + this.position = position; + } + + public Set getBooks() { + return books; + } + + public void setBooks(Set books) { + this.books = books; + } + } + + ; + + @Entity( name = "Book" ) + @Table( name = "BOOK_T" ) + public static class Book { + @Id + private String fileid; + + private String title; + + @ManyToOne( optional = false, fetch = FetchType.EAGER ) + @JoinTable( name = "SHELF_BOOK" ) + @JoinColumn( name = "shelfid" ) + private Shelf shelf; + + public Book() { + } + + public Book(final String fileid) { + this.fileid = fileid; + } + + public String getFileId() { + return fileid; + } + + public void setFileId(final String fileid) { + this.fileid = fileid; + } + + public String getTitle() { + return title; + } + + public void setTitle(final String title) { + this.title = title; + } + + public Shelf getShelf() { + return shelf; + } + + public void setShelf(Shelf shelf) { + this.shelf = shelf; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/sql/NativeQueryResultBuilderColumnDeduplicationTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/sql/NativeQueryResultBuilderColumnDeduplicationTest.java new file mode 100644 index 000000000000..0e327fbc9f22 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/sql/NativeQueryResultBuilderColumnDeduplicationTest.java @@ -0,0 +1,49 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.query.sql; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Test; + +@DomainModel(annotatedClasses = { + NativeQueryResultBuilderColumnDeduplicationTest.MyEntity.class +}) +@SessionFactory +@Jira("https://hibernate.atlassian.net/browse/HHH-19712") +public class NativeQueryResultBuilderColumnDeduplicationTest { + + @Test + public void test(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + session.createNativeQuery( "select {t.*} from MyEntity t", Object.class ) + .addEntity( "t", MyEntity.class ) + .getResultList(); + } + ); + } + + @Entity(name = "MyEntity") + public static class MyEntity { + @EmbeddedId + private MyEntityPk id; + @Column(insertable = false, updatable = false) + private String name; + private String description; + } + + @Embeddable + public static class MyEntityPk { + private String id; + private String name; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/sql/NativeQueryResultBuilderTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/sql/NativeQueryResultBuilderTests.java index b04e132ed4ea..147af62497c7 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/sql/NativeQueryResultBuilderTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/sql/NativeQueryResultBuilderTests.java @@ -20,6 +20,7 @@ import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.mapping.ModelPart; import org.hibernate.metamodel.mapping.internal.BasicAttributeMapping; +import org.hibernate.testing.orm.domain.gambit.BasicEntity; import org.hibernate.type.descriptor.converter.spi.BasicValueConverter; import org.hibernate.type.descriptor.converter.spi.JpaAttributeConverter; import org.hibernate.query.sql.spi.NativeQueryImplementor; @@ -28,6 +29,7 @@ import org.hibernate.testing.orm.domain.StandardDomainModel; import org.hibernate.testing.orm.domain.gambit.EntityOfBasics; import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; import org.hibernate.testing.orm.junit.SessionFactory; import org.hibernate.testing.orm.junit.SessionFactoryScope; import org.junit.jupiter.api.AfterEach; @@ -279,6 +281,45 @@ public void testConvertedAttributeBasedBuilder(SessionFactoryScope scope) { ); } + @Test + @JiraKey("HHH-18629") + public void testNativeQueryWithResultClass(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + final String sql = "select data, id from BasicEntity"; + final NativeQueryImplementor query = session.createNativeQuery( sql, BasicEntity.class ); + + final List results = query.list(); + assertThat( results.size(), is( 1 ) ); + + final BasicEntity result = (BasicEntity) results.get( 0 ); + + assertThat( result.getData(), is( STRING_VALUE ) ); + assertThat( result.getId(), is( 1 ) ); + } + ); + } + + @Test + @JiraKey("HHH-18629") + public void testNativeQueryWithResultClassAndPlaceholders(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + final String sql = "select {be.*} from BasicEntity be"; + final NativeQueryImplementor query = session.createNativeQuery( sql, BasicEntity.class ); + query.addEntity( "be", BasicEntity.class ); + + final List results = query.list(); + assertThat( results.size(), is( 1 ) ); + + final BasicEntity result = (BasicEntity) results.get( 0 ); + + assertThat( result.getData(), is( STRING_VALUE ) ); + assertThat( result.getId(), is( 1 ) ); + } + ); + } + @BeforeAll public void verifyModel(SessionFactoryScope scope) { final EntityMappingType entityDescriptor = scope.getSessionFactory() @@ -317,13 +358,16 @@ public void prepareData(SessionFactoryScope scope) throws MalformedURLException entityOfBasics.setTheInstant( Instant.EPOCH ); session.persist( entityOfBasics ); + + session.persist( new BasicEntity( 1, STRING_VALUE ) ); } ); scope.inTransaction( session -> { - final EntityOfBasics entity = session.get( EntityOfBasics.class, 1 ); - assertThat( entity, notNullValue() ); + assertThat( session.get( EntityOfBasics.class, 1 ), notNullValue() ); + + assertThat( session.get( BasicEntity.class, 1 ), notNullValue() ); } ); } @@ -331,7 +375,10 @@ public void prepareData(SessionFactoryScope scope) throws MalformedURLException @AfterEach public void cleanUpData(SessionFactoryScope scope) { scope.inTransaction( - session -> session.createQuery( "delete EntityOfBasics" ).executeUpdate() + session -> { + session.createQuery( "delete EntityOfBasics" ).executeUpdate(); + session.createQuery( "delete BasicEntity" ).executeUpdate(); + } ); } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/sqm/SelfRenderingSqmFunctionWithoutArgumentsTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/sqm/SelfRenderingSqmFunctionWithoutArgumentsTest.java new file mode 100644 index 000000000000..e848c2159e75 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/sqm/SelfRenderingSqmFunctionWithoutArgumentsTest.java @@ -0,0 +1,64 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.orm.test.query.sqm; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import org.hibernate.testing.orm.junit.DialectFeatureChecks.SupportPartitionBy; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.RequiresDialectFeature; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +@DomainModel( + annotatedClasses = SelfRenderingSqmFunctionWithoutArgumentsTest.Dummy.class +) +@SessionFactory +@JiraKey("HHH-19719") +@RequiresDialectFeature(feature = SupportPartitionBy.class) +public class SelfRenderingSqmFunctionWithoutArgumentsTest { + + @BeforeAll + static void init(SessionFactoryScope scope) { + scope.inTransaction( session -> { + session.persist( new Dummy(1, "John Doe") ); + session.persist( new Dummy(2, "Dave Default") ); + } ); + } + + @Test + void test(SessionFactoryScope scope) { + scope.inSession( session -> { + session.createQuery("with tmp as (" + + " select id id, name name, row_number() over (order by name) pos" + + " from Dummy" + + ")" + + "select id, name, pos from tmp").getResultList(); + } ); + + } + + @Entity(name = "Dummy") + static class Dummy { + @Id + private Integer id; + + private String name; + + private Dummy() { + // for use by Hibernate + } + + public Dummy(Integer id, String name) { + this.id = id; + this.name = name; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/querycache/EntityWithCollectionReloadCacheInheritanceTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/querycache/EntityWithCollectionReloadCacheInheritanceTest.java new file mode 100644 index 000000000000..03977b12df7f --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/querycache/EntityWithCollectionReloadCacheInheritanceTest.java @@ -0,0 +1,174 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.querycache; + +import jakarta.persistence.Column; +import jakarta.persistence.DiscriminatorColumn; +import jakarta.persistence.DiscriminatorValue; +import jakarta.persistence.Entity; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.Inheritance; +import jakarta.persistence.InheritanceType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.JoinTable; +import jakarta.persistence.ManyToMany; +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.type.SqlTypes; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.util.Arrays; +import java.util.List; +import java.util.Set; + +import static jakarta.persistence.EnumType.STRING; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Miroslav Silhavy + */ +@DomainModel(annotatedClasses = { + EntityWithCollectionReloadCacheInheritanceTest.HighSchoolStudent.class, + EntityWithCollectionReloadCacheInheritanceTest.Subject.class, + EntityWithCollectionReloadCacheInheritanceTest.EnglishSubject.class +}) +@SessionFactory +@Jira("https://hibernate.atlassian.net/browse/HHH-19387") +public class EntityWithCollectionReloadCacheInheritanceTest { + + @Test + public void test(SessionFactoryScope scope) { + scope.inTransaction( session -> { + List highSchoolStudents = session.createQuery( + "select s" + + " from HighSchoolStudent s left join fetch s.subjects m" + + " where s.name in :names", HighSchoolStudent.class + ) + .setParameter( "names", Arrays.asList( "Brian" ) ) + .setCacheable( true ) + .list(); + + assertThat( highSchoolStudents ).hasSize( 1 ); + + highSchoolStudents = session.createQuery( + "select s" + + " from HighSchoolStudent s left join fetch s.subjects m" + + " where s.name in :names", HighSchoolStudent.class + ) + .setParameter( "names", Arrays.asList( "Andrew", "Brian" ) ) + .setCacheable( true ) + .list(); + + assertThat( highSchoolStudents ).hasSize( 2 ); + } ); + } + + @BeforeAll + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + HighSchoolStudent s1 = new HighSchoolStudent(); + s1.setName( "Andrew" ); + session.persist( s1 ); + + HighSchoolStudent s2 = new HighSchoolStudent(); + s2.setName( "Brian" ); + session.persist( s2 ); + } ); + } + + @AfterAll + public void tearDown(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } + + @Entity(name = "HighSchoolStudent") + static class HighSchoolStudent { + + @Id + @GeneratedValue + @Column(name = "id") + private Long id; + + @Column(name = "name") + private String name; + + @ManyToMany(targetEntity = Subject.class, fetch = FetchType.LAZY) + @JoinTable(name = "STUDENT_SUBJECT", + joinColumns = { @JoinColumn(name = "student_id") }, + inverseJoinColumns = { @JoinColumn(name = "subject_id") } + ) + private Set subjects; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Set getSubjects() { + return subjects; + } + + public void setSubjects(Set subjects) { + this.subjects = subjects; + } + + } + + @Entity(name = "Subject") + @Inheritance(strategy = InheritanceType.SINGLE_TABLE) + @DiscriminatorValue("DEFAULT") + @DiscriminatorColumn(name = "TYPE", length = 20) + static class Subject { + + @Id + @GeneratedValue + @Column(name = "id") + private Long id; + + @Column(name = "TYPE", nullable = false, length = 20, insertable = false, updatable = false) + @Enumerated(STRING) + @JdbcTypeCode(SqlTypes.VARCHAR) + private SubjectType type; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + } + + @Entity(name = "EnglishSubject") + @DiscriminatorValue("ENGLISH") + static class EnglishSubject extends Subject { + } + + enum SubjectType { + DEFAULT, + ENGLISH + } + +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/querycache/EntityWithCollectionReloadCacheTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/querycache/EntityWithCollectionReloadCacheTest.java new file mode 100644 index 000000000000..a2af4ac5dd19 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/querycache/EntityWithCollectionReloadCacheTest.java @@ -0,0 +1,444 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.querycache; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.UUID; + +import org.hibernate.Hibernate; +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Tuple; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Marco Belladelli + */ +@DomainModel( annotatedClasses = { + EntityWithCollectionReloadCacheTest.Course.class, + EntityWithCollectionReloadCacheTest.Subject.class, + EntityWithCollectionReloadCacheTest.Demand.class, + EntityWithCollectionReloadCacheTest.Student.class, + EntityWithCollectionReloadCacheTest.Major.class, + EntityWithCollectionReloadCacheTest.StudentMajor.class, +} ) +@SessionFactory +@Jira( "https://hibernate.atlassian.net/browse/HHH-18478" ) +public class EntityWithCollectionReloadCacheTest { + @Test + public void test(SessionFactoryScope scope) { + scope.inTransaction( session -> { + for ( String subject : new String[] { "MATH", "BIOL", "CS" } ) { + final List resultList = session.createQuery( + "select d.id, d.course, s " + + "from Demand d inner join d.student s left join fetch s.majors " + + "where d.course.subject.name = :subject", + Tuple.class + ) + .setParameter( "subject", subject ) + .setCacheable( true ) + .getResultList(); + assertThat( resultList ).hasSize( subject.equals( "MATH" ) ? 5 : 4 ).allSatisfy( tuple -> { + assertThat( tuple.get( 1, Course.class ).getSubject().getName() ).isEqualTo( subject ); + assertThat( tuple.get( 2, Student.class ).getMajors() ).matches( Hibernate::isInitialized ); + } ); + } + } ); + } + + @BeforeAll + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + // Create two majors + final Major m1 = new Major(); + m1.setName( "Biology" ); + session.persist( m1 ); + final Major m2 = new Major(); + m2.setName( "Computer Science" ); + session.persist( m2 ); + + // Create three students + final Student s1 = new Student(); + s1.setName( "Andrew" ); + final StudentMajor sm1 = new StudentMajor(); + sm1.setStudent( s1 ); + sm1.setMajor( m1 ); + sm1.setClassification( "01" ); + s1.addToMajors( sm1 ); + session.persist( s1 ); + + final Student s2 = new Student(); + s2.setName( "Brian" ); + final StudentMajor sm2 = new StudentMajor(); + sm2.setStudent( s2 ); + sm2.setMajor( m1 ); + sm2.setClassification( "02" ); + s2.addToMajors( sm2 ); + session.persist( s2 ); + + final Student s3 = new Student(); + s3.setName( "Charlie" ); + final StudentMajor sm3 = new StudentMajor(); + sm3.setStudent( s3 ); + sm3.setMajor( m1 ); + sm3.setClassification( "01" ); + s3.addToMajors( sm3 ); + final StudentMajor sm4 = new StudentMajor(); + sm4.setStudent( s3 ); + sm4.setMajor( m2 ); + sm4.setClassification( "02" ); + s3.addToMajors( sm4 ); + session.persist( s3 ); + + // Create two subjects + final Subject math = new Subject(); + math.setName( "MATH" ); + session.persist( math ); + final Subject biology = new Subject(); + biology.setName( "BIOL" ); + session.persist( biology ); + final Subject cs = new Subject(); + cs.setName( "CS" ); + session.persist( cs ); + + // Create a few courses + final Course c1 = new Course(); + c1.setSubject( math ); + c1.setNumber( "101" ); + session.persist( c1 ); + final Course c2 = new Course(); + c2.setSubject( math ); + c2.setNumber( "201" ); + session.persist( c2 ); + final Course c3 = new Course(); + c3.setSubject( biology ); + c3.setNumber( "101" ); + session.persist( c3 ); + final Course c4 = new Course(); + c4.setSubject( biology ); + c4.setNumber( "201" ); + session.persist( c4 ); + final Course c5 = new Course(); + c5.setSubject( cs ); + c5.setNumber( "101" ); + session.persist( c5 ); + final Course c6 = new Course(); + c6.setSubject( cs ); + c6.setNumber( "201" ); + session.persist( c6 ); + + // Create some course demands + final Demand d1 = new Demand(); + d1.setCourse( c1 ); + d1.setStudent( s1 ); + session.persist( d1 ); + final Demand d2 = new Demand(); + d2.setCourse( c1 ); + d2.setStudent( s2 ); + session.persist( d2 ); + final Demand d3 = new Demand(); + d3.setCourse( c2 ); + d3.setStudent( s2 ); + session.persist( d3 ); + final Demand d4 = new Demand(); + d4.setCourse( c2 ); + d4.setStudent( s3 ); + session.persist( d4 ); + final Demand d5 = new Demand(); + d5.setCourse( c3 ); + d5.setStudent( s1 ); + session.persist( d5 ); + final Demand d6 = new Demand(); + d6.setCourse( c3 ); + d6.setStudent( s3 ); + session.persist( d6 ); + final Demand d7 = new Demand(); + d7.setCourse( c4 ); + d7.setStudent( s1 ); + session.persist( d7 ); + final Demand d8 = new Demand(); + d8.setCourse( c5 ); + d8.setStudent( s2 ); + session.persist( d8 ); + final Demand d9 = new Demand(); + d9.setCourse( c6 ); + d9.setStudent( s2 ); + session.persist( d9 ); + final Demand d0 = new Demand(); + d0.setCourse( c6 ); + d0.setStudent( s3 ); + session.persist( d0 ); + } ); + } + + @AfterAll + public void tearDown(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } + + @Entity( name = "Course" ) + static class Course { + private UUID id; + private String number; + private Subject subject; + + @Id + @GeneratedValue + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + @Column( name = "number_col" ) + public String getNumber() { + return number; + } + + public void setNumber(String number) { + this.number = number; + } + + @ManyToOne + @JoinColumn( name = "subject_id" ) + public Subject getSubject() { + return subject; + } + + public void setSubject(Subject subject) { + this.subject = subject; + } + + @Override + public String toString() { + return getSubject() + " " + getNumber(); + } + } + + @Entity( name = "Subject" ) + static class Subject { + private UUID id; + private String name; + + @Id + @GeneratedValue + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String toString() { + return name; + } + } + + @Entity( name = "Demand" ) + static class Demand { + private UUID id; + private Student student; + private Course course; + + @Id + @GeneratedValue + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + @ManyToOne + @JoinColumn( name = "student_id" ) + public Student getStudent() { + return student; + } + + public void setStudent(Student student) { + this.student = student; + } + + @ManyToOne + @JoinColumn( name = "course_id" ) + public Course getCourse() { + return course; + } + + public void setCourse(Course course) { + this.course = course; + } + + @Override + public String toString() { + return getStudent() + " for " + getCourse(); + } + } + + @Entity( name = "Student" ) + static class Student { + private UUID id; + private String name; + private Set majors; + + @Id + @GeneratedValue + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @OneToMany( fetch = FetchType.LAZY, mappedBy = "student", cascade = { CascadeType.ALL } ) + @Cache( usage = CacheConcurrencyStrategy.READ_WRITE ) + public Set getMajors() { + return majors; + } + + public void setMajors(Set majors) { + this.majors = majors; + } + + public void addToMajors(StudentMajor major) { + if ( this.majors == null ) { + this.majors = new HashSet(); + } + this.majors.add( major ); + } + + @Override + public String toString() { + return getName() + " " + getMajors(); + } + } + + @Entity( name = "StudentMajor" ) + static class StudentMajor { + private UUID id; + private Student student; + private Major major; + private String classification; + + @Id + @GeneratedValue + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public String getClassification() { + return classification; + } + + public void setClassification(String classification) { + this.classification = classification; + } + + @ManyToOne + @JoinColumn( name = "student_id" ) + public Student getStudent() { + return student; + } + + public void setStudent(Student student) { + this.student = student; + } + + @ManyToOne + @JoinColumn( name = "major_id" ) + public Major getMajor() { + return major; + } + + public void setMajor(Major major) { + this.major = major; + } + + @Override + public String toString() { + return getMajor().getName() + " " + getClassification(); + } + } + + @Entity( name = "Major" ) + static class Major { + private UUID id; + + @Column( name = "name_col" ) + private String name; + + @Id + @GeneratedValue + public UUID getId() { + return id; + } + + public void setId(UUID id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Override + public String toString() { + return name; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/querycache/NativeQueryCacheWithExtraColumnsTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/querycache/NativeQueryCacheWithExtraColumnsTest.java new file mode 100644 index 000000000000..4ce05ad2d6e5 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/querycache/NativeQueryCacheWithExtraColumnsTest.java @@ -0,0 +1,273 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.querycache; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.Tuple; +import org.hibernate.annotations.Cache; +import org.hibernate.annotations.CacheConcurrencyStrategy; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.dialect.H2Dialect; +import org.hibernate.stat.Statistics; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.RequiresDialect; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.Setting; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@DomainModel(annotatedClasses = NativeQueryCacheWithExtraColumnsTest.TestUser.class) +@SessionFactory(generateStatistics = true) +@ServiceRegistry(settings = { + @Setting(name = AvailableSettings.USE_SECOND_LEVEL_CACHE, value = "true"), + @Setting(name = AvailableSettings.USE_QUERY_CACHE, value = "true") +}) +@Jira("https://hibernate.atlassian.net/browse/HHH-20176") +@RequiresDialect(H2Dialect.class) +public class NativeQueryCacheWithExtraColumnsTest { + + private static final String NATIVE_QUERY = "SELECT u1.* FROM TEST_USER u1, TEST_USER u2 WHERE u2.ID = u1.ID"; + + private static final String NATIVE_QUERY_EXTRA_COLS_FIRST = + "SELECT u1.EXTRA_COL1, u1.EXTRA_COL2, u1.ID, u1.NAME, u1.EMAIL, u1.AGE, u1.ADDRESS, u1.PHONE" + + " FROM TEST_USER u1"; + + @BeforeAll + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + // Add extra columns to the table that are not mapped in the entity + session.createNativeMutationQuery( "ALTER TABLE TEST_USER ADD COLUMN IF NOT EXISTS EXTRA_COL1 VARCHAR(50)" ) + .executeUpdate(); + session.createNativeMutationQuery( "ALTER TABLE TEST_USER ADD COLUMN IF NOT EXISTS EXTRA_COL2 VARCHAR(50)" ) + .executeUpdate(); + + // Insert test data with extra columns + session.createNativeMutationQuery( + "INSERT INTO TEST_USER (ID, NAME, EMAIL, AGE, ADDRESS, PHONE, EXTRA_COL1, EXTRA_COL2) VALUES " + + "(1, 'john', 'john@test.com', 30, 'ny', '123456', 'ext1', 'ext2')" ) + .executeUpdate(); + } ); + } + + @AfterEach + public void cleanCache(SessionFactoryScope scope) { + scope.getSessionFactory().getCache().evictQueryRegions(); + } + + @AfterAll + public void tearDown(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } + + @Test + public void testNativeQueryWithCacheAndExtraColumns(SessionFactoryScope scope) { + final var statistics = scope.getSessionFactory().getStatistics(); + statistics.clear(); + + // First query - should populate the cache + scope.inSession( session -> { + final var query = session.createNativeQuery( NATIVE_QUERY, TestUser.class ); + query.setCacheable( true ); + assertTestUser( query.getResultList() ); + } ); + + assertQueryCacheStatistics( statistics, 0, 1, 1 ); + + // Second query - should use the cache + scope.inSession( session -> { + final var query = session.createNativeQuery( NATIVE_QUERY, TestUser.class ); + query.setCacheable( true ); + assertTestUser( query.getResultList() ); + } ); + + assertQueryCacheStatistics( statistics, 1, 0, 0 ); + } + + private static void assertTestUser(List users) { + assertThat( users ).hasSize( 1 ); + final var user = users.get( 0 ); + assertThat( user.getName() ).isEqualTo( "john" ); + assertThat( user.getEmail() ).isEqualTo( "john@test.com" ); + assertThat( user.getAge() ).isEqualTo( 30 ); + assertThat( user.getAddress() ).isEqualTo( "ny" ); + assertThat( user.getPhone() ).isEqualTo( "123456" ); + } + + @Test + public void testNativeQueryWithCacheAndTupleResult(SessionFactoryScope scope) { + final var statistics = scope.getSessionFactory().getStatistics(); + statistics.clear(); + + // First query - should populate the cache + scope.inSession( session -> { + final var query = session.createNativeQuery( NATIVE_QUERY, Tuple.class ); + query.setCacheable( true ); + assertTestUserTuple( query.getResultList() ); + } ); + + assertQueryCacheStatistics( statistics, 0, 1, 1 ); + + // Second query - should use the cache + scope.inSession( session -> { + final var query = session.createNativeQuery( NATIVE_QUERY, Tuple.class ); + query.setCacheable( true ); + assertTestUserTuple( query.getResultList() ); + } ); + + assertQueryCacheStatistics( statistics, 1, 0, 0 ); + } + + private static void assertTestUserTuple(List tuples) { + assertThat( tuples ).hasSize( 1 ); + final var tuple = tuples.get( 0 ); + assertThat( tuple.get( "NAME", String.class ) ).isEqualTo( "john" ); + assertThat( tuple.get( "EMAIL", String.class ) ).isEqualTo( "john@test.com" ); + assertThat( tuple.get( "AGE", Integer.class ) ).isEqualTo( 30 ); + assertThat( tuple.get( "ADDRESS", String.class ) ).isEqualTo( "ny" ); + assertThat( tuple.get( "PHONE", String.class ) ).isEqualTo( "123456" ); + assertThat( tuple.get( "EXTRA_COL1", String.class ) ).isEqualTo( "ext1" ); + assertThat( tuple.get( "EXTRA_COL2", String.class ) ).isEqualTo( "ext2" ); + } + + @Test + public void testExtraColumnsBefore(SessionFactoryScope scope) { + final var statistics = scope.getSessionFactory().getStatistics(); + statistics.clear(); + + // First query - should populate the cache (works fine, reads from live ResultSet) + scope.inSession( session -> { + final var query = session.createNativeQuery( NATIVE_QUERY_EXTRA_COLS_FIRST, TestUser.class ); + query.setCacheable( true ); + assertTestUser( query.getResultList() ); + } ); + + assertQueryCacheStatistics( statistics, 0, 1, 1 ); + + // Second query - should use the cache + scope.inSession( session -> { + final var query = session.createNativeQuery( NATIVE_QUERY_EXTRA_COLS_FIRST, TestUser.class ); + query.setCacheable( true ); + assertTestUser( query.getResultList() ); + } ); + + assertQueryCacheStatistics( statistics, 1, 0, 0 ); + } + + @Test + public void testExtraColumnsBeforeTuple(SessionFactoryScope scope) { + final var statistics = scope.getSessionFactory().getStatistics(); + statistics.clear(); + + // First query - should populate the cache (works fine, reads from live ResultSet) + scope.inSession( session -> { + final var query = session.createNativeQuery( NATIVE_QUERY_EXTRA_COLS_FIRST, Tuple.class ); + query.setCacheable( true ); + assertTestUserTuple( query.getResultList() ); + } ); + + assertQueryCacheStatistics( statistics, 0, 1, 1 ); + + // Second query - should use the cache + scope.inSession( session -> { + final var query = session.createNativeQuery( NATIVE_QUERY_EXTRA_COLS_FIRST, Tuple.class ); + query.setCacheable( true ); + assertTestUserTuple( query.getResultList() ); + } ); + + assertQueryCacheStatistics( statistics, 1, 0, 0 ); + } + + private static void assertQueryCacheStatistics(Statistics statistics, int hits, int misses, int puts) { + assertThat( statistics.getQueryCacheHitCount() ).isEqualTo( hits ); + assertThat( statistics.getQueryCacheMissCount() ).isEqualTo( misses ); + assertThat( statistics.getQueryCachePutCount() ).isEqualTo( puts ); + statistics.clear(); + } + + @Entity(name = "TestUser") + @Table(name = "TEST_USER") + @Cache(usage = CacheConcurrencyStrategy.READ_WRITE) + public static class TestUser { + @Id + @Column(name = "ID") + private Long id; + + @Column(name = "NAME") + private String name; + + @Column(name = "EMAIL") + private String email; + + @Column(name = "AGE") + private Integer age; + + @Column(name = "ADDRESS") + private String address; + + @Column(name = "PHONE") + private String phone; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public String getEmail() { + return email; + } + + public void setEmail(String email) { + this.email = email; + } + + public Integer getAge() { + return age; + } + + public void setAge(Integer age) { + this.age = age; + } + + public String getAddress() { + return address; + } + + public void setAddress(String address) { + this.address = address; + } + + public String getPhone() { + return phone; + } + + public void setPhone(String phone) { + this.phone = phone; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/querycache/QueryCacheNullValueTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/querycache/QueryCacheNullValueTest.java new file mode 100644 index 000000000000..a20b3e3a12b7 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/querycache/QueryCacheNullValueTest.java @@ -0,0 +1,77 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.querycache; + +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.stat.spi.StatisticsImplementor; + +import org.hibernate.testing.orm.domain.gambit.BasicEntity; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.Setting; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Marco Belladelli + */ +@DomainModel( annotatedClasses = { + BasicEntity.class +} ) +@SessionFactory( generateStatistics = true ) +@ServiceRegistry( settings = { + @Setting( name = AvailableSettings.USE_QUERY_CACHE, value = "true" ), + @Setting( name = AvailableSettings.USE_SECOND_LEVEL_CACHE, value = "true" ) +} ) +@Jira( "https://hibernate.atlassian.net/browse/HHH-18439" ) +public class QueryCacheNullValueTest { + @Test + public void testNullProperty(SessionFactoryScope scope) { + executeQuery( scope, "select data from BasicEntity" ); + } + + @Test + public void testNullLiteral(SessionFactoryScope scope) { + executeQuery( scope, "select null from BasicEntity" ); + } + + private static void executeQuery(SessionFactoryScope scope, String hql) { + scope.getSessionFactory().getCache().evictQueryRegions(); + final StatisticsImplementor statistics = scope.getSessionFactory().getStatistics(); + statistics.clear(); + + for ( int i = 0; i < 2; i++ ) { + final int hitCount = i; + scope.inTransaction( session -> { + assertThat( session.createQuery( hql, String.class ) + .setCacheable( true ) + .getSingleResult() ).isNull(); + // 0 hits, 1 miss, 1 put + assertThat( statistics.getQueryCacheHitCount() ).isEqualTo( hitCount ); + assertThat( statistics.getQueryCacheMissCount() ).isEqualTo( 1 ); + assertThat( statistics.getQueryCachePutCount() ).isEqualTo( 1 ); + session.clear(); + } ); + } + } + + @BeforeAll + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> session.persist( new BasicEntity( 1, null ) ) ); + } + + @AfterAll + public void tearDown(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/queryhint/OracleQueryHintTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/queryhint/OracleQueryHintTest.java index 91c0ee258b8e..a2724cb61cdb 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/queryhint/OracleQueryHintTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/queryhint/OracleQueryHintTest.java @@ -96,7 +96,7 @@ public void testQueryHint(SessionFactoryScope scope) { } ); statementInspector.assertExecutedCount( 1 ); - assertTrue( statementInspector.getSqlQueries().get( 0 ).contains( "select /*+ ALL_ROWS, USE_CONCAT */" ) ); + assertTrue( statementInspector.getSqlQueries().get( 0 ).contains( "select /*+ ALL_ROWS USE_CONCAT */" ) ); statementInspector.clear(); // ensure the insertion logic can handle a comment appended to the front diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/schemaupdate/SchemaMigratorHaltOnErrorTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/schemaupdate/SchemaMigratorHaltOnErrorTest.java index 5607fe94d5f6..1338da0ed814 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/schemaupdate/SchemaMigratorHaltOnErrorTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/schemaupdate/SchemaMigratorHaltOnErrorTest.java @@ -17,6 +17,7 @@ import org.hibernate.cfg.AvailableSettings; import org.hibernate.community.dialect.AltibaseDialect; import org.hibernate.community.dialect.FirebirdDialect; +import org.hibernate.community.dialect.InformixDialect; import org.hibernate.dialect.DB2Dialect; import org.hibernate.dialect.DerbyDialect; import org.hibernate.orm.test.jpa.BaseEntityManagerFunctionalTestCase; @@ -31,6 +32,7 @@ @SkipForDialect(value = DerbyDialect.class, comment = "Derby is far more resistant to the reserved keyword usage.") @SkipForDialect(value = FirebirdDialect.class, comment = "FirebirdDialect has autoQuoteKeywords enabled, so it is far more resistant to the reserved keyword usage.") @SkipForDialect(value = AltibaseDialect.class, comment = "AltibaseDialect has autoQuoteKeywords enabled, so it is far more resistant to the reserved keyword usage.") +@SkipForDialect(value = InformixDialect.class, comment = "Informix is far more resistant to the reserved keyword usage.") public class SchemaMigratorHaltOnErrorTest extends BaseEntityManagerFunctionalTestCase { @Override diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/schemaupdate/SchemaUpdateHaltOnErrorTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/schemaupdate/SchemaUpdateHaltOnErrorTest.java index 0c5183f120db..3552251c96ed 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/schemaupdate/SchemaUpdateHaltOnErrorTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/schemaupdate/SchemaUpdateHaltOnErrorTest.java @@ -22,6 +22,7 @@ import org.hibernate.boot.spi.MetadataImplementor; import org.hibernate.community.dialect.AltibaseDialect; import org.hibernate.community.dialect.FirebirdDialect; +import org.hibernate.community.dialect.InformixDialect; import org.hibernate.dialect.DB2Dialect; import org.hibernate.testing.SkipForDialect; import org.hibernate.testing.junit4.CustomRunner; @@ -44,6 +45,7 @@ @SkipForDialect(value = DerbyDialect.class, comment = "Derby is far more resistant to the reserved keyword usage.") @SkipForDialect(value = FirebirdDialect.class, comment = "FirebirdDialect has autoQuoteKeywords enabled, so it is far more resistant to the reserved keyword usage.") @SkipForDialect(value = AltibaseDialect.class, comment = "AltibaseDialect has autoQuoteKeywords enabled, so it is far more resistant to the reserved keyword usage.") +@SkipForDialect(value = InformixDialect.class, comment = "Informix is far more resistant to the reserved keyword usage.") @RunWith(CustomRunner.class) public class SchemaUpdateHaltOnErrorTest { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/secondarytable/HHH18813Test.java b/hibernate-core/src/test/java/org/hibernate/orm/test/secondarytable/HHH18813Test.java new file mode 100644 index 000000000000..35490c1d43e5 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/secondarytable/HHH18813Test.java @@ -0,0 +1,122 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.orm.test.secondarytable; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.Inheritance; +import jakarta.persistence.InheritanceType; +import jakarta.persistence.SecondaryTable; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + + +/** + * Test for a Bugfix described in HHH-18813. + * The CteUpdateHandler generated an Insert-Query, + * which contained columns that do not exist in the target table. + * + * @author Peter Bambazek + */ +@JiraKey(value = "HHH-18813") +@DomainModel( + annotatedClasses = {HHH18813Test.SecondaryTableEntitySub.class, HHH18813Test.SecondaryTableEntityBase.class}) +@SessionFactory +class HHH18813Test { + + @Test + void hhh18813Test(SessionFactoryScope scope) { + + // prepare + scope.inTransaction( session -> { + SecondaryTableEntitySub entitySub = new SecondaryTableEntitySub(); + entitySub.setB( 111L ); + entitySub.setC( 222L ); + session.persist( entitySub ); + } ); + + // asset before + scope.inTransaction( session -> { + SecondaryTableEntitySub entitySub = session.createQuery( + "select s from SecondaryTableEntitySub s", + SecondaryTableEntitySub.class ).getSingleResult(); + assertNotNull( entitySub ); + assertEquals( 111L, entitySub.getB() ); + assertEquals( 222L, entitySub.getC() ); + } ); + + // update + scope.inTransaction( session -> { + session.createMutationQuery( "update SecondaryTableEntitySub e set e.b=:b, e.c=:c" ) + .setParameter( "b", 333L ) + .setParameter( "c", 444L ) + .executeUpdate(); + } ); + + // asset after + scope.inTransaction( session -> { + SecondaryTableEntitySub entitySub = session.createQuery( "select s from SecondaryTableEntitySub s", + SecondaryTableEntitySub.class ).getSingleResult(); + assertNotNull( entitySub ); + assertEquals( 333L, entitySub.getB() ); + assertEquals( 444L, entitySub.getC() ); + } ); + } + + @Entity(name = "SecondaryTableEntitySub") + @Inheritance(strategy = InheritanceType.JOINED) + @SecondaryTable(name = "test") + public static class SecondaryTableEntitySub extends SecondaryTableEntityBase { + + @Column + private Long b; + + @Column(table = "test") + private Long c; + + public Long getB() { + return b; + } + + public void setB(Long b) { + this.b = b; + } + + public Long getC() { + return c; + } + + public void setC(Long c) { + this.c = c; + } + } + + @Entity + @Inheritance(strategy = InheritanceType.JOINED) + public static class SecondaryTableEntityBase { + + @Id + @GeneratedValue + private Long id; + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/ToOneTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/ToOneTests.java index a2367e71ffad..280c81a6cbb6 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/ToOneTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/softdelete/ToOneTests.java @@ -111,6 +111,23 @@ void basicSelectedTest(SessionFactoryScope scope) { } ); } + @Test + void fkAccessTest(SessionFactoryScope scope) { + final SQLStatementInspector sqlInspector = scope.getCollectingStatementInspector(); + sqlInspector.clear(); + + scope.inTransaction( (session) -> { + final Integer issue2Reporter = session.createQuery( "select i.reporter.id from Issue i where i.id = 2", Integer.class ).getSingleResultOrNull(); + assertThat( issue2Reporter ).isNull(); + + assertThat( sqlInspector.getSqlQueries() ).hasSize( 1 ); + assertThat( sqlInspector.getSqlQueries().get( 0 ) ).contains( " join " ); + assertThat( sqlInspector.getSqlQueries().get( 0 ) ).contains( ".reporter_fk" ); + assertThat( sqlInspector.getSqlQueries().get( 0 ) ).containsAnyOf( ".active='Y'", ".active=N'Y'" ); + assertThat( sqlInspector.getSqlQueries().get( 0 ) ).containsOnlyOnce( "active" ); + } ); + } + @Entity(name="Issue") @Table(name="issues") public static class Issue { diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/sql/SQLQueryParserUnitTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/SQLQueryParserUnitTests.java new file mode 100644 index 000000000000..98c211350237 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/SQLQueryParserUnitTests.java @@ -0,0 +1,65 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.sql; + +import org.hibernate.dialect.H2Dialect; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.query.sql.internal.SQLQueryParser; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.RequiresDialect; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link SQLQueryParser} + * + * @author Steve Ebersole + */ +@SuppressWarnings("JUnitMalformedDeclaration") +public class SQLQueryParserUnitTests { + + @ParameterizedTest + @DomainModel + @SessionFactory + @RequiresDialect(H2Dialect.class) + @ValueSource(strings = { + "{d '2025-06-18'}", + "{t '14:00'}", + "{t '14:00:00'}", + "{ts '2025-06-18T14:00'}", + "{ts '2025-06-18T14:00:00'}", + "{ts '2025-06-18T14:00:00.123'}", + "{ts '2025-06-18T14:00:00+01:00'}"}) + void testJDBCEscapeSyntaxParsing(String variant, SessionFactoryScope scope) { + final SessionFactoryImplementor sessionFactory = scope.getSessionFactory(); + final String sqlQuery = "select id, name from {h-domain}the_table where date = " + variant; + + final String full = processSqlString( sqlQuery, "my_catalog", "my_schema", sessionFactory ); + assertThat( full ).contains( variant ); + + final String catalogOnly = processSqlString( sqlQuery, "my_catalog", null, sessionFactory ); + assertThat( catalogOnly ).contains( variant ); + + final String schemaOnly = processSqlString( sqlQuery, null, "my_schema", sessionFactory ); + assertThat( schemaOnly ).contains( variant ); + + final String none = processSqlString( sqlQuery, null, null, sessionFactory ); + assertThat( none ).contains( variant ); + } + + private static String processSqlString( + String sqlQuery, + String catalogName, + String schemaName, + SessionFactoryImplementor sessionFactory) { + return new SQLQueryParser( sqlQuery, null, sessionFactory ).process(); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/sql/TemplateTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/TemplateTest.java index 87a0cb1a7de1..96c3f0253e72 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/sql/TemplateTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/sql/TemplateTest.java @@ -7,13 +7,9 @@ package org.hibernate.orm.test.sql; -import org.hibernate.dialect.Dialect; import org.hibernate.engine.spi.SessionFactoryImplementor; -import org.hibernate.query.sqm.function.SqmFunctionRegistry; import org.hibernate.sql.Template; -import org.hibernate.type.spi.TypeConfiguration; -import org.hibernate.testing.orm.domain.StandardDomainModel; import org.hibernate.testing.orm.junit.DomainModel; import org.hibernate.testing.orm.junit.JiraKey; import org.hibernate.testing.orm.junit.SessionFactory; @@ -23,23 +19,33 @@ import static org.junit.jupiter.api.Assertions.assertEquals; @SessionFactory -@DomainModel(standardModels = StandardDomainModel.GAMBIT) +@DomainModel public class TemplateTest { @Test @JiraKey("HHH-18256") public void templateLiterals(SessionFactoryScope scope) { - assertWhereStringTemplate( "N'a'", scope.getSessionFactory() ); - assertWhereStringTemplate( "X'a'", scope.getSessionFactory() ); - assertWhereStringTemplate( "BX'a'", scope.getSessionFactory() ); - assertWhereStringTemplate( "VARBYTE'a'", scope.getSessionFactory() ); - assertWhereStringTemplate( "bytea 'a'", scope.getSessionFactory() ); - assertWhereStringTemplate( "bytea 'a'", scope.getSessionFactory() ); - assertWhereStringTemplate( "date 'a'", scope.getSessionFactory() ); - assertWhereStringTemplate( "time 'a'", scope.getSessionFactory() ); - assertWhereStringTemplate( "timestamp 'a'", scope.getSessionFactory() ); - assertWhereStringTemplate( "timestamp with time zone 'a'", scope.getSessionFactory() ); - assertWhereStringTemplate( "time with time zone 'a'", scope.getSessionFactory() ); + SessionFactoryImplementor factory = scope.getSessionFactory(); + assertWhereStringTemplate( "N'a'", factory ); + assertWhereStringTemplate( "X'a'", factory ); + assertWhereStringTemplate( "BX'a'", factory); + assertWhereStringTemplate( "VARBYTE'a'", factory ); + assertWhereStringTemplate( "bytea 'a'", factory ); + assertWhereStringTemplate( "bytea 'a'", factory ); + assertWhereStringTemplate( "date 'a'", factory ); + assertWhereStringTemplate( "time 'a'", factory ); + assertWhereStringTemplate( "timestamp 'a'", factory ); + assertWhereStringTemplate( "timestamp with time zone 'a'", factory ); + assertWhereStringTemplate( "time with time zone 'a'", factory ); + assertWhereStringTemplate( "date", "$PlaceHolder$.date", factory ); + assertWhereStringTemplate( "time", "$PlaceHolder$.time", factory ); + assertWhereStringTemplate( "zone", "$PlaceHolder$.zone", factory ); + assertWhereStringTemplate("select date from thetable", + "select $PlaceHolder$.date from thetable", factory ); + assertWhereStringTemplate("select date '2000-12-1' from thetable", + "select date '2000-12-1' from thetable", factory ); + assertWhereStringTemplate("where date between date '2000-12-1' and date '2002-12-2'", + "where $PlaceHolder$.date between date '2000-12-1' and date '2002-12-2'", factory ); } private static void assertWhereStringTemplate(String sql, SessionFactoryImplementor sf) { @@ -52,4 +58,14 @@ private static void assertWhereStringTemplate(String sql, SessionFactoryImplemen assertEquals( sql, template ); } + private static void assertWhereStringTemplate(String sql, String result, SessionFactoryImplementor sf) { + final String template = Template.renderWhereStringTemplate( + sql, + sf.getJdbcServices().getDialect(), + sf.getTypeConfiguration(), + sf.getQueryEngine().getSqmFunctionRegistry() + ); + assertEquals( result, template ); + } + } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/subquery/MultipleIdenticalColumnsInSubqueryTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/subquery/MultipleIdenticalColumnsInSubqueryTest.java new file mode 100644 index 000000000000..5d0f5ce5e303 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/subquery/MultipleIdenticalColumnsInSubqueryTest.java @@ -0,0 +1,79 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ + +package org.hibernate.orm.test.subquery; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.Tuple; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +@DomainModel(annotatedClasses = MultipleIdenticalColumnsInSubqueryTest.Something.class) +@SessionFactory +@JiraKey("HHH-19396") +class MultipleIdenticalColumnsInSubqueryTest { + + @BeforeEach + void init(SessionFactoryScope scope) { + scope.inTransaction( session -> session.persist( new Something() ) ); + } + + @AfterEach + void clean(SessionFactoryScope scope) { + scope.inTransaction( session -> session.createMutationQuery( "delete from Something" ).executeUpdate() ); + } + + @Test + @DisplayName("Temporary table with same column selected twice, deduplication should be turned off") + void CTE_with_same_column_selected_twice(SessionFactoryScope scope) { + var r = scope.fromSession( session -> + session.createSelectionQuery( + "WITH S0 AS (SELECT foo AS foo, foo AS bar FROM Something) SELECT foo AS foo FROM S0", + String.class ).getSingleResult() ); + assertEquals( "a", r ); + } + + @Test + @DisplayName("Subquery with same column selected twice, deduplication should be turned off") + void CTE_with_same_column_selected_twice_some_aliases_removed(SessionFactoryScope scope) { + var r = scope.fromSession( session -> + session.createSelectionQuery( + "SELECT foo AS foo FROM (SELECT foo AS foo, foo AS foo2 FROM Something)", + String.class ).getSingleResult() ); + assertEquals( "a", r ); + } + + @Test + @DisplayName("Simple query with same column selected twice, deduplication should be turned on") + void simple_query_with_same_column_selected_twice(SessionFactoryScope scope) { + var tuple = scope.fromSession( session -> + session.createSelectionQuery( + "SELECT foo AS foo, foo as bar FROM Something", + Tuple.class ).getSingleResult() ); + assertEquals( 2, tuple.getElements().size() ); + assertEquals( "a", tuple.get( "foo" ) ); + assertEquals( "a", tuple.get( "bar" ) ); + } + + @Entity(name = "Something") + static class Something { + @Id + @GeneratedValue + private Long id; + private String foo = "a"; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/temporal/FractionalSecondsTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/temporal/FractionalSecondsTests.java index 7fe9ae3ff946..3bb0bd086e6b 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/temporal/FractionalSecondsTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/temporal/FractionalSecondsTests.java @@ -16,6 +16,7 @@ import org.hibernate.annotations.FractionalSeconds; import org.hibernate.boot.spi.MetadataImplementor; import org.hibernate.community.dialect.AltibaseDialect; +import org.hibernate.community.dialect.InformixDialect; import org.hibernate.dialect.CockroachDialect; import org.hibernate.dialect.DerbyDialect; import org.hibernate.dialect.Dialect; @@ -121,6 +122,7 @@ void testUsage(SessionFactoryScope scope) { @SkipForDialect(dialectClass = HANADialect.class, reason = "HANA does not support specifying a precision on timestamps") @SkipForDialect(dialectClass = SybaseDialect.class, reason = "Because... Sybase...", matchSubTypes = true) @SkipForDialect(dialectClass = AltibaseDialect.class, reason = "Altibase does not support specifying a precision on timestamps") + @SkipForDialect(dialectClass = InformixDialect.class, reason = "Informix only supports precision from 1 to 5") void testUsage0(SessionFactoryScope scope) { final Instant start = Instant.now(); @@ -172,6 +174,7 @@ void testUsage3(SessionFactoryScope scope) { @SkipForDialect(dialectClass = PostgreSQLDialect.class, reason = "PostgreSQL only supports precision <= 6", matchSubTypes = true) @SkipForDialect(dialectClass = CockroachDialect.class, reason = "CockroachDB only supports precision <= 6") @SkipForDialect(dialectClass = HANADialect.class, reason = "HANA does not support specifying a precision on timestamps") + @SkipForDialect(dialectClass = InformixDialect.class, reason = "Informix only supports precision from 1 to 5") void testUsage9(SessionFactoryScope scope) { final Instant start = Instant.now(); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/temporal/JavaTimeFractionalSecondsTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/temporal/JavaTimeFractionalSecondsTests.java index b84fbda1ea41..0e40a61fc1ed 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/temporal/JavaTimeFractionalSecondsTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/temporal/JavaTimeFractionalSecondsTests.java @@ -17,6 +17,7 @@ import org.hibernate.annotations.JdbcTypeCode; import org.hibernate.boot.spi.MetadataImplementor; import org.hibernate.community.dialect.AltibaseDialect; +import org.hibernate.community.dialect.InformixDialect; import org.hibernate.dialect.CockroachDialect; import org.hibernate.dialect.DerbyDialect; import org.hibernate.dialect.Dialect; @@ -123,6 +124,7 @@ void testUsage(SessionFactoryScope scope) { @SkipForDialect(dialectClass = HANADialect.class, reason = "HANA does not support specifying a precision on timestamps") @SkipForDialect(dialectClass = SybaseDialect.class, reason = "Because... Sybase...", matchSubTypes = true) @SkipForDialect(dialectClass = AltibaseDialect.class, reason = "Altibase does not support specifying a precision on timestamps") + @SkipForDialect(dialectClass = InformixDialect.class, reason = "Informix only supports precision from 1 to 5") void testUsage0(SessionFactoryScope scope) { final Instant start = Instant.now(); @@ -174,6 +176,7 @@ void testUsage3(SessionFactoryScope scope) { @SkipForDialect(dialectClass = PostgreSQLDialect.class, reason = "PostgreSQL only supports precision <= 6", matchSubTypes = true) @SkipForDialect(dialectClass = CockroachDialect.class, reason = "CockroachDB only supports precision <= 6") @SkipForDialect(dialectClass = HANADialect.class, reason = "HANA does not support specifying a precision on timestamps") + @SkipForDialect(dialectClass = InformixDialect.class, reason = "Informix only supports precision from 1 to 5") void testUsage9(SessionFactoryScope scope) { final Instant start = Instant.now(); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/temporal/MySQLTimestampFspFunctionTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/temporal/MySQLTimestampFspFunctionTest.java index 372d21dccceb..58681dfc1069 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/temporal/MySQLTimestampFspFunctionTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/temporal/MySQLTimestampFspFunctionTest.java @@ -6,6 +6,7 @@ */ package org.hibernate.orm.test.temporal; +import java.sql.Time; import java.sql.Timestamp; import org.hibernate.dialect.MySQLDialect; @@ -18,6 +19,7 @@ import org.hibernate.testing.orm.junit.SessionFactory; import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.type.descriptor.java.JdbcTimeJavaType; import org.junit.jupiter.api.Test; import static org.junit.Assert.assertEquals; @@ -48,11 +50,13 @@ public void testTimeStampFunctions(SessionFactoryScope scope) { ); Object[] oArray = (Object[]) q.uniqueResult(); for ( Object o : oArray ) { - ( (Timestamp) o ).setNanos( 0 ); + if ( o instanceof Timestamp) { + ( (Timestamp) o ).setNanos( 0 ); + } } final Timestamp now = (Timestamp) oArray[0]; assertEquals( now, oArray[1] ); - assertEquals( now, oArray[2] ); + assertTrue( JdbcTimeJavaType.INSTANCE.areEqual( new Time( now.getTime() ), (Time) oArray[2] ) ); assertEquals( now, oArray[3] ); assertTrue( now.compareTo( (Timestamp) oArray[4] ) <= 0 ); } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/tenantid/TenantIdToOneBidirectionalTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/tenantid/TenantIdToOneBidirectionalTest.java new file mode 100644 index 000000000000..b48881eef31c --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/tenantid/TenantIdToOneBidirectionalTest.java @@ -0,0 +1,165 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.tenantid; + +import jakarta.persistence.Column; +import org.hibernate.annotations.TenantId; +import org.hibernate.boot.SessionFactoryBuilder; +import org.hibernate.boot.spi.MetadataImplementor; +import org.hibernate.cfg.AvailableSettings; +import org.hibernate.context.spi.CurrentTenantIdentifierResolver; +import org.hibernate.engine.spi.SessionFactoryImplementor; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.ServiceRegistry; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryProducer; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.Setting; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToOne; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Marco Belladelli + */ +@DomainModel( annotatedClasses = { + TenantIdToOneBidirectionalTest.RootEntity.class, + TenantIdToOneBidirectionalTest.ChildEntity.class, +} ) +@SessionFactory +@ServiceRegistry( settings = @Setting( name = AvailableSettings.HBM2DDL_AUTO, value = "create-drop" ) ) +@Jira( "https://hibernate.atlassian.net/browse/HHH-18617" ) +public class TenantIdToOneBidirectionalTest implements SessionFactoryProducer { + private static String currentTenant; + + @Test + public void testExistingRoot(SessionFactoryScope scope) { + currentTenant = "tenant_1"; + scope.inTransaction( session -> { + final var child = session.find( ChildEntity.class, 1L ); + assertThat( child.getRoot() ).isNotNull().extracting( RootEntity::getChild ).isSameAs( child ); + } ); + } + + @Test + public void testRemovedRoot(SessionFactoryScope scope) { + currentTenant = "tenant_2"; + scope.inTransaction( session -> { + final var child = session.find( ChildEntity.class, 2L ); + assertThat( child.getRoot() ).isNull(); + } ); + } + + @Test + public void testNoRoot(SessionFactoryScope scope) { + currentTenant = "tenant_3"; + scope.inTransaction( session -> { + final var child = session.find( ChildEntity.class, 3L ); + assertThat( child.getRoot() ).isNull(); + } ); + } + + @BeforeAll + public void setUp(SessionFactoryScope scope) { + currentTenant = "tenant_1"; + scope.inTransaction( session -> { + final var withChild = new RootEntity( 1L ); + withChild.setChild( new ChildEntity( 1L ) ); + session.persist( withChild ); + } ); + currentTenant = "tenant_2"; + scope.inTransaction( session -> { + final var deletedRoot = new RootEntity( 2L ); + final var child = new ChildEntity( 2L ); + deletedRoot.setChild( child ); + session.persist( deletedRoot ); + + session.flush(); + session.clear(); + session.remove( deletedRoot ); + } ); + currentTenant = "tenant_3"; + scope.inTransaction( session -> session.persist( new ChildEntity( 3L ) ) ); + } + + @Override + public SessionFactoryImplementor produceSessionFactory(MetadataImplementor model) { + final SessionFactoryBuilder sfb = model.getSessionFactoryBuilder(); + sfb.applyCurrentTenantIdentifierResolver( new CurrentTenantIdentifierResolver() { + @Override + public String resolveCurrentTenantIdentifier() { + return currentTenant; + } + + @Override + public boolean validateExistingCurrentSessions() { + return false; + } + } ); + return (SessionFactoryImplementor) sfb.build(); + } + + @Entity( name = "RootEntity" ) + static class RootEntity { + @Id + private Long id; + + @TenantId + @Column(name="tenant_col") + private String tenant; + + @OneToOne( cascade = CascadeType.PERSIST ) + @JoinColumn + private ChildEntity child; + + public RootEntity() { + } + + public RootEntity(Long id) { + this.id = id; + } + + public ChildEntity getChild() { + return child; + } + + public void setChild(ChildEntity child) { + this.child = child; + } + } + + @Entity( name = "ChildEntity" ) + static class ChildEntity { + @Id + private Long id; + + @TenantId + @Column(name="tenant_col") + private String tenant; + + @OneToOne( mappedBy = "child" ) + private RootEntity root; + + public ChildEntity() { + } + + public ChildEntity(Long id) { + this.id = id; + } + + public RootEntity getRoot() { + return root; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/timezones/AutoZonedTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/timezones/AutoZonedTest.java index 421fd7deb3b5..9a88ffee9321 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/timezones/AutoZonedTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/timezones/AutoZonedTest.java @@ -54,12 +54,12 @@ public class AutoZonedTest { Zoned z = s.find(Zoned.class, id); final Dialect dialect = scope.getSessionFactory().getJdbcServices().getDialect(); assertEquals( - DateTimeUtils.roundToDefaultPrecision( nowZoned.toInstant(), dialect ), - DateTimeUtils.roundToDefaultPrecision( z.zonedDateTime.toInstant(), dialect ) + DateTimeUtils.adjustToDefaultPrecision( nowZoned.toInstant(), dialect ), + DateTimeUtils.adjustToDefaultPrecision( z.zonedDateTime.toInstant(), dialect ) ); assertEquals( - DateTimeUtils.roundToDefaultPrecision( nowOffset.toInstant(), dialect ), - DateTimeUtils.roundToDefaultPrecision( z.offsetDateTime.toInstant(), dialect ) + DateTimeUtils.adjustToDefaultPrecision( nowOffset.toInstant(), dialect ), + DateTimeUtils.adjustToDefaultPrecision( z.offsetDateTime.toInstant(), dialect ) ); assertEquals( nowZoned.toOffsetDateTime().getOffset(), z.zonedDateTime.toOffsetDateTime().getOffset() ); assertEquals( nowOffset.getOffset(), z.offsetDateTime.getOffset() ); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/timezones/ColumnZonedTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/timezones/ColumnZonedTest.java index dbbcb3a62eca..245b6f2d997f 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/timezones/ColumnZonedTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/timezones/ColumnZonedTest.java @@ -54,12 +54,12 @@ public class ColumnZonedTest { Zoned z = s.find(Zoned.class, id); final Dialect dialect = scope.getSessionFactory().getJdbcServices().getDialect(); assertEquals( - DateTimeUtils.roundToDefaultPrecision( nowZoned.toInstant(), dialect ), - DateTimeUtils.roundToDefaultPrecision( z.zonedDateTime.toInstant(), dialect ) + DateTimeUtils.adjustToDefaultPrecision( nowZoned.toInstant(), dialect ), + DateTimeUtils.adjustToDefaultPrecision( z.zonedDateTime.toInstant(), dialect ) ); assertEquals( - DateTimeUtils.roundToDefaultPrecision( nowOffset.toInstant(), dialect ), - DateTimeUtils.roundToDefaultPrecision( z.offsetDateTime.toInstant(), dialect ) + DateTimeUtils.adjustToDefaultPrecision( nowOffset.toInstant(), dialect ), + DateTimeUtils.adjustToDefaultPrecision( z.offsetDateTime.toInstant(), dialect ) ); assertEquals( nowZoned.toOffsetDateTime().getOffset(), z.zonedDateTime.toOffsetDateTime().getOffset() ); assertEquals( nowOffset.getOffset(), z.offsetDateTime.getOffset() ); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/timezones/DefaultZonedTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/timezones/DefaultZonedTest.java index 0bb8a1f3b220..37361b7353d1 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/timezones/DefaultZonedTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/timezones/DefaultZonedTest.java @@ -51,12 +51,12 @@ public class DefaultZonedTest { Zoned z = s.find(Zoned.class, id); final Dialect dialect = scope.getSessionFactory().getJdbcServices().getDialect(); assertEquals( - DateTimeUtils.roundToDefaultPrecision( nowZoned.toInstant(), dialect ), - DateTimeUtils.roundToDefaultPrecision( z.zonedDateTime.toInstant(), dialect ) + DateTimeUtils.adjustToDefaultPrecision( nowZoned.toInstant(), dialect ), + DateTimeUtils.adjustToDefaultPrecision( z.zonedDateTime.toInstant(), dialect ) ); assertEquals( - DateTimeUtils.roundToDefaultPrecision( nowOffset.toInstant(), dialect ), - DateTimeUtils.roundToDefaultPrecision( z.offsetDateTime.toInstant(), dialect ) + DateTimeUtils.adjustToDefaultPrecision( nowOffset.toInstant(), dialect ), + DateTimeUtils.adjustToDefaultPrecision( z.offsetDateTime.toInstant(), dialect ) ); if ( dialect.getTimeZoneSupport() == TimeZoneSupport.NATIVE ) { assertEquals( nowZoned.toOffsetDateTime().getOffset(), z.zonedDateTime.toOffsetDateTime().getOffset() ); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/timezones/JDBCTimeZoneZonedTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/timezones/JDBCTimeZoneZonedTest.java index f77bc9ced6a0..aeb27acc9e11 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/timezones/JDBCTimeZoneZonedTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/timezones/JDBCTimeZoneZonedTest.java @@ -58,12 +58,12 @@ public class JDBCTimeZoneZonedTest { ZoneOffset systemOffset = systemZone.getRules().getOffset( Instant.now() ); final Dialect dialect = scope.getSessionFactory().getJdbcServices().getDialect(); assertEquals( - DateTimeUtils.roundToDefaultPrecision( nowZoned.toInstant(), dialect ), - DateTimeUtils.roundToDefaultPrecision( z.zonedDateTime.toInstant(), dialect ) + DateTimeUtils.adjustToDefaultPrecision( nowZoned.toInstant(), dialect ), + DateTimeUtils.adjustToDefaultPrecision( z.zonedDateTime.toInstant(), dialect ) ); assertEquals( - DateTimeUtils.roundToDefaultPrecision( nowOffset.toInstant(), dialect ), - DateTimeUtils.roundToDefaultPrecision( z.offsetDateTime.toInstant(), dialect ) + DateTimeUtils.adjustToDefaultPrecision( nowOffset.toInstant(), dialect ), + DateTimeUtils.adjustToDefaultPrecision( z.offsetDateTime.toInstant(), dialect ) ); assertEquals( systemZone, z.zonedDateTime.getZone() ); assertEquals( systemOffset, z.offsetDateTime.getOffset() ); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/timezones/PassThruZonedTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/timezones/PassThruZonedTest.java index 8404eb25e475..440e066af4b8 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/timezones/PassThruZonedTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/timezones/PassThruZonedTest.java @@ -57,12 +57,12 @@ public class PassThruZonedTest { ZoneOffset systemOffset = systemZone.getRules().getOffset( Instant.now() ); final Dialect dialect = scope.getSessionFactory().getJdbcServices().getDialect(); assertEquals( - DateTimeUtils.roundToDefaultPrecision( nowZoned.toInstant(), dialect ), - DateTimeUtils.roundToDefaultPrecision( z.zonedDateTime.toInstant(), dialect ) + DateTimeUtils.adjustToDefaultPrecision( nowZoned.toInstant(), dialect ), + DateTimeUtils.adjustToDefaultPrecision( z.zonedDateTime.toInstant(), dialect ) ); assertEquals( - DateTimeUtils.roundToDefaultPrecision( nowOffset.toInstant(), dialect ), - DateTimeUtils.roundToDefaultPrecision( z.offsetDateTime.toInstant(), dialect ) + DateTimeUtils.adjustToDefaultPrecision( nowOffset.toInstant(), dialect ), + DateTimeUtils.adjustToDefaultPrecision( z.offsetDateTime.toInstant(), dialect ) ); assertEquals( systemZone, z.zonedDateTime.getZone() ); assertEquals( systemOffset, z.offsetDateTime.getOffset() ); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/timezones/UTCNormalizedInstantTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/timezones/UTCNormalizedInstantTest.java index dae2242aa801..fb6000ad13b3 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/timezones/UTCNormalizedInstantTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/timezones/UTCNormalizedInstantTest.java @@ -46,12 +46,12 @@ public class UTCNormalizedInstantTest { final Zoned z = s.find(Zoned.class, id); final Dialect dialect = scope.getSessionFactory().getJdbcServices().getDialect(); assertEquals( - DateTimeUtils.roundToDefaultPrecision( z.utcInstant, dialect ), - DateTimeUtils.roundToDefaultPrecision( instant, dialect ) + DateTimeUtils.adjustToDefaultPrecision( z.utcInstant, dialect ), + DateTimeUtils.adjustToDefaultPrecision( instant, dialect ) ); assertEquals( - DateTimeUtils.roundToDefaultPrecision( z.localInstant, dialect ), - DateTimeUtils.roundToDefaultPrecision( instant, dialect ) + DateTimeUtils.adjustToDefaultPrecision( z.localInstant, dialect ), + DateTimeUtils.adjustToDefaultPrecision( instant, dialect ) ); }); } @@ -80,12 +80,12 @@ public class UTCNormalizedInstantTest { final Zoned z = s.find(Zoned.class, id); final Dialect dialect = scope.getSessionFactory().getJdbcServices().getDialect(); assertEquals( - DateTimeUtils.roundToDefaultPrecision( z.utcInstant, dialect ), - DateTimeUtils.roundToDefaultPrecision( instant, dialect ) + DateTimeUtils.adjustToDefaultPrecision( z.utcInstant, dialect ), + DateTimeUtils.adjustToDefaultPrecision( instant, dialect ) ); assertEquals( - DateTimeUtils.roundToDefaultPrecision( z.localInstant, dialect ), - DateTimeUtils.roundToDefaultPrecision( instant, dialect ) + DateTimeUtils.adjustToDefaultPrecision( z.localInstant, dialect ), + DateTimeUtils.adjustToDefaultPrecision( instant, dialect ) ); }); } diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/timezones/UTCNormalizedZonedTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/timezones/UTCNormalizedZonedTest.java index cbab481e0f1d..92c3865749ea 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/timezones/UTCNormalizedZonedTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/timezones/UTCNormalizedZonedTest.java @@ -54,12 +54,12 @@ public class UTCNormalizedZonedTest { Zoned z = s.find(Zoned.class, id); final Dialect dialect = scope.getSessionFactory().getJdbcServices().getDialect(); assertEquals( - DateTimeUtils.roundToDefaultPrecision( nowZoned.toInstant(), dialect ), - DateTimeUtils.roundToDefaultPrecision( z.zonedDateTime.toInstant(), dialect ) + DateTimeUtils.adjustToDefaultPrecision( nowZoned.toInstant(), dialect ), + DateTimeUtils.adjustToDefaultPrecision( z.zonedDateTime.toInstant(), dialect ) ); assertEquals( - DateTimeUtils.roundToDefaultPrecision( nowOffset.toInstant(), dialect ), - DateTimeUtils.roundToDefaultPrecision( z.offsetDateTime.toInstant(), dialect ) + DateTimeUtils.adjustToDefaultPrecision( nowOffset.toInstant(), dialect ), + DateTimeUtils.adjustToDefaultPrecision( z.offsetDateTime.toInstant(), dialect ) ); assertEquals( ZoneId.of("Z"), z.zonedDateTime.getZone() ); assertEquals( ZoneOffset.ofHours(0), z.offsetDateTime.getOffset() ); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/tool/schema/DefaultImportFileExecutionTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/tool/schema/DefaultImportFileExecutionTest.java index ed486db44300..dcf17761f81f 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/tool/schema/DefaultImportFileExecutionTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/tool/schema/DefaultImportFileExecutionTest.java @@ -3,8 +3,12 @@ import java.io.File; import java.io.FileWriter; import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URI; import java.net.URL; +import java.net.URLClassLoader; import java.nio.file.Files; +import java.nio.file.Path; import java.util.ArrayList; import java.util.EnumSet; import java.util.HashMap; @@ -13,9 +17,10 @@ import org.hibernate.boot.Metadata; import org.hibernate.boot.MetadataSources; +import org.hibernate.boot.registry.BootstrapServiceRegistryBuilder; import org.hibernate.boot.registry.StandardServiceRegistry; -import org.hibernate.boot.registry.StandardServiceRegistryBuilder; import org.hibernate.boot.registry.classloading.spi.ClassLoaderService; +import org.hibernate.bytecode.enhance.spi.Enhancer; import org.hibernate.cfg.AvailableSettings; import org.hibernate.tool.schema.SourceType; import org.hibernate.tool.schema.TargetType; @@ -30,6 +35,7 @@ import org.hibernate.tool.schema.spi.SourceDescriptor; import org.hibernate.tool.schema.spi.TargetDescriptor; +import org.hibernate.testing.orm.junit.Jira; import org.hibernate.testing.util.ServiceRegistryUtil; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -42,8 +48,8 @@ import static org.assertj.core.api.AssertionsForClassTypes.assertThat; import static org.hibernate.tool.schema.internal.SchemaCreatorImpl.DEFAULT_IMPORT_FILE; +@Jira( "https://hibernate.atlassian.net/browse/HHH-15717" ) public class DefaultImportFileExecutionTest { - private File defaultImportFile; private StandardServiceRegistry serviceRegistry; private static final String COMMAND = "INSERT INTO TEST_ENTITY (id, name) values (1,'name')"; @@ -52,7 +58,9 @@ public class DefaultImportFileExecutionTest { @BeforeEach public void setUp() throws Exception { defaultImportFile = createDefaultImportFile( "import.sql" ); - serviceRegistry = ServiceRegistryUtil.serviceRegistry(); + serviceRegistry = ServiceRegistryUtil.serviceRegistryBuilder( + new BootstrapServiceRegistryBuilder().applyClassLoader( toClassLoader( defaultImportFile.getParentFile() ) ).build() + ).build(); } @AfterEach @@ -99,11 +107,9 @@ private void createSchema(TargetDescriptorImpl targetDescriptor) { ); } - private static File createDefaultImportFile(String fileName) throws Exception { - URL myUrl = Thread.currentThread().getContextClassLoader().getResource( "hibernate.properties" ); - String path = myUrl.getPath().replace( "hibernate.properties", fileName ); - final File file = new File( path ); - file.createNewFile(); + private static File createDefaultImportFile(@SuppressWarnings( "SameParameterValue" ) String fileName) throws Exception { + final Path tmp = Files.createTempDirectory( "default_import" ); + final File file = new File( tmp.toString() + File.separator + fileName ); try (final FileWriter myWriter = new FileWriter( file )) { myWriter.write( COMMAND ); @@ -112,6 +118,17 @@ private static File createDefaultImportFile(String fileName) throws Exception { return file; } + private static ClassLoader toClassLoader(File classesDir) { + final URI classesDirUri = classesDir.toURI(); + try { + final URL url = classesDirUri.toURL(); + return new URLClassLoader( new URL[] { url }, Enhancer.class.getClassLoader() ); + } + catch (MalformedURLException e) { + throw new RuntimeException( "Unable to resolve classpath entry to URL : " + classesDir.getAbsolutePath(), e ); + } + } + private Metadata buildMappings(StandardServiceRegistry registry) { return new MetadataSources( registry ) .addAnnotatedClass( TestEntity.class ) diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/tool/schema/SkipDefaultImportFileExecutionTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/tool/schema/SkipDefaultImportFileExecutionTest.java new file mode 100644 index 000000000000..ba384e57e62d --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/tool/schema/SkipDefaultImportFileExecutionTest.java @@ -0,0 +1,243 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.tool.schema; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import org.hibernate.boot.Metadata; +import org.hibernate.boot.MetadataSources; +import org.hibernate.boot.registry.BootstrapServiceRegistryBuilder; +import org.hibernate.boot.registry.StandardServiceRegistry; +import org.hibernate.boot.registry.classloading.spi.ClassLoaderService; +import org.hibernate.bytecode.enhance.spi.Enhancer; +import org.hibernate.testing.util.ServiceRegistryUtil; +import org.hibernate.tool.schema.SourceType; +import org.hibernate.tool.schema.TargetType; +import org.hibernate.tool.schema.internal.SchemaCreatorImpl; +import org.hibernate.tool.schema.spi.CommandAcceptanceException; +import org.hibernate.tool.schema.spi.ContributableMatcher; +import org.hibernate.tool.schema.spi.ExceptionHandler; +import org.hibernate.tool.schema.spi.ExecutionOptions; +import org.hibernate.tool.schema.spi.ScriptSourceInput; +import org.hibernate.tool.schema.spi.ScriptTargetOutput; +import org.hibernate.tool.schema.spi.SourceDescriptor; +import org.hibernate.tool.schema.spi.TargetDescriptor; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.io.File; +import java.io.FileWriter; +import java.io.IOException; +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.EnumSet; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; +import static org.hibernate.cfg.SchemaToolingSettings.HBM2DDL_SKIP_DEFAULT_IMPORT_FILE; +import static org.hibernate.tool.schema.internal.SchemaCreatorImpl.DEFAULT_IMPORT_FILE; + +public class SkipDefaultImportFileExecutionTest { + private File defaultImportFile; + private StandardServiceRegistry serviceRegistry; + private static final String COMMAND = "INSERT INTO TEST_ENTITY (id, name) values (1,'name')"; + + + @BeforeEach + public void setUp() throws Exception { + defaultImportFile = createDefaultImportFile( "import.sql" ); + serviceRegistry = ServiceRegistryUtil.serviceRegistryBuilder( + new BootstrapServiceRegistryBuilder().applyClassLoader( + toClassLoader( defaultImportFile.getParentFile() ) ).build() + ).build(); + } + + @AfterEach + public void tearDown() { + serviceRegistry.close(); + if ( defaultImportFile.exists() ) { + try { + Files.delete( defaultImportFile.toPath() ); + } + catch (IOException e) { + throw new RuntimeException( e ); + } + } + } + + @Test + public void testImportScriptIsNotExecutedWhitSkipDefaultImportFileSetting() { + assertThat( serviceRegistry.getService( ClassLoaderService.class ) + .locateResource( DEFAULT_IMPORT_FILE ) ).isNotNull(); + + TargetDescriptorImpl targetDescriptor = TargetDescriptorImpl.INSTANCE; + + createSchema( targetDescriptor ); + + TestScriptTargetOutput targetOutput = (TestScriptTargetOutput) targetDescriptor.getScriptTargetOutput(); + + assertNoInsertCommandsFromDeaultImportFileHasBeenExecuted( targetOutput ); + } + + private static void assertNoInsertCommandsFromDeaultImportFileHasBeenExecuted(TestScriptTargetOutput targetOutput) { + assertThat( targetOutput.getInsertCommands().size() ).isEqualTo( 0 ) + .as( "Despite setting hibernate.hbm2ddl.skip_default_import_file to true the default import.sql file insert command hs been executed" ); + } + + private void createSchema(TargetDescriptorImpl targetDescriptor) { + + final Metadata mappings = buildMappings( serviceRegistry ); + + final SchemaCreatorImpl schemaCreator = new SchemaCreatorImpl( serviceRegistry ); + + final Map options = new HashMap<>(); + options.put( HBM2DDL_SKIP_DEFAULT_IMPORT_FILE, "true" ); + schemaCreator.doCreation( + mappings, + new ExecutionOptionsTestImpl( options ), + ContributableMatcher.ALL, + SourceDescriptorImpl.INSTANCE, + targetDescriptor + ); + } + + private static File createDefaultImportFile(@SuppressWarnings("SameParameterValue") String fileName) + throws Exception { + final Path tmp = Files.createTempDirectory( "default_import" ); + final File file = new File( tmp.toString() + File.separator + fileName ); + + try (final FileWriter myWriter = new FileWriter( file )) { + myWriter.write( COMMAND ); + } + + return file; + } + + private static ClassLoader toClassLoader(File classesDir) { + final URI classesDirUri = classesDir.toURI(); + try { + final URL url = classesDirUri.toURL(); + return new URLClassLoader( new URL[] {url}, Enhancer.class.getClassLoader() ); + } + catch (MalformedURLException e) { + throw new RuntimeException( "Unable to resolve classpath entry to URL : " + classesDir.getAbsolutePath(), + e ); + } + } + + private Metadata buildMappings(StandardServiceRegistry registry) { + return new MetadataSources( registry ) + .addAnnotatedClass( TestEntity.class ) + .buildMetadata(); + } + + @Entity(name = "TestEntity") + @Table(name = "TEST_ENTITY") + public static class TestEntity { + @Id + private long id; + + private String name; + } + + public class ExecutionOptionsTestImpl implements ExecutionOptions, ExceptionHandler { + Map configValues; + + public ExecutionOptionsTestImpl(Map configValues) { + this.configValues = configValues; + } + + @Override + public Map getConfigurationValues() { + return configValues; + } + + @Override + public boolean shouldManageNamespaces() { + return true; + } + + @Override + public ExceptionHandler getExceptionHandler() { + return this; + } + + @Override + public void handleException(CommandAcceptanceException exception) { + throw exception; + } + } + + private static class SourceDescriptorImpl implements SourceDescriptor { + /** + * Singleton access + */ + public static final SourceDescriptorImpl INSTANCE = new SourceDescriptorImpl(); + + @Override + public SourceType getSourceType() { + return SourceType.METADATA; + } + + @Override + public ScriptSourceInput getScriptSourceInput() { + return null; + } + } + + private static class TargetDescriptorImpl implements TargetDescriptor { + /** + * Singleton access + */ + public static final TargetDescriptorImpl INSTANCE = new TargetDescriptorImpl(); + + @Override + public EnumSet getTargetTypes() { + return EnumSet.of( TargetType.SCRIPT ); + } + + @Override + public ScriptTargetOutput getScriptTargetOutput() { + return TestScriptTargetOutput.INSTANCE; + } + } + + private static class TestScriptTargetOutput implements ScriptTargetOutput { + + public static final TestScriptTargetOutput INSTANCE = new TestScriptTargetOutput(); + + List insertCommands = new ArrayList<>(); + + @Override + public void prepare() { + + } + + @Override + public void accept(String command) { + if ( command.toLowerCase().startsWith( "insert" ) ) { + insertCommands.add( command ); + } + } + + @Override + public void release() { + + } + + public List getInsertCommands() { + return insertCommands; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/type/PostgresIntervalSecondTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/type/PostgresIntervalSecondTest.java index cd3c874fbf8d..2f32a016cfe0 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/type/PostgresIntervalSecondTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/type/PostgresIntervalSecondTest.java @@ -7,6 +7,7 @@ import org.hibernate.annotations.JdbcTypeCode; import org.hibernate.cfg.AvailableSettings; import org.hibernate.dialect.PostgreSQLDialect; +import org.hibernate.dialect.PostgreSQLCastingIntervalSecondJdbcType; import org.hibernate.dialect.PostgreSQLIntervalSecondJdbcType; import org.hibernate.metamodel.spi.MappingMetamodelImplementor; import org.hibernate.persister.entity.EntityPersister; @@ -55,7 +56,7 @@ public void verifyMappings(SessionFactoryScope scope) { assertThat( durationJdbcType ).isEqualTo( NumericJdbcType.INSTANCE ); final JdbcType intervalType = jdbcTypeRegistry.getDescriptor( SqlTypes.INTERVAL_SECOND ); - assertThat( intervalType ).isOfAnyClassIn( PostgreSQLIntervalSecondJdbcType.class ); + assertThat( intervalType ).isOfAnyClassIn( PostgreSQLIntervalSecondJdbcType.class, PostgreSQLCastingIntervalSecondJdbcType.class ); // a simple duration field with no overrides - so should be using a default JdbcType assertThat( entityDescriptor.findAttributeMapping( "duration" ) diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/type/UUIDTypeConverterTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/type/UUIDTypeConverterTest.java index 6502be63b0de..ea36dfe72f4d 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/type/UUIDTypeConverterTest.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/type/UUIDTypeConverterTest.java @@ -11,10 +11,13 @@ import java.util.List; import java.util.UUID; +import org.hibernate.community.dialect.InformixDialect; + import org.hibernate.testing.TestForIssue; import org.hibernate.testing.orm.junit.DomainModel; import org.hibernate.testing.orm.junit.SessionFactory; import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.testing.orm.junit.SkipForDialect; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; @@ -38,6 +41,7 @@ ) @SessionFactory @TestForIssue(jiraKey = "HHH-15417") +@SkipForDialect(dialectClass = InformixDialect.class, reason = "Informix does not support unique / primary constraints on binary columns") public class UUIDTypeConverterTest { @AfterEach diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/type/contributor/usertype/hhh18787/CustomData.java b/hibernate-core/src/test/java/org/hibernate/orm/test/type/contributor/usertype/hhh18787/CustomData.java new file mode 100644 index 000000000000..278a8e3d9201 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/type/contributor/usertype/hhh18787/CustomData.java @@ -0,0 +1,36 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.type.contributor.usertype.hhh18787; + +/** + * Simple object holding some properties + */ +public class CustomData { + private String text; + private Long number; + + public CustomData(String text, Long number) { + this.text = text; + this.number = number; + } + + public String getText() { + return text; + } + + public void setText(String text) { + this.text = text; + } + + public Long getNumber() { + return number; + } + + public void setNumber(Long number) { + this.number = number; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/type/contributor/usertype/hhh18787/CustomDataType.java b/hibernate-core/src/test/java/org/hibernate/orm/test/type/contributor/usertype/hhh18787/CustomDataType.java new file mode 100644 index 000000000000..2fb91707f38a --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/type/contributor/usertype/hhh18787/CustomDataType.java @@ -0,0 +1,98 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.type.contributor.usertype.hhh18787; + +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.type.SqlTypes; +import org.hibernate.usertype.UserType; + +import java.io.Serializable; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Types; +import java.util.Arrays; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +/** + * Custom type implementing {@link UserType} so CustomData[] can be converted. + */ +public class CustomDataType implements UserType { + + public static final CustomDataType INSTANCE = new CustomDataType(); + + @Override + public int getSqlType() { + return SqlTypes.VARCHAR; + } + + @Override + public Class returnedClass() { + return CustomData[].class; + } + + @Override + public boolean equals(CustomData[] x, CustomData[] y) { + return Arrays.equals(x, y); + } + + @Override + public int hashCode(CustomData[] x) { + return Arrays.hashCode(x); + } + + @Override + public CustomData[] nullSafeGet(ResultSet rs, int position, SharedSessionContractImplementor session, + Object owner) throws SQLException { + + final var customDataStr = rs.getString(position); + return rs.wasNull() ? new CustomData[0] : parseDataFromString(customDataStr); + } + + @Override + public void nullSafeSet(PreparedStatement st, CustomData[] value, int index, + SharedSessionContractImplementor session) throws SQLException { + + if (value == null || value.length == 0) { + st.setNull(index, Types.VARCHAR); + } + else { + final var str = + Stream.of(value).map(u -> String.format("%s|%s", u.getText(), u.getNumber())).collect(Collectors.joining(",")); + + st.setString(index, str); + } + } + + @Override + public CustomData[] deepCopy(CustomData[] value) { + return Arrays.copyOf(value, value.length); + } + + @Override + public boolean isMutable() { + return true; + } + + @Override + public Serializable disassemble(CustomData[] value) { + return deepCopy(value); + } + + @Override + public CustomData[] assemble(Serializable cached, Object owner) { + return deepCopy((CustomData[]) cached); + } + + private CustomData[] parseDataFromString(String value) { + return Arrays.stream(value.split(",")).map(singleValue -> { + final var params = singleValue.split("\\|"); + return new CustomData(params[0], Long.parseLong(params[1])); + }).toArray(CustomData[]::new); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/type/contributor/usertype/hhh18787/SomeEntity.java b/hibernate-core/src/test/java/org/hibernate/orm/test/type/contributor/usertype/hhh18787/SomeEntity.java new file mode 100644 index 000000000000..33d9e006dcc3 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/type/contributor/usertype/hhh18787/SomeEntity.java @@ -0,0 +1,50 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.type.contributor.usertype.hhh18787; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +/** + * Some entity, important is the property customData. + */ +@Entity +@Table(name = "whatever") +public class SomeEntity { + @Id + @GeneratedValue + private Long id; + + @Column + private CustomData[] customData; + + public SomeEntity() { + } + + public SomeEntity(CustomData[] customData) { + this.customData = customData; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public CustomData[] getCustomData() { + return customData; + } + + public void setCustomData(CustomData[] custom) { + this.customData = custom; + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/type/contributor/usertype/hhh18787/TypesContributor.java b/hibernate-core/src/test/java/org/hibernate/orm/test/type/contributor/usertype/hhh18787/TypesContributor.java new file mode 100644 index 000000000000..dfbe50e90300 --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/type/contributor/usertype/hhh18787/TypesContributor.java @@ -0,0 +1,22 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.type.contributor.usertype.hhh18787; + +import org.hibernate.boot.model.TypeContributions; +import org.hibernate.boot.model.TypeContributor; +import org.hibernate.service.ServiceRegistry; + +/** + * Registering custom user type {@link CustomDataType}. + */ +public class TypesContributor implements TypeContributor { + + @Override + public void contribute(TypeContributions typeContributions, ServiceRegistry serviceRegistry) { + typeContributions.contributeType(CustomDataType.INSTANCE); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/type/contributor/usertype/hhh18787/UserTypeNotRecognisedTestCase.java b/hibernate-core/src/test/java/org/hibernate/orm/test/type/contributor/usertype/hhh18787/UserTypeNotRecognisedTestCase.java new file mode 100644 index 000000000000..364909cbc17a --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/type/contributor/usertype/hhh18787/UserTypeNotRecognisedTestCase.java @@ -0,0 +1,59 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.type.contributor.usertype.hhh18787; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +@DomainModel( + annotatedClasses = SomeEntity.class, + typeContributors = TypesContributor.class +) +@SessionFactory +@JiraKey( "HHH-18787" ) +class UserTypeNotRecognisedTestCase { + + @Test + void customUserTypeWithTypeContributorRegistrationTest(SessionFactoryScope scope) { + final var data = new CustomData( "whatever", 1L ); + scope.inTransaction( em -> { + // creating some data, flushing and clearing context + em.merge( new SomeEntity( new CustomData[] {data} ) ); + } ); + + scope.inSession( em -> { + // getting the data + final var query = em.createQuery( "select s from SomeEntity s where id is not null", SomeEntity.class ); + final var resultList = query.getResultList(); + + // single result should be present + assertNotNull( resultList ); + assertEquals( 1, resultList.size() ); + + // the entity shouldn't be null + final var entity = resultList.get( 0 ); + assertNotNull( entity ); + + // custom data array shouldn't be null and there should be single object present + final var customData = entity.getCustomData(); + assertNotNull( customData ); + assertEquals( 1, customData.length ); + + // custom data object shouldn't be null and all fields should be set with correct values + final var singleCustomData = customData[0]; + assertNotNull( singleCustomData ); + assertEquals( data.getText(), singleCustomData.getText() ); + assertEquals( data.getNumber(), singleCustomData.getNumber() ); + } ); + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/typedescriptor/NullTest.java b/hibernate-core/src/test/java/org/hibernate/orm/test/typedescriptor/NullTest.java new file mode 100644 index 000000000000..77aa747e025a --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/typedescriptor/NullTest.java @@ -0,0 +1,100 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.orm.test.typedescriptor; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; + +import static org.junit.jupiter.api.Assertions.assertNotNull; + +/** + * @author Yanming Zhou + */ +@DomainModel( + annotatedClasses = NullTest.SimpleEntity.class +) +@SessionFactory +public class NullTest { + + @BeforeEach + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( + session -> session.persist( new SimpleEntity() ) + ); + } + + @AfterEach + public void tearDown(SessionFactoryScope scope) { + scope.inTransaction( + session -> + session.createMutationQuery( "delete from SimpleEntity" ).executeUpdate() + ); + } + + @Test + @JiraKey("HHH-18581") + public void passingNullAsParameterOfNativeQuery(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + SimpleEntity persisted = session.createNativeQuery( + "select * from SimpleEntity where name is null or name=:name", + SimpleEntity.class + ).setParameter( "name", null ).uniqueResult(); + + assertNotNull( persisted ); + } + ); + } + + @Test + public void passingNullAsParameterOfQuery(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + SimpleEntity persisted = session.createQuery( + "from SimpleEntity where name is null or name=:name", + SimpleEntity.class + ).setParameter( "name", null ).uniqueResult(); + + assertNotNull( persisted ); + } + ); + } + + @Entity(name = "SimpleEntity") + static class SimpleEntity { + @Id + @GeneratedValue + private Integer id; + + private String name; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } +} diff --git a/hibernate-core/src/test/java/org/hibernate/persister/entity/JoinedSubclassEntityPersisterTest.java b/hibernate-core/src/test/java/org/hibernate/persister/entity/JoinedSubclassEntityPersisterTest.java new file mode 100644 index 000000000000..2556fee9bcfc --- /dev/null +++ b/hibernate-core/src/test/java/org/hibernate/persister/entity/JoinedSubclassEntityPersisterTest.java @@ -0,0 +1,74 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.persister.entity; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.Inheritance; +import jakarta.persistence.Table; +import org.hibernate.metamodel.MappingMetamodel; +import org.hibernate.metamodel.model.domain.internal.JpaMetamodelImpl; +import org.hibernate.testing.orm.junit.EntityManagerFactoryScope; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.Jpa; +import org.junit.jupiter.api.Test; + +import static jakarta.persistence.InheritanceType.JOINED; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * @author Vincent Bouthinon + */ +@Jpa( + annotatedClasses = { + JoinedSubclassEntityPersisterTest.Animal.class, + JoinedSubclassEntityPersisterTest.Dog.class, + } +) +@JiraKey("HHH-18703") +class JoinedSubclassEntityPersisterTest { + + @Test + void the_table_name_must_match_the_attribute_s_column(EntityManagerFactoryScope scope) { + scope.inTransaction( + entityManager -> { + JpaMetamodelImpl metamodel = (JpaMetamodelImpl) entityManager.getMetamodel(); + MappingMetamodel mappingMetamodel = metamodel.getMappingMetamodel(); + AbstractEntityPersister entityDescriptor = (AbstractEntityPersister) mappingMetamodel.getEntityDescriptor( Dog.class ); + String table = entityDescriptor.getTableName( entityDescriptor.determineTableNumberForColumn( "name" ) ); + assertEquals( "TANIMAL", table ); + } + ); + } + + @Entity + @Inheritance(strategy = JOINED) + @Table(name = "TANIMAL") + public static class Animal { + + @Id + @GeneratedValue + public Integer id; + + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + + @Entity + @Table(name = "TDOG") + public static class Dog extends Animal { + + } +} diff --git a/hibernate-core/src/test/java17/org/hibernate/orm/test/records/RecordEmbeddedPropertyNamesTest.java b/hibernate-core/src/test/java17/org/hibernate/orm/test/records/RecordEmbeddedPropertyNamesTest.java new file mode 100644 index 000000000000..62dbf9e3b4ec --- /dev/null +++ b/hibernate-core/src/test/java17/org/hibernate/orm/test/records/RecordEmbeddedPropertyNamesTest.java @@ -0,0 +1,152 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.orm.test.records; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Embeddable; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Marco Belladelli + */ +@DomainModel( annotatedClasses = { + RecordEmbeddedPropertyNamesTest.Fee.class, + RecordEmbeddedPropertyNamesTest.Getaway.class, + RecordEmbeddedPropertyNamesTest.Vacation.class, + RecordEmbeddedPropertyNamesTest.TestFee.class, + RecordEmbeddedPropertyNamesTest.TestGetaway.class, + RecordEmbeddedPropertyNamesTest.TestVacation.class, +} ) +@SessionFactory +@Jira( "https://hibernate.atlassian.net/browse/HHH-18445" ) +public class RecordEmbeddedPropertyNamesTest { + @Test + public void testFee(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final TestFee result = session.find( TestFee.class, 1L ); + assertThat( result.getFee().issuedA() ).isFalse(); + assertThat( result.getFee().issuedB() ).isTrue(); + } ); + } + + @Test + public void testGetaway(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final TestGetaway result = session.find( TestGetaway.class, 1L ); + assertThat( result.getGetaway().getawayA() ).isEqualTo( "A" ); + assertThat( result.getGetaway().getawayB() ).isEqualTo( "B" ); + } ); + } + + @Test + public void testVacation(SessionFactoryScope scope) { + scope.inTransaction( session -> { + final TestVacation result = session.find( TestVacation.class, 1L ); + assertThat( result.getVacation().amount() ).isEqualTo( 7 ); + assertThat( result.getVacation().issued() ).isTrue(); + } ); + } + + @BeforeAll + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( session -> { + session.persist( new TestFee( 1L, new Fee( true, false ) ) ); + session.persist( new TestGetaway( 1L, new Getaway( "B", "A" ) ) ); + session.persist( new TestVacation( 1L, new Vacation( true, 7 ) ) ); + } ); + } + + @AfterAll + public void tearDown(SessionFactoryScope scope) { + scope.getSessionFactory().getSchemaManager().truncateMappedObjects(); + } + + @Embeddable + public record Fee(Boolean issuedB, Boolean issuedA) { + } + + @Entity( name = "TestFee" ) + static class TestFee { + @Id + private Long id; + @Embedded + private Fee fee; + + public TestFee() { + } + + public TestFee(Long id, Fee fee) { + this.id = id; + this.fee = fee; + } + + public Fee getFee() { + return fee; + } + } + + @Embeddable + record Getaway(String getawayB, String getawayA) { + } + + @Entity( name = "TestGetaway" ) + static class TestGetaway { + @Id + private Long id; + + @Embedded + private Getaway getaway; + + public TestGetaway() { + } + + public TestGetaway(Long id, Getaway getaway) { + this.id = id; + this.getaway = getaway; + } + + public Getaway getGetaway() { + return getaway; + } + } + + @Embeddable + record Vacation(Boolean issued, Integer amount) { + } + + @Entity( name = "TestVacation" ) + static class TestVacation { + @Id + private Long id; + + @Embedded + private Vacation vacation; + + public TestVacation() { + } + + public TestVacation(Long id, Vacation vacation) { + this.id = id; + this.vacation = vacation; + } + + public Vacation getVacation() { + return vacation; + } + } +} diff --git a/hibernate-core/src/test/java17/org/hibernate/orm/test/records/RecordNestedEmbeddedWithASecondaryTableTest.java b/hibernate-core/src/test/java17/org/hibernate/orm/test/records/RecordNestedEmbeddedWithASecondaryTableTest.java new file mode 100644 index 000000000000..13f40d09c6e1 --- /dev/null +++ b/hibernate-core/src/test/java17/org/hibernate/orm/test/records/RecordNestedEmbeddedWithASecondaryTableTest.java @@ -0,0 +1,144 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.orm.test.records; + +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.SecondaryTable; +import jakarta.persistence.Table; +import org.hibernate.AnnotationException; +import org.hibernate.boot.MetadataSources; +import org.hibernate.boot.registry.StandardServiceRegistry; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.ServiceRegistryScope; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +@JiraKey("HHH-19542") +@DomainModel(annotatedClasses = { + RecordNestedEmbeddedWithASecondaryTableTest.UserEntity.class +}) +@SessionFactory +class RecordNestedEmbeddedWithASecondaryTableTest { + + private UserEntity user; + + @BeforeAll + void prepare(SessionFactoryScope scope) { + scope.inTransaction( session -> { + Person person = new Person( new FullName( "Sylvain", "Lecoy" ), 38 ); + user = new UserEntity( person ); + session.persist( user ); + } ); + } + + @Test + void test(SessionFactoryScope scope) { + scope.inTransaction(session -> { + UserEntity entity = session.find( UserEntity.class, user.id ); + assertThat( entity ).isNotNull(); + assertThat( entity.id ).isEqualTo( user.id ); + assertThat( entity.person ).isNotNull(); + assertThat( entity.person.age ).isEqualTo( 38 ); + assertThat( entity.person.fullName.firstName ).isEqualTo( "Sylvain" ); + assertThat( entity.person.fullName.lastName ).isEqualTo( "Lecoy" ); + }); + } + + @Test + void test(ServiceRegistryScope scope) { + final StandardServiceRegistry registry = scope.getRegistry(); + final MetadataSources sources = new MetadataSources( registry ).addAnnotatedClass( UserEntity1.class ); + + try { + sources.buildMetadata(); + fail( "Expecting to fail" ); + } catch (AnnotationException expected) { + assertThat( expected ).hasMessageContaining( "all properties of the embeddable class must map to the same table" ); + } + } + + @Entity + @Table(name = "UserEntity") + @SecondaryTable(name = "Person") + static class UserEntity { + @Id + @GeneratedValue + private Integer id; + private Person person; + + public UserEntity( + final Person person) { + this.person = person; + } + + protected UserEntity() { + + } + } + + @Embeddable + record Person( + FullName fullName, + @Column(table = "Person") + Integer age) { + + } + + @Embeddable + record FullName( + @Column(table = "Person") + String firstName, + @Column(table = "Person") + String lastName) { + + } + + @Entity + @Table(name = "UserEntity") + @SecondaryTable(name = "Person") + public static class UserEntity1 { + @Id + @GeneratedValue + private Integer id; + private Person1 person; + + public UserEntity1( + final Person1 person) { + this.person = person; + } + + protected UserEntity1() { + + } + } + + @Embeddable + public record Person1( + FullName1 fullName, + @Column(table = "Person") + Integer age) { + + } + + @Embeddable + public record FullName1( + @Column(table = "Person") + String firstName, + String lastName) { + + } +} diff --git a/hibernate-core/src/test/resources/org/hibernate/orm/test/columndiscriminator/orm.xml b/hibernate-core/src/test/resources/org/hibernate/orm/test/columndiscriminator/orm.xml new file mode 100644 index 000000000000..02e59602ee07 --- /dev/null +++ b/hibernate-core/src/test/resources/org/hibernate/orm/test/columndiscriminator/orm.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/hibernate-core/src/test/resources/org/hibernate/orm/test/dynamicmap/artist.hbm.xml b/hibernate-core/src/test/resources/org/hibernate/orm/test/dynamicmap/artist.hbm.xml new file mode 100644 index 000000000000..590155b72dfc --- /dev/null +++ b/hibernate-core/src/test/resources/org/hibernate/orm/test/dynamicmap/artist.hbm.xml @@ -0,0 +1,9 @@ + + + + + + + + diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/boot/internal/EnversIntegrator.java b/hibernate-envers/src/main/java/org/hibernate/envers/boot/internal/EnversIntegrator.java index 97de0810ef4e..be4e140a1ccc 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/boot/internal/EnversIntegrator.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/boot/internal/EnversIntegrator.java @@ -20,6 +20,7 @@ import org.hibernate.envers.event.spi.EnversPreCollectionRemoveEventListenerImpl; import org.hibernate.envers.event.spi.EnversPreCollectionUpdateEventListenerImpl; import org.hibernate.envers.event.spi.EnversPreUpdateEventListenerImpl; +import org.hibernate.envers.internal.tools.ReflectionTools; import org.hibernate.event.service.spi.EventListenerRegistry; import org.hibernate.event.spi.EventType; import org.hibernate.integrator.spi.Integrator; @@ -119,6 +120,6 @@ public void integrate( @Override public void disintegrate(SessionFactoryImplementor sessionFactory, SessionFactoryServiceRegistry serviceRegistry) { - // nothing to do + ReflectionTools.reset(); } } diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/event/spi/EnversPostUpdateEventListenerImpl.java b/hibernate-envers/src/main/java/org/hibernate/envers/event/spi/EnversPostUpdateEventListenerImpl.java index 4c01fa70991b..edbb37264224 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/event/spi/EnversPostUpdateEventListenerImpl.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/event/spi/EnversPostUpdateEventListenerImpl.java @@ -6,6 +6,9 @@ */ package org.hibernate.envers.event.spi; +import org.hibernate.bytecode.enhance.spi.LazyPropertyInitializer; +import org.hibernate.bytecode.enhance.spi.interceptor.LazyAttributeLoadingInterceptor; +import org.hibernate.bytecode.spi.BytecodeEnhancementMetadata; import org.hibernate.envers.boot.internal.EnversService; import org.hibernate.envers.internal.synchronization.AuditProcess; import org.hibernate.envers.internal.synchronization.work.AuditWorkUnit; @@ -73,12 +76,30 @@ private Object[] postUpdateDBState(PostUpdateEvent event) { final Object[] newDbState = event.getState().clone(); if ( event.getOldState() != null ) { final EntityPersister entityPersister = event.getPersister(); + final Object entity = event.getEntity(); + final BytecodeEnhancementMetadata instrumentationMetadata = entityPersister.getInstrumentationMetadata(); + final LazyAttributeLoadingInterceptor lazyAttributeLoadingInterceptor; + if ( instrumentationMetadata.isEnhancedForLazyLoading() ) { + lazyAttributeLoadingInterceptor = instrumentationMetadata.extractInterceptor( entity ); + } + else { + lazyAttributeLoadingInterceptor = null; + } for ( int i = 0; i < entityPersister.getPropertyNames().length; ++i ) { if ( !entityPersister.getPropertyUpdateability()[i] ) { // Assuming that PostUpdateEvent#getOldState() returns database state of the record before modification. // Otherwise, we would have to execute SQL query to be sure of @Column(updatable = false) column value. newDbState[i] = event.getOldState()[i]; } + // Properties that have not been initialized need to be fetched in order to bind their value in the + // AUDIT insert statement. + if ( newDbState[i] == LazyPropertyInitializer.UNFETCHED_PROPERTY ) { + assert lazyAttributeLoadingInterceptor != null : "Entity:`" + entityPersister.getEntityName() + "` with uninitialized property:` " + entityPersister.getPropertyNames()[i] + "` hasn't an associated LazyAttributeLoadingInterceptor"; + event.getOldState()[i] = newDbState[i] = lazyAttributeLoadingInterceptor.fetchAttribute( + entity, + entityPersister.getPropertyNames()[i] + ); + } } } return newDbState; diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/relation/BasicCollectionMapper.java b/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/relation/BasicCollectionMapper.java index d84bb582334d..d3ede1e2fae8 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/relation/BasicCollectionMapper.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/internal/entities/mapper/relation/BasicCollectionMapper.java @@ -200,7 +200,7 @@ private boolean isCollectionElementSame( // Currently the tuple is { owner_id, entity_id, rev } and so having this special // treatment is critical to avoid HHH-13080. // - if ( elementType.isEntityType() && !revisionTypeInId ) { + if ( elementType instanceof EntityType && !revisionTypeInId ) { // This is a short-circuit to check for reference equality only. // There is no need to delegate to the identifier if the objects are reference equal. diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/internal/synchronization/work/AbstractAuditWorkUnit.java b/hibernate-envers/src/main/java/org/hibernate/envers/internal/synchronization/work/AbstractAuditWorkUnit.java index c5688add9cfe..0d9f0951d3d8 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/internal/synchronization/work/AbstractAuditWorkUnit.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/internal/synchronization/work/AbstractAuditWorkUnit.java @@ -58,6 +58,8 @@ protected void fillDataWithId(Map data, Object revision) { data.put( configuration.getRevisionTypePropertyName(), revisionType ); data.put( configuration.getOriginalIdPropertyName(), originalId ); + // The $type$ property holds the name of the (versions) entity + data.put( "$type$", configuration.getAuditEntityName( entityName ) ); } @Override diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/internal/tools/ReflectionTools.java b/hibernate-envers/src/main/java/org/hibernate/envers/internal/tools/ReflectionTools.java index 0c67f2faecb1..0f9dbb4858e6 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/internal/tools/ReflectionTools.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/internal/tools/ReflectionTools.java @@ -186,4 +186,9 @@ public static Class loadClass(String name, ClassLoaderService classLoader throw new ClassLoadingException( "Unable to load class [" + name + "]", e ); } } + + public static void reset() { + SETTER_CACHE.clear(); + GETTER_CACHE.clear(); + } } diff --git a/hibernate-envers/src/main/java/org/hibernate/envers/strategy/internal/ValidityAuditStrategy.java b/hibernate-envers/src/main/java/org/hibernate/envers/strategy/internal/ValidityAuditStrategy.java index a2beb9f79d45..4703d54b4f31 100644 --- a/hibernate-envers/src/main/java/org/hibernate/envers/strategy/internal/ValidityAuditStrategy.java +++ b/hibernate-envers/src/main/java/org/hibernate/envers/strategy/internal/ValidityAuditStrategy.java @@ -433,7 +433,7 @@ private boolean isNonIdentifierWhereConditionsRequired(String entityName, String final Type propertyType = session.getSessionFactory() .getMappingMetamodel() .getEntityDescriptor( entityName ).getPropertyType( propertyName ); - if ( propertyType.isCollectionType() ) { + if ( propertyType instanceof CollectionType ) { final CollectionType collectionType = (CollectionType) propertyType; final Type collectionElementType = collectionType.getElementType( session.getSessionFactory() ); if ( collectionElementType instanceof ComponentType ) { diff --git a/hibernate-envers/src/test/java/org/hibernate/orm/test/envers/integration/blob/BasicBlobTest.java b/hibernate-envers/src/test/java/org/hibernate/orm/test/envers/integration/blob/BasicBlobTest.java new file mode 100644 index 000000000000..c8b1988ff3cb --- /dev/null +++ b/hibernate-envers/src/test/java/org/hibernate/orm/test/envers/integration/blob/BasicBlobTest.java @@ -0,0 +1,159 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.envers.integration.blob; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import org.hamcrest.Matchers; +import org.hibernate.dialect.PostgreSQLDialect; +import org.hibernate.dialect.SQLServerDialect; +import org.hibernate.dialect.SybaseDialect; +import org.hibernate.engine.jdbc.BlobProxy; +import org.hibernate.envers.Audited; +import org.hibernate.orm.test.envers.BaseEnversJPAFunctionalTestCase; +import org.hibernate.orm.test.envers.Priority; +import org.hibernate.testing.SkipForDialect; +import org.junit.Test; + +import java.io.BufferedInputStream; +import java.io.InputStream; +import java.net.URISyntaxException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.sql.Blob; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hibernate.testing.transaction.TransactionUtil.doInJPA; +import static org.junit.Assert.fail; + +/** + * @author Chris Cranford + */ +public class BasicBlobTest extends BaseEnversJPAFunctionalTestCase { + + @Override + protected Class[] getAnnotatedClasses() { + return new Class[] {Asset.class}; + } + + @Test + @Priority(10) + public void testGenerateProxyNoStream() throws URISyntaxException { + final Path path = Path.of( Thread.currentThread().getContextClassLoader() + .getResource( "org/hibernate/orm/test/envers/integration/blob/blob.txt" ).toURI() ); + doInJPA( this::entityManagerFactory, entityManager -> { + final Asset asset = new Asset(); + asset.setFileName( "blob.txt" ); + + try (final InputStream stream = new BufferedInputStream( Files.newInputStream( path ) )) { + assertThat( stream.markSupported(), Matchers.is( true ) ); + + // We use the method readAllBytes instead of passing the raw stream to the proxy + // since this is the only guaranteed way that will work across all dialects in a + // deterministic way. Postgres and Sybase will automatically close the stream + // after the blob has been written by the driver, which prevents Envers from + // then writing the contents of the stream to the audit table. + // + // If the driver and dialect are known not to close the input stream after the + // contents have been written by the driver, then it's safe to pass the stream + // here instead and the stream will be automatically marked and reset so that + // Envers can serialize the data after Hibernate has done so. Dialects like + // H2, MySQL, Oracle, SQL Server work this way. + // + // + Blob blob = BlobProxy.generateProxy( stream.readAllBytes() ); + + asset.setData( blob ); + entityManager.persist( asset ); + } + catch (Exception e) { + e.printStackTrace(); + fail( "Failed to persist the entity" ); + } + } ); + + } + + @Test + @Priority(10) + @SkipForDialect(value = PostgreSQLDialect.class, + comment = "The driver closes the stream, so it cannot be reused by envers") + @SkipForDialect(value = SQLServerDialect.class, + comment = "The driver closes the stream, so it cannot be reused by envers") + @SkipForDialect(value = SybaseDialect.class, + comment = "The driver closes the stream, so it cannot be reused by envers") + public void testGenerateProxyStream() throws URISyntaxException { + final Path path = Path.of( Thread.currentThread().getContextClassLoader() + .getResource( "org/hibernate/orm/test/envers/integration/blob/blob.txt" ).toURI() ); + + try (final InputStream stream = new BufferedInputStream( Files.newInputStream( path ) )) { + final long length = Files.size( path ); + doInJPA( this::entityManagerFactory, entityManager -> { + final Asset asset = new Asset(); + asset.setFileName( "blob.txt" ); + + assertThat( stream.markSupported(), Matchers.is( true ) ); + + // We use the method readAllBytes instead of passing the raw stream to the proxy + // since this is the only guaranteed way that will work across all dialects in a + // deterministic way. Postgres and Sybase will automatically close the stream + // after the blob has been written by the driver, which prevents Envers from + // then writing the contents of the stream to the audit table. + // + // If the driver and dialect are known not to close the input stream after the + // contents have been written by the driver, then it's safe to pass the stream + // here instead and the stream will be automatically marked and reset so that + // Envers can serialize the data after Hibernate has done so. Dialects like + // H2, MySQL, Oracle, SQL Server work this way. + // + // + Blob blob = BlobProxy.generateProxy( stream, length ); + + asset.setData( blob ); + entityManager.persist( asset ); + } ); + } + catch (Exception e) { + e.printStackTrace(); + fail( "Failed to persist the entity" ); + } + } + + @Audited + @Entity(name = "Asset") + public static class Asset { + @Id + @GeneratedValue + private Integer id; + private String fileName; + private Blob data; + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getFileName() { + return fileName; + } + + public void setFileName(String fileName) { + this.fileName = fileName; + } + + public Blob getData() { + return data; + } + + public void setData(Blob data) { + this.data = data; + } + } + +} diff --git a/hibernate-envers/src/test/java/org/hibernate/orm/test/envers/integration/lazy/LazyFieldsTest.java b/hibernate-envers/src/test/java/org/hibernate/orm/test/envers/integration/lazy/LazyFieldsTest.java new file mode 100644 index 000000000000..df4efcb12421 --- /dev/null +++ b/hibernate-envers/src/test/java/org/hibernate/orm/test/envers/integration/lazy/LazyFieldsTest.java @@ -0,0 +1,100 @@ +package org.hibernate.orm.test.envers.integration.lazy; + +import org.hibernate.annotations.LazyGroup; +import org.hibernate.envers.Audited; + +import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Basic; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; + +@JiraKey("") +@BytecodeEnhanced +@DomainModel( + annotatedClasses = { + LazyFieldsTest.TestEntity.class, + } +) +@SessionFactory +public class LazyFieldsTest { + + private static final Long ID = 1L; + + @BeforeEach + public void setUp(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + TestEntity testEntity = new TestEntity( ID, "test data", "lazyString", "group A" ); + session.persist( testEntity ); + } + ); + } + + @Test + public void testUpdate(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + TestEntity testEntity = session.find( TestEntity.class, ID ); + testEntity.setData( "modified test data" ); + } + ); + } + + @Entity(name = "TestEntity") + public static class TestEntity { + @Id + private Long id; + + @Audited + @Basic(optional = false) + private String data; + + @Audited + @Basic(fetch = FetchType.LAZY) + private String lazyString; + + @Audited + @Basic(fetch = FetchType.LAZY) + @LazyGroup("a") + private String lazyStringGroupA; + + public TestEntity() { + } + + public TestEntity(Long id, String data, String lazyString, String anotherLazyString) { + this.id = id; + this.data = data; + this.lazyString = lazyString; + this.lazyStringGroupA = anotherLazyString; + } + + public Long getId() { + return id; + } + + public String getData() { + return data; + } + + public void setData(String data) { + this.data = data; + } + + public String getLazyString() { + return lazyString; + } + + public String getLazyStringGroupA() { + return lazyStringGroupA; + } + } + +} diff --git a/hibernate-envers/src/test/java/org/hibernate/orm/test/envers/integration/lob/LargeObjectMappingTest.java b/hibernate-envers/src/test/java/org/hibernate/orm/test/envers/integration/lob/LargeObjectMappingTest.java new file mode 100644 index 000000000000..5da080e81766 --- /dev/null +++ b/hibernate-envers/src/test/java/org/hibernate/orm/test/envers/integration/lob/LargeObjectMappingTest.java @@ -0,0 +1,88 @@ +/* + * SPDX-License-Identifier: Apache-2.0 + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.envers.integration.lob; + +import java.sql.Types; +import java.util.Arrays; +import java.util.Objects; + +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.envers.Audited; +import org.hibernate.mapping.PersistentClass; +import org.hibernate.mapping.Property; +import org.hibernate.orm.test.envers.BaseEnversJPAFunctionalTestCase; + +import org.hibernate.testing.orm.junit.JiraKey; +import org.junit.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; + +import static org.junit.Assert.assertTrue; + +/** + * @author Armin Krezović (armin.krezovic at ziragroup dot com) + */ +@JiraKey(value = "HHH-16253") +public class LargeObjectMappingTest extends BaseEnversJPAFunctionalTestCase { + + @Entity + @Audited + public static class LargeObjectTestEntity { + @Id + @GeneratedValue + private Integer id; + + @JdbcTypeCode(Types.CLOB) + private String clob; + + @JdbcTypeCode(Types.BLOB) + private byte[] blob; + + public LargeObjectTestEntity() { + } + + public LargeObjectTestEntity(Integer id, String clob, byte[] blob) { + this.id = id; + this.clob = clob; + this.blob = blob; + } + + @Override + public boolean equals(Object o) { + if ( o == null || getClass() != o.getClass() ) { + return false; + } + + final LargeObjectTestEntity that = (LargeObjectTestEntity) o; + return Objects.equals( id, that.id ) && Objects.equals( + clob, + that.clob + ) && Arrays.equals( blob, that.blob ); + } + + @Override + public int hashCode() { + return Objects.hash( id, clob, Arrays.hashCode( blob ) ); + } + } + + @Override + protected Class[] getAnnotatedClasses() { + return new Class[] { LargeObjectTestEntity.class }; + } + + @Test + public void testLobTypeMapping() { + PersistentClass entityBinding = metadata().getEntityBinding( LargeObjectTestEntity.class.getName() + "_AUD" ); + + Property blobProperty = entityBinding.getProperty( "blob" ); + Property clobProperty = entityBinding.getProperty( "clob" ); + + assertTrue( blobProperty.isLob() ); + assertTrue( clobProperty.isLob() ); + } +} diff --git a/hibernate-envers/src/test/java/org/hibernate/orm/test/envers/integration/manytoone/bidirectional/ManyToOneCustomRevisionListenerTest.java b/hibernate-envers/src/test/java/org/hibernate/orm/test/envers/integration/manytoone/bidirectional/ManyToOneCustomRevisionListenerTest.java new file mode 100644 index 000000000000..460f0f64dbe9 --- /dev/null +++ b/hibernate-envers/src/test/java/org/hibernate/orm/test/envers/integration/manytoone/bidirectional/ManyToOneCustomRevisionListenerTest.java @@ -0,0 +1,204 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.envers.integration.manytoone.bidirectional; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.Entity; +import jakarta.persistence.EntityManager; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; +import org.hibernate.envers.AuditReader; +import org.hibernate.envers.Audited; +import org.hibernate.envers.EntityTrackingRevisionListener; +import org.hibernate.envers.RevisionEntity; +import org.hibernate.envers.RevisionListener; +import org.hibernate.envers.RevisionNumber; +import org.hibernate.envers.RevisionTimestamp; +import org.hibernate.envers.RevisionType; +import org.hibernate.orm.test.envers.BaseEnversJPAFunctionalTestCase; +import org.hibernate.orm.test.envers.Priority; +import org.hibernate.testing.orm.junit.Jira; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@Jira( "https://hibernate.atlassian.net/browse/HHH-17652" ) +public class ManyToOneCustomRevisionListenerTest extends BaseEnversJPAFunctionalTestCase { + private static final ThreadLocal auditReader = ThreadLocal.withInitial( () -> null ); + + @Override + protected Class[] getAnnotatedClasses() { + return new Class[] { + Document.class, + DocumentAuthorEmployee.class, + Employee.class, + CustomRevisionEntity.class, + }; + } + + @Test + @Priority(10) + public void initData() { + // store in thread-local to use it in custom revision listener + auditReader.set( getAuditReader() ); + + EntityManager em = getEntityManager(); + em.getTransaction().begin(); + + final Employee bilbo = new Employee( "Bilbo Baggins" ); + em.persist( bilbo ); + final Employee frodo = new Employee( "Frodo Baggins" ); + em.persist( frodo ); + + em.getTransaction().commit(); + + em.getTransaction().begin(); + + final Document document = new Document( "The Hobbit" ); + document.getAuthors().add( new DocumentAuthorEmployee( 1L, document, bilbo ) ); + document.getAuthors().add( new DocumentAuthorEmployee( 2L, document, frodo ) ); + em.persist( document ); + + em.getTransaction().commit(); + } + + @Test + public void testDocumentAuthorEmployeeRevisions() { + final AuditReader reader = getAuditReader(); + assertLastRevision( reader, 1L, "Bilbo Baggins" ); + assertLastRevision( reader, 2L, "Frodo Baggins" ); + getEntityManager().close(); + } + + private static void assertLastRevision(AuditReader reader, Long id, String employee) { + final List revisions = reader.getRevisions( DocumentAuthorEmployee.class, id ); + final Number revisionNumber = revisions.get( revisions.size() - 1 ); + final DocumentAuthorEmployee result = reader.find( DocumentAuthorEmployee.class, id, revisionNumber ); + assertThat( result.getEmployee().getName() ).isEqualTo( employee ); + assertThat( result.getDocument().getTitle() ).isEqualTo( "The Hobbit" ); + } + + @Audited(withModifiedFlag = true) + @Entity(name = "Document") + static class Document { + @Id + @GeneratedValue + private Long id; + + private String title; + + @OneToMany(mappedBy = "document", cascade = CascadeType.ALL) + private List authors = new ArrayList<>(); + + public Document() { + } + + public Document(String title) { + this.title = title; + } + + public List getAuthors() { + return authors; + } + + public String getTitle() { + return title; + } + } + + @Audited(withModifiedFlag = true) + @Entity(name = "DocumentAuthorEmployee") + static class DocumentAuthorEmployee { + @Id + private Long id; + + @ManyToOne + @JoinColumn(name = "document_id") + private Document document; + + @ManyToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "employee_id") + private Employee employee; + + public DocumentAuthorEmployee() { + } + + public DocumentAuthorEmployee(Long id, Document document, Employee employee) { + this.id = id; + this.document = document; + this.employee = employee; + } + + public Long getId() { + return id; + } + + public Document getDocument() { + return document; + } + + public Employee getEmployee() { + return employee; + } + } + + @Audited(withModifiedFlag = true) + @Entity(name = "Employee") + static class Employee { + @Id + @GeneratedValue + private Long id; + + private String name; + + public Employee() { + } + + public Employee(String name) { + this.name = name; + } + + public String getName() { + return name; + } + } + + @Entity(name = "CustomRevisionEntity") + @RevisionEntity(CustomRevisionListener.class) + static class CustomRevisionEntity { + @Id + @GeneratedValue + @RevisionNumber + private int id; + + @RevisionTimestamp + private long timestamp; + } + + static class CustomRevisionListener implements RevisionListener, EntityTrackingRevisionListener { + @SuppressWarnings({"unchecked", "rawtypes"}) + @Override + public void entityChanged(Class entityClass, String entityName, Object entityId, RevisionType revisionType, Object revisionEntity) { + final AuditReader reader = auditReader.get(); + final List revisions = reader.getRevisions( entityClass, entityId ); + final Number revisionNumber = revisions.get( revisions.size() - 1 ); + + // This is what triggered the NPE + final Object obj = reader.find( entityClass, entityId, revisionNumber ); + assertThat( obj ).isNotNull(); + } + + @Override + public void newRevision(Object revisionEntity) { + } + } +} diff --git a/hibernate-envers/src/test/java/org/hibernate/orm/test/envers/integration/metamodel/RevisionEntitiesMetamodelTest.java b/hibernate-envers/src/test/java/org/hibernate/orm/test/envers/integration/metamodel/RevisionEntitiesMetamodelTest.java new file mode 100644 index 000000000000..78504c8bd1c7 --- /dev/null +++ b/hibernate-envers/src/test/java/org/hibernate/orm/test/envers/integration/metamodel/RevisionEntitiesMetamodelTest.java @@ -0,0 +1,99 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.orm.test.envers.integration.metamodel; + +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import org.hibernate.annotations.CreationTimestamp; +import org.hibernate.boot.MetadataSources; +import org.hibernate.boot.registry.StandardServiceRegistryBuilder; +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.envers.Audited; +import org.hibernate.envers.configuration.EnversSettings; +import org.hibernate.internal.HEMLogging; +import org.hibernate.metamodel.internal.MetadataContext; +import org.hibernate.testing.logger.LogInspectionHelper; +import org.hibernate.testing.logger.TriggerOnPrefixLogListener; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.util.ServiceRegistryUtil; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; + +import java.time.Instant; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Marco Belladelli + */ +@Jira( "https://hibernate.atlassian.net/browse/HHH-17612" ) +@TestInstance( TestInstance.Lifecycle.PER_CLASS ) +public class RevisionEntitiesMetamodelTest { + private TriggerOnPrefixLogListener trigger; + + @BeforeAll + public void setUp() { + trigger = new TriggerOnPrefixLogListener( "HHH015007: Illegal argument on static metamodel field injection" ); + LogInspectionHelper.registerListener( trigger, HEMLogging.messageLogger( MetadataContext.class ) ); + } + + @Test + public void testDefaultRevisionEntity() { + try (final SessionFactoryImplementor ignored = buildSessionFactory( false, true )) { + assertThat( trigger.wasTriggered() ).isFalse(); + } + } + + @Test + public void testSequenceIdRevisionEntity() { + try (final SessionFactoryImplementor ignored = buildSessionFactory( false, false )) { + assertThat( trigger.wasTriggered() ).isFalse(); + } + } + + @Test + public void testDefaultTrackingModifiedEntitiesRevisionEntity() { + try (final SessionFactoryImplementor ignored = buildSessionFactory( true, true )) { + assertThat( trigger.wasTriggered() ).isFalse(); + } + } + + @Test + public void testSequenceIdTrackingModifiedEntitiesRevisionEntity() { + try (final SessionFactoryImplementor ignored = buildSessionFactory( true, false )) { + assertThat( trigger.wasTriggered() ).isFalse(); + } + } + + @SuppressWarnings( "resource" ) + private static SessionFactoryImplementor buildSessionFactory(boolean trackEntities, boolean nativeId) { + final StandardServiceRegistryBuilder registryBuilder = ServiceRegistryUtil.serviceRegistryBuilder(); + registryBuilder.applySetting( EnversSettings.TRACK_ENTITIES_CHANGED_IN_REVISION, trackEntities ); + registryBuilder.applySetting( EnversSettings.USE_REVISION_ENTITY_WITH_NATIVE_ID, nativeId ); + return new MetadataSources( registryBuilder.build() ) + .addAnnotatedClasses( Customer.class ) + .buildMetadata() + .buildSessionFactory() + .unwrap( SessionFactoryImplementor.class ); + } + + @Audited + @Entity( name = "Customer" ) + @SuppressWarnings( "unused" ) + public static class Customer { + @Id + private Long id; + + private String firstName; + + private String lastName; + + @Column( name = "created_on" ) + @CreationTimestamp + private Instant createdOn; + } +} diff --git a/hibernate-envers/src/test/resources/org/hibernate/orm/test/envers/integration/blob/blob.txt b/hibernate-envers/src/test/resources/org/hibernate/orm/test/envers/integration/blob/blob.txt new file mode 100644 index 000000000000..7a1354040f95 --- /dev/null +++ b/hibernate-envers/src/test/resources/org/hibernate/orm/test/envers/integration/blob/blob.txt @@ -0,0 +1,31 @@ + + + + + + + create-drop + + false + false + + org.hibernate.dialect.H2Dialect + jdbc:h2:mem:envers + org.h2.Driver + sa + + + + + + + + + + + diff --git a/hibernate-hikaricp/src/main/java/org/hibernate/hikaricp/internal/HikariCPConnectionProvider.java b/hibernate-hikaricp/src/main/java/org/hibernate/hikaricp/internal/HikariCPConnectionProvider.java index 725976271bec..c9859499a97d 100644 --- a/hibernate-hikaricp/src/main/java/org/hibernate/hikaricp/internal/HikariCPConnectionProvider.java +++ b/hibernate-hikaricp/src/main/java/org/hibernate/hikaricp/internal/HikariCPConnectionProvider.java @@ -8,9 +8,10 @@ package org.hibernate.hikaricp.internal; import java.sql.Connection; +import java.sql.DatabaseMetaData; import java.sql.SQLException; -import java.util.Map; import javax.sql.DataSource; +import java.util.Map; import org.hibernate.HibernateException; import org.hibernate.dialect.Dialect; @@ -18,6 +19,7 @@ import org.hibernate.engine.jdbc.connections.spi.ConnectionProvider; import org.hibernate.engine.jdbc.connections.spi.DatabaseConnectionInfo; import org.hibernate.internal.log.ConnectionInfoLogger; +import org.hibernate.internal.util.StringHelper; import org.hibernate.service.UnknownUnwrapTypeException; import org.hibernate.service.spi.Configurable; import org.hibernate.service.spi.Stoppable; @@ -25,6 +27,8 @@ import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; +import static org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator.allowJdbcMetadataAccess; + /** * HikariCP Connection provider for Hibernate. * @@ -34,6 +38,7 @@ public class HikariCPConnectionProvider implements ConnectionProvider, Configurable, Stoppable { private static final long serialVersionUID = -9131625057941275711L; + private boolean isMetadataAccessAllowed = true; /** * HikariCP configuration. @@ -52,6 +57,8 @@ public class HikariCPConnectionProvider implements ConnectionProvider, Configura @Override public void configure(Map props) throws HibernateException { try { + isMetadataAccessAllowed = allowJdbcMetadataAccess( props ); + ConnectionInfoLogger.INSTANCE.configureConnectionPool( "HikariCP" ); hcfg = HikariConfigurationUtil.loadConfiguration( props ); @@ -86,7 +93,9 @@ public boolean supportsAggressiveRelease() { public DatabaseConnectionInfo getDatabaseConnectionInfo(Dialect dialect) { return new DatabaseConnectionInfoImpl( hcfg.getJdbcUrl(), - hcfg.getDriverClassName(), + // Attempt to resolve the driver name from the dialect, in case it wasn't explicitly set and access to + // the database metadata is allowed + !StringHelper.isBlank( hcfg.getDriverClassName() ) ? hcfg.getDriverClassName() : extractDriverNameFromMetadata(), dialect.getVersion(), Boolean.toString( hcfg.isAutoCommit() ), hcfg.getTransactionIsolation(), @@ -95,6 +104,19 @@ public DatabaseConnectionInfo getDatabaseConnectionInfo(Dialect dialect) { ); } + private String extractDriverNameFromMetadata() { + if (isMetadataAccessAllowed) { + try ( Connection conn = getConnection() ) { + DatabaseMetaData dbmd = conn.getMetaData(); + return dbmd.getDriverName(); + } + catch (SQLException e) { + // Do nothing + } + } + return null; + } + @Override public boolean isUnwrappableAs(Class unwrapType) { return ConnectionProvider.class.equals( unwrapType ) diff --git a/hibernate-jfr/src/main/java/org/hibernate/event/jfr/internal/JfrEventManager.java b/hibernate-jfr/src/main/java/org/hibernate/event/jfr/internal/JfrEventManager.java index 992846428c07..d39ac9642ea8 100644 --- a/hibernate-jfr/src/main/java/org/hibernate/event/jfr/internal/JfrEventManager.java +++ b/hibernate-jfr/src/main/java/org/hibernate/event/jfr/internal/JfrEventManager.java @@ -56,10 +56,10 @@ public SessionOpenEvent beginSessionOpenEvent() { @Override public void completeSessionOpenEvent( - HibernateMonitoringEvent event, + HibernateMonitoringEvent monitoringEvent, SharedSessionContractImplementor session) { - if ( event != null ) { - final SessionOpenEvent sessionOpenEvent = (SessionOpenEvent) event; + if ( monitoringEvent != null ) { + final SessionOpenEvent sessionOpenEvent = (SessionOpenEvent) monitoringEvent; sessionOpenEvent.end(); if ( sessionOpenEvent.shouldCommit() ) { sessionOpenEvent.sessionIdentifier = getSessionIdentifier( session ); @@ -82,10 +82,10 @@ public SessionClosedEvent beginSessionClosedEvent() { @Override public void completeSessionClosedEvent( - HibernateMonitoringEvent event, + HibernateMonitoringEvent monitoringEvent, SharedSessionContractImplementor session) { - if ( event != null ) { - final SessionClosedEvent sessionClosedEvent = (SessionClosedEvent) event; + if ( monitoringEvent != null ) { + final SessionClosedEvent sessionClosedEvent = (SessionClosedEvent) monitoringEvent; sessionClosedEvent.end(); if ( sessionClosedEvent.shouldCommit() ) { sessionClosedEvent.sessionIdentifier = getSessionIdentifier( session ); @@ -108,11 +108,11 @@ public JdbcConnectionAcquisitionEvent beginJdbcConnectionAcquisitionEvent() { @Override public void completeJdbcConnectionAcquisitionEvent( - HibernateMonitoringEvent event, + HibernateMonitoringEvent monitoringEvent, SharedSessionContractImplementor session, Object tenantId) { - if ( event != null ) { - final JdbcConnectionAcquisitionEvent jdbcConnectionAcquisitionEvent = (JdbcConnectionAcquisitionEvent) event; + if ( monitoringEvent != null ) { + final JdbcConnectionAcquisitionEvent jdbcConnectionAcquisitionEvent = (JdbcConnectionAcquisitionEvent) monitoringEvent; jdbcConnectionAcquisitionEvent.end(); if ( jdbcConnectionAcquisitionEvent.shouldCommit() ) { jdbcConnectionAcquisitionEvent.sessionIdentifier = getSessionIdentifier( session ); @@ -138,11 +138,11 @@ public JdbcConnectionReleaseEvent beginJdbcConnectionReleaseEvent() { @Override public void completeJdbcConnectionReleaseEvent( - HibernateMonitoringEvent event, + HibernateMonitoringEvent monitoringEvent, SharedSessionContractImplementor session, Object tenantId) { - if ( event != null ) { - final JdbcConnectionReleaseEvent jdbcConnectionReleaseEvent = (JdbcConnectionReleaseEvent) event; + if ( monitoringEvent != null ) { + final JdbcConnectionReleaseEvent jdbcConnectionReleaseEvent = (JdbcConnectionReleaseEvent) monitoringEvent; jdbcConnectionReleaseEvent.end(); if ( jdbcConnectionReleaseEvent.shouldCommit() ) { jdbcConnectionReleaseEvent.sessionIdentifier = getSessionIdentifier( session ); @@ -168,10 +168,10 @@ public JdbcPreparedStatementCreationEvent beginJdbcPreparedStatementCreationEven @Override public void completeJdbcPreparedStatementCreationEvent( - HibernateMonitoringEvent event, + HibernateMonitoringEvent monitoringEvent, String preparedStatementSql) { - if ( event != null ) { - final JdbcPreparedStatementCreationEvent jdbcPreparedStatementCreation = (JdbcPreparedStatementCreationEvent) event; + if ( monitoringEvent != null ) { + final JdbcPreparedStatementCreationEvent jdbcPreparedStatementCreation = (JdbcPreparedStatementCreationEvent) monitoringEvent; jdbcPreparedStatementCreation.end(); if ( jdbcPreparedStatementCreation.shouldCommit() ) { jdbcPreparedStatementCreation.sql = preparedStatementSql; @@ -194,10 +194,10 @@ public JdbcPreparedStatementExecutionEvent beginJdbcPreparedStatementExecutionEv @Override public void completeJdbcPreparedStatementExecutionEvent( - HibernateMonitoringEvent event, + HibernateMonitoringEvent monitoringEvent, String preparedStatementSql) { - if ( event != null ) { - final JdbcPreparedStatementExecutionEvent jdbcPreparedStatementExecutionEvent = (JdbcPreparedStatementExecutionEvent) event; + if ( monitoringEvent != null ) { + final JdbcPreparedStatementExecutionEvent jdbcPreparedStatementExecutionEvent = (JdbcPreparedStatementExecutionEvent) monitoringEvent; jdbcPreparedStatementExecutionEvent.end(); if ( jdbcPreparedStatementExecutionEvent.shouldCommit() ) { jdbcPreparedStatementExecutionEvent.sql = preparedStatementSql; @@ -220,10 +220,10 @@ public JdbcBatchExecutionEvent beginJdbcBatchExecutionEvent() { @Override public void completeJdbcBatchExecutionEvent( - HibernateMonitoringEvent event, + HibernateMonitoringEvent monitoringEvent, String statementSql) { - if ( event != null ) { - final JdbcBatchExecutionEvent jdbcBatchExecutionEvent = (JdbcBatchExecutionEvent) event; + if ( monitoringEvent != null ) { + final JdbcBatchExecutionEvent jdbcBatchExecutionEvent = (JdbcBatchExecutionEvent) monitoringEvent; jdbcBatchExecutionEvent.end(); if ( jdbcBatchExecutionEvent.shouldCommit() ) { jdbcBatchExecutionEvent.sql = statementSql; @@ -246,13 +246,13 @@ public HibernateMonitoringEvent beginCachePutEvent() { @Override public void completeCachePutEvent( - HibernateMonitoringEvent event, + HibernateMonitoringEvent monitoringEvent, SharedSessionContractImplementor session, Region region, boolean cacheContentChanged, CacheActionDescription description) { - if ( event != null ) { - final CachePutEvent cachePutEvent = (CachePutEvent) event; + if ( monitoringEvent != null ) { + final CachePutEvent cachePutEvent = (CachePutEvent) monitoringEvent; cachePutEvent.end(); if ( cachePutEvent.shouldCommit() ) { cachePutEvent.sessionIdentifier = getSessionIdentifier( session ); @@ -266,14 +266,14 @@ public void completeCachePutEvent( @Override public void completeCachePutEvent( - HibernateMonitoringEvent event, + HibernateMonitoringEvent monitoringEvent, SharedSessionContractImplementor session, CachedDomainDataAccess cachedDomainDataAccess, EntityPersister persister, boolean cacheContentChanged, CacheActionDescription description) { completeCachePutEvent( - event, + monitoringEvent, session, cachedDomainDataAccess, persister, @@ -285,15 +285,15 @@ public void completeCachePutEvent( @Override public void completeCachePutEvent( - HibernateMonitoringEvent event, + HibernateMonitoringEvent monitoringEvent, SharedSessionContractImplementor session, CachedDomainDataAccess cachedDomainDataAccess, EntityPersister persister, boolean cacheContentChanged, boolean isNatualId, CacheActionDescription description) { - if ( event != null ) { - final CachePutEvent cachePutEvent = (CachePutEvent) event; + if ( monitoringEvent != null ) { + final CachePutEvent cachePutEvent = (CachePutEvent) monitoringEvent; cachePutEvent.end(); if ( cachePutEvent.shouldCommit() ) { cachePutEvent.sessionIdentifier = getSessionIdentifier( session ); @@ -309,14 +309,14 @@ public void completeCachePutEvent( @Override public void completeCachePutEvent( - HibernateMonitoringEvent event, + HibernateMonitoringEvent monitoringEvent, SharedSessionContractImplementor session, CachedDomainDataAccess cachedDomainDataAccess, CollectionPersister persister, boolean cacheContentChanged, CacheActionDescription description) { - if ( event != null ) { - final CachePutEvent cachePutEvent = (CachePutEvent) event; + if ( monitoringEvent != null ) { + final CachePutEvent cachePutEvent = (CachePutEvent) monitoringEvent; cachePutEvent.end(); if ( cachePutEvent.shouldCommit() ) { cachePutEvent.sessionIdentifier = getSessionIdentifier( session ); @@ -343,12 +343,12 @@ public HibernateMonitoringEvent beginCacheGetEvent() { @Override public void completeCacheGetEvent( - HibernateMonitoringEvent event, + HibernateMonitoringEvent monitoringEvent, SharedSessionContractImplementor session, Region region, boolean hit) { - if ( event != null ) { - final CacheGetEvent cacheGetEvent = (CacheGetEvent) event; + if ( monitoringEvent != null ) { + final CacheGetEvent cacheGetEvent = (CacheGetEvent) monitoringEvent; cacheGetEvent.end(); if ( cacheGetEvent.shouldCommit() ) { cacheGetEvent.sessionIdentifier = getSessionIdentifier( session ); @@ -361,14 +361,14 @@ public void completeCacheGetEvent( @Override public void completeCacheGetEvent( - HibernateMonitoringEvent event, + HibernateMonitoringEvent monitoringEvent, SharedSessionContractImplementor session, Region region, EntityPersister persister, boolean isNaturalKey, boolean hit) { - if ( event != null ) { - final CacheGetEvent cacheGetEvent = (CacheGetEvent) event; + if ( monitoringEvent != null ) { + final CacheGetEvent cacheGetEvent = (CacheGetEvent) monitoringEvent; cacheGetEvent.end(); if ( cacheGetEvent.shouldCommit() ) { cacheGetEvent.sessionIdentifier = getSessionIdentifier( session ); @@ -383,13 +383,13 @@ public void completeCacheGetEvent( @Override public void completeCacheGetEvent( - HibernateMonitoringEvent event, + HibernateMonitoringEvent monitoringEvent, SharedSessionContractImplementor session, Region region, CollectionPersister persister, boolean hit) { - if ( event != null ) { - final CacheGetEvent cacheGetEvent = (CacheGetEvent) event; + if ( monitoringEvent != null ) { + final CacheGetEvent cacheGetEvent = (CacheGetEvent) monitoringEvent; cacheGetEvent.end(); if ( cacheGetEvent.shouldCommit() ) { cacheGetEvent.sessionIdentifier = getSessionIdentifier( session ); @@ -422,18 +422,18 @@ public void completeFlushEvent( @Override public void completeFlushEvent( - HibernateMonitoringEvent hibernateMonitoringEvent, - org.hibernate.event.spi.FlushEvent event, + HibernateMonitoringEvent monitoringEvent, + org.hibernate.event.spi.FlushEvent hibernateFlushEvent, boolean autoFlush) { - if ( hibernateMonitoringEvent != null ) { - final FlushEvent flushEvent = (FlushEvent) hibernateMonitoringEvent; - flushEvent.end(); - if ( flushEvent.shouldCommit() ) { - flushEvent.sessionIdentifier = getSessionIdentifier( event.getSession() ); - flushEvent.numberOfEntitiesProcessed = event.getNumberOfEntitiesProcessed(); - flushEvent.numberOfCollectionsProcessed = event.getNumberOfCollectionsProcessed(); - flushEvent.isAutoFlush = autoFlush; - flushEvent.commit(); + if ( monitoringEvent != null ) { + final FlushEvent jfrFlushEvent = (FlushEvent) monitoringEvent; + jfrFlushEvent.end(); + if ( jfrFlushEvent.shouldCommit() ) { + jfrFlushEvent.sessionIdentifier = getSessionIdentifier( hibernateFlushEvent.getSession() ); + jfrFlushEvent.numberOfEntitiesProcessed = hibernateFlushEvent.getNumberOfEntitiesProcessed(); + jfrFlushEvent.numberOfCollectionsProcessed = hibernateFlushEvent.getNumberOfCollectionsProcessed(); + jfrFlushEvent.isAutoFlush = autoFlush; + jfrFlushEvent.commit(); } } } @@ -452,17 +452,17 @@ public PartialFlushEvent beginPartialFlushEvent() { @Override public void completePartialFlushEvent( - HibernateMonitoringEvent hibernateMonitoringEvent, - AutoFlushEvent event) { - if ( event != null ) { - final PartialFlushEvent flushEvent = (PartialFlushEvent) hibernateMonitoringEvent; - flushEvent.end(); - if ( flushEvent.shouldCommit() ) { - flushEvent.sessionIdentifier = getSessionIdentifier( event.getSession() ); - flushEvent.numberOfEntitiesProcessed = event.getNumberOfEntitiesProcessed(); - flushEvent.numberOfCollectionsProcessed = event.getNumberOfCollectionsProcessed(); - flushEvent.isAutoFlush = true; - flushEvent.commit(); + HibernateMonitoringEvent monitoringEvent, + AutoFlushEvent hibernateAutoFlushEvent) { + if ( monitoringEvent != null) { + final PartialFlushEvent jfrPartialFlushEvent = (PartialFlushEvent) monitoringEvent; + jfrPartialFlushEvent.end(); + if ( jfrPartialFlushEvent.shouldCommit() ) { + jfrPartialFlushEvent.sessionIdentifier = getSessionIdentifier( hibernateAutoFlushEvent.getSession() ); + jfrPartialFlushEvent.numberOfEntitiesProcessed = hibernateAutoFlushEvent.getNumberOfEntitiesProcessed(); + jfrPartialFlushEvent.numberOfCollectionsProcessed = hibernateAutoFlushEvent.getNumberOfCollectionsProcessed(); + jfrPartialFlushEvent.isAutoFlush = true; + jfrPartialFlushEvent.commit(); } } } @@ -481,13 +481,13 @@ public DirtyCalculationEvent beginDirtyCalculationEvent() { @Override public void completeDirtyCalculationEvent( - HibernateMonitoringEvent event, + HibernateMonitoringEvent monitoringEvent, SharedSessionContractImplementor session, EntityPersister persister, EntityEntry entry, int[] dirtyProperties) { - if ( event != null ) { - final DirtyCalculationEvent dirtyCalculationEvent = (DirtyCalculationEvent) event; + if ( monitoringEvent != null ) { + final DirtyCalculationEvent dirtyCalculationEvent = (DirtyCalculationEvent) monitoringEvent; dirtyCalculationEvent.end(); if ( dirtyCalculationEvent.shouldCommit() ) { dirtyCalculationEvent.sessionIdentifier = getSessionIdentifier( session ); @@ -513,10 +513,10 @@ public PrePartialFlushEvent beginPrePartialFlush() { @Override public void completePrePartialFlush( - HibernateMonitoringEvent event, + HibernateMonitoringEvent monitoringEvent, SharedSessionContractImplementor session) { - if ( event != null ) { - final PrePartialFlushEvent prePartialFlushEvent = (PrePartialFlushEvent) event; + if ( monitoringEvent != null ) { + final PrePartialFlushEvent prePartialFlushEvent = (PrePartialFlushEvent) monitoringEvent; prePartialFlushEvent.end(); if ( prePartialFlushEvent.shouldCommit() ) { prePartialFlushEvent.sessionIdentifier = getSessionIdentifier( session ); diff --git a/hibernate-jfr/src/test/java/org/hibernate/event/jfr/flush/AutoFlushJfrDisabledTests.java b/hibernate-jfr/src/test/java/org/hibernate/event/jfr/flush/AutoFlushJfrDisabledTests.java new file mode 100644 index 000000000000..6ccc5bcd01f2 --- /dev/null +++ b/hibernate-jfr/src/test/java/org/hibernate/event/jfr/flush/AutoFlushJfrDisabledTests.java @@ -0,0 +1,60 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.event.jfr.flush; + +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.JiraKey; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.junit.jupiter.api.Test; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; + + +@DomainModel(annotatedClasses = { + AutoFlushJfrDisabledTests.TestEntity.class, +}) +@SessionFactory +@JiraKey(value = "HHH-18770") +public class AutoFlushJfrDisabledTests { + + /** + * Execute a query (and a flush) with the jfr module on the classpath but jfr disabled + */ + @Test + public void testFlushEventWithPartialFlushEventDisabled(SessionFactoryScope scope) { + scope.inTransaction( + session -> { + TestEntity entity = new TestEntity( 1, "name_1" ); + session.persist( entity ); + session.createQuery( "select t from TestEntity t" ).list(); + + session.remove( entity ); + } + ); + } + + @Entity(name = "TestEntity") + public static class TestEntity { + @Id + private Integer id; + + private String name; + + public TestEntity() { + } + + public TestEntity(Integer id, String name) { + this.id = id; + this.name = name; + } + + public void setName(String name) { + this.name = name; + } + } + +} diff --git a/hibernate-platform/hibernate-platform.gradle b/hibernate-platform/hibernate-platform.gradle index 46cd490f6f57..8b969ec63de6 100644 --- a/hibernate-platform/hibernate-platform.gradle +++ b/hibernate-platform/hibernate-platform.gradle @@ -4,7 +4,6 @@ plugins { description = 'Gradle platform for Hibernate ORM' -apply from: rootProject.file( 'gradle/releasable.gradle' ) apply from: rootProject.file( "gradle/base-information.gradle" ) apply from: rootProject.file( "gradle/publishing-pom.gradle" ) @@ -68,6 +67,17 @@ dependencies { } } +task releasePrepare { + group "release-prepare" + description "See :release:releasePrepare for details." + + dependsOn generateMetadataFileForPublishedArtifactsPublication + dependsOn generatePomFileForPublishedArtifactsPublication + // we depend on publishAllPublicationsToStagingRepository to make sure that the artifacts are "published" to a local staging directory + // used by JReleaser during the release process + dependsOn publishAllPublicationsToStagingRepository +} + publishing { publications { publishedArtifacts { @@ -75,9 +85,3 @@ publishing { } } } - -project( ":release" ).getTasks().named( "publishReleaseArtifacts" ).configure { - dependsOn tasks.release -} - -tasks.release.dependsOn tasks.publishToSonatype diff --git a/hibernate-spatial/src/test/java/org/hibernate/spatial/dialect/postgis/PostgisDollarQuoteNativeQueryTest.java b/hibernate-spatial/src/test/java/org/hibernate/spatial/dialect/postgis/PostgisDollarQuoteNativeQueryTest.java new file mode 100644 index 000000000000..e6b0e6af5c9a --- /dev/null +++ b/hibernate-spatial/src/test/java/org/hibernate/spatial/dialect/postgis/PostgisDollarQuoteNativeQueryTest.java @@ -0,0 +1,92 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.spatial.dialect.postgis; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import org.geolatte.geom.G2D; +import org.geolatte.geom.Point; +import org.hibernate.annotations.JdbcTypeCode; +import org.hibernate.dialect.PostgreSQLDialect; +import org.hibernate.testing.orm.junit.DomainModel; +import org.hibernate.testing.orm.junit.Jira; +import org.hibernate.testing.orm.junit.RequiresDialect; +import org.hibernate.testing.orm.junit.SessionFactory; +import org.hibernate.testing.orm.junit.SessionFactoryScope; +import org.hibernate.type.SqlTypes; +import org.junit.jupiter.api.Test; + + +import static org.geolatte.geom.builder.DSL.g; +import static org.geolatte.geom.builder.DSL.point; +import static org.geolatte.geom.crs.CoordinateReferenceSystems.WGS84; +import static org.junit.jupiter.api.Assertions.assertEquals; + +@DomainModel(annotatedClasses = PostgisDollarQuoteNativeQueryTest.Location.class ) +@SessionFactory +@RequiresDialect(PostgreSQLDialect.class) +@Jira( "https://hibernate.atlassian.net/browse/HHH-18956" ) +class PostgisDollarQuoteNativeQueryTest { + + private static final Long LOCATION_ID = 123412L; + private static final String DOLLAR_QUOTE = "$asdas$"; + + @Test + void test(SessionFactoryScope scope) { + scope.inTransaction( s -> { + final Point point = point( WGS84, g( 30.5, 50.4 ) ); + //noinspection SqlSourceToSinkFlow + s.createNativeMutationQuery( + String.format( + "INSERT INTO location (id, point) " + + "VALUES (%s, %s) " + + "ON CONFLICT DO NOTHING;", + DOLLAR_QUOTE + LOCATION_ID + DOLLAR_QUOTE, + String.format( + "ST_SetSRID(ST_GeomFromGeoJSON(%s%s%s), 4326)", + DOLLAR_QUOTE, + toJsonString( point ), + DOLLAR_QUOTE + ) + ) + ).executeUpdate(); + + final Location location = s.find( Location.class, LOCATION_ID ); + assertEquals( point, location.point ); + } ); + } + + private static String toJsonString(Point point) { + return "{\"type\":\"" + point.getGeometryType().getCamelCased() + "\",\"coordinates\":" + point.getPosition() + "}"; + } + + @Entity(name = "Location") + public static class Location { + + @Id + Long id; + + @JdbcTypeCode(SqlTypes.GEOMETRY) + Point point; + + public Location() { + } + + public Location(Long id, Point point) { + this.id = id; + this.point = point; + } + + @Override + public String toString() { + return "Location{" + + "id=" + id + + ", point=" + point + + '}'; + } + } +} diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/bytecode/enhancement/BytecodeEnhancerRunner.java b/hibernate-testing/src/main/java/org/hibernate/testing/bytecode/enhancement/BytecodeEnhancerRunner.java index dfdd5218d863..22e518345f87 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/bytecode/enhancement/BytecodeEnhancerRunner.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/bytecode/enhancement/BytecodeEnhancerRunner.java @@ -24,6 +24,7 @@ import org.hibernate.bytecode.enhance.spi.UnloadedField; import org.hibernate.cfg.Environment; +import org.hibernate.bytecode.enhance.spi.UnsupportedEnhancementStrategy; import org.hibernate.testing.junit4.CustomRunner; import org.junit.runner.Runner; import org.junit.runner.notification.RunNotifier; @@ -32,6 +33,7 @@ import org.junit.runners.model.InitializationError; import org.junit.runners.model.RunnerBuilder; +import static org.hibernate.bytecode.enhance.spi.UnsupportedEnhancementStrategy.SKIP; import static org.hibernate.bytecode.internal.BytecodeProviderInitiator.buildDefaultBytecodeProvider; /** @@ -116,6 +118,12 @@ public boolean hasLazyLoadableAttributes(UnloadedClass classDescriptor) { public boolean isLazyLoadable(UnloadedField field) { return options.lazyLoading() && super.isLazyLoadable( field ); } + + @Override + public UnsupportedEnhancementStrategy getUnsupportedEnhancementStrategy() { + final UnsupportedEnhancementStrategy strategy = options.unsupportedEnhancementStrategy(); + return strategy != SKIP ? strategy : super.getUnsupportedEnhancementStrategy(); + } }; } diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/bytecode/enhancement/EnhancementOptions.java b/hibernate-testing/src/main/java/org/hibernate/testing/bytecode/enhancement/EnhancementOptions.java index 7853126bec3b..043181934598 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/bytecode/enhancement/EnhancementOptions.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/bytecode/enhancement/EnhancementOptions.java @@ -6,6 +6,8 @@ */ package org.hibernate.testing.bytecode.enhancement; +import org.hibernate.bytecode.enhance.spi.UnsupportedEnhancementStrategy; + import java.lang.annotation.ElementType; import java.lang.annotation.Inherited; import java.lang.annotation.Retention; @@ -23,4 +25,5 @@ boolean inlineDirtyChecking() default false; boolean lazyLoading() default false; boolean extendedEnhancement() default false; + UnsupportedEnhancementStrategy unsupportedEnhancementStrategy() default UnsupportedEnhancementStrategy.SKIP; } diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/bytecode/enhancement/extension/engine/BytecodeEnhancedClassUtils.java b/hibernate-testing/src/main/java/org/hibernate/testing/bytecode/enhancement/extension/engine/BytecodeEnhancedClassUtils.java index 78cda58e4530..8e18e8457464 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/bytecode/enhancement/extension/engine/BytecodeEnhancedClassUtils.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/bytecode/enhancement/extension/engine/BytecodeEnhancedClassUtils.java @@ -6,6 +6,7 @@ */ package org.hibernate.testing.bytecode.enhancement.extension.engine; +import static org.hibernate.bytecode.enhance.spi.UnsupportedEnhancementStrategy.SKIP; import static org.hibernate.bytecode.internal.BytecodeProviderInitiator.buildDefaultBytecodeProvider; import java.io.BufferedInputStream; @@ -29,6 +30,7 @@ import org.hibernate.bytecode.enhance.spi.UnloadedClass; import org.hibernate.bytecode.enhance.spi.UnloadedField; +import org.hibernate.bytecode.enhance.spi.UnsupportedEnhancementStrategy; import org.hibernate.testing.bytecode.enhancement.ClassEnhancementSelector; import org.hibernate.testing.bytecode.enhancement.ClassEnhancementSelectors; import org.hibernate.testing.bytecode.enhancement.ClassSelector; @@ -114,6 +116,12 @@ public boolean hasLazyLoadableAttributes(UnloadedClass classDescriptor) { public boolean isLazyLoadable(UnloadedField field) { return options.lazyLoading() && super.isLazyLoadable( field ); } + + @Override + public UnsupportedEnhancementStrategy getUnsupportedEnhancementStrategy() { + final UnsupportedEnhancementStrategy strategy = options.unsupportedEnhancementStrategy(); + return strategy != SKIP ? strategy : super.getUnsupportedEnhancementStrategy(); + } }; } diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/bytecode/enhancement/extension/engine/BytecodeEnhancedEngineDescriptor.java b/hibernate-testing/src/main/java/org/hibernate/testing/bytecode/enhancement/extension/engine/BytecodeEnhancedEngineDescriptor.java index e8e279614cdf..dabdb9d7417f 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/bytecode/enhancement/extension/engine/BytecodeEnhancedEngineDescriptor.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/bytecode/enhancement/extension/engine/BytecodeEnhancedEngineDescriptor.java @@ -8,10 +8,18 @@ import org.junit.jupiter.engine.config.JupiterConfiguration; import org.junit.jupiter.engine.descriptor.JupiterEngineDescriptor; +import org.junit.platform.engine.TestDescriptor; import org.junit.platform.engine.UniqueId; public class BytecodeEnhancedEngineDescriptor extends JupiterEngineDescriptor { public BytecodeEnhancedEngineDescriptor(UniqueId uniqueId, JupiterConfiguration configuration) { super( uniqueId, configuration ); } + + public BytecodeEnhancedEngineDescriptor(JupiterEngineDescriptor engineDescriptor) { + super( engineDescriptor.getUniqueId(), engineDescriptor.getConfiguration() ); + for ( TestDescriptor child : engineDescriptor.getChildren() ) { + addChild( child ); + } + } } diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/bytecode/enhancement/extension/engine/BytecodeEnhancedTestEngine.java b/hibernate-testing/src/main/java/org/hibernate/testing/bytecode/enhancement/extension/engine/BytecodeEnhancedTestEngine.java index 0ec122aae6b2..69fee12afe8a 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/bytecode/enhancement/extension/engine/BytecodeEnhancedTestEngine.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/bytecode/enhancement/extension/engine/BytecodeEnhancedTestEngine.java @@ -11,7 +11,11 @@ import static org.hibernate.testing.bytecode.enhancement.extension.engine.BytecodeEnhancedClassUtils.enhanceTestClass; import static org.junit.platform.commons.util.AnnotationUtils.findAnnotation; +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; +import java.lang.reflect.Proxy; import java.util.Arrays; import java.util.HashSet; import java.util.List; @@ -19,33 +23,21 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; -import java.util.function.Function; -import java.util.function.Predicate; -import java.util.function.Supplier; import org.hibernate.testing.bytecode.enhancement.extension.BytecodeEnhanced; -import org.hibernate.testing.junit4.BaseUnitTestCase; import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.ClassOrderer; import org.junit.jupiter.api.DisplayNameGenerator; -import org.junit.jupiter.api.MethodOrderer; -import org.junit.jupiter.api.TestInstance; -import org.junit.jupiter.api.extension.ExecutionCondition; import org.junit.jupiter.api.extension.ExtensionContext; -import org.junit.jupiter.api.io.CleanupMode; -import org.junit.jupiter.api.io.TempDirFactory; -import org.junit.jupiter.api.parallel.ExecutionMode; -import org.junit.jupiter.engine.config.CachingJupiterConfiguration; -import org.junit.jupiter.engine.config.DefaultJupiterConfiguration; +import org.junit.jupiter.engine.JupiterTestEngine; import org.junit.jupiter.engine.config.JupiterConfiguration; import org.junit.jupiter.engine.descriptor.ClassBasedTestDescriptor; import org.junit.jupiter.engine.descriptor.ClassTestDescriptor; import org.junit.jupiter.engine.descriptor.JupiterEngineDescriptor; import org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor; import org.junit.jupiter.engine.descriptor.TestTemplateTestDescriptor; -import org.junit.jupiter.engine.discovery.DiscoverySelectorResolver; import org.junit.jupiter.engine.execution.JupiterEngineExecutionContext; import org.junit.platform.engine.EngineDiscoveryRequest; +import org.junit.platform.engine.EngineExecutionListener; import org.junit.platform.engine.ExecutionRequest; import org.junit.platform.engine.TestDescriptor; import org.junit.platform.engine.UniqueId; @@ -62,10 +54,9 @@ public String getId() { @Override public TestDescriptor discover(EngineDiscoveryRequest discoveryRequest, UniqueId uniqueId) { - JupiterConfiguration configuration = new CachingJupiterConfiguration( - new DefaultJupiterConfiguration( discoveryRequest.getConfigurationParameters() ) ); - JupiterEngineDescriptor engineDescriptor = new BytecodeEnhancedEngineDescriptor( uniqueId, configuration ); - new DiscoverySelectorResolver().resolveSelectors( discoveryRequest, engineDescriptor ); + final BytecodeEnhancedEngineDescriptor engineDescriptor = new BytecodeEnhancedEngineDescriptor( + (JupiterEngineDescriptor) new JupiterTestEngine().discover( discoveryRequest, uniqueId ) + ); for ( TestDescriptor testDescriptor : new HashSet<>( engineDescriptor.getChildren() ) ) { if ( testDescriptor instanceof ClassBasedTestDescriptor ) { @@ -146,9 +137,13 @@ private void replaceWithEnhanced(Class enhanced, ClassBasedTestDescriptor des Set children, TestDescriptor parent, String[] testEnhancedClasses, Object enhancementContextId) throws NoSuchMethodException { - DelegatingJupiterConfiguration configuration = new DelegatingJupiterConfiguration( jc, enhancementContextId ); + final JupiterConfiguration configuration = (JupiterConfiguration) Proxy.newProxyInstance( + BytecodeEnhancedTestEngine.class.getClassLoader(), + new Class[] { JupiterConfiguration.class }, + new JupiterConfigurationInvocationHandler( jc, enhancementContextId ) + ); - ClassTestDescriptor updated = new ClassTestDescriptor( + final ClassTestDescriptor updated = new ClassTestDescriptor( convertUniqueId( descriptor.getUniqueId(), enhancementContextId ), enhanced, configuration @@ -213,6 +208,24 @@ private Method findMethodReplacement(ClassTestDescriptor updated, Method testMet @Override protected JupiterEngineExecutionContext createExecutionContext(ExecutionRequest request) { + try { + // Try constructing the JupiterEngineExecutionContext the way it is done in 5.13+ + final Class storeFacadeClass = + Class.forName( "org.junit.jupiter.engine.descriptor.LauncherStoreFacade" ); + final Method getStore = ExecutionRequest.class.getMethod( "getStore" ); + final Constructor storeConstructor = storeFacadeClass.getConstructor( getStore.getReturnType() ); + final Constructor constructor = JupiterEngineExecutionContext.class + .getConstructor( EngineExecutionListener.class, JupiterConfiguration.class, storeFacadeClass ); + return constructor.newInstance( + request.getEngineExecutionListener(), + this.getJupiterConfiguration( request ), + storeConstructor.newInstance( getStore.invoke( request ) ) + ); + } + catch (ClassNotFoundException | NoSuchMethodException | InstantiationException | IllegalAccessException | InvocationTargetException e) { + // Ignore errors as they are probably due to version mismatches and try the 5.12 way + } + return new JupiterEngineExecutionContext( request.getEngineExecutionListener(), this.getJupiterConfiguration( request ) @@ -240,111 +253,51 @@ public Context(ExecutionRequest request) { } } - private static class DelegatingJupiterConfiguration implements JupiterConfiguration { + private static class JupiterConfigurationInvocationHandler implements InvocationHandler { private final JupiterConfiguration configuration; - private final DelegatingDisplayNameGenerator displayNameGenerator; + private final DisplayNameGenerator displayNameGenerator; - private DelegatingJupiterConfiguration(JupiterConfiguration configuration, Object id) { + private JupiterConfigurationInvocationHandler(JupiterConfiguration configuration, Object id) { this.configuration = configuration; - displayNameGenerator = new DelegatingDisplayNameGenerator( - configuration.getDefaultDisplayNameGenerator(), - id + displayNameGenerator = (DisplayNameGenerator) Proxy.newProxyInstance( + BytecodeEnhancedTestEngine.class.getClassLoader(), + new Class[]{ DisplayNameGenerator.class }, + new DisplayNameGeneratorInvocationHandler( configuration.getDefaultDisplayNameGenerator(), id ) ); } @Override - public Optional getRawConfigurationParameter(String s) { - return configuration.getRawConfigurationParameter( s ); - } - - @Override - public Optional getRawConfigurationParameter(String s, Function function) { - return configuration.getRawConfigurationParameter( s, function ); - } - - @Override - public boolean isParallelExecutionEnabled() { - return configuration.isParallelExecutionEnabled(); - } - - @Override - public boolean isExtensionAutoDetectionEnabled() { - return configuration.isExtensionAutoDetectionEnabled(); - } - - @Override - public ExecutionMode getDefaultExecutionMode() { - return configuration.getDefaultExecutionMode(); - } - - @Override - public ExecutionMode getDefaultClassesExecutionMode() { - return configuration.getDefaultClassesExecutionMode(); - } - - @Override - public TestInstance.Lifecycle getDefaultTestInstanceLifecycle() { - return configuration.getDefaultTestInstanceLifecycle(); - } - - @Override - public Predicate getExecutionConditionFilter() { - return configuration.getExecutionConditionFilter(); - } - - @Override - public DisplayNameGenerator getDefaultDisplayNameGenerator() { - return displayNameGenerator; - } - - @Override - public Optional getDefaultTestMethodOrderer() { - return configuration.getDefaultTestMethodOrderer(); - } - - @Override - public Optional getDefaultTestClassOrderer() { - return configuration.getDefaultTestClassOrderer(); - } - - @Override - public CleanupMode getDefaultTempDirCleanupMode() { - return configuration.getDefaultTempDirCleanupMode(); - } - - @Override - public Supplier getDefaultTempDirFactorySupplier() { - return configuration.getDefaultTempDirFactorySupplier(); + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + if ( "getDefaultDisplayNameGenerator".equals( method.getName() ) ) { + return displayNameGenerator; + } + else { + return method.invoke( configuration, args ); + } } } - private static class DelegatingDisplayNameGenerator implements DisplayNameGenerator { + private static class DisplayNameGeneratorInvocationHandler implements InvocationHandler { private final DisplayNameGenerator delegate; private final Object id; - private DelegatingDisplayNameGenerator(DisplayNameGenerator delegate, Object id) { + private DisplayNameGeneratorInvocationHandler(DisplayNameGenerator delegate, Object id) { this.delegate = delegate; this.id = id; } - @Override - public String generateDisplayNameForClass(Class aClass) { - return prefix() + delegate.generateDisplayNameForClass( aClass ); - } - private String prefix() { return "Enhanced" + ( id == null ? "" : "[" + id + "]" ) + ":"; } @Override - public String generateDisplayNameForNestedClass(Class aClass) { - return prefix() + delegate.generateDisplayNameForNestedClass( aClass ); - } - - @Override - public String generateDisplayNameForMethod(Class aClass, Method method) { - return prefix() + delegate.generateDisplayNameForMethod( aClass, method ); + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + final Object result = method.invoke( delegate, args ); + if ( method.getDeclaringClass() == DisplayNameGenerator.class ) { + return prefix() + result; + } + return result; } } @@ -353,9 +306,7 @@ private static class EnhancementWorkedCheckMethodTestDescriptor extends TestMeth private final boolean enhanced; private final String[] classes; - public EnhancementWorkedCheckMethodTestDescriptor(UniqueId uniqueId, Class testClass, - JupiterConfiguration configuration, - boolean enhanced, String[] classes) { + public EnhancementWorkedCheckMethodTestDescriptor(UniqueId uniqueId, Class testClass, JupiterConfiguration configuration, boolean enhanced, String[] classes) { super( prepareId( uniqueId, testMethod( enhanced ) ), testClass, testMethod( enhanced ), diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/jdbc/SharedDriverManagerConnectionProviderImpl.java b/hibernate-testing/src/main/java/org/hibernate/testing/jdbc/SharedDriverManagerConnectionProviderImpl.java index b23aa4502d40..2010c5dd7985 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/jdbc/SharedDriverManagerConnectionProviderImpl.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/jdbc/SharedDriverManagerConnectionProviderImpl.java @@ -175,6 +175,49 @@ else if ( "org.postgresql.Driver".equals( config.driverClassName ) ) { } } ); } + else if ( "com.edb.Driver".equals( config.driverClassName ) ) { + validateConnections( c -> { + // Until pgjdbc provides a method for this out of the box, we have to do this manually + // See https://github.com/pgjdbc/pgjdbc/issues/3049 + try { + final Class pgConnection = Class.forName( "com.edb.jdbc.PgConnection" ); + final Object connection = c.unwrap( pgConnection ); + final Object typeInfo = pgConnection.getMethod( "getTypeInfo" ).invoke( connection ); + final Class typeInfoCacheClass = Class.forName( "com.edb.jdbc.TypeInfoCache" ); + final Field oidToPgNameField = typeInfoCacheClass.getDeclaredField( "oidToPgName" ); + final Field pgNameToOidField = typeInfoCacheClass.getDeclaredField( "pgNameToOid" ); + final Field pgNameToSQLTypeField = typeInfoCacheClass.getDeclaredField( "pgNameToSQLType" ); + final Field oidToSQLTypeField = typeInfoCacheClass.getDeclaredField( "oidToSQLType" ); + oidToPgNameField.setAccessible( true ); + pgNameToOidField.setAccessible( true ); + pgNameToSQLTypeField.setAccessible( true ); + oidToSQLTypeField.setAccessible( true ); + //noinspection unchecked + final Map oidToPgName = (Map) oidToPgNameField.get( typeInfo ); + //noinspection unchecked + final Map pgNameToOid = (Map) pgNameToOidField.get( typeInfo ); + //noinspection unchecked + final Map pgNameToSQLType = (Map) pgNameToSQLTypeField.get( typeInfo ); + //noinspection unchecked + final Map oidToSQLType = (Map) oidToSQLTypeField.get( typeInfo ); + for ( Iterator> iter = pgNameToOid.entrySet().iterator(); iter.hasNext(); ) { + Map.Entry entry = iter.next(); + final String typeName = entry.getKey(); + if ( !PGJDBC_STANDARD_TYPE_NAMES.contains( typeName ) ) { + final Integer oid = entry.getValue(); + oidToPgName.remove( oid ); + oidToSQLType.remove( oid ); + pgNameToSQLType.remove( typeName ); + iter.remove(); + } + } + return true; + } + catch (Exception e) { + throw new RuntimeException( e ); + } + } ); + } } public void onDefaultTimeZoneChange() { diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/jta/JtaAwareConnectionProviderImpl.java b/hibernate-testing/src/main/java/org/hibernate/testing/jta/JtaAwareConnectionProviderImpl.java index f90e6ad9ae9d..1964e46281d5 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/jta/JtaAwareConnectionProviderImpl.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/jta/JtaAwareConnectionProviderImpl.java @@ -214,7 +214,7 @@ private void delist(Connection connection) { System.err.println( "!!!Error trying to reset synchronization registry!!!" ); } try { - delegate.closeConnection( connection ); + delegate.closeConnection( ((ConnectionWrapper) connection).delegate ); } catch (SQLException e) { System.err.println( "!!!Error trying to close JDBC connection from delist callbacks!!!" ); diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/junit4/CustomRunner.java b/hibernate-testing/src/main/java/org/hibernate/testing/junit4/CustomRunner.java index 507a8a36c2e1..a764ca743f1a 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/junit4/CustomRunner.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/junit4/CustomRunner.java @@ -13,6 +13,7 @@ import java.util.Comparator; import java.util.LinkedHashMap; import java.util.List; +import java.util.Locale; import org.hibernate.dialect.DatabaseVersion; import org.hibernate.dialect.Dialect; @@ -28,7 +29,9 @@ import org.hibernate.testing.SkipForDialect; import org.hibernate.testing.SkipForDialects; import org.hibernate.testing.orm.junit.DialectContext; +import org.hibernate.testing.orm.junit.DialectFeatureCheck; import org.hibernate.testing.orm.junit.DialectFilterExtension; +import org.hibernate.testing.orm.junit.RequiresDialectFeatureGroup; import org.hibernate.testing.orm.junit.SkipForDialectGroup; import org.hibernate.testing.orm.junit.TestingUtil; import org.junit.BeforeClass; @@ -445,6 +448,30 @@ protected Ignore convertSkipToIgnore(FrameworkMethod frameworkMethod) { } } + Collection effectiveRequiresDialectFeatures = Helper.collectAnnotations( + org.hibernate.testing.orm.junit.RequiresDialectFeature.class, + RequiresDialectFeatureGroup.class, + frameworkMethod, + getTestClass() + ); + + for ( org.hibernate.testing.orm.junit.RequiresDialectFeature effectiveRequiresDialectFeature : effectiveRequiresDialectFeatures ) { + try { + final Class featureClass = effectiveRequiresDialectFeature.feature(); + final DialectFeatureCheck featureCheck = featureClass.getConstructor().newInstance(); + boolean testResult = featureCheck.apply( dialect ); + if ( effectiveRequiresDialectFeature.reverse() ) { + testResult = !testResult; + } + if ( !testResult ) { + return buildIgnore( effectiveRequiresDialectFeature ); + } + } + catch (ReflectiveOperationException e) { + throw new RuntimeException( "Unable to instantiate DialectFeatureCheck class", e ); + } + } + return null; } @@ -568,6 +595,14 @@ private Ignore buildIgnore(RequiresDialectFeature requiresDialectFeature) { ); } + private Ignore buildIgnore(org.hibernate.testing.orm.junit.RequiresDialectFeature requiresDialectFeature) { + return buildIgnore( + String.format( Locale.ROOT, "Failed @RequiresDialectFeature [%s]", requiresDialectFeature.feature() ), + requiresDialectFeature.comment(), + requiresDialectFeature.jiraKey() + ); + } + private boolean isMatch(Class condition) { try { Skip.Matcher matcher = condition.newInstance(); diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/memory/GlobalMemoryUsageSnapshotter.java b/hibernate-testing/src/main/java/org/hibernate/testing/memory/GlobalMemoryUsageSnapshotter.java new file mode 100644 index 000000000000..05cabd0bc432 --- /dev/null +++ b/hibernate-testing/src/main/java/org/hibernate/testing/memory/GlobalMemoryUsageSnapshotter.java @@ -0,0 +1,100 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.testing.memory; + +import java.lang.management.ManagementFactory; +import java.lang.management.MemoryPoolMXBean; +import java.util.List; +import java.util.Objects; + +final class GlobalMemoryUsageSnapshotter implements MemoryAllocationSnapshotter { + + private static final GlobalMemoryUsageSnapshotter INSTANCE = new GlobalMemoryUsageSnapshotter( + ManagementFactory.getMemoryPoolMXBeans() + ); + + private final List heapPoolBeans; + private final Runnable gcAndWait; + + private GlobalMemoryUsageSnapshotter(List heapPoolBeans) { + this.heapPoolBeans = heapPoolBeans; + this.gcAndWait = () -> { + for (int i = 0; i < 3; i++) { + System.gc(); + try { + Thread.sleep( 50 ); + } + catch (InterruptedException ignored) { + } + } + }; + } + + public static GlobalMemoryUsageSnapshotter getInstance() { + return INSTANCE; + } + + @Override + public MemoryAllocationSnapshot snapshot() { + final long peakUsage = heapPoolBeans.stream().mapToLong(p -> p.getPeakUsage().getUsed()).sum(); + gcAndWait.run(); + final long retainedUsage = heapPoolBeans.stream().mapToLong(p -> p.getUsage().getUsed()).sum(); + heapPoolBeans.forEach(MemoryPoolMXBean::resetPeakUsage); + return new GlobalMemoryAllocationSnapshot( peakUsage, retainedUsage ); + } + + final static class GlobalMemoryAllocationSnapshot implements MemoryAllocationSnapshot { + private final long peakUsage; + private final long retainedUsage; + + GlobalMemoryAllocationSnapshot(long peakUsage, long retainedUsage) { + this.peakUsage = peakUsage; + this.retainedUsage = retainedUsage; + } + + public long peakUsage() { + return peakUsage; + } + + public long retainedUsage() { + return retainedUsage; + } + + @Override + public long difference(MemoryAllocationSnapshot before) { + // When doing the "before" snapshot, the peak usage is reset. + // Since this object is the "after" snapshot, we can simply estimate the memory usage of an operation + // to be the peak usage of that operation minus the usage after GC + return peakUsage - retainedUsage; + } + + @Override + public boolean equals(Object obj) { + if ( obj == this ) { + return true; + } + if ( obj == null || obj.getClass() != this.getClass() ) { + return false; + } + var that = (GlobalMemoryAllocationSnapshot) obj; + return this.peakUsage == that.peakUsage && + this.retainedUsage == that.retainedUsage; + } + + @Override + public int hashCode() { + return Objects.hash( peakUsage, retainedUsage ); + } + + @Override + public String toString() { + return "GlobalMemoryAllocationSnapshot[" + + "peakUsage=" + peakUsage + ", " + + "retainedUsage=" + retainedUsage + ']'; + } + } +} diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/memory/HotspotPerThreadAllocationSnapshotter.java b/hibernate-testing/src/main/java/org/hibernate/testing/memory/HotspotPerThreadAllocationSnapshotter.java new file mode 100644 index 000000000000..a767d8a758d9 --- /dev/null +++ b/hibernate-testing/src/main/java/org/hibernate/testing/memory/HotspotPerThreadAllocationSnapshotter.java @@ -0,0 +1,169 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.testing.memory; + +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.lang.management.ManagementFactory; +import java.lang.management.ThreadMXBean; +import java.lang.reflect.Method; +import java.util.HashMap; +import java.util.Objects; + +final class HotspotPerThreadAllocationSnapshotter implements MemoryAllocationSnapshotter { + + private static final @Nullable HotspotPerThreadAllocationSnapshotter INSTANCE; + private static final Method GET_THREAD_ALLOCATED_BYTES; + + static { + ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean(); + Method method = null; + try { + @SuppressWarnings("unchecked") + Class hotspotInterface = + (Class) Class.forName( "com.sun.management.ThreadMXBean" ); + try { + method = hotspotInterface.getMethod( "getThreadAllocatedBytes", long[].class ); + } + catch (Exception e) { + // Ignore + } + + if ( !hotspotInterface.isInstance( threadMXBean ) ) { + threadMXBean = ManagementFactory.getPlatformMXBean( hotspotInterface ); + } + } + catch (Throwable e) { + // Ignore + } + + GET_THREAD_ALLOCATED_BYTES = method; + + HotspotPerThreadAllocationSnapshotter instance = null; + if ( method != null && threadMXBean != null ) { + try { + instance = new HotspotPerThreadAllocationSnapshotter( threadMXBean ); + instance.snapshot(); + } + catch (Exception e) { + instance = null; + } + } + INSTANCE = instance; + } + + public static @Nullable HotspotPerThreadAllocationSnapshotter getInstance() { + return INSTANCE; + } + + @Override + public MemoryAllocationSnapshot snapshot() { + long[] threadIds = threadMXBean.getAllThreadIds(); + try { + return new PerThreadMemoryAllocationSnapshot( + threadIds, + (long[]) GET_THREAD_ALLOCATED_BYTES.invoke( threadMXBean, (Object) threadIds ) + ); + } + catch (Exception e) { + throw new RuntimeException( e ); + } + } + + final static class PerThreadMemoryAllocationSnapshot implements MemoryAllocationSnapshot { + private final long[] threadIds; + private final long[] threadAllocatedBytes; + + PerThreadMemoryAllocationSnapshot(long[] threadIds, long[] threadAllocatedBytes) { + this.threadIds = threadIds; + this.threadAllocatedBytes = threadAllocatedBytes; + } + + public long[] threadIds() { + return threadIds; + } + + public long[] threadAllocatedBytes() { + return threadAllocatedBytes; + } + + @Override + public long difference(MemoryAllocationSnapshot before) { + final PerThreadMemoryAllocationSnapshot other = (PerThreadMemoryAllocationSnapshot) before; + final HashMap previousThreadIdToIndexMap = new HashMap<>(); + for ( int i = 0; i < other.threadIds.length; i++ ) { + previousThreadIdToIndexMap.put( other.threadIds[i], i ); + } + long allocatedBytes = 0; + for ( int i = 0; i < threadIds.length; i++ ) { + allocatedBytes += threadAllocatedBytes[i]; + final Integer previousThreadIndex = previousThreadIdToIndexMap.get( threadIds[i] ); + if ( previousThreadIndex != null ) { + allocatedBytes -= other.threadAllocatedBytes[previousThreadIndex]; + } + } + return allocatedBytes; + } + + @Override + public boolean equals(Object obj) { + if ( obj == this ) { + return true; + } + if ( obj == null || obj.getClass() != this.getClass() ) { + return false; + } + var that = (PerThreadMemoryAllocationSnapshot) obj; + return Objects.equals( this.threadIds, that.threadIds ) && + Objects.equals( this.threadAllocatedBytes, that.threadAllocatedBytes ); + } + + @Override + public int hashCode() { + return Objects.hash( threadIds, threadAllocatedBytes ); + } + + @Override + public String toString() { + return "PerThreadMemoryAllocationSnapshot[" + + "threadIds=" + threadIds + ", " + + "threadAllocatedBytes=" + threadAllocatedBytes + ']'; + } + } + private final ThreadMXBean threadMXBean; + + HotspotPerThreadAllocationSnapshotter(ThreadMXBean threadMXBean) { + this.threadMXBean = threadMXBean; + } + + public ThreadMXBean threadMXBean() { + return threadMXBean; + } + + @Override + public boolean equals(Object obj) { + if ( obj == this ) { + return true; + } + if ( obj == null || obj.getClass() != this.getClass() ) { + return false; + } + var that = (HotspotPerThreadAllocationSnapshotter) obj; + return Objects.equals( this.threadMXBean, that.threadMXBean ); + } + + @Override + public int hashCode() { + return Objects.hash( threadMXBean ); + } + + @Override + public String toString() { + return "HotspotPerThreadAllocationSnapshotter[" + + "threadMXBean=" + threadMXBean + ']'; + } +} diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/memory/HotspotTotalThreadBytesSnapshotter.java b/hibernate-testing/src/main/java/org/hibernate/testing/memory/HotspotTotalThreadBytesSnapshotter.java new file mode 100644 index 000000000000..238eaeb7539f --- /dev/null +++ b/hibernate-testing/src/main/java/org/hibernate/testing/memory/HotspotTotalThreadBytesSnapshotter.java @@ -0,0 +1,148 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.testing.memory; + +import org.checkerframework.checker.nullness.qual.Nullable; + +import java.lang.management.ManagementFactory; +import java.lang.management.ThreadMXBean; +import java.lang.reflect.Method; +import java.util.Objects; + +final class HotspotTotalThreadBytesSnapshotter implements MemoryAllocationSnapshotter { + + private static final @Nullable HotspotTotalThreadBytesSnapshotter INSTANCE; + private static final Method GET_TOTAL_THREAD_ALLOCATED_BYTES; + + static { + ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean(); + Method method = null; + try { + @SuppressWarnings("unchecked") + Class hotspotInterface = + (Class) Class.forName( "com.sun.management.ThreadMXBean" ); + try { + method = hotspotInterface.getMethod( "getTotalThreadAllocatedBytes" ); + } + catch (Exception e) { + // Ignore + } + + if ( !hotspotInterface.isInstance( threadMXBean ) ) { + threadMXBean = ManagementFactory.getPlatformMXBean( hotspotInterface ); + } + } + catch (Throwable e) { + // Ignore + } + + GET_TOTAL_THREAD_ALLOCATED_BYTES = method; + + HotspotTotalThreadBytesSnapshotter instance = null; + if ( method != null && threadMXBean != null ) { + try { + instance = new HotspotTotalThreadBytesSnapshotter( threadMXBean ); + instance.snapshot(); + } + catch (Exception e) { + instance = null; + } + } + INSTANCE = instance; + } + + public static @Nullable HotspotTotalThreadBytesSnapshotter getInstance() { + return INSTANCE; + } + + @Override + public MemoryAllocationSnapshot snapshot() { + try { + return new GlobalMemoryAllocationSnapshot( (long) GET_TOTAL_THREAD_ALLOCATED_BYTES.invoke( threadMXBean ) ); + } + catch (Exception e) { + throw new RuntimeException( e ); + } + } + + final static class GlobalMemoryAllocationSnapshot implements MemoryAllocationSnapshot { + private final long allocatedBytes; + + GlobalMemoryAllocationSnapshot(long allocatedBytes) { + if ( allocatedBytes == -1L ) { + throw new IllegalArgumentException( "getTotalThreadAllocatedBytes is disabled" ); + } + this.allocatedBytes = allocatedBytes; + } + + @Override + public long difference(MemoryAllocationSnapshot before) { + final GlobalMemoryAllocationSnapshot other = (GlobalMemoryAllocationSnapshot) before; + return Math.max( allocatedBytes - other.allocatedBytes, 0L ); + } + + public long allocatedBytes() { + return allocatedBytes; + } + + @Override + public boolean equals(Object obj) { + if ( obj == this ) { + return true; + } + if ( obj == null || obj.getClass() != this.getClass() ) { + return false; + } + var that = (GlobalMemoryAllocationSnapshot) obj; + return this.allocatedBytes == that.allocatedBytes; + } + + @Override + public int hashCode() { + return Objects.hash( allocatedBytes ); + } + + @Override + public String toString() { + return "GlobalMemoryAllocationSnapshot[" + + "allocatedBytes=" + allocatedBytes + ']'; + } + } + + private final ThreadMXBean threadMXBean; + + HotspotTotalThreadBytesSnapshotter(ThreadMXBean threadMXBean) { + this.threadMXBean = threadMXBean; + } + + public ThreadMXBean threadMXBean() { + return threadMXBean; + } + + @Override + public boolean equals(Object obj) { + if ( obj == this ) { + return true; + } + if ( obj == null || obj.getClass() != this.getClass() ) { + return false; + } + var that = (HotspotTotalThreadBytesSnapshotter) obj; + return Objects.equals( this.threadMXBean, that.threadMXBean ); + } + + @Override + public int hashCode() { + return Objects.hash( threadMXBean ); + } + + @Override + public String toString() { + return "HotspotTotalThreadBytesSnapshotter[" + + "threadMXBean=" + threadMXBean + ']'; + } +} diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/memory/MemoryAllocationSnapshot.java b/hibernate-testing/src/main/java/org/hibernate/testing/memory/MemoryAllocationSnapshot.java new file mode 100644 index 000000000000..1de3f1afaf5d --- /dev/null +++ b/hibernate-testing/src/main/java/org/hibernate/testing/memory/MemoryAllocationSnapshot.java @@ -0,0 +1,11 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.testing.memory; + +interface MemoryAllocationSnapshot { + long difference(MemoryAllocationSnapshot before); +} diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/memory/MemoryAllocationSnapshotter.java b/hibernate-testing/src/main/java/org/hibernate/testing/memory/MemoryAllocationSnapshotter.java new file mode 100644 index 000000000000..a0bf0192cb57 --- /dev/null +++ b/hibernate-testing/src/main/java/org/hibernate/testing/memory/MemoryAllocationSnapshotter.java @@ -0,0 +1,11 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.testing.memory; + +interface MemoryAllocationSnapshotter { + MemoryAllocationSnapshot snapshot(); +} diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/memory/MemoryUsageUtil.java b/hibernate-testing/src/main/java/org/hibernate/testing/memory/MemoryUsageUtil.java new file mode 100644 index 000000000000..9f352de0da16 --- /dev/null +++ b/hibernate-testing/src/main/java/org/hibernate/testing/memory/MemoryUsageUtil.java @@ -0,0 +1,29 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.testing.memory; + +public class MemoryUsageUtil { + + private static final MemoryAllocationSnapshotter SNAPSHOTTER; + + static { + MemoryAllocationSnapshotter snapshotter = HotspotTotalThreadBytesSnapshotter.getInstance(); + if ( snapshotter == null ) { + snapshotter = HotspotPerThreadAllocationSnapshotter.getInstance(); + } + if ( snapshotter == null ) { + snapshotter = GlobalMemoryUsageSnapshotter.getInstance(); + } + SNAPSHOTTER = snapshotter; + } + + public static long estimateMemoryUsage(Runnable runnable) { + final MemoryAllocationSnapshot beforeSnapshot = SNAPSHOTTER.snapshot(); + runnable.run(); + return SNAPSHOTTER.snapshot().difference( beforeSnapshot ); + } +} diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/orm/jpa/PersistenceUnitInfoImpl.java b/hibernate-testing/src/main/java/org/hibernate/testing/orm/jpa/PersistenceUnitInfoImpl.java index e664301d0280..db47c5e4faae 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/orm/jpa/PersistenceUnitInfoImpl.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/orm/jpa/PersistenceUnitInfoImpl.java @@ -40,6 +40,7 @@ public class PersistenceUnitInfoImpl implements PersistenceUnitInfo { private List mappingFiles; private List managedClassNames; private boolean excludeUnlistedClasses; + private ClassLoader classLoader; public PersistenceUnitInfoImpl(String name) { this.name = name; @@ -120,6 +121,15 @@ public void setExcludeUnlistedClasses(boolean excludeUnlistedClasses) { this.excludeUnlistedClasses = excludeUnlistedClasses; } + @Override + public ClassLoader getClassLoader() { + return classLoader; + } + + public void setClassLoader(ClassLoader classLoader) { + this.classLoader = classLoader; + } + @Override public String getPersistenceXMLSchemaVersion() { return null; @@ -145,11 +155,6 @@ public URL getPersistenceUnitRootUrl() { return null; } - @Override - public ClassLoader getClassLoader() { - return null; - } - @Override public void addTransformer(ClassTransformer transformer) { diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java index d775d6cbc7d4..6e89a85d0c20 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFeatureChecks.java @@ -7,9 +7,53 @@ package org.hibernate.testing.orm.junit; import java.sql.Types; - +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.UUID; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +import org.hibernate.DuplicateMappingException; +import org.hibernate.SessionFactory; +import org.hibernate.annotations.CollectionTypeRegistration; +import org.hibernate.annotations.common.reflection.XClass; +import org.hibernate.boot.MappingException; +import org.hibernate.boot.SessionFactoryBuilder; +import org.hibernate.boot.internal.MetadataBuilderImpl; +import org.hibernate.boot.internal.NamedProcedureCallDefinitionImpl; +import org.hibernate.boot.model.FunctionContributions; +import org.hibernate.boot.model.IdentifierGeneratorDefinition; +import org.hibernate.boot.model.NamedEntityGraphDefinition; import org.hibernate.boot.model.TruthValue; +import org.hibernate.boot.model.TypeContributions; +import org.hibernate.boot.model.TypeDefinition; +import org.hibernate.boot.model.TypeDefinitionRegistry; +import org.hibernate.boot.model.convert.spi.ConverterAutoApplyHandler; +import org.hibernate.boot.model.convert.spi.ConverterDescriptor; +import org.hibernate.boot.model.convert.spi.ConverterRegistry; +import org.hibernate.boot.model.convert.spi.RegisteredConversion; +import org.hibernate.boot.model.internal.AnnotatedClassType; +import org.hibernate.boot.model.naming.Identifier; +import org.hibernate.boot.model.naming.ObjectNameNormalizer; +import org.hibernate.boot.model.relational.AuxiliaryDatabaseObject; +import org.hibernate.boot.model.relational.Database; +import org.hibernate.boot.query.NamedHqlQueryDefinition; +import org.hibernate.boot.query.NamedNativeQueryDefinition; +import org.hibernate.boot.query.NamedProcedureCallDefinition; +import org.hibernate.boot.query.NamedResultSetMappingDescriptor; +import org.hibernate.boot.spi.BootstrapContext; +import org.hibernate.boot.spi.InFlightMetadataCollector; +import org.hibernate.boot.spi.MappingDefaults; +import org.hibernate.boot.spi.MetadataBuildingContext; +import org.hibernate.boot.spi.MetadataBuildingOptions; +import org.hibernate.boot.spi.NaturalIdUniqueKeyBinder; +import org.hibernate.boot.spi.PropertyData; +import org.hibernate.boot.spi.SecondPass; import org.hibernate.community.dialect.FirebirdDialect; +import org.hibernate.community.dialect.InformixDialect; import org.hibernate.dialect.AbstractHANADialect; import org.hibernate.dialect.CockroachDialect; import org.hibernate.dialect.DB2Dialect; @@ -23,17 +67,45 @@ import org.hibernate.dialect.NationalizationSupport; import org.hibernate.dialect.OracleDialect; import org.hibernate.dialect.PostgreSQLDialect; +import org.hibernate.dialect.PostgreSQLDriverKind; import org.hibernate.dialect.SQLServerDialect; import org.hibernate.dialect.SpannerDialect; +import org.hibernate.dialect.SybaseASEDialect; import org.hibernate.dialect.SybaseDialect; import org.hibernate.dialect.SybaseDriverKind; import org.hibernate.dialect.TiDBDialect; import org.hibernate.dialect.TimeZoneSupport; +import org.hibernate.engine.spi.FilterDefinition; +import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.mapping.AggregateColumn; +import org.hibernate.mapping.Collection; import org.hibernate.mapping.Column; +import org.hibernate.mapping.Component; +import org.hibernate.mapping.FetchProfile; +import org.hibernate.mapping.Join; +import org.hibernate.mapping.MappedSuperclass; +import org.hibernate.mapping.PersistentClass; +import org.hibernate.mapping.Table; +import org.hibernate.metamodel.CollectionClassification; +import org.hibernate.metamodel.mapping.DiscriminatorType; +import org.hibernate.metamodel.spi.EmbeddableInstantiator; +import org.hibernate.query.named.NamedObjectRepository; import org.hibernate.query.sqm.FetchClauseType; +import org.hibernate.query.sqm.function.SqmFunctionDescriptor; +import org.hibernate.query.sqm.function.SqmFunctionRegistry; +import org.hibernate.service.ServiceRegistry; import org.hibernate.sql.ast.spi.StringBuilderSqlAppender; import org.hibernate.type.SqlTypes; +import org.hibernate.type.Type; +import org.hibernate.type.descriptor.java.JavaType; +import org.hibernate.type.descriptor.jdbc.JdbcType; +import org.hibernate.type.spi.TypeConfiguration; +import org.hibernate.usertype.CompositeUserType; +import org.hibernate.usertype.UserType; + +import org.hibernate.testing.boot.BootstrapContextImpl; + +import jakarta.persistence.AttributeConverter; /** * Container class for different implementation of the {@link DialectFeatureCheck} interface. @@ -322,8 +394,9 @@ public boolean apply(Dialect dialect) { public static class SupportsRepeat implements DialectFeatureCheck { public boolean apply(Dialect dialect) { dialect = DialectDelegateWrapper.extractRealDialect( dialect ); - // Derby doesn't support the `REPLACE` function - return !( dialect instanceof DerbyDialect ); + // Derby doesn't support the `REPEAT` function + return !( dialect instanceof DerbyDialect + || dialect instanceof InformixDialect ); } } @@ -489,6 +562,7 @@ public boolean apply(Dialect dialect) { || dialect instanceof DerbyDialect || dialect instanceof FirebirdDialect || dialect instanceof DB2Dialect && ( (DB2Dialect) dialect ).getDB2Version().isBefore( 11 ) ) + || dialect instanceof InformixDialect || dialect instanceof MariaDBDialect; } } @@ -668,6 +742,12 @@ public boolean apply(Dialect dialect) { } } + public static class IsPgJdbc implements DialectFeatureCheck { + public boolean apply(Dialect dialect) { + return dialect instanceof PostgreSQLDialect && ( (PostgreSQLDialect) dialect ).getDriverKind() == PostgreSQLDriverKind.PG_JDBC; + } + } + public static class SupportsCommentOn implements DialectFeatureCheck { public boolean apply(Dialect dialect) { return dialect.supportsCommentOn(); @@ -686,6 +766,12 @@ public boolean apply(Dialect dialect) { } } + public static class SupportsTypedArrays implements DialectFeatureCheck { + public boolean apply(Dialect dialect) { + return dialect.getPreferredSqlTypeCodeForArray() == SqlTypes.ARRAY; + } + } + public static class SupportsUpsertOrMerge implements DialectFeatureCheck { public boolean apply(Dialect dialect) { return !( dialect instanceof DerbyDialect ); @@ -705,4 +791,779 @@ public boolean apply(Dialect dialect) { return dialect.getNationalizationSupport() == NationalizationSupport.EXPLICIT; } } + + public static class SupportsUnicodeNClob implements DialectFeatureCheck { + @Override + public boolean apply(Dialect dialect) { + return !( dialect instanceof SybaseASEDialect ) + // The jconn driver apparently doesn't support unicode characters + || ( (SybaseASEDialect) dialect ).getDriverKind() == SybaseDriverKind.JTDS; + } + } + + private static final HashMap FUNCTION_REGISTRIES = new HashMap<>(); + + public static boolean definesFunction(Dialect dialect, String functionName) { + return getSqmFunctionRegistry( dialect ).findFunctionDescriptor( functionName ) != null; + } + + public static class SupportsSubqueryInSelect implements DialectFeatureCheck { + @Override + public boolean apply(Dialect dialect) { + return dialect.supportsSubqueryInSelect(); + } + } + + public static class SupportSubqueryAsLeftHandSideInPredicate implements DialectFeatureCheck { + @Override + public boolean apply(Dialect dialect) { + return dialect.supportsSubselectAsInPredicateLHS(); + } + } + + + private static SqmFunctionRegistry getSqmFunctionRegistry(Dialect dialect) { + SqmFunctionRegistry sqmFunctionRegistry = FUNCTION_REGISTRIES.get( dialect ); + if ( sqmFunctionRegistry == null ) { + final TypeConfiguration typeConfiguration = new TypeConfiguration(); + final SqmFunctionRegistry functionRegistry = new SqmFunctionRegistry(); + typeConfiguration.scope( new FakeMetadataBuildingContext( typeConfiguration, functionRegistry ) ); + final FakeTypeContributions typeContributions = new FakeTypeContributions( typeConfiguration ); + final FakeFunctionContributions functionContributions = new FakeFunctionContributions( + dialect, + typeConfiguration, + functionRegistry + ); + dialect.contribute( typeContributions, typeConfiguration.getServiceRegistry() ); + dialect.initializeFunctionRegistry( functionContributions ); + FUNCTION_REGISTRIES.put( dialect, sqmFunctionRegistry = functionContributions.functionRegistry ); + } + return sqmFunctionRegistry; + } + + public static class FakeTypeContributions implements TypeContributions { + private final TypeConfiguration typeConfiguration; + + public FakeTypeContributions(TypeConfiguration typeConfiguration) { + this.typeConfiguration = typeConfiguration; + } + + @Override + public TypeConfiguration getTypeConfiguration() { + return typeConfiguration; + } + } + + public static class FakeFunctionContributions implements FunctionContributions { + private final Dialect dialect; + private final TypeConfiguration typeConfiguration; + private final SqmFunctionRegistry functionRegistry; + + public FakeFunctionContributions(Dialect dialect, TypeConfiguration typeConfiguration, SqmFunctionRegistry functionRegistry) { + this.dialect = dialect; + this.typeConfiguration = typeConfiguration; + this.functionRegistry = functionRegistry; + } + + @Override + public Dialect getDialect() { + return dialect; + } + + @Override + public TypeConfiguration getTypeConfiguration() { + return typeConfiguration; + } + + @Override + public SqmFunctionRegistry getFunctionRegistry() { + return functionRegistry; + } + + @Override + public ServiceRegistry getServiceRegistry() { + return null; + } + } + + public static class FakeMetadataBuildingContext implements MetadataBuildingContext, InFlightMetadataCollector { + + private final TypeConfiguration typeConfiguration; + private final SqmFunctionRegistry functionRegistry; + private final MetadataBuilderImpl.MetadataBuildingOptionsImpl options; + private final BootstrapContextImpl bootstrapContext; + private final Database database; + + public FakeMetadataBuildingContext(TypeConfiguration typeConfiguration, SqmFunctionRegistry functionRegistry) { + this.typeConfiguration = typeConfiguration; + this.functionRegistry = functionRegistry; + this.bootstrapContext = new BootstrapContextImpl(); + this.options = new MetadataBuilderImpl.MetadataBuildingOptionsImpl( bootstrapContext.getServiceRegistry() ); + this.options.setBootstrapContext( bootstrapContext ); + this.database = new Database( options, null ); + } + + @Override + public BootstrapContext getBootstrapContext() { + return bootstrapContext; + } + + @Override + public MetadataBuildingOptions getBuildingOptions() { + return options; + } + + @Override + public Database getDatabase() { + return database; + } + + @Override + public MetadataBuildingOptions getMetadataBuildingOptions() { + return options; + } + + @Override + public TypeConfiguration getTypeConfiguration() { + return typeConfiguration; + } + + @Override + public SqmFunctionRegistry getFunctionRegistry() { + return functionRegistry; + } + + // The rest are no-ops + + + @Override + public void registerEmbeddableSubclass(XClass superclass, XClass subclass) { + + } + + @Override + public List getEmbeddableSubclasses(XClass superclass) { + return List.of(); + } + + @Override + public AnnotatedClassType addClassType(XClass clazz) { + return null; + } + + @Override + public AnnotatedClassType getClassType(XClass clazz) { + return null; + } + + @Override + public PropertyData getPropertyAnnotatedWithMapsId(XClass persistentXClass, String propertyName) { + return null; + } + + @Override + public void addPropertyAnnotatedWithMapsId(XClass entity, PropertyData propertyAnnotatedElement) { + + } + + @Override + public void addPropertyAnnotatedWithMapsIdSpecj(XClass entity, PropertyData specJPropertyData, String s) { + + } + + @Override + public void addToOneAndIdProperty(XClass entity, PropertyData propertyAnnotatedElement) { + + } + + @Override + public PropertyData getPropertyAnnotatedWithIdAndToOne(XClass persistentXClass, String propertyName) { + return null; + } + + @Override + public MappingDefaults getMappingDefaults() { + return null; + } + + @Override + public NamedObjectRepository buildNamedQueryRepository(SessionFactoryImplementor sessionFactory) { + return null; + } + + @Override + public InFlightMetadataCollector getMetadataCollector() { + return this; + } + + @Override + public ObjectNameNormalizer getObjectNameNormalizer() { + return null; + } + + @Override + public TypeDefinitionRegistry getTypeDefinitionRegistry() { + return null; + } + + @Override + public String getCurrentContributorName() { + return ""; + } + + @Override + public void addEntityBinding(PersistentClass persistentClass) throws DuplicateMappingException { + + } + + @Override + public Map getEntityBindingMap() { + return Map.of(); + } + + @Override + public void registerComponent(Component component) { + + } + + @Override + public void registerGenericComponent(Component component) { + + } + + @Override + public void addImport(String importName, String className) throws DuplicateMappingException { + + } + + @Override + public void addCollectionBinding(Collection collection) throws DuplicateMappingException { + + } + + @Override + public Table addTable( + String schema, + String catalog, + String name, + String subselect, + boolean isAbstract, + MetadataBuildingContext buildingContext) { + return null; + } + + @Override + public Table addDenormalizedTable( + String schema, + String catalog, + String name, + boolean isAbstract, + String subselect, + Table includedTable, + MetadataBuildingContext buildingContext) throws DuplicateMappingException { + return null; + } + + @Override + public void addNamedQuery(NamedHqlQueryDefinition query) throws DuplicateMappingException { + + } + + @Override + public void addNamedNativeQuery(NamedNativeQueryDefinition query) throws DuplicateMappingException { + + } + + @Override + public void addResultSetMapping(NamedResultSetMappingDescriptor resultSetMappingDefinition) + throws DuplicateMappingException { + + } + + @Override + public void addNamedProcedureCallDefinition(NamedProcedureCallDefinition definition) + throws DuplicateMappingException { + + } + + @Override + public void addNamedEntityGraph(NamedEntityGraphDefinition namedEntityGraphDefinition) { + + } + + @Override + public void addTypeDefinition(TypeDefinition typeDefinition) { + + } + + @Override + public void addFilterDefinition(FilterDefinition definition) { + + } + + @Override + public void addAuxiliaryDatabaseObject(AuxiliaryDatabaseObject auxiliaryDatabaseObject) { + + } + + @Override + public void addFetchProfile(FetchProfile profile) { + + } + + @Override + public void addIdentifierGenerator(IdentifierGeneratorDefinition generatorDefinition) { + + } + + @Override + public ConverterRegistry getConverterRegistry() { + return null; + } + + @Override + public void addAttributeConverter(ConverterDescriptor descriptor) { + + } + + @Override + public void addAttributeConverter(Class> converterClass) { + + } + + @Override + public void addRegisteredConversion(RegisteredConversion conversion) { + + } + + @Override + public ConverterAutoApplyHandler getAttributeConverterAutoApplyHandler() { + return null; + } + + @Override + public void addSecondPass(SecondPass secondPass) { + + } + + @Override + public void addSecondPass(SecondPass sp, boolean onTopOfTheQueue) { + + } + + @Override + public void addTableNameBinding(Identifier logicalName, Table table) { + + } + + @Override + public void addTableNameBinding( + String schema, + String catalog, + String logicalName, + String realTableName, + Table denormalizedSuperTable) { + + } + + @Override + public String getLogicalTableName(Table ownerTable) { + return ""; + } + + @Override + public String getPhysicalTableName(Identifier logicalName) { + return ""; + } + + @Override + public String getPhysicalTableName(String logicalName) { + return ""; + } + + @Override + public void addColumnNameBinding(Table table, Identifier logicalColumnName, Column column) { + + } + + @Override + public void addColumnNameBinding(Table table, String logicalColumnName, Column column) { + + } + + @Override + public String getPhysicalColumnName(Table table, Identifier logicalName) throws MappingException { + return ""; + } + + @Override + public String getPhysicalColumnName(Table table, String logicalName) throws MappingException { + return ""; + } + + @Override + public String getLogicalColumnName(Table table, Identifier physicalName) { + return ""; + } + + @Override + public String getLogicalColumnName(Table table, String physicalName) { + return ""; + } + + @Override + public void addDefaultIdentifierGenerator(IdentifierGeneratorDefinition generatorDefinition) { + + } + + @Override + public void addDefaultQuery(NamedHqlQueryDefinition queryDefinition) { + + } + + @Override + public void addDefaultNamedNativeQuery(NamedNativeQueryDefinition query) { + + } + + @Override + public void addDefaultResultSetMapping(NamedResultSetMappingDescriptor definition) { + + } + + @Override + public void addDefaultNamedProcedureCall(NamedProcedureCallDefinitionImpl procedureCallDefinition) { + + } + + @Override + public void addMappedSuperclass(Class type, MappedSuperclass mappedSuperclass) { + + } + + @Override + public MappedSuperclass getMappedSuperclass(Class type) { + return null; + } + + @Override + public boolean isInSecondPass() { + return false; + } + + @Override + public NaturalIdUniqueKeyBinder locateNaturalIdUniqueKeyBinder(String entityName) { + return null; + } + + @Override + public void registerNaturalIdUniqueKeyBinder(String entityName, NaturalIdUniqueKeyBinder ukBinder) { + + } + + @Override + public void registerValueMappingResolver(Function resolver) { + + } + + @Override + public void addJavaTypeRegistration(Class javaType, JavaType jtd) { + + } + + @Override + public void addJdbcTypeRegistration(int typeCode, JdbcType jdbcType) { + + } + + @Override + public void registerEmbeddableInstantiator( + Class embeddableType, + Class instantiator) { + + } + + @Override + public Class findRegisteredEmbeddableInstantiator(Class embeddableType) { + return null; + } + + @Override + public void registerCompositeUserType(Class embeddableType, Class> userType) { + + } + + @Override + public Class> findRegisteredCompositeUserType(Class embeddableType) { + return null; + } + + @Override + public void registerUserType(Class embeddableType, Class> userType) { + + } + + @Override + public Class> findRegisteredUserType(Class basicType) { + return null; + } + + @Override + public void addCollectionTypeRegistration(CollectionTypeRegistration registrationAnnotation) { + + } + + @Override + public void addCollectionTypeRegistration( + CollectionClassification classification, + CollectionTypeRegistrationDescriptor descriptor) { + + } + + @Override + public CollectionTypeRegistrationDescriptor findCollectionTypeRegistration(CollectionClassification classification) { + return null; + } + + @Override + public void addDelayedPropertyReferenceHandler(DelayedPropertyReferenceHandler handler) { + + } + + @Override + public void addPropertyReference(String entityName, String propertyName) { + + } + + @Override + public void addUniquePropertyReference(String entityName, String propertyName) { + + } + + @Override + public void addPropertyReferencedAssociation(String entityName, String propertyName, String syntheticPropertyName) { + + } + + @Override + public String getPropertyReferencedAssociation(String entityName, String mappedBy) { + return ""; + } + + @Override + public void addMappedBy(String name, String mappedBy, String propertyName) { + + } + + @Override + public String getFromMappedBy(String ownerEntityName, String propertyName) { + return ""; + } + + @Override + public EntityTableXref getEntityTableXref(String entityName) { + return null; + } + + @Override + public EntityTableXref addEntityTableXref( + String entityName, + Identifier primaryTableLogicalName, + Table primaryTable, + EntityTableXref superEntityTableXref) { + return null; + } + + @Override + public Map getJoins(String entityName) { + return Map.of(); + } + + @Override + public SessionFactoryBuilder getSessionFactoryBuilder() { + return null; + } + + @Override + public SessionFactory buildSessionFactory() { + return null; + } + + @Override + public UUID getUUID() { + return null; + } + + @Override + public java.util.Collection getEntityBindings() { + return List.of(); + } + + @Override + public PersistentClass getEntityBinding(String entityName) { + return null; + } + + @Override + public java.util.Collection getCollectionBindings() { + return List.of(); + } + + @Override + public Collection getCollectionBinding(String role) { + return null; + } + + @Override + public Map getImports() { + return Map.of(); + } + + @Override + public NamedHqlQueryDefinition getNamedHqlQueryMapping(String name) { + return null; + } + + @Override + public void visitNamedHqlQueryDefinitions(Consumer definitionConsumer) { + + } + + @Override + public NamedNativeQueryDefinition getNamedNativeQueryMapping(String name) { + return null; + } + + @Override + public void visitNamedNativeQueryDefinitions(Consumer definitionConsumer) { + + } + + @Override + public NamedProcedureCallDefinition getNamedProcedureCallMapping(String name) { + return null; + } + + @Override + public void visitNamedProcedureCallDefinition(Consumer definitionConsumer) { + + } + + @Override + public NamedResultSetMappingDescriptor getResultSetMapping(String name) { + return null; + } + + @Override + public void visitNamedResultSetMappingDefinition(Consumer definitionConsumer) { + + } + + @Override + public TypeDefinition getTypeDefinition(String typeName) { + return null; + } + + @Override + public Map getFilterDefinitions() { + return Map.of(); + } + + @Override + public FilterDefinition getFilterDefinition(String name) { + return null; + } + + @Override + public FetchProfile getFetchProfile(String name) { + return null; + } + + @Override + public java.util.Collection getFetchProfiles() { + return List.of(); + } + + @Override + public NamedEntityGraphDefinition getNamedEntityGraph(String name) { + return null; + } + + @Override + public Map getNamedEntityGraphs() { + return Map.of(); + } + + @Override + public IdentifierGeneratorDefinition getIdentifierGenerator(String name) { + return null; + } + + @Override + public java.util.Collection collectTableMappings() { + return List.of(); + } + + @Override + public Map getSqlFunctionMap() { + return Map.of(); + } + + @Override + public Set getContributors() { + return Set.of(); + } + + @Override + public void orderColumns(boolean forceOrdering) { + + } + + @Override + public void validate() throws MappingException { + + } + + @Override + public Set getMappedSuperclassMappingsCopy() { + return Set.of(); + } + + @Override + public void initSessionFactory(SessionFactoryImplementor sessionFactoryImplementor) { + + } + + @Override + public void visitRegisteredComponents(Consumer consumer) { + + } + + @Override + public Component getGenericComponent(Class componentClass) { + return null; + } + + @Override + public DiscriminatorType resolveEmbeddableDiscriminatorType( + Class embeddableClass, + Supplier> supplier) { + return null; + } + + @Override + public Type getIdentifierType(String className) throws MappingException { + return null; + } + + @Override + public String getIdentifierPropertyName(String className) throws MappingException { + return ""; + } + + @Override + public Type getReferencedPropertyType(String className, String propertyName) throws MappingException { + return null; + } + } } diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFilterExtension.java b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFilterExtension.java index fb857ac3810c..c365a747b407 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFilterExtension.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/DialectFilterExtension.java @@ -10,8 +10,10 @@ import java.util.LinkedHashMap; import java.util.Locale; +import org.hibernate.dialect.DatabaseVersion; import org.hibernate.dialect.Dialect; +import org.hibernate.dialect.SimpleDatabaseVersion; import org.junit.jupiter.api.extension.ConditionEvaluationResult; import org.junit.jupiter.api.extension.ExecutionCondition; import org.junit.jupiter.api.extension.ExtensionContext; @@ -158,27 +160,78 @@ public static boolean versionsMatch( } } + static class DialectVersionKey { + private final Class dialect; + private final DatabaseVersion version; + + public DialectVersionKey(Class dialect, DatabaseVersion version) { + this.dialect = dialect; + this.version = version; + } + + public static DialectVersionKey of(SkipForDialect annotation) { + final Class dialectClass = annotation.dialectClass(); + int majorVersion = DatabaseVersion.NO_VERSION; + int minorVersion = DatabaseVersion.NO_VERSION; + int microVersion = DatabaseVersion.NO_VERSION; + if ( annotation.majorVersion() != -1 ) { + majorVersion = annotation.majorVersion(); + if ( annotation.minorVersion() != -1 ) { + minorVersion += annotation.minorVersion(); + if ( annotation.microVersion() != -1 ) { + microVersion += annotation.microVersion(); + } + } + } + return new DialectVersionKey( dialectClass, new SimpleDatabaseVersion( majorVersion, minorVersion, microVersion ) ); + } + + @Override + public boolean equals(Object o) { + if ( this == o ) { + return true; + } + if ( o == null || getClass() != o.getClass() ) { + return false; + } + DialectVersionKey that = (DialectVersionKey) o; + return dialect.equals( that.dialect ) + && version.getDatabaseMajorVersion() == that.version.getDatabaseMajorVersion() + && version.getDatabaseMinorVersion() == that.version.getDatabaseMinorVersion() + && version.getDatabaseMicroVersion() == that.version.getDatabaseMicroVersion(); + } + + @Override + public int hashCode() { + int result = dialect.hashCode(); + result = 31 * result + version.getDatabaseMajorVersion(); + result = 31 * result + version.getDatabaseMinorVersion(); + result = 31 * result + version.getDatabaseMicroVersion(); + return result; + } + } + private ConditionEvaluationResult evaluateSkipConditions(ExtensionContext context, Dialect dialect, String enabledResult) { final Collection effectiveSkips = TestingUtil.collectAnnotations( context, SkipForDialect.class, SkipForDialectGroup.class, (methodAnnotation, methodAnnotations, classAnnotation, classAnnotations) -> { - final LinkedHashMap, SkipForDialect> map = new LinkedHashMap<>(); + final LinkedHashMap map = new LinkedHashMap<>(); if ( classAnnotation != null ) { - map.put( classAnnotation.dialectClass(), classAnnotation ); + map.put( DialectVersionKey.of( classAnnotation ), classAnnotation ); } if ( classAnnotations != null ) { for ( SkipForDialect annotation : classAnnotations ) { - map.put( annotation.dialectClass(), annotation ); + map.put( DialectVersionKey.of( annotation ), annotation ); } } if ( methodAnnotation != null ) { - map.put( methodAnnotation.dialectClass(), methodAnnotation ); + map.put( DialectVersionKey.of( methodAnnotation ), methodAnnotation ); } if ( methodAnnotations != null ) { for ( SkipForDialect annotation : methodAnnotations ) { - map.put( annotation.dialectClass(), annotation ); + map.put( DialectVersionKey.of( annotation ), annotation ); } } return map.values(); diff --git a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/EntityManagerFactoryExtension.java b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/EntityManagerFactoryExtension.java index 0a4ad57e7317..79b7953c2b88 100644 --- a/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/EntityManagerFactoryExtension.java +++ b/hibernate-testing/src/main/java/org/hibernate/testing/orm/junit/EntityManagerFactoryExtension.java @@ -90,6 +90,9 @@ public static EntityManagerFactoryScope findEntityManagerFactoryScope( pui.getProperties().put( key, value ) ); + // Use the context class loader for entity loading if configured, + // to make enhancement work for tests + pui.setClassLoader( Thread.currentThread().getContextClassLoader() ); pui.setTransactionType( emfAnn.transactionType() ); pui.setCacheMode( emfAnn.sharedCacheMode() ); pui.setValidationMode( emfAnn.validationMode() ); diff --git a/hibernate-vector/src/main/java/org/hibernate/vector/PGVectorFunctionContributor.java b/hibernate-vector/src/main/java/org/hibernate/vector/PGVectorFunctionContributor.java index d6a294c3f8f3..b508351cdaa8 100644 --- a/hibernate-vector/src/main/java/org/hibernate/vector/PGVectorFunctionContributor.java +++ b/hibernate-vector/src/main/java/org/hibernate/vector/PGVectorFunctionContributor.java @@ -29,7 +29,7 @@ public void contributeFunctions(FunctionContributions functionContributions) { if ( dialect instanceof PostgreSQLDialect ) { final BasicType doubleType = basicTypeRegistry.resolve( StandardBasicTypes.DOUBLE ); final BasicType integerType = basicTypeRegistry.resolve( StandardBasicTypes.INTEGER ); - functionRegistry.patternDescriptorBuilder( "cosine_distance", "?1<=>?2" ) + functionRegistry.patternDescriptorBuilder( "cosine_distance", "(?1<=>?2)" ) .setArgumentsValidator( StandardArgumentsValidators.composite( StandardArgumentsValidators.exactly( 2 ), VectorArgumentValidator.INSTANCE @@ -37,7 +37,7 @@ public void contributeFunctions(FunctionContributions functionContributions) { .setArgumentTypeResolver( VectorArgumentTypeResolver.INSTANCE ) .setReturnTypeResolver( StandardFunctionReturnTypeResolvers.invariant( doubleType ) ) .register(); - functionRegistry.patternDescriptorBuilder( "euclidean_distance", "?1<->?2" ) + functionRegistry.patternDescriptorBuilder( "euclidean_distance", "(?1<->?2)" ) .setArgumentsValidator( StandardArgumentsValidators.composite( StandardArgumentsValidators.exactly( 2 ), VectorArgumentValidator.INSTANCE @@ -56,7 +56,7 @@ public void contributeFunctions(FunctionContributions functionContributions) { .register(); functionRegistry.registerAlternateKey( "taxicab_distance", "l1_distance" ); - functionRegistry.patternDescriptorBuilder( "negative_inner_product", "?1<#>?2" ) + functionRegistry.patternDescriptorBuilder( "negative_inner_product", "(?1<#>?2)" ) .setArgumentsValidator( StandardArgumentsValidators.composite( StandardArgumentsValidators.exactly( 2 ), VectorArgumentValidator.INSTANCE @@ -64,7 +64,7 @@ public void contributeFunctions(FunctionContributions functionContributions) { .setArgumentTypeResolver( VectorArgumentTypeResolver.INSTANCE ) .setReturnTypeResolver( StandardFunctionReturnTypeResolvers.invariant( doubleType ) ) .register(); - functionRegistry.patternDescriptorBuilder( "inner_product", "(?1<#>?2)*-1" ) + functionRegistry.patternDescriptorBuilder( "inner_product", "((?1<#>?2)*-1)" ) .setArgumentsValidator( StandardArgumentsValidators.composite( StandardArgumentsValidators.exactly( 2 ), VectorArgumentValidator.INSTANCE diff --git a/hibernate-vector/src/test/java/org/hibernate/vector/OracleByteVectorTest.java b/hibernate-vector/src/test/java/org/hibernate/vector/OracleByteVectorTest.java index e26cc56f668d..0e115f06d51b 100644 --- a/hibernate-vector/src/test/java/org/hibernate/vector/OracleByteVectorTest.java +++ b/hibernate-vector/src/test/java/org/hibernate/vector/OracleByteVectorTest.java @@ -11,6 +11,7 @@ import org.hibernate.annotations.Array; import org.hibernate.annotations.JdbcTypeCode; import org.hibernate.dialect.OracleDialect; +import org.hibernate.testing.orm.junit.SkipForDialect; import org.hibernate.type.SqlTypes; import org.hibernate.testing.orm.junit.DomainModel; @@ -170,6 +171,7 @@ public void testVectorDims(SessionFactoryScope scope) { } @Test + @SkipForDialect(dialectClass = OracleDialect.class, reason = "Oracle 23.9 bug") public void testVectorNorm(SessionFactoryScope scope) { scope.inTransaction( em -> { //tag::vector-norm-example[] diff --git a/hibernate-vector/src/test/java/org/hibernate/vector/OracleDoubleVectorTest.java b/hibernate-vector/src/test/java/org/hibernate/vector/OracleDoubleVectorTest.java index ef482deac140..4158c2677182 100644 --- a/hibernate-vector/src/test/java/org/hibernate/vector/OracleDoubleVectorTest.java +++ b/hibernate-vector/src/test/java/org/hibernate/vector/OracleDoubleVectorTest.java @@ -11,6 +11,7 @@ import org.hibernate.annotations.Array; import org.hibernate.annotations.JdbcTypeCode; import org.hibernate.dialect.OracleDialect; +import org.hibernate.testing.orm.junit.SkipForDialect; import org.hibernate.type.SqlTypes; import org.hibernate.testing.orm.junit.DomainModel; @@ -169,6 +170,7 @@ public void testVectorDims(SessionFactoryScope scope) { } @Test + @SkipForDialect(dialectClass = OracleDialect.class, reason = "Oracle 23.9 bug") public void testVectorNorm(SessionFactoryScope scope) { scope.inTransaction( em -> { //tag::vector-norm-example[] diff --git a/hibernate-vector/src/test/java/org/hibernate/vector/OracleFloatVectorTest.java b/hibernate-vector/src/test/java/org/hibernate/vector/OracleFloatVectorTest.java index c813479bd8a4..288013188dfc 100644 --- a/hibernate-vector/src/test/java/org/hibernate/vector/OracleFloatVectorTest.java +++ b/hibernate-vector/src/test/java/org/hibernate/vector/OracleFloatVectorTest.java @@ -11,6 +11,7 @@ import org.hibernate.annotations.Array; import org.hibernate.annotations.JdbcTypeCode; import org.hibernate.dialect.OracleDialect; +import org.hibernate.testing.orm.junit.SkipForDialect; import org.hibernate.type.SqlTypes; import org.hibernate.testing.orm.junit.DomainModel; @@ -188,6 +189,7 @@ public void testVectorDims(SessionFactoryScope scope) { } @Test + @SkipForDialect(dialectClass = OracleDialect.class, reason = "Oracle 23.9 bug") public void testVectorNorm(SessionFactoryScope scope) { scope.inTransaction( em -> { //tag::vector-norm-example[] diff --git a/hibernate-vector/src/test/java/org/hibernate/vector/OracleGenericVectorTest.java b/hibernate-vector/src/test/java/org/hibernate/vector/OracleGenericVectorTest.java index 3641398c4886..d753cb034283 100644 --- a/hibernate-vector/src/test/java/org/hibernate/vector/OracleGenericVectorTest.java +++ b/hibernate-vector/src/test/java/org/hibernate/vector/OracleGenericVectorTest.java @@ -11,6 +11,7 @@ import org.hibernate.annotations.Array; import org.hibernate.annotations.JdbcTypeCode; import org.hibernate.dialect.OracleDialect; +import org.hibernate.testing.orm.junit.SkipForDialect; import org.hibernate.type.SqlTypes; import org.hibernate.testing.orm.junit.DomainModel; @@ -189,6 +190,7 @@ public void testVectorDims(SessionFactoryScope scope) { } @Test + @SkipForDialect(dialectClass = OracleDialect.class, reason = "Oracle 23.9 bug") public void testVectorNorm(SessionFactoryScope scope) { scope.inTransaction( em -> { //tag::vector-norm-example[] diff --git a/hibernate-vibur/src/main/java/org/hibernate/vibur/internal/ViburDBCPConnectionProvider.java b/hibernate-vibur/src/main/java/org/hibernate/vibur/internal/ViburDBCPConnectionProvider.java index e2aad4b54b42..52a81ec8fd08 100644 --- a/hibernate-vibur/src/main/java/org/hibernate/vibur/internal/ViburDBCPConnectionProvider.java +++ b/hibernate-vibur/src/main/java/org/hibernate/vibur/internal/ViburDBCPConnectionProvider.java @@ -8,6 +8,7 @@ package org.hibernate.vibur.internal; import java.sql.Connection; +import java.sql.DatabaseMetaData; import java.sql.SQLException; import java.util.Map; import java.util.Properties; @@ -17,6 +18,7 @@ import org.hibernate.engine.jdbc.connections.spi.ConnectionProvider; import org.hibernate.engine.jdbc.connections.spi.DatabaseConnectionInfo; import org.hibernate.internal.log.ConnectionInfoLogger; +import org.hibernate.internal.util.StringHelper; import org.hibernate.service.UnknownUnwrapTypeException; import org.hibernate.service.spi.Configurable; import org.hibernate.service.spi.Stoppable; @@ -29,6 +31,7 @@ import static org.hibernate.cfg.AvailableSettings.PASS; import static org.hibernate.cfg.AvailableSettings.URL; import static org.hibernate.cfg.AvailableSettings.USER; +import static org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator.allowJdbcMetadataAccess; /** *

    ViburDBCP connection provider for Hibernate integration. @@ -60,9 +63,12 @@ public class ViburDBCPConnectionProvider implements ConnectionProvider, Configur private static final String VIBUR_PREFIX = VIBUR_CONFIG_PREFIX + "."; private ViburDBCPDataSource dataSource = null; + private boolean isMetadataAccessAllowed = true; @Override public void configure(Map configurationValues) { + isMetadataAccessAllowed = allowJdbcMetadataAccess( configurationValues ); + ConnectionInfoLogger.INSTANCE.configureConnectionPool( "Vibur" ); dataSource = new ViburDBCPDataSource( transform( configurationValues ) ); @@ -97,7 +103,9 @@ public boolean supportsAggressiveRelease() { public DatabaseConnectionInfo getDatabaseConnectionInfo(Dialect dialect) { return new DatabaseConnectionInfoImpl( dataSource.getJdbcUrl(), - dataSource.getDriverClassName(), + // Attempt to resolve the driver name from the dialect, in case it wasn't explicitly set and access to + // the database metadata is allowed + !StringHelper.isBlank( dataSource.getDriverClassName() ) ? dataSource.getDriverClassName() : extractDriverNameFromMetadata(), dialect.getVersion(), String.valueOf( dataSource.getDefaultAutoCommit() ), dataSource.getDefaultTransactionIsolation(), @@ -106,6 +114,19 @@ public DatabaseConnectionInfo getDatabaseConnectionInfo(Dialect dialect) { ); } + private String extractDriverNameFromMetadata() { + if (isMetadataAccessAllowed) { + try ( Connection conn = getConnection() ) { + DatabaseMetaData dbmd = conn.getMetaData(); + return dbmd.getDriverName(); + } + catch (SQLException e) { + // Do nothing + } + } + return null; + } + @Override public boolean isUnwrappableAs(Class unwrapType) { return ConnectionProvider.class.equals( unwrapType ) diff --git a/local-build-plugins/build.gradle b/local-build-plugins/build.gradle index 1419125bfb6b..7e7d43e5ded5 100644 --- a/local-build-plugins/build.gradle +++ b/local-build-plugins/build.gradle @@ -52,10 +52,6 @@ gradlePlugin { id = 'org.hibernate.orm.build.reports' implementationClass = 'org.hibernate.orm.post.ReportGenerationPlugin' } - docPubPlugin { - id = 'org.hibernate.orm.build.doc-pub' - implementationClass = 'org.hibernate.orm.docs.DocumentationPublishingPlugin' - } envSettings { id = 'org.hibernate.orm.build.env-settings' implementationClass = 'org.hibernate.orm.env.EnvironmentSettingsPlugin' diff --git a/local-build-plugins/src/main/java/org/hibernate/orm/docs/DescriptorAccess.java b/local-build-plugins/src/main/java/org/hibernate/orm/docs/DescriptorAccess.java deleted file mode 100644 index 596800e7650b..000000000000 --- a/local-build-plugins/src/main/java/org/hibernate/orm/docs/DescriptorAccess.java +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Hibernate, Relational Persistence for Idiomatic Java - * - * License: GNU Lesser General Public License (LGPL), version 2.1 or later - * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html - */ -package org.hibernate.orm.docs; - -import java.io.File; -import java.io.FileWriter; -import java.io.IOException; - -import jakarta.json.bind.Jsonb; -import jakarta.json.bind.JsonbBuilder; -import jakarta.json.bind.JsonbConfig; -import org.apache.http.HttpEntity; -import org.apache.http.client.methods.CloseableHttpResponse; -import org.apache.http.client.methods.HttpGet; -import org.apache.http.impl.client.CloseableHttpClient; -import org.apache.http.impl.client.HttpClientBuilder; - -/** - * Models the published doc descriptor file - * - * @author Steve Ebersole - */ -public class DescriptorAccess { - public static final String DETAILS_URL = "https://docs.jboss.org/hibernate/_outdated-content/orm.json"; - - /** - * Load the descriptor - */ - public static ProjectDocumentationDescriptor loadProject() { - try ( final CloseableHttpClient httpClient = HttpClientBuilder.create().build() ) { - final HttpGet request = new HttpGet( DETAILS_URL ); - - try ( final CloseableHttpResponse response = httpClient.execute( request ) ) { - final HttpEntity responseEntity = response.getEntity(); - //noinspection resource - final Jsonb jsonb = JsonbBuilder.create( new JsonbConfig().withFormatting( true ) ); - return jsonb.fromJson( responseEntity.getContent(), ProjectDocumentationDescriptor.class ); - } - } - catch (IOException e) { - throw new RuntimeException( "Unable to create HttpClient", e ); - } - } - - public static void storeProject(ProjectDocumentationDescriptor project, File jsonFile) { - prepareJsonFile( jsonFile ); - - //noinspection resource - final Jsonb jsonb = JsonbBuilder.create( new JsonbConfig().withFormatting( true ) ); - - try ( final FileWriter writer = new FileWriter( jsonFile ) ) { - jsonb.toJson( project, writer ); - } - catch (IOException e) { - throw new RuntimeException( "Unable to open write for JSON file : " + jsonFile.getPath(), e ); - } - } - - private static void prepareJsonFile(File jsonFile) { - if ( jsonFile.exists() ) { - final boolean deleted = jsonFile.delete(); - assert deleted; - } - - if ( ! jsonFile.getParentFile().exists() ) { - final boolean dirsMade = jsonFile.getParentFile().mkdirs(); - assert dirsMade; - } - - try { - final boolean created = jsonFile.createNewFile(); - assert created; - } - catch (IOException e) { - throw new RuntimeException( "Unable to create JSON file : `" + jsonFile.getPath() + "`", e ); - } - } -} diff --git a/local-build-plugins/src/main/java/org/hibernate/orm/docs/DocumentationPublishing.java b/local-build-plugins/src/main/java/org/hibernate/orm/docs/DocumentationPublishing.java deleted file mode 100644 index 41dfc6dba5d8..000000000000 --- a/local-build-plugins/src/main/java/org/hibernate/orm/docs/DocumentationPublishing.java +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Hibernate, Relational Persistence for Idiomatic Java - * - * License: GNU Lesser General Public License (LGPL), version 2.1 or later - * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html - */ -package org.hibernate.orm.docs; - -import javax.inject.Inject; - -import org.gradle.api.Project; -import org.gradle.api.file.DirectoryProperty; -import org.gradle.api.file.RegularFile; -import org.gradle.api.file.RegularFileProperty; -import org.gradle.api.provider.Property; -import org.gradle.api.provider.Provider; - -import org.hibernate.orm.ReleaseFamilyIdentifier; - -/** - * Gradle DSL extension for configuring documentation publishing - * - * @author Steve Ebersole - */ -public class DocumentationPublishing { - public static final String DSL_NAME = "documentationPublishing"; - - private final Project project; - - private final DirectoryProperty stagingDirectory; - private final Property docServerUrl; - - private final Property docDescriptorUploadUrl; - private final RegularFileProperty updatedJsonFile; - - private final ReleaseFamilyIdentifier releaseFamilyIdentifier; - - @Inject - public DocumentationPublishing(Project project) { - this.project = project; - - stagingDirectory = project.getObjects() - .directoryProperty() - .convention( project.getLayout().getBuildDirectory().dir( "documentation" ) ); - - docServerUrl = project.getObjects() - .property( String.class ) - .convention( "filemgmt-prod-sync.jboss.org:/docs_htdocs/hibernate/orm" ); - - docDescriptorUploadUrl = project.getObjects() - .property( String.class ) - .convention( "filemgmt-prod-sync.jboss.org:/docs_htdocs/hibernate/_outdated-content/orm.json" ); - - - updatedJsonFile = project.getObjects() - .fileProperty() - .convention( project.getLayout().getBuildDirectory().file( "doc-pub/orm.json" ) ); - - releaseFamilyIdentifier = ReleaseFamilyIdentifier.parse( project.getVersion().toString() ); - } - - public ReleaseFamilyIdentifier getReleaseFamilyIdentifier() { - return releaseFamilyIdentifier; - } - - public Property getDocServerUrl() { - return docServerUrl; - } - - public DirectoryProperty getStagingDirectory() { - return stagingDirectory; - } - - /** - * Where to upload the {@link #getUpdatedJsonFile() documentation descriptor} - */ - public Property getDocDescriptorUploadUrl() { - return docDescriptorUploadUrl; - } - - /** - * THe ORM documentation descriptor - */ - public Provider getUpdatedJsonFile() { - return updatedJsonFile; - } - - public void setUpdatedJsonFile(Object ref) { - updatedJsonFile.fileValue( project.file( ref ) ); - } - - public void updatedJsonFile(Object ref) { - updatedJsonFile.fileValue( project.file( ref ) ); - } -} diff --git a/local-build-plugins/src/main/java/org/hibernate/orm/docs/DocumentationPublishingPlugin.java b/local-build-plugins/src/main/java/org/hibernate/orm/docs/DocumentationPublishingPlugin.java deleted file mode 100644 index 0318221e1bb0..000000000000 --- a/local-build-plugins/src/main/java/org/hibernate/orm/docs/DocumentationPublishingPlugin.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Hibernate, Relational Persistence for Idiomatic Java - * - * License: GNU Lesser General Public License (LGPL), version 2.1 or later - * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html - */ -package org.hibernate.orm.docs; - -import org.gradle.api.Plugin; -import org.gradle.api.Project; -import org.gradle.api.tasks.TaskProvider; - -import static org.hibernate.orm.docs.DocumentationPublishing.DSL_NAME; -import static org.hibernate.orm.docs.GenerateDescriptorTask.GEN_DESC_TASK_NAME; -import static org.hibernate.orm.docs.PublishDescriptorTask.UPLOAD_DESC_TASK_NAME; -import static org.hibernate.orm.docs.PublishTask.UPLOAD_TASK_NAME; - -/** - * Plugin for helping with publishing documentation to the doc server -

      - *
    • Publishes a config extension ({@link DocumentationPublishing}) under {@value DocumentationPublishing#DSL_NAME}
    • - *
    • Creates a task ({@value PublishTask#UPLOAD_TASK_NAME}) to upload the documentation to the doc server
    • - *
    • Creates a task ({@value GenerateDescriptorTask#GEN_DESC_TASK_NAME}) to create the "published doc descriptor" (JSON) file
    • - *
    • Creates a task ({@value PublishDescriptorTask#UPLOAD_DESC_TASK_NAME}) to upload the "published doc descriptor" (JSON) file to the doc server
    • - *
    - * - * @author Steve Ebersole - */ -public class DocumentationPublishingPlugin implements Plugin { - @Override - public void apply(Project project) { - final DocumentationPublishing docPubDsl = project.getExtensions().create( DSL_NAME, DocumentationPublishing.class ); - - final boolean isSnapshot = project.getVersion().toString().endsWith( "-SNAPSHOT" ); - - final TaskProvider generateDescriptorTask = project.getTasks().register( - GEN_DESC_TASK_NAME, - GenerateDescriptorTask.class, - (task) -> { - task.getCurrentlyBuildingFamily().convention( docPubDsl.getReleaseFamilyIdentifier() ); - task.getJsonFile().convention( docPubDsl.getUpdatedJsonFile() ); - } - ); - - final TaskProvider uploadDescriptorTask = project.getTasks().register( - UPLOAD_DESC_TASK_NAME, - PublishDescriptorTask.class, - (task) -> { - task.getDocDescriptorUploadUrl().convention( docPubDsl.getDocDescriptorUploadUrl() ); - task.getJsonFile().convention( docPubDsl.getUpdatedJsonFile() ); - - task.dependsOn( generateDescriptorTask ); - task.onlyIf( (t) -> !isSnapshot && generateDescriptorTask.get().getDidWork() ); - } - ); - - //noinspection unused - final TaskProvider publishMigrationGuideTask = project.getTasks().register( - PublishMigrationGuide.NAME, - PublishMigrationGuide.class, - (task) -> { - task.getCurrentlyBuildingFamily().convention( docPubDsl.getReleaseFamilyIdentifier() ); - task.getDocServerUrl().convention( docPubDsl.getDocServerUrl() ); - task.getMigrationGuideDirectory().convention( project.getLayout().getBuildDirectory().dir( "documentation/migration-guide" ) ); - } - ); - - //noinspection unused - final TaskProvider uploadTask = project.getTasks().register( - UPLOAD_TASK_NAME, - PublishTask.class, - (task) -> { - task.getBuildingFamily().convention( docPubDsl.getReleaseFamilyIdentifier() ); - task.getDocServerUrl().convention( docPubDsl.getDocServerUrl() ); - task.getStagingDirectory().convention( docPubDsl.getStagingDirectory() ); - - task.dependsOn( uploadDescriptorTask ); - task.onlyIf( (t) -> !isSnapshot ); - } - ); - } -} diff --git a/local-build-plugins/src/main/java/org/hibernate/orm/docs/GenerateDescriptorTask.java b/local-build-plugins/src/main/java/org/hibernate/orm/docs/GenerateDescriptorTask.java deleted file mode 100644 index d2a52c7a6b05..000000000000 --- a/local-build-plugins/src/main/java/org/hibernate/orm/docs/GenerateDescriptorTask.java +++ /dev/null @@ -1,88 +0,0 @@ -/* - * Hibernate, Relational Persistence for Idiomatic Java - * - * License: GNU Lesser General Public License (LGPL), version 2.1 or later - * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html - */ -package org.hibernate.orm.docs; - -import java.io.File; - -import org.gradle.api.DefaultTask; -import org.gradle.api.file.RegularFileProperty; -import org.gradle.api.provider.Property; -import org.gradle.api.tasks.Input; -import org.gradle.api.tasks.OutputFile; -import org.gradle.api.tasks.TaskAction; - -import org.hibernate.orm.ReleaseFamilyIdentifier; - -/** - * @author Steve Ebersole - */ -public abstract class GenerateDescriptorTask extends DefaultTask { - public static final String GEN_DESC_TASK_NAME = "generateDocumentationDescriptor"; - private final RegularFileProperty jsonFile; - private final Property currentlyBuildingFamily; - - public GenerateDescriptorTask() { - setGroup( "documentation" ); - setDescription( "Generates the documentation publication descriptor (JSON)" ); - - jsonFile = getProject().getObjects().fileProperty(); - currentlyBuildingFamily = getProject().getObjects().property( ReleaseFamilyIdentifier.class ); - } - - @Input - public Property getCurrentlyBuildingFamily() { - return currentlyBuildingFamily; - } - - @OutputFile - public RegularFileProperty getJsonFile() { - return jsonFile; - } - - @TaskAction - public void generateDescriptor() { - final ProjectDocumentationDescriptor descriptor = DescriptorAccess.loadProject(); - - ReleaseFamilyIdentifier newest = null; - boolean foundCurrentRelease = false; - - for ( ReleaseFamilyDocumentation releaseFamily : descriptor.getReleaseFamilies() ) { - if ( newest == null - || releaseFamily.getName().newerThan( newest ) ) { - newest = releaseFamily.getName(); - } - - if ( releaseFamily.getName().equals( currentlyBuildingFamily.get() ) ) { - foundCurrentRelease = true; - } - } - - if ( ! foundCurrentRelease ) { - final ReleaseFamilyDocumentation newEntry = new ReleaseFamilyDocumentation(); - newEntry.setName( currentlyBuildingFamily.get() ); - descriptor.addReleaseFamily( newEntry ); - setDidWork( true ); - } - - // we only want to update "stable" to `currentlyBuildingFamily` when- - // 1. we are currently building a Final - // 2. currentlyBuildingFamily is the newest - - if ( currentlyBuildingFamily.get().newerThan( newest ) ) { - descriptor.setStableFamily( currentlyBuildingFamily.get() ); - setDidWork( true ); - } - - DescriptorAccess.storeProject( descriptor, jsonFile.get().getAsFile() ); - } - - public static void main(String... args) { - final File jsonFile = new File( "/home/sebersole/projects/hibernate-orm/6.0/hibernate-orm-build/target/doc-pub/orm.json" ); - final ProjectDocumentationDescriptor projectDoc = DescriptorAccess.loadProject(); - DescriptorAccess.storeProject( projectDoc, jsonFile ); - } -} diff --git a/local-build-plugins/src/main/java/org/hibernate/orm/docs/ProjectDocumentationDescriptor.java b/local-build-plugins/src/main/java/org/hibernate/orm/docs/ProjectDocumentationDescriptor.java deleted file mode 100644 index 5213f22e816a..000000000000 --- a/local-build-plugins/src/main/java/org/hibernate/orm/docs/ProjectDocumentationDescriptor.java +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Hibernate, Relational Persistence for Idiomatic Java - * - * License: GNU Lesser General Public License (LGPL), version 2.1 or later - * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html - */ -package org.hibernate.orm.docs; - -import java.util.List; -import java.util.Map; - -import org.hibernate.orm.ReleaseFamilyIdentifier; - -import jakarta.json.bind.annotation.JsonbProperty; -import jakarta.json.bind.annotation.JsonbPropertyOrder; -import jakarta.json.bind.annotation.JsonbTypeAdapter; - -/** - * Binding for the doc-pub descriptor (JSON) file - * - * @author Steve Ebersole - */ -@JsonbPropertyOrder( {"name", "stableFamily", "singlePageDetails", "multiPageDetails", "releaseFamilies" } ) -public class ProjectDocumentationDescriptor { - @JsonbProperty( "project" ) - private String name; - - @JsonbProperty( "stable" ) - @JsonbTypeAdapter( ReleaseFamilyIdentifierMarshalling.class ) - private ReleaseFamilyIdentifier stableFamily; - - @JsonbProperty( "versions" ) - private List releaseFamilies; - - @JsonbProperty( "multi" ) - private Map multiPageDetails; - @JsonbProperty( "single" ) - private Map singlePageDetails; - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public ReleaseFamilyIdentifier getStableFamily() { - return stableFamily; - } - - public void setStableFamily(ReleaseFamilyIdentifier stableFamily) { - this.stableFamily = stableFamily; - } - - public List getReleaseFamilies() { - return releaseFamilies; - } - - public void setReleaseFamilies(List releaseFamilies) { - this.releaseFamilies = releaseFamilies; - } - - public void addReleaseFamily(ReleaseFamilyDocumentation familyDetails) { - // Add new entries at the top - releaseFamilies.add( 0, familyDetails ); - } - - public Map getMultiPageDetails() { - return multiPageDetails; - } - - public void setMultiPageDetails(Map multiPageDetails) { - this.multiPageDetails = multiPageDetails; - } - - public Map getSinglePageDetails() { - return singlePageDetails; - } - - public void setSinglePageDetails(Map singlePageDetails) { - this.singlePageDetails = singlePageDetails; - } -} diff --git a/local-build-plugins/src/main/java/org/hibernate/orm/docs/PublishDescriptorTask.java b/local-build-plugins/src/main/java/org/hibernate/orm/docs/PublishDescriptorTask.java deleted file mode 100644 index 13d2d354b286..000000000000 --- a/local-build-plugins/src/main/java/org/hibernate/orm/docs/PublishDescriptorTask.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Hibernate, Relational Persistence for Idiomatic Java - * - * License: GNU Lesser General Public License (LGPL), version 2.1 or later - * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html - */ -package org.hibernate.orm.docs; - -import org.gradle.api.DefaultTask; -import org.gradle.api.file.RegularFileProperty; -import org.gradle.api.provider.Property; -import org.gradle.api.provider.Provider; -import org.gradle.api.tasks.Input; -import org.gradle.api.tasks.InputFile; -import org.gradle.api.tasks.SkipWhenEmpty; -import org.gradle.api.tasks.TaskAction; - -/** - * @author Steve Ebersole - */ -public abstract class PublishDescriptorTask extends DefaultTask { - public static final String UPLOAD_DESC_TASK_NAME = "uploadDocumentationDescriptor"; - - private final Provider projectVersion; - private final Property docDescriptorUploadUrl; - private final RegularFileProperty jsonFile; - - public PublishDescriptorTask() { - setGroup( "documentation" ); - setDescription( "Publishes the documentation publication descriptor (JSON)" ); - - projectVersion = getProject().provider( () -> getProject().getVersion() ); - docDescriptorUploadUrl = getProject().getObjects().property( String.class ); - jsonFile = getProject().getObjects().fileProperty(); - } - - @InputFile - @SkipWhenEmpty - public RegularFileProperty getJsonFile() { - return jsonFile; - } - - @Input - public Property getDocDescriptorUploadUrl() { - return docDescriptorUploadUrl; - } - - @Input - public Provider getProjectVersion() { - return projectVersion; - } - - - @TaskAction - public void uploadDescriptor() { - final String url = docDescriptorUploadUrl.get(); - RsyncHelper.rsync( jsonFile.get(), url, getProject() ); - } -} diff --git a/local-build-plugins/src/main/java/org/hibernate/orm/docs/PublishMigrationGuide.java b/local-build-plugins/src/main/java/org/hibernate/orm/docs/PublishMigrationGuide.java deleted file mode 100644 index b7d5cb47a03d..000000000000 --- a/local-build-plugins/src/main/java/org/hibernate/orm/docs/PublishMigrationGuide.java +++ /dev/null @@ -1,71 +0,0 @@ -package org.hibernate.orm.docs; - -import org.gradle.api.DefaultTask; -import org.gradle.api.file.DirectoryProperty; -import org.gradle.api.provider.Property; -import org.gradle.api.provider.Provider; -import org.gradle.api.tasks.Input; -import org.gradle.api.tasks.InputDirectory; -import org.gradle.api.tasks.PathSensitive; -import org.gradle.api.tasks.PathSensitivity; -import org.gradle.api.tasks.TaskAction; - -import org.hibernate.orm.ReleaseFamilyIdentifier; - -/** - * @author Steve Ebersole - */ -public abstract class PublishMigrationGuide extends DefaultTask { - public static final String NAME = "publishMigrationGuide"; - - private final Provider projectVersion; - private final Property currentlyBuildingFamily; - private final Property docServerUrl; - private final DirectoryProperty migrationGuideDirectory; - - public PublishMigrationGuide() { - setGroup( "documentation" ); - setDescription( "Publishes the migration-guide associated with the current branch. " + - "Intended for incremental publishing of the guide for corrections, etc. without doing a full release. " + - "Note that this is not needed when doing a release as the migration-guide is published as part of that workflow." ); - - getInputs().property( "hibernate-version", getProject().getVersion() ); - - projectVersion = getProject().provider( () -> getProject().getVersion() ); - currentlyBuildingFamily = getProject().getObjects().property( ReleaseFamilyIdentifier.class ); - - docServerUrl = getProject().getObjects().property( String.class ); - migrationGuideDirectory = getProject().getObjects().directoryProperty(); - } - - @Input - public Provider getProjectVersion() { - return projectVersion; - } - - @InputDirectory - @PathSensitive(PathSensitivity.RELATIVE) - public DirectoryProperty getMigrationGuideDirectory() { - return migrationGuideDirectory; - } - - @Input - public Property getDocServerUrl() { - return docServerUrl; - } - - - @Input - public Property getCurrentlyBuildingFamily() { - return currentlyBuildingFamily; - } - - @TaskAction - public void uploadMigrationGuide() { - final String base = docServerUrl.get(); - final String normalizedBase = base.endsWith( "/" ) ? base : base + "/"; - final String url = normalizedBase + currentlyBuildingFamily.get().toExternalForm() + "/migration-guide/"; - - RsyncHelper.rsync( migrationGuideDirectory.get(), url, getProject() ); - } -} diff --git a/local-build-plugins/src/main/java/org/hibernate/orm/docs/PublishTask.java b/local-build-plugins/src/main/java/org/hibernate/orm/docs/PublishTask.java deleted file mode 100644 index 77e48c154799..000000000000 --- a/local-build-plugins/src/main/java/org/hibernate/orm/docs/PublishTask.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Hibernate, Relational Persistence for Idiomatic Java - * - * License: GNU Lesser General Public License (LGPL), version 2.1 or later - * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html - */ -package org.hibernate.orm.docs; - -import org.gradle.api.DefaultTask; -import org.gradle.api.file.Directory; -import org.gradle.api.file.DirectoryProperty; -import org.gradle.api.provider.Property; -import org.gradle.api.tasks.Input; -import org.gradle.api.tasks.InputDirectory; -import org.gradle.api.tasks.TaskAction; -import org.gradle.process.ExecResult; - -import org.hibernate.orm.ReleaseFamilyIdentifier; - -/** - * @author Steve Ebersole - */ -public abstract class PublishTask extends DefaultTask { - public static final String UPLOAD_TASK_NAME = "uploadDocumentation"; - - private final Property buildingFamily; - private final Property docServerUrl; - private final DirectoryProperty stagingDirectory; - - public PublishTask() { - setGroup( "documentation" ); - setDescription( "Publish documentation to the doc server" ); - - buildingFamily = getProject().getObjects().property( ReleaseFamilyIdentifier.class ); - docServerUrl = getProject().getObjects().property( String.class ); - stagingDirectory = getProject().getObjects().directoryProperty(); - } - - @Input - public Property getDocServerUrl() { - return docServerUrl; - } - - @Input - public Property getBuildingFamily() { - return buildingFamily; - } - - @InputDirectory - public Property getStagingDirectory() { - return stagingDirectory; - } - - @TaskAction - public void uploadDocumentation() { - final String releaseFamily = buildingFamily.get().toExternalForm(); - final String base = docServerUrl.get(); - final String normalizedBase = base.endsWith( "/" ) ? base : base + "/"; - final String url = normalizedBase + releaseFamily; - - final String stagingDirPath = stagingDirectory.get().getAsFile().getAbsolutePath(); - final String stagingDirPathContent = stagingDirPath.endsWith( "/" ) ? stagingDirPath : stagingDirPath + "/"; - - getProject().getLogger().lifecycle( "Uploading documentation `{}` -> `{}`", stagingDirPath, url ); - final ExecResult result = getProject().exec( (exec) -> { - exec.executable( "rsync" ); - exec.args("--port=2222", "-avz", "--links", "--delete", stagingDirPathContent, url ); - } ); - getProject().getLogger().lifecycle( "Done uploading documentation - {}", result.getExitValue() == 0 ? "success" : "failure" ); - } -} diff --git a/local-build-plugins/src/main/java/org/hibernate/orm/docs/ReleaseFamilyDocumentation.java b/local-build-plugins/src/main/java/org/hibernate/orm/docs/ReleaseFamilyDocumentation.java deleted file mode 100644 index 96e71c5ad560..000000000000 --- a/local-build-plugins/src/main/java/org/hibernate/orm/docs/ReleaseFamilyDocumentation.java +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Hibernate, Relational Persistence for Idiomatic Java - * - * License: GNU Lesser General Public License (LGPL), version 2.1 or later - * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html - */ -package org.hibernate.orm.docs; - -import java.util.HashMap; -import java.util.Map; - -import org.hibernate.orm.ReleaseFamilyIdentifier; - -import jakarta.json.bind.annotation.JsonbProperty; -import jakarta.json.bind.annotation.JsonbPropertyOrder; -import jakarta.json.bind.annotation.JsonbTypeAdapter; - -/** - * Binding for the doc-pub descriptor (JSON) related to a specific release family. - * - * @see ProjectDocumentationDescriptor - * - * @author Steve Ebersole - */ -@JsonbPropertyOrder( { "name", "redirects" } ) -public class ReleaseFamilyDocumentation { - @JsonbProperty( "version" ) - @JsonbTypeAdapter( ReleaseFamilyIdentifierMarshalling.class ) - private ReleaseFamilyIdentifier name; - private Map redirects; - - public ReleaseFamilyDocumentation() { - } - - /** - * The release family, e.g. `6.0` or `5.6` - */ - public ReleaseFamilyIdentifier getName() { - return name; - } - - public void setName(ReleaseFamilyIdentifier name) { - this.name = name; - } - - public Map getRedirects() { - return redirects; - } - - public void setRedirects(Map redirects) { - this.redirects = redirects; - } - - public void redirect(String from, String to) { - if ( redirects == null ) { - redirects = new HashMap<>(); - } - redirects.put( from, to ); - } - - @Override - public String toString() { - return "ReleaseFamilyDocumentation( " + name + ")"; - } -} diff --git a/local-build-plugins/src/main/java/org/hibernate/orm/docs/ReleaseFamilyIdentifierMarshalling.java b/local-build-plugins/src/main/java/org/hibernate/orm/docs/ReleaseFamilyIdentifierMarshalling.java deleted file mode 100644 index f2e9d17cddaa..000000000000 --- a/local-build-plugins/src/main/java/org/hibernate/orm/docs/ReleaseFamilyIdentifierMarshalling.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Hibernate, Relational Persistence for Idiomatic Java - * - * License: GNU Lesser General Public License (LGPL), version 2.1 or later - * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html - */ -package org.hibernate.orm.docs; - -import org.hibernate.orm.ReleaseFamilyIdentifier; - -import jakarta.json.Json; -import jakarta.json.JsonString; -import jakarta.json.JsonValue; -import jakarta.json.bind.adapter.JsonbAdapter; - -/** - * @author Steve Ebersole - */ -public class ReleaseFamilyIdentifierMarshalling implements JsonbAdapter { - @Override - public JsonValue adaptToJson(ReleaseFamilyIdentifier obj) throws Exception { - return Json.createValue( obj.toExternalForm() ); - } - - @Override - public ReleaseFamilyIdentifier adaptFromJson(JsonValue obj) throws Exception { - assert obj.getValueType() == JsonValue.ValueType.STRING; - final JsonString jsonString = (JsonString) obj; - return ReleaseFamilyIdentifier.parse( jsonString.getString() ); - } -} diff --git a/local-build-plugins/src/main/java/org/hibernate/orm/docs/RsyncHelper.java b/local-build-plugins/src/main/java/org/hibernate/orm/docs/RsyncHelper.java deleted file mode 100644 index 2cc8a0b52eac..000000000000 --- a/local-build-plugins/src/main/java/org/hibernate/orm/docs/RsyncHelper.java +++ /dev/null @@ -1,22 +0,0 @@ -package org.hibernate.orm.docs; - -import org.gradle.api.Project; -import org.gradle.api.file.FileSystemLocation; - -/** - * Helper for performing rsync system commands, mainly used to centralize - * the command options - * - * @author Steve Ebersole - */ -public class RsyncHelper { - public static void rsync( - FileSystemLocation source, - String targetUrl, - Project project) { - project.exec( (exec) -> { - exec.executable( "rsync" ); - exec.args( "--port=2222", "-avz", source.getAsFile().getAbsolutePath(), targetUrl ); - } ); - } -} diff --git a/local-build-plugins/src/main/java/org/hibernate/orm/post/DialectReportTask.java b/local-build-plugins/src/main/java/org/hibernate/orm/post/DialectReportTask.java index ba751f5a688a..ef0b65099708 100644 --- a/local-build-plugins/src/main/java/org/hibernate/orm/post/DialectReportTask.java +++ b/local-build-plugins/src/main/java/org/hibernate/orm/post/DialectReportTask.java @@ -28,8 +28,6 @@ import org.gradle.api.tasks.SourceSetContainer; import org.gradle.api.tasks.TaskAction; -import org.hibernate.orm.env.HibernateVersion; - import org.jboss.jandex.ClassInfo; import org.jboss.jandex.Index; @@ -39,24 +37,30 @@ * @author Steve Ebersole */ public abstract class DialectReportTask extends AbstractJandexAwareTask { + private final Property sourceProject; + private final Property sourcePackage; private final Property reportFile; - private final Property generateHeading; public DialectReportTask() { - setDescription( "Generates a report of the supported Dialects" ); + setDescription( "Generates a report of Dialects" ); + sourceProject = getProject().getObjects().property(String.class); + sourcePackage = getProject().getObjects().property(String.class); reportFile = getProject().getObjects().fileProperty(); - reportFile.convention( getProject().getLayout().getBuildDirectory().file( "orm/generated/dialect/dialect.adoc" ) ); - generateHeading = getProject().getObjects().property( Boolean.class ).convention( true ); } - @OutputFile - public Property getReportFile() { - return reportFile; + @Input + public Property getSourceProject() { + return sourceProject; } @Input - public Property getGenerateHeading() { - return generateHeading; + public Property getSourcePackage() { + return sourcePackage; + } + + @OutputFile + public Property getReportFile() { + return reportFile; } @Override @@ -66,16 +70,19 @@ protected Provider getTaskReportFileReference() { @TaskAction public void generateDialectReport() { - // the ones we want are all in the hibernate-core project - final Project coreProject = getProject().getRootProject().project( "hibernate-core" ); - final SourceSetContainer sourceSets = coreProject.getExtensions().getByType( SourceSetContainer.class ); + // TODO this probably messes up the cache since we don't declare an explicit dependency to a source set + // but the problem is pre-existing and I don't have time to investigate. + Project sourceProject = getProject().getRootProject().project( this.sourceProject.get() ); + final SourceSetContainer sourceSets = sourceProject.getExtensions().getByType( SourceSetContainer.class ); final SourceSet sourceSet = sourceSets.getByName( SourceSet.MAIN_SOURCE_SET_NAME ); - final ClassLoader classLoader = Helper.asClassLoader( sourceSet, coreProject.getConfigurations().getByName( "testRuntimeClasspath" ) ); + final ClassLoader classLoader = Helper.asClassLoader( sourceSet, sourceProject.getConfigurations().getByName( "testRuntimeClasspath" ) ); final DialectClassDelegate dialectClassDelegate = new DialectClassDelegate( classLoader ); final Index index = getIndexManager().getIndex(); final Collection allDialectClasses = index.getAllKnownSubclasses( DialectClassDelegate.DIALECT_CLASS_NAME ); + String sourcePackagePrefix = this.sourcePackage.get() + "."; + allDialectClasses.removeIf( c -> !c.name().toString().startsWith( sourcePackagePrefix ) ); if ( allDialectClasses.isEmpty() ) { throw new RuntimeException( "Unable to find Dialects" ); } @@ -128,15 +135,6 @@ private void writeDialectReport( private void writeDialectReportHeader(OutputStreamWriter fileWriter) { try { - if ( this.generateHeading.get() ) { - fileWriter.write( "= Supported Dialects\n\n" ); - fileWriter.write( - "Supported Dialects along with the minimum supported version of the underlying database.\n\n\n" ); - - HibernateVersion ormVersion = (HibernateVersion) getProject().getRootProject().getExtensions().getByName( "ormVersion" ); - fileWriter.write( "NOTE: Hibernate version " + ormVersion.getFamily() + "\n\n" ); - } - fileWriter.write( "[cols=\"a,a\", options=\"header\"]\n" ); fileWriter.write( "|===\n" ); fileWriter.write( "|Dialect |Minimum Database Version\n" ); @@ -148,7 +146,10 @@ private void writeDialectReportHeader(OutputStreamWriter fileWriter) { private void writeDialectReportEntry(DialectDelegate dialectDelegate, OutputStreamWriter fileWriter) { try { - final String version = dialectDelegate.getMinimumVersion(); + String version = dialectDelegate.getMinimumVersion(); + if ( "0.0".equals( version ) ) { + version = "N/A"; + } fileWriter.write( '|' ); fileWriter.write( dialectDelegate.getDialectImplClass().getSimpleName() ); fileWriter.write( '|' ); diff --git a/local-build-plugins/src/main/java/org/hibernate/orm/post/ReportGenerationPlugin.java b/local-build-plugins/src/main/java/org/hibernate/orm/post/ReportGenerationPlugin.java index 66bd42a20b34..7fbc1200f681 100644 --- a/local-build-plugins/src/main/java/org/hibernate/orm/post/ReportGenerationPlugin.java +++ b/local-build-plugins/src/main/java/org/hibernate/orm/post/ReportGenerationPlugin.java @@ -57,22 +57,28 @@ public void apply(Project project) { (task) -> task.dependsOn( indexerTask ) ); - final TaskProvider dialectTask = project.getTasks().register( - "generateDialectReport", - DialectReportTask.class, - (task) -> task.dependsOn( indexerTask ) - ); - final TaskProvider dialectTableTask = project.getTasks().register( "generateDialectTableReport", DialectReportTask.class, (task) -> { task.dependsOn( indexerTask ); - task.setProperty( "generateHeading", false ); + task.setProperty( "sourceProject", "hibernate-core" ); + task.setProperty( "sourcePackage", "org.hibernate.dialect" ); task.setProperty( "reportFile", project.getLayout().getBuildDirectory().file( "orm/generated/dialect/dialect-table.adoc" ) ); } ); + final TaskProvider communityDialectTableTask = project.getTasks().register( + "generateCommunityDialectTableReport", + DialectReportTask.class, + (task) -> { + task.dependsOn( indexerTask ); + task.setProperty( "sourceProject", "hibernate-community-dialects" ); + task.setProperty( "sourcePackage", "org.hibernate.community.dialect" ); + task.setProperty( "reportFile", project.getLayout().getBuildDirectory().file( "orm/generated/dialect/dialect-table-community.adoc" ) ); + } + ); + final Task groupingTask = project.getTasks().maybeCreate( "generateReports" ); groupingTask.setGroup( TASK_GROUP_NAME ); groupingTask.dependsOn( indexerTask ); @@ -80,7 +86,7 @@ public void apply(Project project) { groupingTask.dependsOn( deprecationTask ); groupingTask.dependsOn( internalsTask ); groupingTask.dependsOn( loggingTask ); - groupingTask.dependsOn( dialectTask ); groupingTask.dependsOn( dialectTableTask ); + groupingTask.dependsOn( communityDialectTableTask ); } } diff --git a/local-build-plugins/src/main/java/org/hibernate/orm/properties/AsciiDocWriter.java b/local-build-plugins/src/main/java/org/hibernate/orm/properties/AsciiDocWriter.java index 3004ea90be7e..6f628226b407 100644 --- a/local-build-plugins/src/main/java/org/hibernate/orm/properties/AsciiDocWriter.java +++ b/local-build-plugins/src/main/java/org/hibernate/orm/properties/AsciiDocWriter.java @@ -68,9 +68,7 @@ private static void write( for ( SettingDescriptor settingDescriptor : sectionSettingDescriptors ) { // write an anchor in the form `[[{anchorNameBase}-{settingName}]]` tryToWriteLine( writer, "[[", anchorNameBase, "-", settingDescriptor.getName(), "]]" ); - - writeSettingName( settingDescriptor, writer ); - writer.write( "::\n" ); + tryToWriteLine( writer, "==== ", settingName( settingDescriptor ) ); writeMetadata( settingDescriptor, writer ); @@ -88,7 +86,6 @@ private static void writeMetadata(SettingDescriptor settingDescriptor, FileWrite return; } - writer.write( "+\n" ); writer.write( "****\n" ); writer.write( @@ -112,6 +109,12 @@ private static void writeMetadata(SettingDescriptor settingDescriptor, FileWrite if ( lifecycleDetails.isDeprecated() ) { writer.write( "WARNING: *_This setting is considered deprecated_*\n\n" ); } + if ( settingDescriptor.isUnsafe() ) { + writer.write( "WARNING: *_This setting is considered unsafe_*\n\n" ); + } + if ( settingDescriptor.isCompatibility() ) { + writer.write( "INFO: *_This setting manages a certain backwards compatibility_*\n\n" ); + } if ( lifecycleDetails.getSince() != null ) { writer.write( "*_Since:_* _" + lifecycleDetails.getSince() + "_\n\n" ); @@ -125,27 +128,24 @@ private static void writeMetadata(SettingDescriptor settingDescriptor, FileWrite writer.write( settingDescriptor.getApiNote() + "\n\n" ); } - writer.write( "****\n+\n" ); + writer.write( "****\n\n" ); } - private static void writeSettingName(SettingDescriptor settingDescriptor, FileWriter writer) throws IOException { - writer.write( "`" ); - if ( settingDescriptor.getLifecycleDetails().isDeprecated() ) { - writer.write( "[.line-through]#" ); - } - else { - writer.write( '*' ); - } - - writer.write( settingDescriptor.getName() ); - + private static String settingName(SettingDescriptor settingDescriptor) { if ( settingDescriptor.getLifecycleDetails().isDeprecated() ) { - writer.write( '#' ); + return String.format( + Locale.ROOT, + "`[.line-through]#%s#`", + settingDescriptor.getName() + ); } else { - writer.write( '*' ); + return String.format( + Locale.ROOT, + "`%s`", + settingDescriptor.getName() + ); } - writer.write( '`' ); } private static void tryToWriteLine(Writer writer, String prefix, String value, String... other) { diff --git a/local-build-plugins/src/main/java/org/hibernate/orm/properties/SettingDescriptor.java b/local-build-plugins/src/main/java/org/hibernate/orm/properties/SettingDescriptor.java index 754512bc28ce..03ed31308893 100644 --- a/local-build-plugins/src/main/java/org/hibernate/orm/properties/SettingDescriptor.java +++ b/local-build-plugins/src/main/java/org/hibernate/orm/properties/SettingDescriptor.java @@ -18,6 +18,8 @@ public class SettingDescriptor { private final String defaultValue; private final String apiNote; private final LifecycleDetails lifecycleDetails; + private final boolean unsafe; + private final boolean compatibility; public SettingDescriptor( String name, @@ -27,6 +29,8 @@ public SettingDescriptor( String comment, String defaultValue, String apiNote, + boolean unsafe, + boolean compatibility, LifecycleDetails lifecycleDetails) { this.name = name; this.settingsClassName = settingsClassName; @@ -35,6 +39,8 @@ public SettingDescriptor( this.publishedJavadocLink = publishedJavadocLink; this.defaultValue = defaultValue; this.apiNote = apiNote; + this.unsafe = unsafe; + this.compatibility = compatibility; this.lifecycleDetails = lifecycleDetails; } @@ -48,7 +54,9 @@ public SettingDescriptor( String apiNote, String since, boolean deprecated, - boolean incubating) { + boolean incubating, + boolean unsafe, + boolean compatibility) { this( name, settingsClassName, @@ -56,6 +64,8 @@ public SettingDescriptor( publishedJavadocLink, comment, defaultValue, apiNote, + unsafe, + compatibility, new LifecycleDetails( since, deprecated, incubating ) ); } @@ -100,6 +110,14 @@ public String getSettingFieldName() { return settingFieldName; } + public boolean isUnsafe() { + return unsafe; + } + + public boolean isCompatibility() { + return compatibility; + } + public LifecycleDetails getLifecycleDetails() { return lifecycleDetails; } diff --git a/local-build-plugins/src/main/java/org/hibernate/orm/properties/SettingWorkingDetails.java b/local-build-plugins/src/main/java/org/hibernate/orm/properties/SettingWorkingDetails.java index d6dbd0b277d2..6fad3071c648 100644 --- a/local-build-plugins/src/main/java/org/hibernate/orm/properties/SettingWorkingDetails.java +++ b/local-build-plugins/src/main/java/org/hibernate/orm/properties/SettingWorkingDetails.java @@ -23,6 +23,8 @@ public class SettingWorkingDetails { private String since; private boolean deprecated; private boolean incubating; + private boolean unsafe; + private boolean compatibility; private List relatedSettingNames; public SettingWorkingDetails( @@ -92,6 +94,22 @@ public void setIncubating(boolean incubating) { this.incubating = incubating; } + public boolean isUnsafe() { + return unsafe; + } + + public void setUnsafe(boolean unsafe) { + this.unsafe = unsafe; + } + + public boolean isCompatibility() { + return compatibility; + } + + public void setCompatibility(boolean compatibility) { + this.compatibility = compatibility; + } + public List getRelatedSettingNames() { return relatedSettingNames; } @@ -134,7 +152,9 @@ public SettingDescriptor buildDescriptor(String asciidoc) { apiNote, since, deprecated, - incubating + incubating, + unsafe, + compatibility ); } } diff --git a/local-build-plugins/src/main/java/org/hibernate/orm/properties/SettingsDocExtension.java b/local-build-plugins/src/main/java/org/hibernate/orm/properties/SettingsDocExtension.java index 779ff12c032a..508126f0e026 100644 --- a/local-build-plugins/src/main/java/org/hibernate/orm/properties/SettingsDocExtension.java +++ b/local-build-plugins/src/main/java/org/hibernate/orm/properties/SettingsDocExtension.java @@ -56,7 +56,7 @@ public DirectoryProperty getJavadocDirectory() { * The base URL for the published doc server. This is used to * replace local hrefs with hrefs on the doc sever *

    - * Defaults to {@code https://docs.jboss.org/hibernate/orm} + * Defaults to {@code https://docs.hibernate.org/orm} */ public Property getPublishedDocsUrl() { return publishedDocsUrl; diff --git a/local-build-plugins/src/main/java/org/hibernate/orm/properties/SettingsDocumentationPlugin.java b/local-build-plugins/src/main/java/org/hibernate/orm/properties/SettingsDocumentationPlugin.java index 8659503844a7..1dc40b843d41 100644 --- a/local-build-plugins/src/main/java/org/hibernate/orm/properties/SettingsDocumentationPlugin.java +++ b/local-build-plugins/src/main/java/org/hibernate/orm/properties/SettingsDocumentationPlugin.java @@ -31,7 +31,7 @@ public void apply(Project project) { final SettingsDocExtension dslExtension = new SettingsDocExtension( project ); project.getExtensions().add( EXTENSION_NAME, dslExtension ); dslExtension.getJavadocDirectory().convention( project.getLayout().getBuildDirectory().dir( "javadocs" ) ); - dslExtension.getPublishedDocsUrl().convention( "https://docs.jboss.org/hibernate/orm" ); + dslExtension.getPublishedDocsUrl().convention( "https://docs.hibernate.org/orm" ); dslExtension.getOutputFile().convention( project.getLayout().getBuildDirectory().file( "asciidoc/fragments/config-settings.adoc" ) ); // create the generation task diff --git a/local-build-plugins/src/main/java/org/hibernate/orm/properties/Utils.java b/local-build-plugins/src/main/java/org/hibernate/orm/properties/Utils.java index 61a7994b7c89..b6e4ce8fcd69 100644 --- a/local-build-plugins/src/main/java/org/hibernate/orm/properties/Utils.java +++ b/local-build-plugins/src/main/java/org/hibernate/orm/properties/Utils.java @@ -14,6 +14,8 @@ import java.util.TreeMap; import java.util.TreeSet; +import org.jsoup.nodes.Element; + /** * @author Steve Ebersole */ @@ -39,4 +41,39 @@ public static Map> createResult } ); return map; } + + public static boolean containsHref(Element fieldJavadocElement, String target) { + final String cssQuery = "a[href$=" + target + "]"; + final Element incubatingMarkerElement = fieldJavadocElement.selectFirst( cssQuery ); + return incubatingMarkerElement != null; + + } + + public static boolean interpretIncubation(Element fieldJavadocElement) { + return containsHref( fieldJavadocElement, "Incubating.html" ); + } + + public static boolean interpretUnsafe(Element fieldJavadocElement) { + return containsHref( fieldJavadocElement, "Unsafe.html" ); + } + + public static boolean interpretCompatibility(Element fieldJavadocElement) { + return containsHref( fieldJavadocElement, "Compatibility.html" ); + } + + public static boolean interpretDeprecation(Element fieldJavadocElement) { + // A setting is considered deprecated with either `@Deprecated` + final Element deprecationDiv = fieldJavadocElement.selectFirst( ".deprecationBlock" ); + // presence of this

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

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

    -

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

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

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

    -

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

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

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

    -

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

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

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

    -

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

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

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

    -

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

    """ - } - } - - emailext( - subject: subject, - body: body, - to: buildEnv.notificationRecipients - ) - } -} - -@NonCPS -String getParallelResult( RunWrapper build, String parallelBranchName ) { - def visitor = new PipelineNodeGraphVisitor( build.rawBuild ) - def branch = visitor.pipelineNodes.find{ it.type == FlowNodeWrapper.NodeType.PARALLEL && parallelBranchName == it.displayName } - if ( branch == null ) { - echo "Couldn't find parallel branch name '$parallelBranchName'. Available parallel branch names:" - visitor.pipelineNodes.findAll{ it.type == FlowNodeWrapper.NodeType.PARALLEL }.each{ - echo " - ${it.displayName}" - } - return null; - } - return branch.status.result -} - -// try-finally construct that properly suppresses exceptions thrown in the finally block. -def tryFinally(Closure main, Closure ... finallies) { - def mainFailure = null - try { - main() - } - catch (Throwable t) { - mainFailure = t - throw t - } - finally { - finallies.each {it -> - try { - it() - } - catch (Throwable t) { - if ( mainFailure ) { - mainFailure.addSuppressed( t ) - } - else { - mainFailure = t - } - } - } - } - if ( mainFailure ) { // We may reach here if only the "finally" failed - throw mainFailure - } -} diff --git a/release-announcement.adoc b/release-announcement.adoc index 5323759d56b6..5cea041f9a04 100644 --- a/release-announcement.adoc +++ b/release-announcement.adoc @@ -5,7 +5,7 @@ Steve Ebersole :family: 6.6 -:docs-url: https://docs.jboss.org/hibernate/orm/{family} +:docs-url: https://docs.hibernate.org/orm/{family} :javadocs-url: {docs-url}/javadocs :migration-guide-url: {docs-url}/migration-guide/migration-guide.html :intro-guide-url: {docs-url}/introduction/html_single/Hibernate_Introduction.html diff --git a/release/README.adoc b/release/README.adoc index e222698144c7..0adfb3f9de84 100644 --- a/release/README.adoc +++ b/release/README.adoc @@ -19,4 +19,4 @@ JBoss doc server... == SourceForge -Release bundles (as ZIP and TGZ) are uploaded to https://sourceforge.net/projects/hibernate/files/hibernate-orm/[SourceForge] \ No newline at end of file +Release bundles (as ZIP and TGZ) are uploaded to https://sourceforge.net/projects/hibernate/files/hibernate-orm/[SourceForge] diff --git a/release/jenkins-release-process.adoc b/release/jenkins-release-process.adoc index bc050ee2d1ca..0dbc67d98e7f 100644 --- a/release/jenkins-release-process.adoc +++ b/release/jenkins-release-process.adoc @@ -15,7 +15,7 @@ First, a list of resources you will need access to in order to perform a release == Steps -1. Perform `./gradlew preVerifyRelease` locally (after pulling all upstream changes). The Jenkins job does only the release steps, and we need to make sure tests and checkstyle especially are ok +1. Perform `./gradlew releasePrepare` locally (after pulling all upstream changes). The Jenkins job does only the release steps, and we need to make sure tests and checkstyle especially are ok 2. Mark the version as released in Jira 3. Close all issues associated with the version as closed. Be sure to remove the version from any issues that are not resolved (e.g. rejected) - the Jira "release notes" mechanism includes all issues with that version as the fix-for regardless of the resolution 4. Start the https://ci.hibernate.org/view/ORM/job/hibernate-orm-release/[Jenkins job]. It is a parameterized build - Jenkins will prompt user for needed information: @@ -34,4 +34,4 @@ The Jenkins job performs the following tasks: == Post-release steps -See <<./post-release-steps.adoc>> \ No newline at end of file +See <<./post-release-steps.adoc>> diff --git a/release/release.gradle b/release/release.gradle index e5b07b329998..2cab8764a481 100644 --- a/release/release.gradle +++ b/release/release.gradle @@ -11,7 +11,6 @@ import groovy.json.JsonSlurper */ apply from: rootProject.file( 'gradle/module.gradle' ) -apply plugin: 'org.hibernate.orm.build.doc-pub' apply plugin: 'org.hibernate.orm.build.jdks' apply plugin: 'idea' @@ -28,7 +27,7 @@ def stageIntegrationGuideTask = tasks.register( "stageIntegrationGuide", Copy ) dependsOn ":documentation:renderIntegrationGuides" from project.provider { project( ":documentation" ).layout.buildDirectory.dir( "asciidoc/integrationguide" ) } - into layout.buildDirectory.dir( "documentation/integrationguide" ) + into rootProject.layout.buildDirectory.dir("staging-deploy/documentation/integrationguide") } def stageQuickstartTask = tasks.register( "stageQuickstart", Copy ) { @@ -37,7 +36,7 @@ def stageQuickstartTask = tasks.register( "stageQuickstart", Copy ) { dependsOn ':documentation:renderGettingStartedGuides' from project.provider { project( ":documentation" ).layout.buildDirectory.dir( "asciidoc/quickstart" ) } - into layout.buildDirectory.dir( "documentation/quickstart" ) + into rootProject.layout.buildDirectory.dir("staging-deploy/documentation/quickstart") } def stageTopicalGuideTask = tasks.register( "stageTopicalGuide", Copy ) { @@ -46,8 +45,7 @@ def stageTopicalGuideTask = tasks.register( "stageTopicalGuide", Copy ) { dependsOn ':documentation:renderTopicalGuides' from project.provider { project( ":documentation" ).layout.buildDirectory.dir( "asciidoc/topical" ) } - into layout.buildDirectory.dir( "documentation/topical" ) - + into rootProject.layout.buildDirectory.dir("staging-deploy/documentation/topical") } def stageIntroductionGuideTask = tasks.register( "stageIntroductionGuide", Copy ) { @@ -56,7 +54,7 @@ def stageIntroductionGuideTask = tasks.register( "stageIntroductionGuide", Copy dependsOn ':documentation:renderIntroductionGuides' from project.provider { project( ":documentation" ).layout.buildDirectory.dir( "asciidoc/introduction" ) } - into layout.buildDirectory.dir( "documentation/introduction" ) + into rootProject.layout.buildDirectory.dir("staging-deploy/documentation/introduction") } def stageQueryGuideTask = tasks.register( "stageQueryGuide", Copy ) { @@ -65,7 +63,7 @@ def stageQueryGuideTask = tasks.register( "stageQueryGuide", Copy ) { dependsOn ':documentation:renderQueryLanguageGuides' from project.provider { project( ":documentation" ).layout.buildDirectory.dir( "asciidoc/querylanguage" ) } - into layout.buildDirectory.dir( "documentation/querylanguage" ) + into rootProject.layout.buildDirectory.dir("staging-deploy/documentation/querylanguage") } def stageRepositoriesGuideTask = tasks.register( "stageRepositoriesGuide", Copy ) { @@ -74,7 +72,7 @@ def stageRepositoriesGuideTask = tasks.register( "stageRepositoriesGuide", Copy dependsOn ':documentation:renderRepositories' from project.provider { project( ":documentation" ).layout.buildDirectory.dir( "asciidoc/repositories" ) } - into layout.buildDirectory.dir( "documentation/repositories" ) + into rootProject.layout.buildDirectory.dir("staging-deploy/documentation/repositories") } def stageUserGuideTask = tasks.register( "stageUserGuide", Copy ) { @@ -85,7 +83,7 @@ def stageUserGuideTask = tasks.register( "stageUserGuide", Copy ) { dependsOn ':documentation:renderUserGuides' from project.provider { project( ":documentation" ).layout.buildDirectory.dir( "asciidoc/userguide" ) } - into layout.buildDirectory.dir( "documentation/userguide" ) + into rootProject.layout.buildDirectory.dir("staging-deploy/documentation/userguide") } @@ -95,11 +93,17 @@ def stageMigrationGuideTask = tasks.register( "stageMigrationGuide", Copy ) { dependsOn ':documentation:renderMigrationGuide' from project.provider { project( ":documentation" ).layout.buildDirectory.dir( "asciidoc/migration-guide" ) } - into layout.buildDirectory.dir( "documentation/migration-guide" ) + into rootProject.layout.buildDirectory.dir("staging-deploy/documentation/migration-guide") } -tasks.named( "publishMigrationGuide" ).configure { - dependsOn stageMigrationGuide +def stageDialectGuideTask = tasks.register( "stageDialectGuide", Copy ) { + group 'documentation' + description "Stages the Dialect Guide as part of preparing for release" + + dependsOn ':documentation:renderDialectGuide' + + from project.provider { project( ":documentation" ).layout.buildDirectory.dir( "asciidoc/dialect" ) } + into rootProject.layout.buildDirectory.dir("staging-deploy/documentation/dialect") } def stageIncubationReportTask = tasks.register( "stageIncubationReport", Copy ) { task -> @@ -110,7 +114,7 @@ def stageIncubationReportTask = tasks.register( "stageIncubationReport", Copy ) tasks.stageOrmReports.dependsOn task from project( ":documentation" ).tasks.generateIncubationReport - into layout.buildDirectory.dir( "documentation/incubating" ) + into rootProject.layout.buildDirectory.dir("staging-deploy/documentation/incubating") } def stageInternalsReportTask = tasks.register( "stageInternalsReport", Copy ) { task -> @@ -119,7 +123,7 @@ def stageInternalsReportTask = tasks.register( "stageInternalsReport", Copy ) { dependsOn ':documentation:generateInternalsReport' from project( ":documentation" ).tasks.generateInternalsReport - into layout.buildDirectory.dir( "documentation/internals" ) + into rootProject.layout.buildDirectory.dir("staging-deploy/documentation/internals") } def stageDeprecationReportTask = tasks.register( "stageDeprecationReport", Copy ) { @@ -129,7 +133,7 @@ def stageDeprecationReportTask = tasks.register( "stageDeprecationReport", Copy dependsOn ':documentation:generateDeprecationReport' from project( ":documentation" ).tasks.generateDeprecationReport - into layout.buildDirectory.dir( "documentation/deprecated" ) + into rootProject.layout.buildDirectory.dir("staging-deploy/documentation/deprecated") } def stageLoggingReportTask = tasks.register( "stageLoggingReport", Copy ) { task -> @@ -139,16 +143,7 @@ def stageLoggingReportTask = tasks.register( "stageLoggingReport", Copy ) { task dependsOn ':documentation:renderLoggingReport' from project( ":documentation" ).tasks.renderLoggingReport - into layout.buildDirectory.dir( "documentation/logging" ) -} - -def stageDialectReportTask = tasks.register( "stageDialectReport", Copy ) { task -> - group 'documentation' - description "Stages the supported Dialects report" - dependsOn ':documentation:renderDialectReport' - - from project( ":documentation" ).tasks.renderDialectReport - into "${buildDir}/documentation/dialect" + into rootProject.layout.buildDirectory.dir("staging-deploy/documentation/logging") } def stageOrmReportsTask = tasks.register( "stageOrmReports" ) { @@ -160,7 +155,6 @@ def stageOrmReportsTask = tasks.register( "stageOrmReports" ) { dependsOn stageInternalsReportTask dependsOn stageDeprecationReportTask dependsOn stageLoggingReportTask - dependsOn stageDialectReportTask } def stageJavadocsTask = tasks.register( "stageJavadocs", Copy ) { @@ -169,7 +163,7 @@ def stageJavadocsTask = tasks.register( "stageJavadocs", Copy ) { dependsOn ':documentation:javadoc' from project( ":documentation" ).tasks.javadoc - into "${buildDir}/documentation/javadocs" + into rootProject.layout.buildDirectory.dir("staging-deploy/documentation/javadocs") } /** @@ -179,7 +173,7 @@ def stageJavadocsTask = tasks.register( "stageJavadocs", Copy ) { */ def assembleDocumentationTask = tasks.register( "assembleDocumentation" ) { group 'documentation' - description 'Assembles all documentation into the {buildDir}/documentation directory' + description 'Assembles all documentation into the {buildDir}/staging-deploy/documentation directory' dependsOn ':documentation:buildDocsForPublishing' dependsOn stageJavadocsTask @@ -191,393 +185,14 @@ def assembleDocumentationTask = tasks.register( "assembleDocumentation" ) { dependsOn stageIntegrationGuideTask dependsOn stageTopicalGuideTask dependsOn stageMigrationGuideTask + dependsOn stageDialectGuideTask dependsOn stageOrmReportsTask } -tasks.named( "uploadDocumentation" ) { - group = "documentation" - description = "Uploads assembled documentation to the doc server" - dependsOn assembleDocumentationTask - - doFirst { - if ( rootProject.ormVersion.isSnapshot ) { - logger.error( "Cannot perform upload of SNAPSHOT documentation" ); - throw new RuntimeException( "Cannot perform upload of SNAPSHOT documentation" ); - } - else { - logger.lifecycle( "Uploading documentation ..." ) - } - } - - doLast { - logger.lifecycle( 'Done uploading documentation' ) - } -} - -def releaseChecksTask = tasks.register( "releaseChecks" ) { - group 'Release' - description 'Checks and preparation for release' - - doFirst { - logger.lifecycle("Checking that the working tree is clean...") - String uncommittedFiles = executeGitCommand('status', '--porcelain') - if (!uncommittedFiles.isEmpty()) { - throw new GradleException( - "Cannot release because there are uncommitted or untracked files in the working tree.\n" + - "Commit or stash your changes first.\n" + - "Uncommitted files:\n " + - uncommittedFiles - ); - } - - String gitBranchLocal - String gitRemoteLocal - - if (project.hasProperty('gitBranch') && !project.property('gitBranch').isEmpty()) { - gitBranchLocal = project.property('gitBranch') - } - else { - gitBranchLocal = executeGitCommand( 'branch', '--show-current' ).trim() - } - - if (project.hasProperty('gitRemote') && !project.property('gitRemote').isEmpty()) { - gitRemoteLocal = project.property('gitRemote') - } - else { - final String remotes = executeGitCommand( 'remote', 'show' ).trim() - final List tokens = remotes.tokenize() - if ( tokens.size() != 1 ) { - throw new GradleException( "Could not determine `gitRemote` property for `releaseChecks` tasks." ) - } - gitRemoteLocal = tokens.get( 0 ) - } - - project.ext { - gitBranch = gitBranchLocal - gitRemote = gitRemoteLocal - } - - logger.lifecycle("Switching to branch '${project.gitBranch}'...") - executeGitCommand('checkout', project.gitBranch) - - logger.lifecycle("Checking that all commits are pushed...") - String diffWithUpstream = executeGitCommand('diff', '@{u}') - if (!diffWithUpstream.isEmpty()) { - throw new GradleException( - "Cannot perform `ciRelease` tasks because there are un-pushed local commits .\n" + - "Push your commits first." - ); - } - } -} - -def preVerifyReleaseTask = tasks.register( "preVerifyRelease" ) { - group 'Release' - description 'Pre-verifies a release job execution (Run locally before a CI release)' - - dependsOn tasks.clean - dependsOn assembleDocumentationTask -} - -def changeLogFileTask = tasks.register( "changeLogFile" ) { - group 'Release' - description 'Updates the changelog.txt file based on the change-log report from Jira' - dependsOn releaseChecksTask - - doFirst { - logger.lifecycle( "Appending version `${project.releaseVersion}` to changelog..." ) - ChangeLogFile.update( ormVersion.fullName ); - } -} - -def changeToReleaseVersionTask = tasks.register( "changeToReleaseVersion" ) { - group 'Release' - description 'Updates `gradle/version.properties` file to the specified release-version' - - dependsOn releaseChecksTask - - doFirst { - logger.lifecycle( "Updating version-file to release-version : `${project.releaseVersion}`" ) - updateVersionFile( project.releaseVersion ) - } -} - -def gitPreparationForReleaseTask = tasks.register( 'gitPreparationForRelease' ) { - dependsOn releaseChecksTask - dependsOn changeLogFileTask - dependsOn changeToReleaseVersionTask - - doLast { - logger.lifecycle( "Performing pre-steps Git commit : `${project.releaseVersion}`" ) - executeGitCommand( 'add', '.' ) - executeGitCommand( 'commit', '-m', "Pre-steps for release : `${project.ormVersion.fullName}`" ) - } -} - -def changeToDevelopmentVersionTask = tasks.register( 'changeToDevelopmentVersion' ) { - group 'Release' - description 'Updates `gradle/version.properties` file to the specified development-version' - - dependsOn releaseChecksTask - - doFirst { - logger.lifecycle( "Updating version-file to development-version : `${project.developmentVersion}`" ) - updateVersionFile( project.developmentVersion ) - } -} - -def releasePreparePostGitTask = tasks.register( 'gitTasksAfterRelease' ) { - dependsOn changeToDevelopmentVersionTask - - doLast { - if ( project.createTag ) { - logger.lifecycle( "Tagging release : `${project.releaseTag}`..." ) - executeGitCommand( 'tag', '-a', project.releaseTag, '-m', "Release $project.ormVersion.fullName" ) - } - - logger.lifecycle( "Performing post-steps Git commit : `${project.releaseVersion}`" ) - executeGitCommand( 'add', '.' ) - executeGitCommand( 'commit', '-m', "Post-steps for release : `${project.ormVersion.fullName}`" ) - } -} - -void updateVersionFile(String version) { - logger.lifecycle( "Updating `gradle/version.properties` version to `${version}`" ) - project.ormVersionFile.text = "hibernateVersion=${version}" -} - -def publishReleaseArtifactsTask = tasks.register( 'publishReleaseArtifacts' ) { - mustRunAfter gitPreparationForReleaseTask - - dependsOn uploadDocumentation - dependsOn uploadDocumentationDescriptor -} - -def releasePerformPostGitTask = tasks.register( 'gitTasksAfterReleasePerform' ) { - - doLast { - if ( project.createTag ) { - logger.lifecycle( "Pushing branch and tag to remote `${project.gitRemote}`..." ) - executeGitCommand( 'push', '--atomic', project.gitRemote, project.gitBranch, project.releaseTag ) - } - else { - logger.lifecycle( "Pushing branch to remote `${project.gitRemote}`..." ) - executeGitCommand( 'push', project.gitRemote, project.gitBranch ) - } - } -} - -def releasePrepareTask = tasks.register( 'releasePrepare' ) { +task releasePrepare { group 'Release' description 'Performs release preparations on local check-out, including updating changelog' // we want to assemble the docs here so that we catch problems early (and even during "dry run" for CI releases) dependsOn assembleDocumentationTask - dependsOn gitPreparationForReleaseTask - - finalizedBy releasePreparePostGitTask -} - -def releasePerformTask = tasks.register( 'releasePerform' ) { - group 'Release' - description 'Performs a release on local check-out, including updating changelog and ' - - dependsOn publishReleaseArtifactsTask - - finalizedBy releasePerformPostGitTask -} - -def releaseTask = tasks.register( 'release' ) { - group 'Release' - description 'Performs a release on local check-out, including updating changelog and ' - - dependsOn releasePrepareTask - dependsOn releasePerformTask -} - -def ciReleaseTask = tasks.register( 'ciRelease' ) { - group 'Release' - description 'Performs a release: the hibernate version is set and the changelog.txt file updated, the changes are pushed to github, then the release is performed, tagged and the hibernate version is set to the development one.' - - dependsOn releaseTask -} - -static String executeGitCommand(Object ... subcommand){ - List command = ['git'] - Collections.addAll( command, subcommand ) - def proc = command.execute() - def code = proc.waitFor() - def stdout = inputStreamToString( proc.getInputStream() ) - def stderr = inputStreamToString( proc.getErrorStream() ) - if ( code != 0 ) { - throw new GradleException( "An error occurred while executing " + command + "\n\nstdout:\n" + stdout + "\n\nstderr:\n" + stderr ) - } - return stdout -} - -static String inputStreamToString(InputStream inputStream) { - inputStream.withCloseable { ins -> - new BufferedInputStream(ins).withCloseable { bis -> - new ByteArrayOutputStream().withCloseable { buf -> - int result = bis.read(); - while (result != -1) { - buf.write((byte) result); - result = bis.read(); - } - return buf.toString( StandardCharsets.UTF_8.name()); - } - } - } -} - -class ChangeLogFile { - - // Get the Release Notes from Jira and add them to the Hibernate changelog.txt file - static void update(String releaseVersion) { - def text = "" - File changelog = new File( "changelog.txt" ) - def newReleaseNoteBlock = getNewReleaseNoteBlock(releaseVersion) - changelog.eachLine { - line -> - if ( line.startsWith( "Note:" ) ) { - text += line + System.lineSeparator() + System.lineSeparator() + newReleaseNoteBlock - } - else { - text += line + System.lineSeparator() - } - } - changelog.text = text - } - - // Get the Release Notes from Jira - static String getNewReleaseNoteBlock(String releaseVersion) { - def restReleaseVersion; - if ( releaseVersion.endsWith( ".Final" ) ) { - restReleaseVersion = releaseVersion.replace( ".Final", "" ) - } - else { - restReleaseVersion = releaseVersion - } - def apiString = "https://hibernate.atlassian.net/rest/api/2/search/?jql=project=HHH%20AND%20fixVersion=${restReleaseVersion}%20order%20by%20issuetype%20ASC" - def apiUrl = new URL( apiString ) - def jsonReleaseNotes = new JsonSlurper().parse( apiUrl ) - def releaseDate = new Date().format( 'MMMM dd, YYYY' ) - def versionId = getVersionId( jsonReleaseNotes, restReleaseVersion ) - - ReleaseNote releaseNotes = new ReleaseNote( releaseVersion, releaseDate, versionId ) - - def issuetype - jsonReleaseNotes.issues.each { - issue -> - if ( issuetype != issue.fields.issuetype.name ) { - issuetype = issue.fields.issuetype.name - releaseNotes.addEmptyLine(); - releaseNotes.addLine( "** ${issue.fields.issuetype.name}" ) - } - releaseNotes.addLine( " * [" + issue.key + "] - " + issue.fields.summary ) - } - releaseNotes.addEmptyLine() - return releaseNotes.notes - } - - private static getVersionId(jsonReleaseNotes, String restReleaseVersion) { - def fixVersions = jsonReleaseNotes.issues.get( 0 ).fields.fixVersions - - for ( def fixVersion : fixVersions ) { - if ( fixVersion.name.equals( restReleaseVersion ) ) { - return fixVersion.id - } - } - throw new GradleException( "Unable to determine the version id of the current release." ) - } -} - -class ReleaseNote { - String notes; - String notesHeaderSeparator = "------------------------------------------------------------------------------------------------------------------------" - - ReleaseNote(String releaseVersion, String releaseDate, String versionId) { - notes = "Changes in ${releaseVersion} (${releaseDate})" + System.lineSeparator() - addHeaderSeparator() - addEmptyLine() - addLine( "https://hibernate.atlassian.net/projects/HHH/versions/${versionId}" ) - } - - void addLine(String text) { - notes += text + System.lineSeparator() - } - - void addHeaderSeparator() { - addLine( notesHeaderSeparator ) - } - - void addEmptyLine() { - notes += System.lineSeparator() - } - - void addEmptyLines(int numberOfLines) { - for ( i in 1..numberOfLines ) { - notes += System.lineSeparator() - } - } -} - - -gradle.getTaskGraph().whenReady {tg-> - - if ( ( tg.hasTask( project.tasks.releasePrepare ) || tg.hasTask( project.tasks.releasePerform ) ) - && ! project.getGradle().getStartParameter().isDryRun() ) { - String releaseVersionLocal - String developmentVersionLocal - - def console = tg.hasTask( project.tasks.release ) && !tg.hasTask( project.tasks.ciRelease ) - ? System.console() - : null - - if (project.hasProperty('releaseVersion')) { - releaseVersionLocal = project.property('releaseVersion') - } - else { - if (console) { - // prompt for `releaseVersion` - releaseVersionLocal = console.readLine('> Enter the release version: ') - } - else { - throw new GradleException( - "`release`-related tasks require the following properties: 'releaseVersion', 'developmentVersion'" - ) - } - } - - if (project.hasProperty('developmentVersion')) { - developmentVersionLocal = project.property('developmentVersion') - } - else { - if (console) { - // prompt for `developmentVersion` - developmentVersionLocal = console.readLine('> Enter the next development version: ') - } - else { - throw new GradleException( - "`release`-related tasks require the following properties: 'releaseVersion', 'developmentVersion'" - ) - } - } - - assert releaseVersionLocal != null && developmentVersionLocal != null; - - // set up information for the release-related tasks - project.ext { - releaseVersion = releaseVersionLocal; - developmentVersion = developmentVersionLocal; - createTag = !project.hasProperty('noTag') - releaseTag = project.createTag ? determineReleaseTag(releaseVersionLocal) : '' - } - } -} - -static String determineReleaseTag(String releaseVersion) { - return releaseVersion.endsWith( '.Final' ) - ? releaseVersion.replace( ".Final", "" ) - : releaseVersion; } diff --git a/release/src/release/announcement-template.adoc b/release/src/release/announcement-template.adoc index a294e7f17cca..75c2bd5629ce 100644 --- a/release/src/release/announcement-template.adoc +++ b/release/src/release/announcement-template.adoc @@ -6,7 +6,7 @@ Steve Ebersole :version: 6.4.0.CR1 :family: 6.4 -:docs-url: https://docs.jboss.org/hibernate/orm/{family} +:docs-url: https://docs.hibernate.org/orm/{family} :javadocs-url: {docs-url}/javadocs :migration-guide-url: {docs-url}/migration-guide/migration-guide.html :intro-guide-url: {docs-url}/introduction/html_single/Hibernate_Introduction.html diff --git a/release_notes.md b/release_notes.md new file mode 100644 index 000000000000..3d58a819f02a --- /dev/null +++ b/release_notes.md @@ -0,0 +1,4 @@ + +* See the [website](https://hibernate.org/orm/releases/{{releaseVersionFamily}}) for requirements and compatibilities. +* See the [What's New](https://hibernate.org/orm/releases/{{releaseVersionFamily}}/#whats-new) guide for details about new features and capabilities. +* See the [Migration Guide](https://docs.hibernate.org/orm/{{releaseVersionFamily}}/migration-guide/) for details about migration to {{releaseVersionFamily}} version. diff --git a/reporting/reporting.gradle b/reporting/reporting.gradle new file mode 100644 index 000000000000..c4ffe3294743 --- /dev/null +++ b/reporting/reporting.gradle @@ -0,0 +1,46 @@ +plugins { + id 'jacoco-report-aggregation' +} +apply from: rootProject.file( 'gradle/module.gradle' ) + +description = 'Reporting module' + +ext { + projectsToIncludeWhenAggregatingCoverage = [ + 'hibernate-agroal', + 'hibernate-c3p0', + 'hibernate-community-dialects', + 'hibernate-core', + 'hibernate-envers', + 'hibernate-graalvm', + 'hibernate-hikaricp', + 'hibernate-jcache', + 'hibernate-jfr', + 'hibernate-micrometer', + 'hibernate-scan-jandex', + 'hibernate-spatial', + 'hibernate-testing', + 'hibernate-vector', + // Tooling: + 'hibernate-ant', + 'hibernate-assistant', + 'hibernate-gradle-plugin', + 'hibernate-maven-plugin', + 'hibernate-processor' + ] +} + +tasks.register('mergeCodeCoverageReport', JacocoReport) { + executionData fileTree(rootProject.layout.projectDirectory.asFile).include("**/target/jacoco/*.exec") + parent.subprojects.each { Project subProject -> + if ( project.projectsToIncludeWhenAggregatingCoverage.contains( subProject.name ) ) { + sourceSets subProject.sourceSets.main + } + } + + reports { + xml.required = true + } + + dependsOn rootProject.subprojects.collect { p -> p.tasks.matching { it.name == 'compileJava' } } +} diff --git a/settings.gradle b/settings.gradle index 6f3436e1b724..c02becbdafe0 100644 --- a/settings.gradle +++ b/settings.gradle @@ -68,17 +68,19 @@ dependencyResolutionManagement { } libs { def antlrVersion = version "antlr", "4.13.0" - def byteBuddyVersion = version "byteBuddy", "1.14.18" + // WARNING: When upgrading to a version of bytebuddy that supports a new bytecode version, + // make sure to remove the now unnecessary net.bytebuddy.experimental=true in relevant CI jobs (Jenkinsfile). + def byteBuddyVersion = version "byteBuddy", "1.17.5" def classmateVersion = version "classmate", "1.5.1" def geolatteVersion = version "geolatte", "1.9.1" - def hcannVersion = version "hcann", "7.0.1.Final" + def hcannVersion = version "hcann", "7.0.3.Final" def jandexVersion = version "jandex", "3.2.0" def jacksonVersion = version "jackson", "2.17.0" def jbossLoggingVersion = version "jbossLogging", "3.5.0.Final" def jbossLoggingToolVersion = version "jbossLoggingTool", "3.0.1.Final" def agroalVersion = version "agroal", "2.0" - def c3poVersion = version"c3p0", "0.9.5.5" + def c3poVersion = version"c3p0", "0.12.0" def hikaricpVersion = version "hikaricp", "3.2.0" def proxoolVersion = version "proxool", "0.8.3" def viburVersion = version "vibur", "25.0" @@ -240,6 +242,7 @@ dependencyResolutionManagement { def oracleVersion = version "oracle", "23.4.0.24.05" def oracleLegacyVersion = version "oracleLegacy", "11.2.0.4" def pgsqlVersion = version "pgsql", "42.7.1" + def edbVersion = version "edb", "42.7.3.3" def sybaseVersion = version "sybase", "1.3.1" def tidbVersion = version "tidb", mysqlVersion def altibaseVersion = version "altibase", "7.3.0.0.3" @@ -251,6 +254,7 @@ dependencyResolutionManagement { library( "derbyTools", "org.apache.derby", "derbytools" ).versionRef( derbyVersion ) library( "postgresql", "org.postgresql", "postgresql" ).versionRef( pgsqlVersion ) library( "cockroachdb", "org.postgresql", "postgresql" ).versionRef( pgsqlVersion ) + library( "edb", "com.enterprisedb", "edb-jdbc" ).versionRef( edbVersion ) library( "mysql", "com.mysql", "mysql-connector-j" ).versionRef( mysqlVersion ) library( "tidb", "com.mysql", "mysql-connector-j" ).versionRef( tidbVersion ) library( "mariadb", "org.mariadb.jdbc", "mariadb-java-client" ).versionRef( mariadbVersion ) @@ -300,7 +304,7 @@ buildCache { enabled = !settings.ext.isCiEnvironment } remote(develocity.buildCache) { - enabled = true + enabled = settings.ext.useRemoteCache // Check access key presence to avoid build cache errors on PR builds when access key is not present def accessKey = System.getenv("DEVELOCITY_ACCESS_KEY") push = settings.ext.populateRemoteBuildCache && accessKey @@ -333,6 +337,7 @@ include 'hibernate-integrationtest-java-modules' include 'documentation' include 'release' +include 'reporting' // Not all JDK implementations support JFR if ( "OpenJDK Runtime Environment".equals( System.getProperty( "java.runtime.name" ) ) ) { diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 000000000000..950c7c7674ef --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,119 @@ +sonar.host.url=https://sonarcloud.io +sonar.organization=hibernate +sonar.projectKey=hibernate_hibernate-orm +sonar.projectName=Hibernate ORM +sonar.projectDescription=Idiomatic persistence for Java and relational databases. + +sonar.language=java +sonar.sourceEncoding=UTF-8 + +sonar.modules=hibernate-agroal,hibernate-c3p0,hibernate-community-dialects,hibernate-core,hibernate-envers,hibernate-graalvm,hibernate-hikaricp,hibernate-jcache,hibernate-jfr,hibernate-micrometer,hibernate-scan-jandex,hibernate-spatial,hibernate-testing,hibernate-vector,hibernate-ant,hibernate-gradle-plugin,hibernate-maven-plugin,metamodel-generator + +hibernate-agroal.sonar.projectName=Hibernate ORM - Agroal +hibernate-agroal.sonar.projectBaseDir=hibernate-agroal +hibernate-agroal.sonar.sources=src/main/java +hibernate-agroal.sonar.tests=src/test/java +hibernate-agroal.sonar.java.binaries=target/classes/java/main + +hibernate-c3p0.sonar.projectName=Hibernate ORM - c3p0 +hibernate-c3p0.sonar.projectBaseDir=hibernate-c3p0 +hibernate-c3p0.sonar.sources=src/main/java +hibernate-c3p0.sonar.tests=src/test/java +hibernate-c3p0.sonar.java.binaries=target/classes/java/main + +hibernate-community-dialects.sonar.projectName=Hibernate ORM - Community Dialects +hibernate-community-dialects.sonar.projectBaseDir=hibernate-community-dialects +hibernate-community-dialects.sonar.sources=src/main/java +hibernate-community-dialects.sonar.tests=src/test/java +hibernate-community-dialects.sonar.java.binaries=target/classes/java/main + +hibernate-core.sonar.projectName=Hibernate ORM - Core +hibernate-core.sonar.projectBaseDir=hibernate-core +hibernate-core.sonar.sources=src/main/java +hibernate-core.sonar.tests=src/test/java +hibernate-core.sonar.java.binaries=target/classes/java/main + +hibernate-envers.sonar.projectName=Hibernate ORM - Envers +hibernate-envers.sonar.projectBaseDir=hibernate-envers +hibernate-envers.sonar.sources=src/main/java +hibernate-envers.sonar.tests=src/test/java +hibernate-envers.sonar.java.binaries=target/classes/java/main + +hibernate-graalvm.sonar.projectName=Hibernate ORM - GraalVM +hibernate-graalvm.sonar.projectBaseDir=hibernate-graalvm +hibernate-graalvm.sonar.sources=src/main/java +hibernate-graalvm.sonar.tests=src/test/java +hibernate-graalvm.sonar.java.binaries=target/classes/java/main + +hibernate-hikaricp.sonar.projectName=Hibernate ORM - HikariCP +hibernate-hikaricp.sonar.projectBaseDir=hibernate-hikaricp +hibernate-hikaricp.sonar.sources=src/main/java +hibernate-hikaricp.sonar.tests=src/test/java +hibernate-hikaricp.sonar.java.binaries=target/classes/java/main + +hibernate-jcache.sonar.projectName=Hibernate ORM - JCache +hibernate-jcache.sonar.projectBaseDir=hibernate-jcache +hibernate-jcache.sonar.sources=src/main/java +hibernate-jcache.sonar.tests=src/test/java +hibernate-jcache.sonar.java.binaries=target/classes/java/main + +hibernate-jfr.sonar.projectName=Hibernate ORM - JFR +hibernate-jfr.sonar.projectBaseDir=hibernate-jfr +hibernate-jfr.sonar.sources=src/main/java +hibernate-jfr.sonar.tests=src/test/java +hibernate-jfr.sonar.java.binaries=target/classes/java/main + +hibernate-micrometer.sonar.projectName=Hibernate ORM - Micrometer +hibernate-micrometer.hibernate-micrometer.sonar.projectBaseDir=hibernate-micrometer +hibernate-micrometer.sonar.sources=src/main/java +hibernate-micrometer.sonar.tests=src/test/java +hibernate-micrometer.sonar.java.binaries=target/classes/java/main + +hibernate-scan-jandex.sonar.projectName=Hibernate ORM - Scan Jandex +hibernate-scan-jandex.sonar.projectBaseDir=hibernate-scan-jandex +hibernate-scan-jandex.sonar.sources=src/main/java +hibernate-scan-jandex.sonar.tests=src/test/java +hibernate-scan-jandex.sonar.java.binaries=target/classes/java/main + +hibernate-spatial.sonar.projectName=Hibernate ORM - Spatial +hibernate-spatial.sonar.projectBaseDir=hibernate-spatial +hibernate-spatial.sonar.sources=src/main/java +hibernate-spatial.sonar.tests=src/test/java +hibernate-spatial.sonar.java.binaries=target/classes/java/main + +hibernate-testing.sonar.projectName=Hibernate ORM - Testing +hibernate-testing.sonar.projectBaseDir=hibernate-testing +hibernate-testing.sonar.sources=src/main/java +hibernate-testing.sonar.tests=src/test/java +hibernate-testing.sonar.java.binaries=target/classes/java/main + +hibernate-vector.sonar.projectName=Hibernate ORM - Vector +hibernate-vector.sonar.projectBaseDir=hibernate-vector +hibernate-vector.sonar.sources=src/main/java +hibernate-vector.sonar.tests=src/test/java +hibernate-vector.sonar.java.binaries=target/classes/java/main + +# Tooling: +hibernate-ant.sonar.projectName=Hibernate ORM - Ant +hibernate-ant.sonar.projectBaseDir=tooling/hibernate-ant +hibernate-ant.sonar.sources=src/main/java +hibernate-ant.sonar.tests=src/test/java +hibernate-ant.sonar.java.binaries=target/classes/java/main + +hibernate-gradle-plugin.sonar.projectName=Hibernate ORM - Gradle Plugin +hibernate-gradle-plugin.sonar.projectBaseDir=tooling/hibernate-gradle-plugin +hibernate-gradle-plugin.sonar.sources=src/main/java +hibernate-gradle-plugin.sonar.tests=src/test/java +hibernate-gradle-plugin.sonar.java.binaries=target/classes/java/main + +hibernate-maven-plugin.sonar.projectName=Hibernate ORM - Maven Plugin +hibernate-maven-plugin.sonar.projectBaseDir=tooling/hibernate-enhance-maven-plugin +hibernate-maven-plugin.sonar.sources=src/main/java +hibernate-maven-plugin.sonar.tests=src/test/java +hibernate-maven-plugin.sonar.java.binaries=target/classes/java/main + +metamodel-generator.sonar.projectName=Hibernate ORM - Metamodel Generator (Annotation processor) +metamodel-generator.sonar.projectBaseDir=tooling/metamodel-generator +metamodel-generator.sonar.sources=src/main/java +metamodel-generator.sonar.tests=src/test/java +metamodel-generator.sonar.java.binaries=target/classes/java/main diff --git a/tooling/hibernate-gradle-plugin/hibernate-gradle-plugin.gradle b/tooling/hibernate-gradle-plugin/hibernate-gradle-plugin.gradle index 551820769781..279b2cde9bed 100644 --- a/tooling/hibernate-gradle-plugin/hibernate-gradle-plugin.gradle +++ b/tooling/hibernate-gradle-plugin/hibernate-gradle-plugin.gradle @@ -19,14 +19,10 @@ plugins { apply from: rootProject.file( 'gradle/module.gradle' ) apply from: rootProject.file( 'gradle/javadoc.gradle' ) -apply from: rootProject.file( 'gradle/releasable.gradle' ) description = "Gradle plugin for integrating Hibernate aspects into your build" -ext { - pluginId = 'org.hibernate.orm' - pluginVersion = project.version -} +def pluginId = 'org.hibernate.orm' dependencies { implementation project(':hibernate-core') @@ -45,12 +41,13 @@ dependencies { testImplementation testLibs.junit5Api testRuntimeOnly testLibs.junit5Engine + testRuntimeOnly testLibs.junit5Launcher } gradlePlugin { plugins { ormPlugin { - id = project.pluginId + id = pluginId implementationClass = 'org.hibernate.orm.tooling.gradle.HibernateOrmPlugin' } } @@ -70,7 +67,7 @@ pluginBundle { plugins { ormPlugin { - id = project.pluginId + id = pluginId displayName = 'Gradle plugin for Hibernate ORM' description = 'Applies Hibernate aspects into the build' } @@ -81,15 +78,6 @@ test { useJUnitPlatform() } -// Publish to the Gradle Plugin Portal -tasks.release.dependsOn tasks.publishPlugins - -// local publishing (SNAPSHOT testing) -tasks.publish.dependsOn tasks.publishPlugins - -// Make sure that the publishReleaseArtifacts task of the release module runs the release task of this sub module -tasks.getByPath( ':release:publishReleaseArtifacts' ).dependsOn tasks.release - // local publishing (SNAPSHOT testing) publishing { repositories { @@ -100,21 +88,6 @@ publishing { } } -// local publishing (SNAPSHOT testing), cont. -// - https://github.com/gradle-nexus/publish-plugin/issues/143 -// - https://github.com/gradle-nexus/publish-plugin/pull/144 -gradle.taskGraph.whenReady { - tasks.withType(PublishToMavenRepository) { PublishToMavenRepository t -> - if ( t.repository == null ) { - logger.info( "Task `{}` had null repository", t.path ) - } - else if ( t.repository.name == "sonatype" ) { - logger.debug( "Disabling task `{}` because it publishes to Sonatype", t.path ) - t.enabled = false - } - } -} - processResources { inputs.property( "orm-version", getVersion() ) description = description + " (injected with Hibernate version)" @@ -153,20 +126,45 @@ tasks.publish.enabled !project.ormVersion.isSnapshot tasks.publishPlugins.enabled !project.ormVersion.isSnapshot gradle.taskGraph.whenReady { tg -> - if ( tg.hasTask( project.tasks.publishPlugins ) && project.tasks.publishPlugins.enabled ) { - // look for sys-prop or env-var overrides of the tokens used for publishing - if ( project.properties.containsKey( 'gradle.publish.key' ) - || project.properties.containsKey( 'gradle.publish.secret' ) ) { - // nothing to do - already explicitly set - } - else { - // use the values from the credentials provider, if any - if ( project.property( 'gradle.publish.key' ) == null ) { - throw new RuntimeException( "`-Pgradle.publish.key=...` not found" ) + // verify credentials for publishing the plugin up front to avoid any work (only if we are publishing) + if ( tg.hasTask( tasks.publishPlugins ) && project.tasks.publishPlugins.enabled ) { + // we are publishing the plugin - make sure there is a credentials pair + // + // first, check the `GRADLE_PUBLISH_KEY` / `GRADLE_PUBLISH_SECRET` combo (env vars) + // and then the `gradle.publish.key` / `gradle.publish.secret` combo (project prop) + // - see https://docs.gradle.org/current/userguide/publishing_gradle_plugins.html#account_setup + if ( System.getenv().get("GRADLE_PUBLISH_KEY") != null ) { + if ( System.getenv().get("GRADLE_PUBLISH_SECRET") == null ) { + throw new RuntimeException( "`GRADLE_PUBLISH_KEY` specified, but not `GRADLE_PUBLISH_SECRET` for publishing Gradle plugin" ) } - if ( project.property( 'gradle.publish.secret' ) == null ) { - throw new RuntimeException( "`-Pgradle.publish.secret=...` not found" ) + } + else if ( project.findProperty( 'gradle.publish.key' ) != null ) { + if ( project.findProperty( 'gradle.publish.secret' ) == null ) { + throw new RuntimeException( "`gradle.publish.key` specified, but not `gradle.publish.secret` for publishing Gradle plugin" ) } } + else { + throw new RuntimeException( "No credentials specified for publishing Gradle plugin" ) + } } -} \ No newline at end of file + + // local publishing (SNAPSHOT testing), cont. + // - https://github.com/gradle-nexus/publish-plugin/issues/143 + // - https://github.com/gradle-nexus/publish-plugin/pull/144 + tasks.withType(PublishToMavenRepository) { PublishToMavenRepository t -> + if ( t.repository == null ) { + logger.info( "Task `{}` had null repository", t.path ) + } + else if ( t.repository.name == "staging" || t.repository.name == "snapshots" ) { + logger.debug( "Disabling task `{}` because it publishes to remote Maven repository", t.path ) + t.enabled = false + } + } +} + +task releaseGradlePluginPerform { + group "release-perform" + description "An explicit task for publishing Gradle Plugins to the Plugin Portal" + + dependsOn tasks.publishPlugins +} diff --git a/tooling/hibernate-gradle-plugin/src/test/java/org/hibernate/orm/tooling/gradle/ModuleInfoProjectTests.java b/tooling/hibernate-gradle-plugin/src/test/java/org/hibernate/orm/tooling/gradle/ModuleInfoProjectTests.java new file mode 100644 index 000000000000..a926488862f1 --- /dev/null +++ b/tooling/hibernate-gradle-plugin/src/test/java/org/hibernate/orm/tooling/gradle/ModuleInfoProjectTests.java @@ -0,0 +1,53 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.orm.tooling.gradle; + +import java.nio.file.Path; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + + +/** + * Basic functional tests + * + * @author Steve Ebersole + */ +class ModuleInfoProjectTests extends TestsBase { + + @Override + protected String getProjectName() { + return "simple-moduleinfo"; + } + + @Override + protected String getSourceSetName() { + return "main"; + } + + @Override + protected String getLanguageName() { + return "java"; + } + + @Override + protected String getCompileTaskName() { + return "compileJava"; + } + + @Test + @Override + public void testEnhancement(@TempDir Path projectDir) throws Exception { + super.testEnhancement( projectDir ); + } + + @Test + @Override + public void testEnhancementUpToDate(@TempDir Path projectDir) throws Exception { + super.testEnhancementUpToDate( projectDir ); + } +} diff --git a/tooling/hibernate-gradle-plugin/src/test/resources/projects/simple-moduleinfo/build.gradle b/tooling/hibernate-gradle-plugin/src/test/resources/projects/simple-moduleinfo/build.gradle new file mode 100644 index 000000000000..33a514518a87 --- /dev/null +++ b/tooling/hibernate-gradle-plugin/src/test/resources/projects/simple-moduleinfo/build.gradle @@ -0,0 +1,40 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ + +plugins { + id 'java' + id 'org.hibernate.orm' +} + +repositories { + mavenCentral() + + maven { + name 'jboss-snapshots-repository' + url 'https://repository.jboss.org/nexus/content/repositories/snapshots' + } +} + +dependencies { + // NOTE : The version used here is irrelevant in terms of testing the plugin. + // We just need a resolvable version + implementation 'org.hibernate.orm:hibernate-core:6.1.0.Final' + implementation 'jakarta.annotation:jakarta.annotation-api:2.1.1' +} + +hibernate { + useSameVersion = false + enhancement { + enableLazyInitialization.set(true) + lazyInitialization = true + + enableDirtyTracking.set(true) + dirtyTracking = true + + enableExtendedEnhancement.set(true) + } +} diff --git a/tooling/hibernate-gradle-plugin/src/test/resources/projects/simple-moduleinfo/settings.gradle b/tooling/hibernate-gradle-plugin/src/test/resources/projects/simple-moduleinfo/settings.gradle new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tooling/hibernate-gradle-plugin/src/test/resources/projects/simple-moduleinfo/src/main/java/TheEmbeddable.java b/tooling/hibernate-gradle-plugin/src/test/resources/projects/simple-moduleinfo/src/main/java/TheEmbeddable.java new file mode 100644 index 000000000000..25b8849aac2c --- /dev/null +++ b/tooling/hibernate-gradle-plugin/src/test/resources/projects/simple-moduleinfo/src/main/java/TheEmbeddable.java @@ -0,0 +1,29 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +import jakarta.persistence.Embeddable; + +@Embeddable +public class TheEmbeddable { + private String valueOne; + private String valueTwo; + + public String getValueOne() { + return valueOne; + } + + public void setValueOne(String valueOne) { + this.valueOne = valueOne; + } + + public String getValueTwo() { + return valueTwo; + } + + public void setValueTwo(String valueTwo) { + this.valueTwo = valueTwo; + } +} diff --git a/tooling/hibernate-gradle-plugin/src/test/resources/projects/simple-moduleinfo/src/main/java/TheEntity.java b/tooling/hibernate-gradle-plugin/src/test/resources/projects/simple-moduleinfo/src/main/java/TheEntity.java new file mode 100644 index 000000000000..b5d9eb5eead4 --- /dev/null +++ b/tooling/hibernate-gradle-plugin/src/test/resources/projects/simple-moduleinfo/src/main/java/TheEntity.java @@ -0,0 +1,88 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +import jakarta.persistence.ElementCollection; +import jakarta.persistence.Embedded; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; + +import org.hibernate.annotations.BatchSize; + +import java.util.Set; + +@Entity +@BatchSize( size = 20 ) +public class TheEntity { + @Id + private Integer id; + private String name; + + @Embedded + private TheEmbeddable theEmbeddable; + + @ManyToOne + @JoinColumn + private TheEntity theManyToOne; + + @OneToMany( mappedBy = "theManyToOne" ) + private Set theOneToMany; + + @ElementCollection + @JoinColumn( name = "owner_id" ) + private Set theEmbeddableCollection; + + + public Integer getId() { + return id; + } + + public void setId(Integer id) { + this.id = id; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public TheEmbeddable getTheEmbeddable() { + return theEmbeddable; + } + + public void setTheEmbeddable(TheEmbeddable theEmbeddable) { + this.theEmbeddable = theEmbeddable; + } + + public TheEntity getTheManyToOne() { + return theManyToOne; + } + + public void setTheManyToOne(TheEntity theManyToOne) { + this.theManyToOne = theManyToOne; + } + + public Set getTheOneToMany() { + return theOneToMany; + } + + public void setTheOneToMany(Set theOneToMany) { + this.theOneToMany = theOneToMany; + } + + public Set getTheEmbeddableCollection() { + return theEmbeddableCollection; + } + + public void setTheEmbeddableCollection(Set theEmbeddableCollection) { + this.theEmbeddableCollection = theEmbeddableCollection; + } +} diff --git a/tooling/hibernate-gradle-plugin/src/test/resources/projects/simple-moduleinfo/src/main/java/module-info.java b/tooling/hibernate-gradle-plugin/src/test/resources/projects/simple-moduleinfo/src/main/java/module-info.java new file mode 100644 index 000000000000..ea488cd33f8f --- /dev/null +++ b/tooling/hibernate-gradle-plugin/src/test/resources/projects/simple-moduleinfo/src/main/java/module-info.java @@ -0,0 +1,5 @@ +module test { + requires org.hibernate.orm.core; + requires jakarta.persistence; + requires jakarta.annotation; +} diff --git a/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/basic/Concrete.java b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/basic/Concrete.java index 049a4c5f92f9..af416bc08fb0 100644 --- a/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/basic/Concrete.java +++ b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/basic/Concrete.java @@ -3,5 +3,5 @@ import jakarta.data.repository.Repository; @Repository -public interface Concrete extends IdOperations { +public interface Concrete extends IdOperations { } diff --git a/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/basic/DataTest.java b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/basic/DataTest.java index 7fcf07a4c89a..5637f5977e5e 100644 --- a/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/basic/DataTest.java +++ b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/basic/DataTest.java @@ -19,7 +19,7 @@ */ public class DataTest extends CompilationTest { @Test - @WithClasses({ Author.class, Book.class, BookAuthorRepository.class, IdOperations.class, Concrete.class }) + @WithClasses({ Author.class, Book.class, BookAuthorRepository.class, IdOperations.class, Concrete.class, Thing.class }) public void test() { System.out.println( getMetaModelSourceAsString( Author.class ) ); System.out.println( getMetaModelSourceAsString( Book.class ) ); diff --git a/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/basic/IdOperations.java b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/basic/IdOperations.java index 0f45d49beb38..0525ad8632a7 100644 --- a/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/basic/IdOperations.java +++ b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/basic/IdOperations.java @@ -23,11 +23,6 @@ import jakarta.data.Sort; import jakarta.data.repository.Query; -/** - * This interface contains common operations for the NaturalNumbers and AsciiCharacters repositories. - * - * @param type of entity. - */ public interface IdOperations { @Query("where id(this) between ?1 and ?2") Stream findByIdBetween(long minimum, long maximum, Sort sort); diff --git a/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/basic/Thing.java b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/basic/Thing.java new file mode 100644 index 000000000000..5181e9babd35 --- /dev/null +++ b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/basic/Thing.java @@ -0,0 +1,9 @@ +package org.hibernate.processor.test.data.basic; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; + +@Entity +public class Thing { + @Id long id; +} diff --git a/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/eg/Bookshop.java b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/eg/Bookshop.java index e57f661dd380..583f29f0281f 100644 --- a/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/eg/Bookshop.java +++ b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/eg/Bookshop.java @@ -1,5 +1,6 @@ package org.hibernate.processor.test.data.eg; +import jakarta.annotation.Nonnull; import jakarta.data.repository.CrudRepository; import jakarta.data.repository.Find; import jakarta.data.repository.Query; @@ -10,12 +11,17 @@ import java.util.List; +import static jakarta.transaction.Transactional.TxType.REQUIRES_NEW; + @Repository public interface Bookshop extends CrudRepository { @Find - @Transactional + @Transactional(REQUIRES_NEW) List byPublisher(String publisher_name); + @Find + List byTitle(@Nonnull String title); + @Query("select isbn where title like ?1 order by isbn") String[] ssns(@NotBlank String title); @@ -25,6 +31,9 @@ public interface Bookshop extends CrudRepository { @Query("select count(this) where this.title like ?1 order by this.isbn") long count2(String title); + @Query("select length(text) where title = ?1") + int length(@Nonnull String title); + @Query("select count(this)") long countAll(); diff --git a/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/embeddedid/EmbeddedIdTest.java b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/embeddedid/EmbeddedIdTest.java new file mode 100644 index 000000000000..fd5d54cd7603 --- /dev/null +++ b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/embeddedid/EmbeddedIdTest.java @@ -0,0 +1,26 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.processor.test.data.embeddedid; + +import org.hibernate.processor.test.util.CompilationTest; +import org.hibernate.processor.test.util.WithClasses; +import org.junit.Test; + +import static org.hibernate.processor.test.util.TestUtil.assertMetamodelClassGeneratedFor; +import static org.hibernate.processor.test.util.TestUtil.getMetaModelSourceAsString; + +/** + * @author Gavin King + */ +public class EmbeddedIdTest extends CompilationTest { + @Test + @WithClasses({ Thing.class, ThingRepo.class }) + public void test() { + System.out.println( getMetaModelSourceAsString( ThingRepo.class ) ); + assertMetamodelClassGeneratedFor( Thing.class, true ); + assertMetamodelClassGeneratedFor( Thing.class ); + assertMetamodelClassGeneratedFor( ThingRepo.class ); + } +} diff --git a/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/embeddedid/Thing.java b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/embeddedid/Thing.java new file mode 100644 index 000000000000..c145b90cf0c4 --- /dev/null +++ b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/embeddedid/Thing.java @@ -0,0 +1,20 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.processor.test.data.embeddedid; + +import jakarta.persistence.Embeddable; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; + +@Entity +public class Thing { + @Embeddable + public static class Id { + long id; + String key; + } + + @EmbeddedId Id id; +} diff --git a/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/embeddedid/ThingRepo.java b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/embeddedid/ThingRepo.java new file mode 100644 index 000000000000..563a97e5a940 --- /dev/null +++ b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/embeddedid/ThingRepo.java @@ -0,0 +1,15 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.processor.test.data.embeddedid; + +import jakarta.data.repository.CrudRepository; +import jakarta.data.repository.Find; +import jakarta.data.repository.Repository; + +@Repository +public interface ThingRepo extends CrudRepository { + @Find + Thing thing(Thing.Id id); +} diff --git a/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/selectenumproperty/Topic.java b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/selectenumproperty/Topic.java new file mode 100644 index 000000000000..7d86f0c0f4e9 --- /dev/null +++ b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/selectenumproperty/Topic.java @@ -0,0 +1,21 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.processor.test.data.selectenumproperty; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; + +@Entity +public class Topic { + + @Id + @GeneratedValue + private Integer topicId; + + private TopicStatus topicStatus; +} diff --git a/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/selectenumproperty/TopicData.java b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/selectenumproperty/TopicData.java new file mode 100644 index 000000000000..1d20af8b056c --- /dev/null +++ b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/selectenumproperty/TopicData.java @@ -0,0 +1,12 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.processor.test.data.selectenumproperty; + + +record TopicData(Integer topicId, TopicStatus topicStatus) { + +} diff --git a/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/selectenumproperty/TopicRepository.java b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/selectenumproperty/TopicRepository.java new file mode 100644 index 000000000000..143a12943569 --- /dev/null +++ b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/selectenumproperty/TopicRepository.java @@ -0,0 +1,20 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.processor.test.data.selectenumproperty; + +import jakarta.data.repository.DataRepository; +import jakarta.data.repository.Query; +import jakarta.data.repository.Repository; + +import java.util.List; + +@Repository +public interface TopicRepository extends DataRepository { + + @Query("select topicId, topicStatus from Topic") + List getTopicData(); +} diff --git a/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/selectenumproperty/TopicStatus.java b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/selectenumproperty/TopicStatus.java new file mode 100644 index 000000000000..8361b88967df --- /dev/null +++ b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/selectenumproperty/TopicStatus.java @@ -0,0 +1,22 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.processor.test.data.selectenumproperty; + +public enum TopicStatus { + TOPIC_UNLOCKED( 0 ), + TOPIC_LOCKED( 1 ); + + private final int topicStatus; + + TopicStatus(final int topicStatus) { + this.topicStatus = topicStatus; + } + + public int topicStatus() { + return topicStatus; + } +} diff --git a/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/selectenumproperty/TopicTypeEnumTest.java b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/selectenumproperty/TopicTypeEnumTest.java new file mode 100644 index 000000000000..bac292994c97 --- /dev/null +++ b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/selectenumproperty/TopicTypeEnumTest.java @@ -0,0 +1,27 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html. + */ +package org.hibernate.processor.test.data.selectenumproperty; + +import org.hibernate.processor.test.util.CompilationTest; +import org.hibernate.processor.test.util.WithClasses; +import org.junit.Test; + +import static org.hibernate.processor.test.util.TestUtil.assertMetamodelClassGeneratedFor; +import static org.hibernate.processor.test.util.TestUtil.getMetaModelSourceAsString; + +public class TopicTypeEnumTest extends CompilationTest { + @Test + @WithClasses({Topic.class, TopicRepository.class}) + public void test() { + System.out.println( getMetaModelSourceAsString( Topic.class ) ); + System.out.println( getMetaModelSourceAsString( Topic.class, true ) ); + System.out.println( getMetaModelSourceAsString( TopicRepository.class ) ); + assertMetamodelClassGeneratedFor( Topic.class, true ); + assertMetamodelClassGeneratedFor( Topic.class ); + assertMetamodelClassGeneratedFor( TopicRepository.class ); + } +} diff --git a/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/versioned/SpecialVersioned.java b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/versioned/SpecialVersioned.java new file mode 100644 index 000000000000..610c7efb6d6d --- /dev/null +++ b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/versioned/SpecialVersioned.java @@ -0,0 +1,7 @@ +package org.hibernate.processor.test.data.versioned; + +import jakarta.persistence.Entity; + +@Entity +public class SpecialVersioned extends Versioned { +} diff --git a/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/versioned/SpecialVersionedRepo.java b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/versioned/SpecialVersionedRepo.java new file mode 100644 index 000000000000..11fb3fdddb68 --- /dev/null +++ b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/versioned/SpecialVersionedRepo.java @@ -0,0 +1,17 @@ +package org.hibernate.processor.test.data.versioned; + +import jakarta.data.repository.Query; +import jakarta.data.repository.Repository; + +@Repository +public interface SpecialVersionedRepo { + @Query("where id(this) = ?1") + SpecialVersioned forId(long id); + + @Query("where id(this) = ?1 and version(this) = ?2") + SpecialVersioned forIdAndVersion(long id, int version); + + @Query("select count(this) from SpecialVersioned") + long count(); + +} diff --git a/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/versioned/Versioned.java b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/versioned/Versioned.java new file mode 100644 index 000000000000..61405f61edab --- /dev/null +++ b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/versioned/Versioned.java @@ -0,0 +1,11 @@ +package org.hibernate.processor.test.data.versioned; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Version; + +@Entity +public class Versioned { + @Id long id; + @Version int version; +} diff --git a/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/versioned/VersionedRepo.java b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/versioned/VersionedRepo.java new file mode 100644 index 000000000000..df312b541573 --- /dev/null +++ b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/versioned/VersionedRepo.java @@ -0,0 +1,17 @@ +package org.hibernate.processor.test.data.versioned; + +import jakarta.data.repository.Query; +import jakarta.data.repository.Repository; + +@Repository +public interface VersionedRepo { + @Query("where id(this) = ?1") + Versioned forId(long id); + + @Query("where id(this) = ?1 and version(this) = ?2") + Versioned forIdAndVersion(long id, int version); + + @Query("select count(this) from Versioned") + long count(); + +} diff --git a/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/versioned/VersionedTest.java b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/versioned/VersionedTest.java new file mode 100644 index 000000000000..fbfc622cc3ee --- /dev/null +++ b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/data/versioned/VersionedTest.java @@ -0,0 +1,31 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later. + * See the lgpl.txt file in the root directory or . + */ +package org.hibernate.processor.test.data.versioned; + +import org.hibernate.processor.test.util.CompilationTest; +import org.hibernate.processor.test.util.WithClasses; +import org.junit.Test; + +import static org.hibernate.processor.test.util.TestUtil.assertMetamodelClassGeneratedFor; +import static org.hibernate.processor.test.util.TestUtil.getMetaModelSourceAsString; + +/** + * @author Gavin King + */ +public class VersionedTest extends CompilationTest { + @Test + @WithClasses({ Versioned.class, VersionedRepo.class, SpecialVersioned.class, SpecialVersionedRepo.class }) + public void test() { + System.out.println( getMetaModelSourceAsString( VersionedRepo.class ) ); + assertMetamodelClassGeneratedFor( Versioned.class, true ); + assertMetamodelClassGeneratedFor( Versioned.class ); + assertMetamodelClassGeneratedFor( SpecialVersioned.class, true ); + assertMetamodelClassGeneratedFor( SpecialVersioned.class ); + assertMetamodelClassGeneratedFor( VersionedRepo.class ); + assertMetamodelClassGeneratedFor( SpecialVersionedRepo.class ); + } +} diff --git a/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/namedquery/Thing.java b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/namedquery/Thing.java new file mode 100644 index 000000000000..29abb055972d --- /dev/null +++ b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/namedquery/Thing.java @@ -0,0 +1,18 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.processor.test.namedquery; + +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.NamedQuery; + +@NamedQuery(name = "#things", + query = "from Thing where name like :name") +@Entity +public class Thing { + @Id @GeneratedValue long id; + String name; +} diff --git a/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/namedquery/ThingTest.java b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/namedquery/ThingTest.java new file mode 100644 index 000000000000..a10682cf3356 --- /dev/null +++ b/tooling/metamodel-generator/src/jakartaData/java/org/hibernate/processor/test/namedquery/ThingTest.java @@ -0,0 +1,26 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.processor.test.namedquery; + +import jakarta.persistence.EntityManager; +import org.hibernate.processor.test.util.CompilationTest; +import org.hibernate.processor.test.util.WithClasses; +import org.junit.Test; + +import static org.hibernate.processor.test.util.TestUtil.assertMetamodelClassGeneratedFor; +import static org.hibernate.processor.test.util.TestUtil.assertPresenceOfFieldInMetamodelFor; +import static org.hibernate.processor.test.util.TestUtil.assertPresenceOfMethodInMetamodelFor; +import static org.hibernate.processor.test.util.TestUtil.getMetaModelSourceAsString; + +public class ThingTest extends CompilationTest { + @Test @WithClasses( Thing.class ) + public void test() { + System.out.println( getMetaModelSourceAsString( Thing.class) ); + System.out.println( getMetaModelSourceAsString( Thing.class, true) ); + assertMetamodelClassGeneratedFor(Thing.class); + assertMetamodelClassGeneratedFor(Thing.class, true); + assertPresenceOfMethodInMetamodelFor( Thing.class, "things", EntityManager.class, String.class ); + } +} diff --git a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/ClassWriter.java b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/ClassWriter.java index fc24d625f027..f7a6479b5574 100644 --- a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/ClassWriter.java +++ b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/ClassWriter.java @@ -10,8 +10,12 @@ import org.hibernate.processor.model.Metamodel; import javax.annotation.processing.FilerException; +import javax.lang.model.element.AnnotationMirror; +import javax.lang.model.element.AnnotationValue; import javax.lang.model.element.ElementKind; import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.TypeMirror; import javax.tools.Diagnostic; import javax.tools.FileObject; import java.io.IOException; @@ -94,7 +98,11 @@ private static StringBuffer generateBody(Metamodel entity, Context context) { if ( context.addSuppressWarningsAnnotation() ) { pw.println( writeSuppressWarnings(context) ); } - entity.inheritedAnnotations().forEach(pw::println); + entity.inheritedAnnotations() + .forEach( annotation -> { + printAnnotation( annotation, pw ); + pw.print('\n'); + } ); printClassDeclaration( entity, pw ); @@ -116,9 +124,10 @@ private static StringBuffer generateBody(Metamodel entity, Context context) { pw.println('\t' + line); if ( line.trim().startsWith("@Override") ) { metaMember.inheritedAnnotations() - .forEach(x -> { + .forEach(annotation -> { pw.print('\t'); - pw.println(x); + printAnnotation( annotation, pw ); + pw.print('\n'); }); } }); @@ -131,6 +140,66 @@ private static StringBuffer generateBody(Metamodel entity, Context context) { } } + private static void printAnnotation(AnnotationMirror annotation, PrintWriter pw) { + pw.print('@'); + final TypeElement type = (TypeElement) annotation.getAnnotationType().asElement(); + pw.print( type.getQualifiedName().toString() ); + var elementValues = annotation.getElementValues(); + if (!elementValues.isEmpty()) { + pw.print('('); + boolean first = true; + for (var entry : elementValues.entrySet()) { + if (first) { + first = false; + } + else { + pw.print(','); + } + pw.print( entry.getKey().getSimpleName() ); + pw.print( '=' ); + printAnnotationValue( pw, entry.getValue() ); + } + pw.print(')'); + } + } + + private static void printAnnotationValue(PrintWriter pw, AnnotationValue value) { + final Object argument = value.getValue(); + if (argument instanceof VariableElement) { + VariableElement variable = (VariableElement) argument; + pw.print( variable.getEnclosingElement() ); + pw.print('.'); + pw.print( variable.getSimpleName().toString() ); + } + else if (argument instanceof AnnotationMirror) { + AnnotationMirror childAnnotation = (AnnotationMirror) argument; + printAnnotation( childAnnotation, pw ); + } + else if (argument instanceof TypeMirror) { + pw.print(argument); + pw.print(".class"); + } + else if (argument instanceof List) { + final List list = + (List) argument; + pw.print('{'); + boolean first = true; + for (AnnotationValue listedValue : list) { + if (first) { + first = false; + } + else { + pw.print(','); + } + printAnnotationValue( pw, listedValue ); + } + pw.print('}'); + } + else { + pw.print( argument ); + } + } + private static void printClassDeclaration(Metamodel entity, PrintWriter pw) { pw.print( "public " ); if ( !entity.isImplementation() && !entity.isJakartaDataStyle() ) { diff --git a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/Context.java b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/Context.java index 95a65d14cb0b..fce3c353c38b 100644 --- a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/Context.java +++ b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/Context.java @@ -412,7 +412,7 @@ public boolean checkNamedQuery(String name) { public void setUsesQuarkusOrm(boolean b) { usesQuarkusOrm = b; } - + public boolean usesQuarkusOrm() { return usesQuarkusOrm; } @@ -420,7 +420,7 @@ public boolean usesQuarkusOrm() { public void setUsesQuarkusReactive(boolean b) { usesQuarkusReactive = b; } - + public boolean usesQuarkusReactive() { return usesQuarkusReactive; } @@ -504,7 +504,21 @@ public Map> getEnumTypesByValue() { return enumTypesByValue; } - public void addEnumValue(String type, String value) { - enumTypesByValue.computeIfAbsent( value, s -> new TreeSet<>() ).add( type ); + public void addEnumValue( + String qualifiedTypeName, String shortTypeName, + @Nullable String outerTypeQualifiedName, @Nullable String outerShortTypeName, + String value) { + addEnumValue( qualifiedTypeName, value ); + addEnumValue( qualifiedTypeName, qualifiedTypeName + '.' + value ); + addEnumValue( qualifiedTypeName, shortTypeName + '.' + value ); + if ( outerShortTypeName != null ) { + addEnumValue( qualifiedTypeName, outerShortTypeName + '.' + shortTypeName + '.' + value ); + addEnumValue( qualifiedTypeName, outerShortTypeName + '$' + shortTypeName + '.' + value ); + addEnumValue( qualifiedTypeName, outerTypeQualifiedName + '$' + shortTypeName + '.' + value ); + } + } + + private void addEnumValue(String qualifiedTypeName, String value) { + enumTypesByValue.computeIfAbsent( value, s -> new TreeSet<>() ).add( qualifiedTypeName ); } } diff --git a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/HibernateProcessor.java b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/HibernateProcessor.java index 65d65b4d2e76..6ab92c1c15b9 100644 --- a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/HibernateProcessor.java +++ b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/HibernateProcessor.java @@ -221,15 +221,18 @@ private boolean handleSettings(ProcessingEnvironment environment) { final PackageElement jakartaContextPackage = context.getProcessingEnvironment().getElementUtils() .getPackageElement( "jakarta.enterprise.context" ); - final PackageElement jakartaTransactionsPackage = + final PackageElement jakartaTransactionPackage = context.getProcessingEnvironment().getElementUtils() - .getPackageElement( "jakarta.transactions" ); + .getPackageElement( "jakarta.transaction" ); final PackageElement jakartaDataPackage = context.getProcessingEnvironment().getElementUtils() .getPackageElement( "jakarta.data" ); final PackageElement quarkusOrmPackage = context.getProcessingEnvironment().getElementUtils() .getPackageElement( "io.quarkus.hibernate.orm" ); + final PackageElement quarkusReactivePackage = + context.getProcessingEnvironment().getElementUtils() + .getPackageElement( "io.quarkus.hibernate.reactive.runtime" ); PackageElement quarkusOrmPanachePackage = context.getProcessingEnvironment().getElementUtils() @@ -251,8 +254,8 @@ && packagePresent(quarkusOrmPanachePackage) ) { context.setAddNonnullAnnotation( packagePresent(jakartaAnnotationPackage) ); context.setAddGeneratedAnnotation( packagePresent(jakartaAnnotationPackage) ); context.setAddDependentAnnotation( packagePresent(jakartaContextPackage) ); - context.setAddTransactionScopedAnnotation( packagePresent(jakartaTransactionsPackage) ); - context.setQuarkusInjection( packagePresent(quarkusOrmPackage) ); + context.setAddTransactionScopedAnnotation( packagePresent(jakartaTransactionPackage) ); + context.setQuarkusInjection( packagePresent(quarkusOrmPackage) || packagePresent(quarkusReactivePackage) ); context.setUsesQuarkusOrm( packagePresent(quarkusOrmPanachePackage) ); context.setUsesQuarkusReactive( packagePresent(quarkusReactivePanachePackage) ); @@ -367,7 +370,7 @@ private void processClasses(RoundEnvironment roundEnvironment) { try { if ( !included( element ) || hasAnnotation( element, Constants.EXCLUDE ) - || hasAnnotation( context.getElementUtils().getPackageOf(element), Constants.EXCLUDE ) ) { + || hasPackageAnnotation( element, Constants.EXCLUDE ) ) { // skip it completely } else if ( isEntityOrEmbeddable( element ) ) { @@ -420,6 +423,11 @@ else if ( element instanceof TypeElement ) { } } + private boolean hasPackageAnnotation(Element element, String annotation) { + final PackageElement pack = context.getElementUtils().getPackageOf( element ); // null for module descriptor + return pack != null && hasAnnotation( pack, annotation ); + } + private void createMetaModelClasses() { for ( Metamodel aux : context.getMetaAuxiliaries() ) { @@ -641,9 +649,15 @@ private void indexEnumValues(TypeMirror type) { final DeclaredType declaredType = (DeclaredType) type; final TypeElement fieldType = (TypeElement) declaredType.asElement(); if ( fieldType.getKind() == ElementKind.ENUM ) { - for (Element enumMember : fieldType.getEnclosedElements() ) { + for ( Element enumMember : fieldType.getEnclosedElements() ) { if ( enumMember.getKind() == ElementKind.ENUM_CONSTANT ) { + final Element enclosingElement = fieldType.getEnclosingElement(); + final boolean hasOuterType = + enclosingElement.getKind().isClass() || enclosingElement.getKind().isInterface(); context.addEnumValue( fieldType.getQualifiedName().toString(), + fieldType.getSimpleName().toString(), + hasOuterType ? ((TypeElement) enclosingElement).getQualifiedName().toString() : null, + hasOuterType ? enclosingElement.getSimpleName().toString() : null, enumMember.getSimpleName().toString() ); } } diff --git a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/AnnotationMeta.java b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/AnnotationMeta.java index 276af407c7d8..4d28e6e20385 100644 --- a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/AnnotationMeta.java +++ b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/AnnotationMeta.java @@ -105,8 +105,10 @@ private void handleNamedQuery(AnnotationMirror mirror, boolean checkHql) { context.getEntityNameMappings(), context.getEnumTypesByValue() ) ); if ( statement instanceof SqmSelectStatement - && isQueryMethodName( name ) ) { + && isQueryMethodName( name ) + && !isJakartaDataStyle() ) { putMember( name, + // TODO: respect @NamedQuery(resultClass) new NamedQueryMethod( this, (SqmSelectStatement) statement, @@ -153,15 +155,17 @@ private void addAuxiliaryMembersForAnnotation(String annotationName, String pref } private void addAuxiliaryMembersForMirror(AnnotationMirror mirror, String prefix) { - mirror.getElementValues().forEach((key, value) -> { - if ( key.getSimpleName().contentEquals("name") ) { - final String name = value.getValue().toString(); - if ( !name.isEmpty() ) { - putMember( prefix + name, - new NameMetaAttribute( this, name, prefix ) ); + if ( !isJakartaDataStyle() ) { + mirror.getElementValues().forEach((key, value) -> { + if ( key.getSimpleName().contentEquals( "name" ) ) { + final String name = value.getValue().toString(); + if ( !name.isEmpty() ) { + putMember( prefix + name, + new NameMetaAttribute( this, name, prefix ) ); + } } - } - }); + }); + } } protected String getSessionVariableName() { diff --git a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/AnnotationMetaEntity.java b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/AnnotationMetaEntity.java index 1083592a0167..2429c83de1bb 100644 --- a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/AnnotationMetaEntity.java +++ b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/AnnotationMetaEntity.java @@ -33,7 +33,6 @@ import org.hibernate.query.sqm.tree.expression.SqmParameter; import org.hibernate.query.sqm.tree.select.SqmSelectStatement; -import javax.annotation.processing.ProcessingEnvironment; import javax.lang.model.element.AnnotationMirror; import javax.lang.model.element.AnnotationValue; import javax.lang.model.element.Element; @@ -51,7 +50,6 @@ import javax.lang.model.type.TypeMirror; import javax.lang.model.type.TypeVariable; import javax.lang.model.type.WildcardType; -import javax.lang.model.util.Elements; import javax.lang.model.util.Types; import javax.tools.Diagnostic; import java.util.ArrayList; @@ -86,10 +84,12 @@ import static org.hibernate.processor.util.TypeUtils.containsAnnotation; import static org.hibernate.processor.util.TypeUtils.determineAccessTypeForHierarchy; import static org.hibernate.processor.util.TypeUtils.determineAnnotationSpecifiedAccessType; +import static org.hibernate.processor.util.TypeUtils.extendsClass; import static org.hibernate.processor.util.TypeUtils.findMappedSuperClass; import static org.hibernate.processor.util.TypeUtils.getAnnotationMirror; import static org.hibernate.processor.util.TypeUtils.getAnnotationValue; import static org.hibernate.processor.util.TypeUtils.hasAnnotation; +import static org.hibernate.processor.util.TypeUtils.implementsInterface; import static org.hibernate.processor.util.TypeUtils.primitiveClassMatchesKind; import static org.hibernate.processor.util.TypeUtils.propertyName; @@ -349,14 +349,15 @@ public boolean isInjectable() { @Override public String scope() { - if (jakartaDataRepository) { - return context.addTransactionScopedAnnotation() - ? "jakarta.transaction.TransactionScoped" - : "jakarta.enterprise.context.RequestScoped"; - } - else { - return "jakarta.enterprise.context.Dependent"; - } + // @TransactionScoped doesn't work here because repositories + // are supposed to be able to demarcate transactions, which + // means they should be injectable when there is no active tx + // @RequestScoped doesn't work because Arc folks think this + // scope should only be active during a HTTP request, which + // is simply wrong according to me, but whatever + // @ApplicationScoped could work in principle, but buys us + // nothing additional, since repositories are stateless + return "jakarta.enterprise.context.Dependent"; } @Override @@ -660,41 +661,18 @@ boolean needsDefaultConstructor() { } private boolean isPanacheType(TypeElement type) { - return isOrmPanacheType( type ) - || isReactivePanacheType( type ); + return context.usesQuarkusOrm() && isOrmPanacheType( type ) + || context.usesQuarkusReactive() && isReactivePanacheType( type ); } private boolean isOrmPanacheType(TypeElement type) { - final ProcessingEnvironment processingEnvironment = context.getProcessingEnvironment(); - final Elements elements = processingEnvironment.getElementUtils(); - final TypeElement panacheRepositorySuperType = elements.getTypeElement( PANACHE_ORM_REPOSITORY_BASE ); - final TypeElement panacheEntitySuperType = elements.getTypeElement( PANACHE_ORM_ENTITY_BASE ); - if ( panacheRepositorySuperType == null || panacheEntitySuperType == null ) { - return false; - } - else { - final Types types = processingEnvironment.getTypeUtils(); - // check against a raw supertype of PanacheRepositoryBase, which .asType() is not - return types.isSubtype( type.asType(), types.getDeclaredType( panacheRepositorySuperType ) ) - || types.isSubtype( type.asType(), panacheEntitySuperType.asType() ); - } + return implementsInterface( type, PANACHE_ORM_REPOSITORY_BASE ) + || extendsClass( type, PANACHE_ORM_ENTITY_BASE ); } private boolean isReactivePanacheType(TypeElement type) { - final ProcessingEnvironment processingEnvironment = context.getProcessingEnvironment(); - final Elements elements = processingEnvironment.getElementUtils(); - final TypeElement panacheRepositorySuperType = elements.getTypeElement( PANACHE_REACTIVE_REPOSITORY_BASE ); - final TypeElement panacheEntitySuperType = elements.getTypeElement( PANACHE_REACTIVE_ENTITY_BASE ); - - if ( panacheRepositorySuperType == null || panacheEntitySuperType == null ) { - return false; - } - else { - final Types types = processingEnvironment.getTypeUtils(); - // check against a raw supertype of PanacheRepositoryBase, which .asType() is not - return types.isSubtype( type.asType(), types.getDeclaredType( panacheRepositorySuperType ) ) - || types.isSubtype( type.asType(), panacheEntitySuperType.asType() ); - } + return implementsInterface( type, PANACHE_REACTIVE_REPOSITORY_BASE ) + || extendsClass( type, PANACHE_REACTIVE_ENTITY_BASE ); } /** @@ -1921,25 +1899,28 @@ private void createSingleParameterFinder( ); break; case NATURAL_ID: - putMember( methodKey, - new NaturalIdFinderMethod( - this, method, - methodName, - returnType.toString(), - containerType, - paramNames, - paramTypes, - parameterNullability(method, entity), - repository, - sessionType[0], - sessionType[1], - profiles, - context.addNonnullAnnotation(), - jakartaDataRepository, - fullReturnType(method) - ) - ); - break; + if ( !usingStatelessSession( sessionType[0] ) ) {// no byNaturalId() lookup API for SS + putMember( methodKey, + new NaturalIdFinderMethod( + this, method, + methodName, + returnType.toString(), + containerType, + paramNames, + paramTypes, + parameterNullability(method, entity), + repository, + sessionType[0], + sessionType[1], + profiles, + context.addNonnullAnnotation(), + jakartaDataRepository, + fullReturnType(method) + ) + ); + break; + } + // else intentionally fall through case BASIC: case MULTIVALUED: final List paramPatterns = parameterPatterns( method ); @@ -2128,7 +2109,8 @@ private void parameterTypeError(TypeElement entityType, VariableElement param, T private boolean finderParameterNullable(TypeElement entity, VariableElement param) { final Element member = memberMatchingPath( entity, parameterName( param ) ); - return member == null || isNullable(member); + return isNullable( param ) + && ( member == null || isNullable( member ) ); } private AccessType getAccessType(TypeElement entity) { @@ -2155,7 +2137,7 @@ private static TypeMirror memberType(Element member) { final AccessType accessType = getAccessType(entityType); final String nextToken = tokens.nextToken(); for ( Element member : context.getAllMembers(entityType) ) { - if ( isIdRef(nextToken) && hasAnnotation( member, ID) ) { + if ( isIdRef(nextToken) && hasAnnotation(member, ID, EMBEDDED_ID) ) { return member; } final Element match = @@ -2705,12 +2687,11 @@ private static boolean constructorMatches( private static boolean parameterMatches(VariableElement parameter, JpaSelection item) { final Class javaType = item.getJavaType(); - return javaType != null && parameterMatches( parameter.asType(), javaType ); + return javaType != null && parameterMatches( parameter.asType(), javaType, item.getJavaTypeName() ); } - private static boolean parameterMatches(TypeMirror parameterType, Class itemType) { + private static boolean parameterMatches(TypeMirror parameterType, Class itemType, String itemTypeName) { final TypeKind kind = parameterType.getKind(); - final String itemTypeName = itemType.getName(); if ( kind == TypeKind.DECLARED ) { final DeclaredType declaredType = (DeclaredType) parameterType; final TypeElement paramTypeElement = (TypeElement) declaredType.asElement(); @@ -2722,7 +2703,7 @@ else if ( kind.isPrimitive() ) { else if ( kind == TypeKind.ARRAY ) { final ArrayType arrayType = (ArrayType) parameterType; return itemType.isArray() - && parameterMatches( arrayType.getComponentType(), itemType.getComponentType() ); + && parameterMatches( arrayType.getComponentType(), itemType.getComponentType(), itemType.getComponentType().getTypeName() ); } else { return false; @@ -2981,30 +2962,29 @@ private static boolean isNullable(Element member) { return false; } case FIELD: + case PARAMETER: if ( member.asType().getKind().isPrimitive() ) { return false; } } - boolean nullable = true; for ( AnnotationMirror mirror : member.getAnnotationMirrors() ) { final TypeElement annotationType = (TypeElement) mirror.getAnnotationType().asElement(); final Name name = annotationType.getQualifiedName(); - if ( name.contentEquals(Constants.ID) ) { - nullable = false; - } - if ( name.contentEquals("jakarta.validation.constraints.NotNull")) { - nullable = false; + if ( name.contentEquals(Constants.ID) + || name.contentEquals(Constants.NOT_NULL) + || name.contentEquals(Constants.NONNULL) ) { + return false; } - if ( name.contentEquals(Constants.BASIC) + else if ( name.contentEquals(Constants.BASIC) || name.contentEquals(Constants.MANY_TO_ONE) || name.contentEquals(Constants.ONE_TO_ONE)) { - AnnotationValue optional = getAnnotationValue(mirror, "optional"); + final AnnotationValue optional = getAnnotationValue(mirror, "optional"); if ( optional != null && optional.getValue().equals(FALSE) ) { - nullable = false; + return false; } } } - return nullable; + return true; } private void checkParameters( diff --git a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/MetaAttributeGenerationVisitor.java b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/MetaAttributeGenerationVisitor.java index abe3c356733d..7b28f26f135e 100644 --- a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/MetaAttributeGenerationVisitor.java +++ b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/MetaAttributeGenerationVisitor.java @@ -29,6 +29,7 @@ import java.util.List; import static org.hibernate.processor.util.Constants.ELEMENT_COLLECTION; +import static org.hibernate.processor.util.Constants.LIST_ATTRIBUTE; import static org.hibernate.processor.util.Constants.MANY_TO_ANY; import static org.hibernate.processor.util.Constants.MANY_TO_MANY; import static org.hibernate.processor.util.Constants.MAP_KEY_CLASS; @@ -72,7 +73,13 @@ private Types typeUtils() { @Override public @Nullable AnnotationMetaAttribute visitArray(ArrayType arrayType, Element element) { - return new AnnotationMetaSingleAttribute( entity, element, toArrayTypeString( arrayType, context ) ); + if ( hasAnnotation( element, MANY_TO_MANY, ONE_TO_MANY, ELEMENT_COLLECTION ) ) { + return new AnnotationMetaCollection( entity, element, LIST_ATTRIBUTE, + toTypeString(arrayType.getComponentType()) ); + } + else { + return new AnnotationMetaSingleAttribute( entity, element, toArrayTypeString( arrayType, context ) ); + } } @Override diff --git a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/NameMetaAttribute.java b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/NameMetaAttribute.java index b2aa1872f46e..3a086348a400 100644 --- a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/NameMetaAttribute.java +++ b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/NameMetaAttribute.java @@ -42,8 +42,11 @@ public String getAttributeDeclarationString() { @Override public String getAttributeNameDeclarationString() { - return new StringBuilder() - .append("public static final ") + final StringBuilder declaration = new StringBuilder(); + if ( !annotationMetaEntity.isJakartaDataStyle() ) { + declaration.append( "public static final " ); + } + return declaration .append(annotationMetaEntity.importType(String.class.getName())) .append(" ") .append(prefix) diff --git a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/NamedQueryMethod.java b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/NamedQueryMethod.java index 3997d80a9cb9..09fa128e88a7 100644 --- a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/NamedQueryMethod.java +++ b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/annotation/NamedQueryMethod.java @@ -67,8 +67,8 @@ public boolean hasStringAttribute() { @Override public String getAttributeDeclarationString() { - final TreeSet> sortedParameters = - new TreeSet<>( select.getSqmParameters() ); + final TreeSet> sortedParameters = new TreeSet<>( SqmParameter.COMPARATOR ); + sortedParameters.addAll( select.getSqmParameters() ); StringBuilder declaration = new StringBuilder(); comment( declaration ); modifiers( declaration ); diff --git a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/util/Constants.java b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/util/Constants.java index 1fa6632df54c..1ceceb79cfb6 100644 --- a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/util/Constants.java +++ b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/util/Constants.java @@ -131,6 +131,8 @@ public final class Constants { public static final String STREAM = "java.util.stream.Stream"; public static final String NULLABLE = "jakarta.annotation.Nullable"; + public static final String NONNULL = "jakarta.annotation.Nonnull"; + public static final String NOT_NULL = "jakarta.validation.constraints.NotNull"; public static final String PANACHE_ORM_REPOSITORY_BASE = "io.quarkus.hibernate.orm.panache.PanacheRepositoryBase"; public static final String PANACHE_ORM_ENTITY_BASE = "io.quarkus.hibernate.orm.panache.PanacheEntityBase"; diff --git a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/util/StringUtil.java b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/util/StringUtil.java index 1d6160635bbe..994857eec775 100644 --- a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/util/StringUtil.java +++ b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/util/StringUtil.java @@ -8,6 +8,10 @@ import java.util.Locale; +import static java.lang.Character.charCount; +import static java.lang.Character.isUpperCase; +import static java.lang.Character.toUpperCase; + /** * @author Hardy Ferentschik */ @@ -102,8 +106,24 @@ public static String nameToMethodName(String name) { return name.replaceAll("[\\s.\\-!@#%=+/*^&|(){}\\[\\],]", "_"); } - public static String getUpperUnderscoreCaseFromLowerCamelCase(String lowerCamelCaseString){ - return lowerCamelCaseString.replaceAll("(.)(\\p{Upper})", "$1_$2").toUpperCase(Locale.ROOT); + public static String getUpperUnderscoreCaseFromLowerCamelCase(String lowerCamelCaseString) { + final StringBuilder result = new StringBuilder(); + int position = 0; + boolean wasLowerCase = false; + while ( position < lowerCamelCaseString.length() ) { + final int codePoint = lowerCamelCaseString.codePointAt( position ); + final boolean isUpperCase = isUpperCase( codePoint ); + if ( wasLowerCase && isUpperCase ) { + result.append('_'); + } + result.appendCodePoint( toUpperCase( codePoint ) ); + position += charCount( codePoint ); + wasLowerCase = !isUpperCase; + } + if ( result.toString().equals( lowerCamelCaseString ) ) { + result.insert(0, '_'); + } + return result.toString(); } private static boolean startsWithSeveralUpperCaseLetters(String string) { diff --git a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/util/TypeUtils.java b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/util/TypeUtils.java index 724dd3c8668f..567cc886ed3b 100644 --- a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/util/TypeUtils.java +++ b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/util/TypeUtils.java @@ -129,12 +129,11 @@ protected TypeMirror defaultAction(TypeMirror e, Void aVoid) { } public static @Nullable TypeElement getSuperclassTypeElement(TypeElement element) { - final TypeMirror superClass = element.getSuperclass(); + final TypeMirror superclass = element.getSuperclass(); //superclass of Object is of NoType which returns some other kind - if ( superClass.getKind() == TypeKind.DECLARED ) { - //F..king Ch...t Have those people used their horrible APIs even once? - final Element superClassElement = ( (DeclaredType) superClass ).asElement(); - return (TypeElement) superClassElement; + if ( superclass.getKind() == TypeKind.DECLARED ) { + final DeclaredType declaredType = (DeclaredType) superclass; + return (TypeElement) declaredType.asElement(); } else { return null; @@ -602,7 +601,7 @@ else if ( element.getKind() == ElementKind.METHOD ) { return elementsUtil.getName(decapitalize(name.substring(3))).toString(); } else if ( name.startsWith( "is" ) ) { - return (elementsUtil.getName(decapitalize(name.substring(2)))).toString(); + return elementsUtil.getName(decapitalize(name.substring(2))).toString(); } return elementsUtil.getName(decapitalize(name)).toString(); } @@ -654,6 +653,33 @@ private static boolean extendsSuperMetaModel(Element superClassElement, boolean || !entityMetaComplete && containsAnnotation( superClassElement, ENTITY, MAPPED_SUPERCLASS ); } + public static boolean implementsInterface(TypeElement type, String interfaceName) { + for ( TypeMirror iface : type.getInterfaces() ) { + if ( iface.getKind() == TypeKind.DECLARED ) { + final DeclaredType declaredType = (DeclaredType) iface; + final TypeElement typeElement = (TypeElement) declaredType.asElement(); + if ( typeElement.getQualifiedName().contentEquals( interfaceName ) + || implementsInterface( typeElement, interfaceName ) ) { + return true; + } + } + } + return false; + } + + public static boolean extendsClass(TypeElement type, String className) { + TypeMirror superclass = type.getSuperclass(); + while ( superclass != null && superclass.getKind() == TypeKind.DECLARED ) { + final DeclaredType declaredType = (DeclaredType) superclass; + final TypeElement typeElement = (TypeElement) declaredType.asElement(); + if ( typeElement.getQualifiedName().contentEquals( className ) ) { + return true; + } + superclass = typeElement.getSuperclass(); + } + return false; + } + static class EmbeddedAttributeVisitor extends SimpleTypeVisitor8<@Nullable TypeElement, Element> { private final Context context; @@ -665,7 +691,7 @@ static class EmbeddedAttributeVisitor extends SimpleTypeVisitor8<@Nullable TypeE public @Nullable TypeElement visitDeclared(DeclaredType declaredType, Element element) { final TypeElement returnedElement = (TypeElement) context.getTypeUtils().asElement( declaredType ); - return containsAnnotation( NullnessUtil.castNonNull( returnedElement ), EMBEDDABLE ) ? returnedElement : null; + return containsAnnotation( castNonNull( returnedElement ), EMBEDDABLE ) ? returnedElement : null; } @Override diff --git a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/validation/MockCollectionPersister.java b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/validation/MockCollectionPersister.java index 7ac32c7a549e..28ef59d27b8d 100644 --- a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/validation/MockCollectionPersister.java +++ b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/validation/MockCollectionPersister.java @@ -12,12 +12,12 @@ import org.hibernate.persister.collection.QueryableCollection; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.type.CollectionType; +import org.hibernate.type.EntityType; import org.hibernate.type.ListType; import org.hibernate.type.MapType; import org.hibernate.type.Type; import static org.hibernate.internal.util.StringHelper.root; -import static org.hibernate.processor.validation.MockSessionFactory.typeConfiguration; /** * @author Gavin King @@ -95,11 +95,11 @@ public Type getKeyType() { @Override public Type getIndexType() { if (collectionType instanceof ListType) { - return typeConfiguration.getBasicTypeForJavaType(Integer.class); + return factory.getTypeConfiguration().getBasicTypeForJavaType(Integer.class); } else if (collectionType instanceof MapType) { //TODO!!! this is incorrect, return the correct key type - return typeConfiguration.getBasicTypeForJavaType(String.class); + return factory.getTypeConfiguration().getBasicTypeForJavaType(String.class); } else { return null; @@ -113,7 +113,7 @@ public Type getElementType() { @Override public Type getIdentifierType() { - return typeConfiguration.getBasicTypeForJavaType(Long.class); + return factory.getTypeConfiguration().getBasicTypeForJavaType(Long.class); } @Override @@ -124,7 +124,7 @@ public boolean hasIndex() { @Override public EntityPersister getElementPersister() { - if (elementType.isEntityType()) { + if (elementType instanceof EntityType ) { return factory.getMetamodel() .entityPersister(elementType.getName()); } @@ -140,7 +140,7 @@ public SessionFactoryImplementor getFactory() { @Override public boolean isOneToMany() { - return elementType.isEntityType(); + return elementType instanceof EntityType; } @Override diff --git a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/validation/MockEntityPersister.java b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/validation/MockEntityPersister.java index db099edb186f..010c206d7a6e 100644 --- a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/validation/MockEntityPersister.java +++ b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/validation/MockEntityPersister.java @@ -13,6 +13,7 @@ import org.hibernate.persister.entity.EntityPersister; import org.hibernate.persister.entity.Queryable; import org.hibernate.tuple.entity.EntityMetamodel; +import org.hibernate.type.BasicType; import org.hibernate.type.Type; import java.io.Serializable; @@ -24,8 +25,6 @@ import java.util.Objects; import java.util.Set; -import static org.hibernate.processor.validation.MockSessionFactory.typeConfiguration; - /** * @author Gavin King */ @@ -117,18 +116,36 @@ public final Type getPropertyType(String propertyPath) { abstract Type createPropertyType(String propertyPath); + /** + * Override on subclasses! + */ + @Override + public String getIdentifierPropertyName() { + return getRootEntityPersister().identifierPropertyName(); + } + + protected abstract String identifierPropertyName(); + + /** + * Override on subclasses! + */ @Override public Type getIdentifierType() { - //TODO: propertyType(getIdentifierPropertyName()) - return typeConfiguration.getBasicTypeForJavaType(Long.class); + return getRootEntityPersister().identifierType(); } + protected abstract Type identifierType(); + + /** + * Override on subclasses! + */ @Override - public String getIdentifierPropertyName() { - //TODO: return the correct @Id property name - return "id"; + public BasicType getVersionType() { + return getRootEntityPersister().versionType(); } + protected abstract BasicType versionType(); + @Override public Type toType(String propertyName) throws QueryException { Type type = getPropertyType(propertyName); @@ -141,13 +158,10 @@ public Type toType(String propertyName) throws QueryException { } @Override - public String getRootEntityName() { - for (MockEntityPersister persister : factory.getMockEntityPersisters()) { - if (this != persister && persister.isSubclassPersister(this)) { - return persister.getRootEntityName(); - } - } - return entityName; + public abstract String getRootEntityName(); + + public MockEntityPersister getRootEntityPersister() { + return factory.createMockEntityPersister(getRootEntityName()); } @Override @@ -204,7 +218,7 @@ public DiscriminatorMetadata getTypeDiscriminatorMetadata() { @Override public Type getResolutionType() { - return typeConfiguration.getBasicTypeForJavaType(Class.class); + return factory.getTypeConfiguration().getBasicTypeForJavaType(Class.class); } @Override @@ -219,7 +233,12 @@ public String toString() { @Override public int getVersionProperty() { - return -66; + return 0; + } + + @Override + public boolean isVersioned() { + return true; } @Override @@ -234,6 +253,11 @@ public boolean consumesEntityAlias() { @Override public Type getDiscriminatorType() { - return typeConfiguration.getBasicTypeForJavaType(String.class); + return factory.getTypeConfiguration().getBasicTypeForJavaType(String.class); + } + + @Override + public boolean isMutable() { + return true; } } diff --git a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/validation/MockSessionFactory.java b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/validation/MockSessionFactory.java index 103cd5c579e3..707e7e584b69 100644 --- a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/validation/MockSessionFactory.java +++ b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/validation/MockSessionFactory.java @@ -6,6 +6,7 @@ */ package org.hibernate.processor.validation; +import jakarta.persistence.metamodel.Bindable; import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.CustomEntityDirtinessStrategy; import org.hibernate.EntityNameResolver; @@ -22,10 +23,12 @@ import org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl; import org.hibernate.boot.model.relational.Database; import org.hibernate.boot.model.relational.SqlStringGenerationContext; -import org.hibernate.boot.registry.BootstrapServiceRegistryBuilder; import org.hibernate.boot.registry.classloading.internal.ClassLoaderServiceImpl; +import org.hibernate.boot.registry.classloading.spi.ClassLoaderService; import org.hibernate.boot.registry.classloading.spi.ClassLoadingException; +import org.hibernate.boot.registry.internal.BootstrapServiceRegistryImpl; import org.hibernate.boot.registry.internal.StandardServiceRegistryImpl; +import org.hibernate.boot.registry.selector.internal.StrategySelectorImpl; import org.hibernate.boot.spi.BootstrapContext; import org.hibernate.boot.spi.MappingDefaults; import org.hibernate.boot.spi.MetadataBuildingContext; @@ -57,16 +60,22 @@ import org.hibernate.metamodel.internal.JpaStaticMetaModelPopulationSetting; import org.hibernate.metamodel.internal.MetadataContext; import org.hibernate.metamodel.internal.RuntimeMetamodelsImpl; +import org.hibernate.metamodel.mapping.EntityIdentifierMapping; import org.hibernate.metamodel.mapping.JdbcMapping; +import org.hibernate.metamodel.model.domain.BasicDomainType; import org.hibernate.metamodel.model.domain.DomainType; +import org.hibernate.metamodel.model.domain.EmbeddableDomainType; import org.hibernate.metamodel.model.domain.EntityDomainType; import org.hibernate.metamodel.model.domain.ManagedDomainType; import org.hibernate.metamodel.model.domain.PersistentAttribute; +import org.hibernate.metamodel.model.domain.SingularPersistentAttribute; import org.hibernate.metamodel.model.domain.internal.AbstractAttribute; import org.hibernate.metamodel.model.domain.internal.AbstractPluralAttribute; import org.hibernate.metamodel.model.domain.internal.BagAttributeImpl; +import org.hibernate.metamodel.model.domain.internal.BasicSqmPathSource; import org.hibernate.metamodel.model.domain.internal.BasicTypeImpl; import org.hibernate.metamodel.model.domain.internal.EmbeddableTypeImpl; +import org.hibernate.metamodel.model.domain.internal.EmbeddedSqmPathSource; import org.hibernate.metamodel.model.domain.internal.EntityTypeImpl; import org.hibernate.metamodel.model.domain.internal.JpaMetamodelImpl; import org.hibernate.metamodel.model.domain.internal.ListAttributeImpl; @@ -102,6 +111,7 @@ import org.hibernate.stat.internal.StatisticsImpl; import org.hibernate.stat.spi.StatisticsImplementor; import org.hibernate.type.BagType; +import org.hibernate.type.BasicType; import org.hibernate.type.CollectionType; import org.hibernate.type.CompositeType; import org.hibernate.type.ListType; @@ -140,9 +150,7 @@ public abstract class MockSessionFactory private static final BasicTypeImpl OBJECT_BASIC_TYPE = new BasicTypeImpl<>(new UnknownBasicJavaType<>(Object.class), ObjectJdbcType.INSTANCE); - // static so other things can get at it - // TODO: make a static instance of this whole object instead! - static TypeConfiguration typeConfiguration; + private final TypeConfiguration typeConfiguration; private final Map entityPersistersByName = new HashMap<>(); private final Map collectionPersistersByName = new HashMap<>(); @@ -155,25 +163,31 @@ public abstract class MockSessionFactory private final MetadataContext metadataContext; public MockSessionFactory() { - - serviceRegistry = StandardServiceRegistryImpl.create( - new BootstrapServiceRegistryBuilder().applyClassLoaderService(new ClassLoaderServiceImpl() { - @Override - @SuppressWarnings("unchecked") - public Class classForName(String className) { - try { - return super.classForName(className); - } - catch (ClassLoadingException e) { - if (isClassDefined(className)) { - return Object[].class; - } - else { - throw e; - } - } + final ClassLoaderService classLoaderService = new ClassLoaderServiceImpl() { + @Override + @SuppressWarnings("unchecked") + public Class classForName(String className) { + try { + return super.classForName( className ); + } + catch (ClassLoadingException e) { + if ( isClassDefined( className ) ) { + return Object[].class; } - }).build(), + else { + throw e; + } + } + } + }; + serviceRegistry = StandardServiceRegistryImpl.create( + new BootstrapServiceRegistryImpl( + true, + classLoaderService, + new StrategySelectorImpl( classLoaderService ), + () -> emptyList() + ), +// new BootstrapServiceRegistryBuilder().applyClassLoaderService( classLoaderService ).build(), singletonList(MockJdbcServicesInitiator.INSTANCE), emptyList(), emptyMap() @@ -338,6 +352,12 @@ public Type getIdentifierType(String className) .getIdentifierType(); } + public BasicType getVersionType(String className) + throws MappingException { + return createEntityPersister(className) + .getVersionType(); + } + @Override public String getIdentifierPropertyName(String className) throws MappingException { @@ -917,9 +937,73 @@ public MockEntityDomainType(String entityName) { metamodel.getJpaMetamodel()); } + @Override + public SingularPersistentAttribute findVersionAttribute() { + final BasicType type = getVersionType(getHibernateEntityName()); + if (type == null) { + return null; + } + else { + return new SingularAttributeImpl<>( + MockEntityDomainType.this, + "{version}", + AttributeClassification.BASIC, + type, + type.getRelationalJavaType(), + null, + false, + true, + false, + false, + metadataContext + ); + } + } + + @Override + public boolean hasVersionAttribute() { + return getVersionType(getHibernateEntityName()) != null; + } + + @Override + public SqmPathSource getIdentifierDescriptor() { + final Type type = getIdentifierType(getHibernateEntityName()); + if (type == null) { + return null; + } + else if (type instanceof BasicDomainType) { + return new BasicSqmPathSource<>( + EntityIdentifierMapping.ID_ROLE_NAME, + null, + (BasicDomainType) type, + MockEntityDomainType.this.getExpressibleJavaType(), + Bindable.BindableType.SINGULAR_ATTRIBUTE, + false + ); + } + else if (type instanceof EmbeddableDomainType) { + return new EmbeddedSqmPathSource<>( + EntityIdentifierMapping.ID_ROLE_NAME, + null, + (EmbeddableDomainType) type, + Bindable.BindableType.SINGULAR_ATTRIBUTE, + false + ); + } + else { + return null; + } + } + @Override public SqmPathSource findSubPathSource(String name, JpaMetamodelImplementor metamodel) { - SqmPathSource source = super.findSubPathSource(name, metamodel); + switch (name) { + case EntityIdentifierMapping.ID_ROLE_NAME: + return getIdentifierDescriptor(); + case "{version}": + return findVersionAttribute(); + } + final SqmPathSource source = super.findSubPathSource(name, metamodel); if ( source != null ) { return source; } diff --git a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/validation/ProcessorSessionFactory.java b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/validation/ProcessorSessionFactory.java index abbbc9967a29..1925e82af4c5 100644 --- a/tooling/metamodel-generator/src/main/java/org/hibernate/processor/validation/ProcessorSessionFactory.java +++ b/tooling/metamodel-generator/src/main/java/org/hibernate/processor/validation/ProcessorSessionFactory.java @@ -10,6 +10,7 @@ import org.checkerframework.checker.nullness.qual.Nullable; import org.hibernate.PropertyNotFoundException; import org.hibernate.engine.spi.Mapping; +import org.hibernate.metamodel.mapping.EntityIdentifierMapping; import org.hibernate.type.BasicType; import org.hibernate.type.CollectionType; import org.hibernate.type.CompositeType; @@ -58,7 +59,6 @@ import static org.hibernate.internal.util.StringHelper.root; import static org.hibernate.internal.util.StringHelper.split; import static org.hibernate.internal.util.StringHelper.unroot; -import static org.hibernate.metamodel.model.domain.internal.JpaMetamodelImpl.addAllowedEnumLiteralsToEnumTypesMap; import static org.hibernate.processor.util.Constants.JAVA_OBJECT; /** @@ -91,7 +91,7 @@ public static MockSessionFactory create( private final Types typeUtil; private final Filer filer; private final Map entityNameMappings; - private final Map> allowedEnumLiteralsToEnumTypeNames; + private final Map> enumTypesByValue; public ProcessorSessionFactory( ProcessingEnvironment processingEnvironment, @@ -101,23 +101,7 @@ public ProcessorSessionFactory( typeUtil = processingEnvironment.getTypeUtils(); filer = processingEnvironment.getFiler(); this.entityNameMappings = entityNameMappings; - final Map> allowedEnumLiteralsToEnumTypeNames = new HashMap<>( enumTypesByValue.size() << 2 ); - for ( Map.Entry> entry : enumTypesByValue.entrySet() ) { - final String enumConstantName = entry.getKey(); - for ( String enumClassName : entry.getValue() ) { - final TypeElement enumTypeElement = elementUtil.getTypeElement( enumClassName ); - if ( enumTypeElement != null ) { - addAllowedEnumLiteralsToEnumTypesMap( - allowedEnumLiteralsToEnumTypeNames, - enumConstantName, - enumTypeElement.getSimpleName().toString(), - elementUtil.getBinaryName( enumTypeElement ).toString(), - enumClassName - ); - } - } - } - this.allowedEnumLiteralsToEnumTypeNames = allowedEnumLiteralsToEnumTypeNames; + this.enumTypesByValue = enumTypesByValue; } @Override @@ -148,6 +132,7 @@ else if (isElementCollectionProperty(property)) { } } + @Override Type propertyType(String typeName, String propertyPath) { TypeElement type = findClassByQualifiedName(typeName); @@ -178,13 +163,13 @@ private static Element dereference(AccessType defaultAccessType, Element symbol, } } - static Type propertyType(Element member, String entityName, String path, AccessType defaultAccessType) { + private Type propertyType(Element member, String entityName, String path, AccessType defaultAccessType) { final TypeMirror memberType = memberType(member); if (isEmbeddedProperty(member)) { - return component.make(asElement(memberType), entityName, path, defaultAccessType); + return component.make(asElement(memberType), entityName, path, defaultAccessType, this); } else if (isToOneAssociation(member)) { - return new ManyToOneType(typeConfiguration, getToOneTargetEntity(member)); + return new ManyToOneType(getTypeConfiguration(), getToOneTargetEntity(member)); } else if (isToManyAssociation(member)) { return collectionType(memberType, qualify(entityName, path)); @@ -193,10 +178,10 @@ else if (isElementCollectionProperty(member)) { return collectionType(memberType, qualify(entityName, path)); } else if (isEnumProperty(member)) { - return enumType( member, memberType ); + return enumType(member, memberType); } else { - return typeConfiguration.getBasicTypeRegistry() + return getTypeConfiguration().getBasicTypeRegistry() .getRegisteredType(qualifiedName(memberType)); } } @@ -234,7 +219,7 @@ private static JdbcType enumJdbcType(Element member) { @Override @Nullable Set getEnumTypesForValue(String value) { - Set result = allowedEnumLiteralsToEnumTypeNames.get( value); + Set result = enumTypesByValue.get(value); if ( result != null ) { return result; } @@ -255,12 +240,13 @@ Set getEnumTypesForValue(String value) { private static Type elementCollectionElementType(TypeElement elementType, String role, String path, - AccessType defaultAccessType) { + AccessType defaultAccessType, + MockSessionFactory factory) { if (isEmbeddableType(elementType)) { - return component.make(elementType, role, path, defaultAccessType); + return component.make(elementType, role, path, defaultAccessType, factory); } else { - return typeConfiguration.getBasicTypeRegistry() + return factory.getTypeConfiguration().getBasicTypeRegistry() .getRegisteredType(qualifiedName(elementType.asType())); } } @@ -277,7 +263,8 @@ public static abstract class Component implements CompositeType { public Component(TypeElement type, String entityName, String path, - AccessType defaultAccessType) { + AccessType defaultAccessType, + ProcessorSessionFactory factory) { this.type = type; List names = new ArrayList<>(); @@ -290,7 +277,7 @@ public Component(TypeElement type, if (isPersistable(member, accessType)) { String name = propertyName(member); Type propertyType = - propertyType(member, entityName, + factory.propertyType(member, entityName, qualify(path, name), defaultAccessType); if (propertyType != null) { names.add(name); @@ -358,14 +345,31 @@ public int getColumnSpan(Mapping mapping) { public static abstract class EntityPersister extends MockEntityPersister { private final TypeElement type; private final Types typeUtil; + private final ProcessorSessionFactory factory; - public EntityPersister(String entityName, TypeElement type, ProcessorSessionFactory that) { - super(entityName, getDefaultAccessType(type), that); + public EntityPersister(String entityName, TypeElement type, ProcessorSessionFactory factory) { + super(entityName, getDefaultAccessType(type), factory); this.type = type; - this.typeUtil = that.typeUtil; + this.typeUtil = factory.typeUtil; + this.factory = factory; initSubclassPersisters(); } + @Override + public String getRootEntityName() { + TypeElement result = type; + TypeMirror superclass = type.getSuperclass(); + while ( superclass!=null && superclass.getKind() == TypeKind.DECLARED ) { + final DeclaredType declaredType = (DeclaredType) superclass; + final TypeElement typeElement = (TypeElement) declaredType.asElement(); + if ( hasAnnotation(typeElement, "Entity") ) { + result = typeElement; + } + superclass = typeElement.getSuperclass(); + } + return ProcessorSessionFactory.getEntityName(result); + } + @Override boolean isSubclassPersister(MockEntityPersister entityPersister) { EntityPersister persister = (EntityPersister) entityPersister; @@ -376,15 +380,44 @@ boolean isSubclassPersister(MockEntityPersister entityPersister) { Type createPropertyType(String propertyPath) { Element symbol = findPropertyByPath(type, propertyPath, defaultAccessType); return symbol == null ? null : - propertyType(symbol, getEntityName(), propertyPath, defaultAccessType); + factory.propertyType(symbol, getEntityName(), propertyPath, defaultAccessType); } + @Override + public String identifierPropertyName() { + for (Element element : type.getEnclosedElements()) { + if ( hasAnnotation(element, "Id") || hasAnnotation(element, "EmbeddedId") ) { + return element.getSimpleName().toString(); + } + } + return "id"; + } + + @Override + public Type identifierType() { + for (Element element : type.getEnclosedElements()) { + if ( hasAnnotation(element, "Id")|| hasAnnotation(element, "EmbeddedId") ) { + return factory.propertyType(element, getEntityName(), EntityIdentifierMapping.ID_ROLE_NAME, defaultAccessType); + } + } + return null; + } + + @Override + public BasicType versionType() { + for (Element element : type.getEnclosedElements()) { + if ( hasAnnotation(element, "Version") ) { + return (BasicType) factory.propertyType(element, getEntityName(), "{version}", defaultAccessType); + } + } + return null; + } } public abstract static class ToManyAssociationPersister extends MockCollectionPersister { public ToManyAssociationPersister(String role, CollectionType collectionType, String targetEntityName, ProcessorSessionFactory that) { super(role, collectionType, - new ManyToOneType(typeConfiguration, targetEntityName), + new ManyToOneType(that.getTypeConfiguration(), targetEntityName), that); } @@ -397,26 +430,29 @@ Type getElementPropertyType(String propertyPath) { public abstract static class ElementCollectionPersister extends MockCollectionPersister { private final TypeElement elementType; private final AccessType defaultAccessType; + private final ProcessorSessionFactory factory; public ElementCollectionPersister(String role, CollectionType collectionType, TypeElement elementType, String propertyPath, AccessType defaultAccessType, - ProcessorSessionFactory that) { + ProcessorSessionFactory factory) { super(role, collectionType, elementCollectionElementType(elementType, role, - propertyPath, defaultAccessType), - that); + propertyPath, defaultAccessType, + factory), + factory); this.elementType = elementType; this.defaultAccessType = defaultAccessType; + this.factory = factory; } @Override Type getElementPropertyType(String propertyPath) { Element symbol = findPropertyByPath(elementType, propertyPath, defaultAccessType); return symbol == null ? null : - propertyType(symbol, getOwnerEntityName(), propertyPath, defaultAccessType); + factory.propertyType(symbol, getOwnerEntityName(), propertyPath, defaultAccessType); } } diff --git a/tooling/metamodel-generator/src/test/java/org/hibernate/processor/test/hhh18858/ArrayTest.java b/tooling/metamodel-generator/src/test/java/org/hibernate/processor/test/hhh18858/ArrayTest.java new file mode 100644 index 000000000000..3c63aa131f6b --- /dev/null +++ b/tooling/metamodel-generator/src/test/java/org/hibernate/processor/test/hhh18858/ArrayTest.java @@ -0,0 +1,91 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.processor.test.hhh18858; + + +import jakarta.persistence.metamodel.ListAttribute; +import jakarta.persistence.metamodel.SingularAttribute; +import org.hibernate.processor.test.util.CompilationTest; +import org.hibernate.processor.test.util.WithClasses; +import org.junit.Test; +import org.junit.jupiter.api.Assertions; + +import java.lang.reflect.Field; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; +import java.util.Collection; + +import static org.hibernate.processor.test.util.TestUtil.getFieldFromMetamodelFor; +import static org.hibernate.processor.test.util.TestUtil.getMetaModelSourceAsString; +import static org.junit.jupiter.api.Assertions.assertEquals; + +/** + * @author Emmanuel Bernard + */ +public class ArrayTest extends CompilationTest { + + @Test + @WithClasses({Competitor.class, Contest.class}) + public void testOneToMany() throws NoSuchFieldException, IllegalAccessException { + System.out.println( getMetaModelSourceAsString( Competitor.class ) ); + assertValidMetamodelField( Competitor.class, "id" ); + assertValidMetamodelField( Competitor.class, "name" ); + + System.out.println( getMetaModelSourceAsString( Contest.class ) ); + assertValidMetamodelField( Contest.class, "id" ); + assertValidMetamodelField( Contest.class, "results" ); + assertValidMetamodelField( Contest.class, "heldIn" ); + } + + private void assertValidMetamodelField(Class entityClass, String fieldName) + throws NoSuchFieldException, IllegalAccessException { + final Field entityField = entityClass.getDeclaredField( fieldName ); + final Class entityFieldType = entityField.getType(); + + final Field modelField = getFieldFromMetamodelFor( entityClass, fieldName ); + final Type modelFieldGenericType = modelField.getGenericType(); + if (modelFieldGenericType instanceof ParameterizedType) { + ParameterizedType parametrized = (ParameterizedType) modelFieldGenericType; + final Type[] typeArguments = parametrized.getActualTypeArguments(); + assertEquals( 2, typeArguments.length ); + assertEquals( entityClass, typeArguments[0] ); + if ( Collection.class.isAssignableFrom( entityFieldType ) || entityFieldType.isArray() ) { + assertEquals( ListAttribute.class, parametrized.getRawType() ); + if ( Collection.class.isAssignableFrom( entityFieldType ) ) { + final ParameterizedType entityFieldGenericType = (ParameterizedType) entityField.getGenericType(); + assertEquals( entityFieldGenericType.getActualTypeArguments()[0], typeArguments[1] ); + } + else if ( entityFieldType.getComponentType().isPrimitive() ) { + assertEquals( + entityFieldType.getComponentType(), + ((Class) typeArguments[1]).getDeclaredField( "TYPE" ).get( null ) + ); + } + else { + assertEquals( entityFieldType.getComponentType(), typeArguments[1] ); + } + } + else { + assertEquals( SingularAttribute.class, parametrized.getRawType() ); + if ( entityFieldType.isPrimitive() ) { + assertEquals( + entityFieldType, + ((Class) typeArguments[1]).getDeclaredField( "TYPE" ).get( null ) + ); + } + else { + assertEquals( entityFieldType, typeArguments[1] ); + } + } + } + else { + Assertions.fail(); + } + + } + +} diff --git a/tooling/metamodel-generator/src/test/java/org/hibernate/processor/test/hhh18858/Competitor.java b/tooling/metamodel-generator/src/test/java/org/hibernate/processor/test/hhh18858/Competitor.java new file mode 100644 index 000000000000..a8b0677cf0cf --- /dev/null +++ b/tooling/metamodel-generator/src/test/java/org/hibernate/processor/test/hhh18858/Competitor.java @@ -0,0 +1,37 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.processor.test.hhh18858; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; + +/** + * @author Emmanuel Bernard + */ +@Entity +public class Competitor { + private int id; + private String name; + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + @Id + @GeneratedValue + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } +} diff --git a/tooling/metamodel-generator/src/test/java/org/hibernate/processor/test/hhh18858/Contest.java b/tooling/metamodel-generator/src/test/java/org/hibernate/processor/test/hhh18858/Contest.java new file mode 100644 index 000000000000..761ad7b287b2 --- /dev/null +++ b/tooling/metamodel-generator/src/test/java/org/hibernate/processor/test/hhh18858/Contest.java @@ -0,0 +1,62 @@ +/* + * Hibernate, Relational Persistence for Idiomatic Java + * + * License: GNU Lesser General Public License (LGPL), version 2.1 or later + * See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html + */ +package org.hibernate.processor.test.hhh18858; + +import jakarta.persistence.CascadeType; +import jakarta.persistence.ElementCollection; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.OneToMany; +import jakarta.persistence.OrderColumn; + +import org.hibernate.annotations.ListIndexBase; + +/** + * @author Emmanuel Bernard + */ +@Entity +public class Contest { + private int id; + private Competitor[] results; + private int[] heldIn; + + @Id + @GeneratedValue + public int getId() { + return id; + } + + public void setId(int id) { + this.id = id; + } + + @OneToMany(cascade = CascadeType.ALL) + @OrderColumn(name = "pos") + public Competitor[] getResults() { + return results; + } + + public void setResults(Competitor[] results) { + this.results = results; + } + + @ElementCollection + @OrderColumn + @ListIndexBase( 1 ) + public int[] getHeldIn() { + return heldIn; + } + + public void setHeldIn(int[] heldIn) { + this.heldIn = heldIn; + } + + /*public enum Month { + January, February, March, April, May, June, July, August, September, October, November, December; + };*/ +} diff --git a/tooling/metamodel-generator/src/test/java/org/hibernate/processor/test/uppercase/Person.java b/tooling/metamodel-generator/src/test/java/org/hibernate/processor/test/uppercase/Person.java new file mode 100644 index 000000000000..078e9e4b1b77 --- /dev/null +++ b/tooling/metamodel-generator/src/test/java/org/hibernate/processor/test/uppercase/Person.java @@ -0,0 +1,14 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.processor.test.uppercase; + +import jakarta.persistence.Entity; +import jakarta.persistence.Id; + +@Entity +public class Person { + @Id String SSN; + String UserID; +} diff --git a/tooling/metamodel-generator/src/test/java/org/hibernate/processor/test/uppercase/UppercaseTest.java b/tooling/metamodel-generator/src/test/java/org/hibernate/processor/test/uppercase/UppercaseTest.java new file mode 100644 index 000000000000..d765288a8246 --- /dev/null +++ b/tooling/metamodel-generator/src/test/java/org/hibernate/processor/test/uppercase/UppercaseTest.java @@ -0,0 +1,29 @@ +/* + * SPDX-License-Identifier: LGPL-2.1-or-later + * Copyright Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.processor.test.uppercase; + +import org.hibernate.processor.test.util.CompilationTest; +import org.hibernate.processor.test.util.WithClasses; +import org.junit.Test; + +import static org.hibernate.processor.test.util.TestUtil.assertMetamodelClassGeneratedFor; +import static org.hibernate.processor.test.util.TestUtil.assertPresenceOfFieldInMetamodelFor; +import static org.hibernate.processor.test.util.TestUtil.getMetaModelSourceAsString; + +public class UppercaseTest extends CompilationTest { + + @Test + @WithClasses(value = Person.class) + public void test() { + System.out.println( getMetaModelSourceAsString( Person.class ) ); + + assertMetamodelClassGeneratedFor( Person.class ); + + assertPresenceOfFieldInMetamodelFor( Person.class, "SSN" ); + assertPresenceOfFieldInMetamodelFor( Person.class, "_SSN" ); + assertPresenceOfFieldInMetamodelFor( Person.class, "UserID" ); + assertPresenceOfFieldInMetamodelFor( Person.class, "USER_ID" ); + } +}