diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 4d1c7e327..fbc73fcb5 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -16,3 +16,53 @@ updates: allow: - dependency-name: "actions/*" - dependency-name: "redhat-actions/*" + + - package-ecosystem: "gradle" + directory: "/" + schedule: + interval: "weekly" + day: "tuesday" + open-pull-requests-limit: 20 + groups: + hibernate-validator: + patterns: + - "org.hibernate.validator*" + - "org.glassfish.expressly*" + hibernate: + patterns: + - "org.hibernate*" + vertx: + patterns: + - "io.vertx*" + mutiny: + patterns: + - "io.smallrye.reactive*" + # Testcontainers plus the JDBC driver we need for testing + testcontainers: + patterns: + - "org.testcontainers*" + - "com.ibm.db2*" + - "com.microsoft.sqlserver*" + - "org.postgresql*" + - "con.ongres.scram*" + - "com.fasterxml.jackson.core*" + - "com.mysql*" + - "org.mariadb.jdbc*" + + ignore: + # For Hibernate Validator, we will need to update major version manually as needed (but we only use it in tests) + - dependency-name: "org.glassfish.expressly*" + update-types: ["version-update-:semver-major"] + # Only patches for Hibernate ORM and Vert.x + - dependency-name: "org.hibernate*" + update-types: ["version-update:semver-major", "version-update:semver-minor"] + - dependency-name: "io.vertx*" + update-types: ["version-update:semver-major", "version-update:semver-minor"] + + # Dockerfiles in tooling/docker/, and database services we use for examples (MySQL and PostgreSQL) + - package-ecosystem: "docker" + directory: "/tooling/docker" + schedule: + interval: "weekly" + allow: + - dependency-type: "all" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 63f7c7764..7e3559931 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -49,8 +49,7 @@ jobs: services: # Label used to access the service container mysql: - # Docker Hub image - image: mysql:9.2.0 + image: container-registry.oracle.com/mysql/community-server:9.3.0 env: MYSQL_ROOT_PASSWORD: hreact MYSQL_DATABASE: hreact @@ -191,8 +190,9 @@ jobs: # and it's useful to test that. - { name: "20", java_version_numeric: 20, jvm_args: '--enable-preview' } - { name: "21", java_version_numeric: 21, jvm_args: '--enable-preview' } - - { name: "24", java_version_numeric: 24, from: 'jdk.java.net', jvm_args: '--enable-preview' } - - { name: "25-ea", java_version_numeric: 25, from: 'jdk.java.net', jvm_args: '--enable-preview' } + - { name: "24", java_version_numeric: 24, jvm_args: '--enable-preview' } + - { name: "25", java_version_numeric: 25, from: 'jdk.java.net', jvm_args: '--enable-preview' } + - { name: "26-ea", java_version_numeric: 26, from: 'jdk.java.net', jvm_args: '--enable-preview' } steps: - name: Checkout ${{ inputs.branch }} uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 diff --git a/README.md b/README.md index 6047808fb..5e4cbf2d4 100644 --- a/README.md +++ b/README.md @@ -31,21 +31,25 @@ Learn more at . Hibernate Reactive has been tested with: - Java 17, 21, 24 -- PostgreSQL 16 +- PostgreSQL 17 - MySQL 9 - MariaDB 11 - Db2 12 -- CockroachDB v24 -- MS SQL Server 2022 +- CockroachDB v25 +- MS SQL Server 2025 - Oracle 23 -- [Hibernate ORM][] 7.0.0.Final -- [Vert.x Reactive PostgreSQL Client](https://vertx.io/docs/vertx-pg-client/java/) 4.5.15 -- [Vert.x Reactive MySQL Client](https://vertx.io/docs/vertx-mysql-client/java/) 4.5.15 -- [Vert.x Reactive Db2 Client](https://vertx.io/docs/vertx-db2-client/java/) 4.5.15 -- [Vert.x Reactive MS SQL Server Client](https://vertx.io/docs/vertx-mssql-client/java/) 4.5.15 -- [Vert.x Reactive Oracle Client](https://vertx.io/docs/vertx-oracle-client/java/) 4.5.15 +- [Hibernate ORM][] 7.1 +- [Vert.x Reactive PostgreSQL Client](https://vertx.io/docs/vertx-pg-client/java/) 4.5 +- [Vert.x Reactive MySQL Client](https://vertx.io/docs/vertx-mysql-client/java/) 4.5 +- [Vert.x Reactive Db2 Client](https://vertx.io/docs/vertx-db2-client/java/) 4.5 +- [Vert.x Reactive MS SQL Server Client](https://vertx.io/docs/vertx-mssql-client/java/) 4.5 +- [Vert.x Reactive Oracle Client](https://vertx.io/docs/vertx-oracle-client/java/) 4.5 - [Quarkus][Quarkus] via the Hibernate Reactive extension +The exact version of the libraries and images are in the +[catalog](https://github.com/hibernate/hibernate-reactive/blob/3.1/gradle/libs.versions.toml) +and in the [tooling/docker](https://github.com/hibernate/hibernate-reactive/tree/3.1/tooling/docker) folder. + [PostgreSQL]: https://www.postgresql.org [MySQL]: https://www.mysql.com [MariaDB]: https://mariadb.com diff --git a/build.gradle b/build.gradle index 452f7ea2c..d6a172b6f 100644 --- a/build.gradle +++ b/build.gradle @@ -3,33 +3,14 @@ plugins { id 'java-library' id 'maven-publish' - id 'com.diffplug.spotless' version '6.25.0' - id 'org.asciidoctor.jvm.convert' version '4.0.2' apply false - id 'io.github.gradle-nexus.publish-plugin' version '1.3.0' + alias(libs.plugins.com.diffplug.spotless) + alias(libs.plugins.org.asciidoctor.jvm.convert) apply false } group = "org.hibernate.reactive" // leverage the ProjectVersion which comes from the `local.versions` plugin version = project.projectVersion.fullName -// Versions which need to be aligned across modules; this also -// allows overriding the build using a parameter, which can be -// useful to monitor compatibility for upcoming versions on CI: -// -// ./gradlew clean build -PhibernateOrmVersion=5.6.15-SNAPSHOT -ext { - // Mainly, to allow CI to test the latest versions of Vert.X - // Example: - // ./gradlew build -PvertxSqlClientVersion=4.0.0-SNAPSHOT - if ( !project.hasProperty( 'vertxSqlClientVersion' ) ) { - vertxSqlClientVersion = '4.5.15' - } - - testcontainersVersion = '1.21.0' - - logger.lifecycle "Vert.x SQL Client Version: " + project.vertxSqlClientVersion -} - subprojects { apply plugin: 'java-library' apply plugin: 'com.diffplug.spotless' @@ -56,9 +37,9 @@ subprojects { // Useful for local development, it should be disabled otherwise mavenLocal() } - // Example: ./gradlew build -PenableSonatypeOpenSourceSnapshotsRep - if ( project.hasProperty('enableSonatypeOpenSourceSnapshotsRep') ) { - maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' } + // Example: ./gradlew build -PenableCentralSonatypeSnapshotsRep + if ( project.hasProperty('enableCentralSonatypeSnapshotsRep') ) { + maven { url 'https://central.sonatype.com/repository/maven-snapshots/' } } mavenCentral() @@ -70,6 +51,12 @@ subprojects { options.encoding = 'UTF-8' } + // Configure test tasks for all subprojects + tasks.withType( Test ).configureEach { + // Set the project root for finding Docker files - available to all modules + systemProperty 'hibernate.reactive.project.root', rootProject.projectDir.absolutePath + } + if ( !gradle.ext.javaToolchainEnabled ) { sourceCompatibility = JavaVersion.toVersion( gradle.ext.baselineJavaVersion ) targetCompatibility = JavaVersion.toVersion( gradle.ext.baselineJavaVersion ) @@ -135,3 +122,10 @@ subprojects { } } +rootProject.afterEvaluate { + // Workaround since "libs.versions.NAME" notation cannot be used here + def libs = project.extensions.getByType(VersionCatalogsExtension).named('libs') + logger.lifecycle "ORM version: ${libs.findVersion('hibernateOrmVersion').get().requiredVersion}" + logger.lifecycle "ORM Gradle plugin version: ${libs.findVersion('hibernateOrmGradlePluginVersion').get().requiredVersion}" + logger.lifecycle "Vert.x SQL Client version: ${libs.findVersion('vertxSqlClientVersion').get().requiredVersion}" +} diff --git a/ci/release/Jenkinsfile b/ci/release/Jenkinsfile index 8627950a1..9a631cf31 100644 --- a/ci/release/Jenkinsfile +++ b/ci/release/Jenkinsfile @@ -1,9 +1,7 @@ #! /usr/bin/groovy /* - * 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 */ /* @@ -17,11 +15,10 @@ import org.hibernate.jenkins.pipeline.helpers.version.Version // Global build configuration env.PROJECT = "reactive" env.JIRA_KEY = "HREACT" -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 +31,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 } @@ -53,6 +57,7 @@ def checkoutReleaseScripts() { } } + // -------------------------------------------- // Pipeline @@ -60,12 +65,15 @@ pipeline { agent { label 'Release' } + triggers { + // Run every week Sunday 1 AM + cron('0 1 * * 0') + } tools { jdk 'OpenJDK 17 Latest' } options { buildDiscarder logRotator(daysToKeepStr: '30', numToKeepStr: '10') - rateLimitBuilds(throttle: [count: 1, durationName: 'day', userBoost: true]) disableConcurrentBuilds(abortPrevious: false) preserveStashes() } @@ -89,9 +97,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 +119,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,14 +132,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).trim() - def secondLastCommitter = sh(script: 'git show -s --format=\'%an\' HEAD~1', returnStdout: true).trim() - echo "Last two commits were performed by '${lastCommitter}'/'${secondLastCommitter}'." - - 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 } @@ -149,17 +164,12 @@ pipeline { env.RELEASE_VERSION = releaseVersion.toString() env.DEVELOPMENT_VERSION = developmentVersion.toString() - // Dry run is not supported at the moment - env.SCRIPT_OPTIONS = params.RELEASE_DRY_RUN ? "-d" : "" + 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 - // This step doesn't work for Hibernate Reactive (the project has been created with a different type on JIRA) - // sh ".release/scripts/determine-jira-version-id.sh ${env.JIRA_KEY} ${releaseVersion.withoutFinalQualifier}" } } } - stage('Release prepare') { + stage('Prepare') { steps { script { checkoutReleaseScripts() @@ -168,13 +178,13 @@ pipeline { configFile(fileId: 'release.config.ssh', targetLocation: "${env.HOME}/.ssh/config"), configFile(fileId: 'release.config.ssh.knownhosts', targetLocation: "${env.HOME}/.ssh/known_hosts") ]) { - - sshagent(['ed25519.Hibernate-CI.github.com', 'hibernate.filemgmt.jboss.org', 'hibernate-ci.frs.sourceforge.net']) { + sshagent(['ed25519.Hibernate-CI.github.com']) { // 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'" ]) { @@ -185,7 +195,7 @@ pipeline { } } } - stage('Publish release') { + stage('Publish') { steps { script { checkoutReleaseScripts() @@ -195,22 +205,48 @@ pipeline { configFile(fileId: 'release.config.ssh.knownhosts', targetLocation: "${env.HOME}/.ssh/known_hosts") ]) { withCredentials([ - // TODO: Once we switch to maven-central publishing (from nexus2) we need to add a new credentials - // to use the following env variable names to set the user/password: - // - JRELEASER_MAVENCENTRAL_USERNAME - // - JRELEASER_MAVENCENTRAL_TOKEN - // Also use the new `credentialsId` for Maven Central, e.g.: - // usernamePassword(credentialsId: '???????', passwordVariable: 'JRELEASER_MAVENCENTRAL_TOKEN', usernameVariable: 'JRELEASER_MAVENCENTRAL_USERNAME'), - usernamePassword(credentialsId: 'ossrh.sonatype.org', passwordVariable: 'JRELEASER_NEXUS2_PASSWORD', usernameVariable: 'JRELEASER_NEXUS2_USERNAME'), - 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: 'JRELEASER_GPG_PASSPHRASE'), - string(credentialsId: 'Hibernate-CI.github.com', variable: 'JRELEASER_GITHUB_TOKEN') + usernamePassword(credentialsId: 'central.sonatype.com', passwordVariable: 'JRELEASER_MAVENCENTRAL_TOKEN', usernameVariable: 'JRELEASER_MAVENCENTRAL_USERNAME'), + 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: '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']) { // performs documentation upload and Sonatype release // push to github - sh ".release/scripts/publish.sh -j ${env.SCRIPT_OPTIONS} ${env.PROJECT} ${env.RELEASE_VERSION} ${env.DEVELOPMENT_VERSION} ${env.GIT_BRANCH}" + withEnv([ + "DISABLE_REMOTE_GRADLE_CACHE=true" + ]) { + def ghReleaseNote = sh('realpath -e github_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('Update website') { + steps { + script { + checkoutReleaseScripts() + + configFileProvider([ + configFile(fileId: 'release.config.ssh', targetLocation: "${env.HOME}/.ssh/config"), + configFile(fileId: 'release.config.ssh.knownhosts', targetLocation: "${env.HOME}/.ssh/known_hosts") + ]) { + withCredentials([ + gitUsernamePassword(credentialsId: 'username-and-token.Hibernate-CI.github.com', gitToolName: 'Default') + ]) { + 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}" + } } } } diff --git a/ci/snapshot-publish.Jenkinsfile b/ci/snapshot-publish.Jenkinsfile index fea160b23..7fb8f9444 100644 --- a/ci/snapshot-publish.Jenkinsfile +++ b/ci/snapshot-publish.Jenkinsfile @@ -40,10 +40,9 @@ pipeline { steps { script { withCredentials([ - // https://github.com/gradle-nexus/publish-plugin#publishing-to-maven-central-via-sonatype-ossrh // TODO: Once we switch to maven-central publishing (from nexus2) we need to update credentialsId: // https://docs.gradle.org/current/samples/sample_publishing_credentials.html#:~:text=via%20environment%20variables - usernamePassword(credentialsId: 'ossrh.sonatype.org', passwordVariable: 'ORG_GRADLE_PROJECT_snapshotsPassword', usernameVariable: 'ORG_GRADLE_PROJECT_snapshotsUsername'), + usernamePassword(credentialsId: 'central.sonatype.com', passwordVariable: 'ORG_GRADLE_PROJECT_snapshotsPassword', usernameVariable: 'ORG_GRADLE_PROJECT_snapshotsUsername'), gitUsernamePassword(credentialsId: 'username-and-token.Hibernate-CI.github.com', gitToolName: 'Default'), string(credentialsId: 'Hibernate-CI.github.com', variable: 'JRELEASER_GITHUB_TOKEN') ]) { diff --git a/documentation/build.gradle b/documentation/build.gradle index aa6a27622..95c3dcb2b 100644 --- a/documentation/build.gradle +++ b/documentation/build.gradle @@ -52,7 +52,7 @@ def aggregateJavadocsTask = tasks.register( 'aggregateJavadocs', Javadoc ) { use = true options.encoding = 'UTF-8' - def matcher = hibernateOrmVersion =~ /\d+\.\d+/ + def matcher = libs.versions.hibernateOrmVersion =~ /\d+\.\d+/ def ormMinorVersion = matcher.find() ? matcher.group() : "5.6"; links = [ diff --git a/examples/native-sql-example/build.gradle b/examples/native-sql-example/build.gradle index c79ec8984..052166b33 100644 --- a/examples/native-sql-example/build.gradle +++ b/examples/native-sql-example/build.gradle @@ -8,9 +8,9 @@ buildscript { mavenLocal() } // Optional: Enables snapshots repository - // Example: ./gradlew build -PenableSonatypeOpenSourceSnapshotsRep - if ( project.hasProperty('enableSonatypeOpenSourceSnapshotsRep') ) { - maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' } + // Example: ./gradlew build -PenableCentralSonatypeSnapshotsRep + if ( project.hasProperty('enableCentralSonatypeSnapshotsRep') ) { + maven { url 'https://central.sonatype.com/repository/maven-snapshots/' } } mavenCentral() } @@ -18,7 +18,7 @@ buildscript { plugins { // Optional: Hibernate Gradle plugin to enable bytecode enhancements - id "org.hibernate.orm" version "${hibernateOrmGradlePluginVersion}" + alias(libs.plugins.org.hibernate.orm) } description = 'Hibernate Reactive native SQL Example' @@ -27,24 +27,24 @@ dependencies { implementation project( ':hibernate-reactive-core' ) // Hibernate Validator (optional) - implementation 'org.hibernate.validator:hibernate-validator:8.0.2.Final' - runtimeOnly 'org.glassfish.expressly:expressly:5.0.0' + implementation(libs.org.hibernate.validator.hibernate.validator) + runtimeOnly(libs.org.glassfish.expressly.expressly) // JPA metamodel generation for criteria queries (optional) - annotationProcessor "org.hibernate.orm:hibernate-jpamodelgen:${hibernateOrmVersion}" + annotationProcessor(libs.org.hibernate.orm.hibernate.jpamodelgen) // database driver for PostgreSQL - runtimeOnly "io.vertx:vertx-pg-client:${vertxSqlClientVersion}" + runtimeOnly(libs.io.vertx.vertx.pg.client) // logging (optional) - runtimeOnly "org.apache.logging.log4j:log4j-core:2.20.0" + runtimeOnly(libs.org.apache.logging.log4j.log4j.core) // Allow authentication to PostgreSQL using SCRAM: - runtimeOnly 'com.ongres.scram:client:2.1' + runtimeOnly(libs.com.ongres.scram.client) } // Optional: enable the bytecode enhancements -hibernate { enhancement } +hibernate { enhancement {} } // Create tasks to run the different API available. // @@ -75,3 +75,36 @@ tasks.register( "runAllExamples" ) { dependsOn = ["runAllExamplesOnPostgreSQL"] description = "Run all examples on ${dbs}" } + +// Optional: Task to print the resolved versions of Hibernate ORM and Vert.x +tasks.register( "printResolvedVersions" ) { + description = "Print the resolved hibernate-orm-core and vert.x versions" + doLast { + def hibernateCoreVersion = "n/a" + def vertxVersion = "n/a" + + // Resolve Hibernate Core and Vert.x versions from compile classpath + configurations.compileClasspath.resolvedConfiguration.resolvedArtifacts.each { artifact -> + if (artifact.moduleVersion.id.name == 'hibernate-core') { + hibernateCoreVersion = artifact.moduleVersion.id.version + } + if (artifact.moduleVersion.id.group == 'io.vertx' && artifact.moduleVersion.id.name == 'vertx-sql-client') { + vertxVersion = artifact.moduleVersion.id.version + } + } + + // Print the resolved versions + println "Resolved Hibernate ORM Core Version: ${hibernateCoreVersion}" + println "Resolved Vert.x SQL client Version: ${vertxVersion}" + } +} + +// Make the version printing task run before tests and JavaExec tasks +tasks.withType( Test ).configureEach { + dependsOn printResolvedVersions +} + +tasks.withType( JavaExec ).configureEach { + dependsOn printResolvedVersions +} + diff --git a/examples/session-example/build.gradle b/examples/session-example/build.gradle index 4da40ba69..f734d6513 100644 --- a/examples/session-example/build.gradle +++ b/examples/session-example/build.gradle @@ -8,9 +8,9 @@ buildscript { mavenLocal() } // Optional: Enables snapshots repository - // Example: ./gradlew build -PenableSonatypeOpenSourceSnapshotsRep - if ( project.hasProperty('enableSonatypeOpenSourceSnapshotsRep') ) { - maven { url 'https://oss.sonatype.org/content/repositories/snapshots/' } + // Example: ./gradlew build -PenableCentralSonatypeSnapshotsRep + if ( project.hasProperty('enableCentralSonatypeSnapshotsRep') ) { + maven { url 'https://central.sonatype.com/repository/maven-snapshots/' } } mavenCentral() } @@ -18,7 +18,7 @@ buildscript { plugins { // Optional: Hibernate Gradle plugin to enable bytecode enhancements - id "org.hibernate.orm" version "${hibernateOrmGradlePluginVersion}" + alias(libs.plugins.org.hibernate.orm) } description = 'Hibernate Reactive Session Examples' @@ -27,25 +27,25 @@ dependencies { implementation project( ':hibernate-reactive-core' ) // Hibernate Validator (optional) - implementation 'org.hibernate.validator:hibernate-validator:8.0.2.Final' - runtimeOnly 'org.glassfish.expressly:expressly:5.0.0' + implementation(libs.org.hibernate.validator.hibernate.validator) + runtimeOnly(libs.org.glassfish.expressly.expressly) // JPA metamodel generation for criteria queries (optional) - annotationProcessor "org.hibernate.orm:hibernate-jpamodelgen:${hibernateOrmVersion}" + annotationProcessor(libs.org.hibernate.orm.hibernate.jpamodelgen) // database drivers for PostgreSQL and MySQL - runtimeOnly "io.vertx:vertx-pg-client:${vertxSqlClientVersion}" - runtimeOnly "io.vertx:vertx-mysql-client:${vertxSqlClientVersion}" + runtimeOnly(libs.io.vertx.vertx.pg.client) + runtimeOnly(libs.io.vertx.vertx.mysql.client) // logging (optional) - runtimeOnly "org.apache.logging.log4j:log4j-core:2.20.0" + runtimeOnly(libs.org.apache.logging.log4j.log4j.core) // Allow authentication to PostgreSQL using SCRAM: - runtimeOnly 'com.ongres.scram:client:2.1' + runtimeOnly(libs.com.ongres.scram.client) } // Optional: enable the bytecode enhancements -hibernate { enhancement } +hibernate { enhancement {} } // Create tasks to run the different API available. // @@ -81,3 +81,35 @@ tasks.register( "runAllExamples" ) { dependsOn = ["runAllExamplesOnPostgreSQL", "runAllExamplesOnMySQL"] description = "Run all examples on ${dbs}" } + +// Optional: Task to print the resolved versions of Hibernate ORM and Vert.x +tasks.register( "printResolvedVersions" ) { + description = "Print the resolved hibernate-orm-core and vert.x versions" + doLast { + def hibernateCoreVersion = "n/a" + def vertxVersion = "n/a" + + // Resolve Hibernate Core and Vert.x versions from compile classpath + configurations.compileClasspath.resolvedConfiguration.resolvedArtifacts.each { artifact -> + if (artifact.moduleVersion.id.name == 'hibernate-core') { + hibernateCoreVersion = artifact.moduleVersion.id.version + } + if (artifact.moduleVersion.id.group == 'io.vertx' && artifact.moduleVersion.id.name == 'vertx-sql-client') { + vertxVersion = artifact.moduleVersion.id.version + } + } + + // Print the resolved versions + println "Resolved Hibernate ORM Core Version: ${hibernateCoreVersion}" + println "Resolved Vert.x SQL client Version: ${vertxVersion}" + } +} + +// Make the version printing task run before tests and JavaExec tasks +tasks.withType( Test ).configureEach { + dependsOn printResolvedVersions +} + +tasks.withType( JavaExec ).configureEach { + dependsOn printResolvedVersions +} diff --git a/github_release_notes.md b/github_release_notes.md new file mode 100644 index 000000000..db3469c5d --- /dev/null +++ b/github_release_notes.md @@ -0,0 +1,3 @@ + +* See the [website](https://hibernate.org/reactive/releases/{{releaseVersionFamily}}) for requirements and compatibilities. +* See the [What's New](https://hibernate.org/reactive/releases/{{releaseVersionFamily}}/#whats-new) guide for details about new features and capabilities. diff --git a/gradle.properties b/gradle.properties index 5cd937337..49109ad93 100644 --- a/gradle.properties +++ b/gradle.properties @@ -18,7 +18,9 @@ org.gradle.java.installations.auto-download=false ######################################################################### # Additional custom gradle build properties. -# Please, leave these properties commented upstream +# Please, leave these properties commented upstream. +# They are meant to be used for local builds or WIP branches. +# The same properties can be set from the command line. ########################################################################## # Enable Testcontainers + Docker when present (value ignored) @@ -28,28 +30,28 @@ org.gradle.java.installations.auto-download=false # Db2, MySql, PostgreSQL, CockroachDB, SqlServer, Oracle #db = MSSQL -# Enable the SonatypeOS maven repository (mainly for Vert.x snapshots) when present (value ignored) -#enableSonatypeOpenSourceSnapshotsRep = true +# Enable the maven Central Snapshot repository, when set to any value (the value is ignored) +#enableCentralSonatypeSnapshotsRep = true # Enable the maven local repository (for local development when needed) when present (value ignored) #enableMavenLocalRepo = true +### Settings the following properties will override the version defined in gradle/libs.versions.toml + # The default Hibernate ORM version (override using `-PhibernateOrmVersion=the.version.you.want`) -hibernateOrmVersion = 7.0.0.Final +#hibernateOrmVersion = 7.1.0.Final # Override default Hibernate ORM Gradle plugin version -# Using the stable version because I don't know how to configure the build to download the snapshot version from -# a remote repository -#hibernateOrmGradlePluginVersion = 7.0.0.Final +#hibernateOrmGradlePluginVersion = 7.1.0.Final # If set to true, skip Hibernate ORM version parsing (default is true, if set to null) # this is required when using intervals or weird versions or the build will fail #skipOrmVersionParsing = true # Override default Vert.x Sql client version -#vertxSqlClientVersion = 4.5.15-SNAPSHOT +#vertxSqlClientVersion = 4.5.16-SNAPSHOT # Override default Vert.x Web client and server versions. For integration tests, both default to vertxSqlClientVersion -#vertxWebVersion = 4.5.15 -#vertxWebtClientVersion = 4.5.15 +#vertxWebVersion = 4.5.16 +#vertxWebtClientVersion = 4.5.16 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 000000000..d5b618e89 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,61 @@ +[versions] +assertjVersion = "3.27.6" +hibernateOrmVersion = "7.2.0.CR2" +hibernateOrmGradlePluginVersion = "7.2.0.CR2" +jacksonDatabindVersion = "2.20.1" +jbossLoggingAnnotationVersion = "3.0.4.Final" +jbossLoggingVersion = "3.6.1.Final" +junitVersion = "6.0.1" +junitPlatformVersion = "1.13.4" +log4jVersion = "2.25.2" +testcontainersVersion = "1.21.3" +vertxSqlClientVersion = "4.5.22" +vertxWebVersion= "4.5.22" +vertxWebClientVersion = "4.5.22" + +[libraries] +com-fasterxml-jackson-core-jackson-databind = { group = "com.fasterxml.jackson.core", name = "jackson-databind", version.ref = "jacksonDatabindVersion" } +com-ibm-db2-jcc = { group = "com.ibm.db2", name = "jcc", version = "12.1.2.0" } +com-microsoft-sqlserver-mssql-jdbc = { group = "com.microsoft.sqlserver", name = "mssql-jdbc", version = "13.2.1.jre11" } +com-mysql-mysql-connector-j = { group = "com.mysql", name = "mysql-connector-j", version = "9.5.0" } +com-ongres-scram-client = { group = "com.ongres.scram", name = "client", version = "2.1" } +io-smallrye-reactive-mutiny = { group = "io.smallrye.reactive", name = "mutiny", version = "2.9.5" } +io-vertx-vertx-db2-client = { group = "io.vertx", name = "vertx-db2-client", version.ref = "vertxSqlClientVersion" } +io-vertx-vertx-junit5 = { group = "io.vertx", name = "vertx-junit5", version.ref = "vertxSqlClientVersion" } +io-vertx-vertx-micrometer-metrics = { group = "io.vertx", name = "vertx-micrometer-metrics", version.ref = "vertxSqlClientVersion" } +io-vertx-vertx-mssql-client = { group = "io.vertx", name = "vertx-mssql-client", version.ref = "vertxSqlClientVersion" } +io-vertx-vertx-mysql-client = { group = "io.vertx", name = "vertx-mysql-client", version.ref = "vertxSqlClientVersion" } +io-vertx-vertx-oracle-client = { group = "io.vertx", name = "vertx-oracle-client", version.ref = "vertxSqlClientVersion" } +io-vertx-vertx-pg-client = { group = "io.vertx", name = "vertx-pg-client", version.ref = "vertxSqlClientVersion" } +io-vertx-vertx-sql-client = { group = "io.vertx", name = "vertx-sql-client", version.ref = "vertxSqlClientVersion" } +io-vertx-vertx-web = { group = "io.vertx", name = "vertx-web", version.ref = "vertxWebVersion" } +io-vertx-vertx-web-client = { group = "io.vertx", name = "vertx-web-client", version.ref = "vertxWebClientVersion" } +org-apache-logging-log4j-log4j-core = { group = "org.apache.logging.log4j", name = "log4j-core", version.ref = "log4jVersion" } +org-assertj-assertj-core = { group = "org.assertj", name = "assertj-core", version.ref = "assertjVersion" } +org-ehcache-ehcache = { group = "org.ehcache", name = "ehcache", version = "3.11.1" } +org-glassfish-expressly-expressly = { group = "org.glassfish.expressly", name = "expressly", version = "5.0.0" } +org-hibernate-orm-hibernate-core = { group = "org.hibernate.orm", name = "hibernate-core", version.ref = "hibernateOrmVersion" } +org-hibernate-orm-hibernate-jcache = { group = "org.hibernate.orm", name = "hibernate-jcache", version.ref = "hibernateOrmVersion" } +org-hibernate-orm-hibernate-jpamodelgen = { group = "org.hibernate.orm", name = "hibernate-jpamodelgen", version.ref = "hibernateOrmVersion" } +org-hibernate-validator-hibernate-validator = { group = "org.hibernate.validator", name = "hibernate-validator", version = "8.0.3.Final" } +org-hibernate-models = { group = "org.hibernate.models", name = "hibernate-models", version = "1.0.1" } +org-jboss-logging-jboss-logging = { group = "org.jboss.logging", name = "jboss-logging", version.ref = "jbossLoggingVersion" } +org-jboss-logging-jboss-logging-annotations = { group = "org.jboss.logging", name = "jboss-logging-annotations", version.ref = "jbossLoggingAnnotationVersion" } +org-jboss-logging-jboss-logging-processor = { group = "org.jboss.logging", name = "jboss-logging-processor", version.ref = "jbossLoggingAnnotationVersion" } +org-junit-jupiter-junit-jupiter-api = { group = "org.junit.jupiter", name = "junit-jupiter-api", version.ref = "junitVersion" } +org-junit-jupiter-junit-jupiter-engine = { group = "org.junit.jupiter", name = "junit-jupiter-engine", version.ref = "junitVersion" } +org-junit-platform-junit-platform-launcher = { group = "org.junit.platform", name = "junit-platform-launcher", version = "6.0.1" } +org-mariadb-jdbc-mariadb-java-client = { group = "org.mariadb.jdbc", name = "mariadb-java-client", version = "3.5.6" } +org-postgresql-postgresql = { group = "org.postgresql", name = "postgresql", version = "42.7.8" } +org-testcontainers-cockroachdb = { group = "org.testcontainers", name = "cockroachdb", version.ref = "testcontainersVersion" } +org-testcontainers-db2 = { group = "org.testcontainers", name = "db2", version.ref = "testcontainersVersion" } +org-testcontainers-mariadb = { group = "org.testcontainers", name = "mariadb", version.ref = "testcontainersVersion" } +org-testcontainers-mssqlserver = { group = "org.testcontainers", name = "mssqlserver", version.ref = "testcontainersVersion" } +org-testcontainers-mysql = { group = "org.testcontainers", name = "mysql", version.ref = "testcontainersVersion" } +org-testcontainers-oracle-xe = { group = "org.testcontainers", name = "oracle-xe", version.ref = "testcontainersVersion" } +org-testcontainers-postgresql = { group = "org.testcontainers", name = "postgresql", version.ref = "testcontainersVersion" } + +[plugins] +com-diffplug-spotless = { id = "com.diffplug.spotless", version = "8.0.0" } +org-asciidoctor-jvm-convert = { id = "org.asciidoctor.jvm.convert", version = "4.0.5" } +org-hibernate-orm = { id = "org.hibernate.orm", version.ref = "hibernateOrmGradlePluginVersion" } diff --git a/gradle/version.properties b/gradle/version.properties index 6b36d7e6b..d21ddfc58 100644 --- a/gradle/version.properties +++ b/gradle/version.properties @@ -1 +1 @@ -projectVersion=3.0.1-SNAPSHOT \ No newline at end of file +projectVersion=3.2.0-SNAPSHOT diff --git a/hibernate-reactive-core/build.gradle b/hibernate-reactive-core/build.gradle index 01f593d80..c8ff30fbf 100644 --- a/hibernate-reactive-core/build.gradle +++ b/hibernate-reactive-core/build.gradle @@ -8,77 +8,82 @@ apply from: publishScript dependencies { - api "org.hibernate.orm:hibernate-core:${hibernateOrmVersion}" + api(libs.org.hibernate.orm.hibernate.core) + compileOnly(libs.org.hibernate.models) - api 'io.smallrye.reactive:mutiny:2.9.0' + api(libs.io.smallrye.reactive.mutiny) //Logging - implementation 'org.jboss.logging:jboss-logging:3.6.1.Final' - annotationProcessor 'org.jboss.logging:jboss-logging:3.6.1.Final' + implementation(libs.org.jboss.logging.jboss.logging) + annotationProcessor(libs.org.jboss.logging.jboss.logging) - compileOnly 'org.jboss.logging:jboss-logging-annotations:3.0.4.Final' - annotationProcessor 'org.jboss.logging:jboss-logging-annotations:3.0.4.Final' - annotationProcessor 'org.jboss.logging:jboss-logging-processor:3.0.4.Final' + compileOnly(libs.org.jboss.logging.jboss.logging.annotations) + annotationProcessor(libs.org.jboss.logging.jboss.logging.annotations) + annotationProcessor(libs.org.jboss.logging.jboss.logging.processor) //Specific implementation details of Hibernate Reactive: - implementation "io.vertx:vertx-sql-client:${vertxSqlClientVersion}" + implementation(libs.io.vertx.vertx.sql.client) // Testing - testImplementation 'org.assertj:assertj-core:3.27.3' - testImplementation "io.vertx:vertx-junit5:${vertxSqlClientVersion}" + testImplementation(libs.org.assertj.assertj.core) + testImplementation(libs.io.vertx.vertx.junit5) // Drivers - testImplementation "io.vertx:vertx-pg-client:${vertxSqlClientVersion}" - testImplementation "io.vertx:vertx-mysql-client:${vertxSqlClientVersion}" - testImplementation "io.vertx:vertx-db2-client:${vertxSqlClientVersion}" - testImplementation "io.vertx:vertx-mssql-client:${vertxSqlClientVersion}" - testImplementation "io.vertx:vertx-oracle-client:${vertxSqlClientVersion}" + testImplementation(libs.io.vertx.vertx.pg.client) + testImplementation(libs.io.vertx.vertx.mysql.client) + testImplementation(libs.io.vertx.vertx.db2.client) + testImplementation(libs.io.vertx.vertx.mssql.client) + testImplementation(libs.io.vertx.vertx.oracle.client) + + // Some tests using JSON need a formatter + testRuntimeOnly(libs.com.fasterxml.jackson.core.jackson.databind) // Metrics - testImplementation "io.vertx:vertx-micrometer-metrics:${vertxSqlClientVersion}" + testImplementation(libs.io.vertx.vertx.micrometer.metrics) // Optional dependency of vertx-pg-client, essential when connecting via SASL SCRAM - testImplementation 'com.ongres.scram:client:2.1' + testImplementation(libs.com.ongres.scram.client) // JUnit Jupiter - testImplementation 'org.junit.jupiter:junit-jupiter-api:5.11.3' - testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.11.3' + testImplementation(libs.org.junit.jupiter.junit.jupiter.api) + testRuntimeOnly(libs.org.junit.jupiter.junit.jupiter.engine) + testRuntimeOnly(libs.org.junit.platform.junit.platform.launcher) // JDBC driver to test with ORM and PostgreSQL - testRuntimeOnly "org.postgresql:postgresql:42.7.5" + testRuntimeOnly(libs.org.postgresql.postgresql) // JDBC driver for Testcontainers with MS SQL Server - testRuntimeOnly "com.microsoft.sqlserver:mssql-jdbc:12.10.0.jre11" + testRuntimeOnly(libs.com.microsoft.sqlserver.mssql.jdbc) // JDBC driver for Testcontainers with MariaDB Server - testRuntimeOnly "org.mariadb.jdbc:mariadb-java-client:3.5.3" + testRuntimeOnly(libs.org.mariadb.jdbc.mariadb.java.client) // JDBC driver for Testcontainers with MYSQL Server - testRuntimeOnly "com.mysql:mysql-connector-j:9.3.0" + testRuntimeOnly(libs.com.mysql.mysql.connector.j) // JDBC driver for Db2 server, for testing - testRuntimeOnly "com.ibm.db2:jcc:12.1.0.0" + testRuntimeOnly(libs.com.ibm.db2.jcc) // EHCache - testRuntimeOnly ("org.ehcache:ehcache:3.10.8") { + testRuntimeOnly(libs.org.ehcache.ehcache) { capabilities { requireCapability 'org.ehcache.modules:ehcache-xml-jakarta' } } - testRuntimeOnly ("org.hibernate.orm:hibernate-jcache:${hibernateOrmVersion}") + testRuntimeOnly(libs.org.hibernate.orm.hibernate.jcache) // log4j - testRuntimeOnly 'org.apache.logging.log4j:log4j-core:2.20.0' + testRuntimeOnly(libs.org.apache.logging.log4j.log4j.core) // Testcontainers - testImplementation "org.testcontainers:postgresql:${testcontainersVersion}" - testImplementation "org.testcontainers:mysql:${testcontainersVersion}" - testImplementation "org.testcontainers:mariadb:${testcontainersVersion}" - testImplementation "org.testcontainers:db2:${testcontainersVersion}" - testImplementation "org.testcontainers:cockroachdb:${testcontainersVersion}" - testImplementation "org.testcontainers:mssqlserver:${testcontainersVersion}" - testImplementation "org.testcontainers:oracle-xe:${testcontainersVersion}" + testImplementation(libs.org.testcontainers.postgresql) + testImplementation(libs.org.testcontainers.mysql) + testImplementation(libs.org.testcontainers.mariadb) + testImplementation(libs.org.testcontainers.db2) + testImplementation(libs.org.testcontainers.cockroachdb) + testImplementation(libs.org.testcontainers.mssqlserver) + testImplementation(libs.org.testcontainers.oracle.xe) } // Reproducible Builds @@ -167,3 +172,35 @@ tasks.register( "testAll", Test ) { description = "Run tests for ${dbs}" dependsOn = dbs.collect( [] as HashSet ) { db -> "testDb${db}" } } + +// Task to print the resolved versions of Hibernate ORM and Vert.x +tasks.register( "printResolvedVersions" ) { + description = "Print the resolved hibernate-orm-core and vert.x versions" + doLast { + def hibernateCoreVersion = "n/a" + def vertxVersion = "n/a" + + // Resolve Hibernate Core and Vert.x versions from compile classpath + configurations.compileClasspath.resolvedConfiguration.resolvedArtifacts.each { artifact -> + if (artifact.moduleVersion.id.name == 'hibernate-core') { + hibernateCoreVersion = artifact.moduleVersion.id.version + } + if (artifact.moduleVersion.id.group == 'io.vertx' && artifact.moduleVersion.id.name == 'vertx-sql-client') { + vertxVersion = artifact.moduleVersion.id.version + } + } + + // Print the resolved versions + println "Resolved Hibernate ORM Core Version: ${hibernateCoreVersion}" + println "Resolved Vert.x SQL client Version: ${vertxVersion}" + } +} + +// Make the version printing task run before tests and JavaExec tasks +tasks.withType( Test ).configureEach { + dependsOn printResolvedVersions +} + +tasks.withType( JavaExec ).configureEach { + dependsOn printResolvedVersions +} diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/boot/spi/ReactiveBootstrapContextAdapter.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/boot/spi/ReactiveBootstrapContextAdapter.java new file mode 100644 index 000000000..8134378ee --- /dev/null +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/boot/spi/ReactiveBootstrapContextAdapter.java @@ -0,0 +1,187 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.boot.spi; + +import org.hibernate.boot.CacheRegionDefinition; +import org.hibernate.boot.archive.scan.spi.ScanEnvironment; +import org.hibernate.boot.archive.scan.spi.ScanOptions; +import org.hibernate.boot.archive.spi.ArchiveDescriptorFactory; +import org.hibernate.boot.model.convert.spi.ConverterDescriptor; +import org.hibernate.boot.model.relational.AuxiliaryDatabaseObject; +import org.hibernate.boot.registry.StandardServiceRegistry; +import org.hibernate.boot.registry.classloading.spi.ClassLoaderService; +import org.hibernate.boot.spi.BootstrapContext; +import org.hibernate.boot.spi.ClassLoaderAccess; +import org.hibernate.boot.spi.ClassmateContext; +import org.hibernate.boot.spi.MetadataBuildingOptions; +import org.hibernate.engine.config.spi.ConfigurationService; +import org.hibernate.jpa.spi.MutableJpaCompliance; +import org.hibernate.metamodel.spi.ManagedTypeRepresentationResolver; +import org.hibernate.models.spi.ModelsContext; +import org.hibernate.query.sqm.function.SqmFunctionDescriptor; +import org.hibernate.query.sqm.function.SqmFunctionRegistry; +import org.hibernate.reactive.metamodel.spi.ReactiveManagedTypeRepresentationResolver; +import org.hibernate.resource.beans.spi.BeanInstanceProducer; +import org.hibernate.resource.beans.spi.ManagedBeanRegistry; +import org.hibernate.type.BasicType; +import org.hibernate.type.spi.TypeConfiguration; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +/** + * Adapt {@link BootstrapContext#getRepresentationStrategySelector()} to return a {@link ReactiveManagedTypeRepresentationResolver} + */ +public class ReactiveBootstrapContextAdapter implements BootstrapContext { + + private final BootstrapContext delegate; + + public ReactiveBootstrapContextAdapter(BootstrapContext bootstrapContext) { + this.delegate = bootstrapContext; + } + + @Override + public StandardServiceRegistry getServiceRegistry() { + return delegate.getServiceRegistry(); + } + + @Override + public MutableJpaCompliance getJpaCompliance() { + return delegate.getJpaCompliance(); + } + + @Override + public TypeConfiguration getTypeConfiguration() { + return delegate.getTypeConfiguration(); + } + + @Override + public ModelsContext getModelsContext() { + return delegate.getModelsContext(); + } + + @Override + public SqmFunctionRegistry getFunctionRegistry() { + return delegate.getFunctionRegistry(); + } + + @Override + public BeanInstanceProducer getCustomTypeProducer() { + return delegate.getCustomTypeProducer(); + } + + @Override + public MetadataBuildingOptions getMetadataBuildingOptions() { + return delegate.getMetadataBuildingOptions(); + } + + @Override + public ClassLoaderService getClassLoaderService() { + return delegate.getClassLoaderService(); + } + + @Override + public ManagedBeanRegistry getManagedBeanRegistry() { + return delegate.getManagedBeanRegistry(); + } + + @Override + public ConfigurationService getConfigurationService() { + return delegate.getConfigurationService(); + } + + @Override + public boolean isJpaBootstrap() { + return delegate.isJpaBootstrap(); + } + + @Override + public void markAsJpaBootstrap() { + delegate.markAsJpaBootstrap(); + } + + @Override + public ClassLoader getJpaTempClassLoader() { + return delegate.getJpaTempClassLoader(); + } + + @Override + public ClassLoaderAccess getClassLoaderAccess() { + return delegate.getClassLoaderAccess(); + } + + @Override + public ClassmateContext getClassmateContext() { + return delegate.getClassmateContext(); + } + + @Override + public ArchiveDescriptorFactory getArchiveDescriptorFactory() { + return delegate.getArchiveDescriptorFactory(); + } + + @Override + public ScanOptions getScanOptions() { + return delegate.getScanOptions(); + } + + @Override + public ScanEnvironment getScanEnvironment() { + return delegate.getScanEnvironment(); + } + + @Override + public Object getScanner() { + return delegate.getScanner(); + } + + @Override + public Object getJandexView() { + return delegate.getJandexView(); + } + + @Override + public Map getSqlFunctions() { + return delegate.getSqlFunctions(); + } + + @Override + public Collection getAuxiliaryDatabaseObjectList() { + return List.of(); + } + + @Override + public Collection> getAttributeConverters() { + return delegate.getAttributeConverters(); + } + + @Override + public Collection getCacheRegionDefinitions() { + return delegate.getCacheRegionDefinitions(); + } + + @Override + public ManagedTypeRepresentationResolver getRepresentationStrategySelector() { + return ReactiveManagedTypeRepresentationResolver.INSTANCE; + } + + @Override + public void release() { + delegate.release(); + } + + @Override + public void registerAdHocBasicType(BasicType basicType) { + delegate.registerAdHocBasicType( basicType ); + } + + @Override + public BasicType resolveAdHocBasicType(String key) { + return delegate.resolveAdHocBasicType( key ); + } + +} diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/bythecode/enhance/spi/interceptor/ReactiveEnhancementAsProxyLazinessInterceptor.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/bythecode/enhance/spi/interceptor/ReactiveEnhancementAsProxyLazinessInterceptor.java new file mode 100644 index 000000000..2b759400b --- /dev/null +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/bythecode/enhance/spi/interceptor/ReactiveEnhancementAsProxyLazinessInterceptor.java @@ -0,0 +1,42 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.bythecode.enhance.spi.interceptor; + +import org.hibernate.bytecode.enhance.spi.interceptor.EnhancementAsProxyLazinessInterceptor; +import org.hibernate.engine.spi.EntityKey; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.reactive.logging.impl.Log; +import org.hibernate.reactive.logging.impl.LoggerFactory; + +import java.lang.invoke.MethodHandles; + +/** + * Reactive version of {@link EnhancementAsProxyLazinessInterceptor}. + * + * It throws a {@link org.hibernate.LazyInitializationException} when a lazy attribute + * is not fetched using {@link org.hibernate.reactive.mutiny.Mutiny#fetch(Object)} + * or {@link org.hibernate.reactive.stage.Stage#fetch(Object)} but transparently + */ +public class ReactiveEnhancementAsProxyLazinessInterceptor extends EnhancementAsProxyLazinessInterceptor { + private static final Log LOG = LoggerFactory.make( Log.class, MethodHandles.lookup() ); + + public ReactiveEnhancementAsProxyLazinessInterceptor( + EntityRelatedState meta, + EntityKey entityKey, + SharedSessionContractImplementor session) { + super( meta, entityKey, session ); + } + + @Override + protected Object handleRead(Object target, String attributeName, Object value) { + if ( isIdentifier( attributeName ) ) { + return super.handleRead( target, attributeName, value ); + } + else { + throw LOG.lazyFieldInitializationException( attributeName, getEntityName() ); + } + } +} diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/bythecode/enhance/spi/internal/ReactiveLazyAttributeLoadingInterceptor.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/bythecode/enhance/spi/internal/ReactiveLazyAttributeLoadingInterceptor.java new file mode 100644 index 000000000..7db34f6fe --- /dev/null +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/bythecode/enhance/spi/internal/ReactiveLazyAttributeLoadingInterceptor.java @@ -0,0 +1,40 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.bythecode.enhance.spi.internal; + +import org.hibernate.bytecode.enhance.spi.interceptor.LazyAttributeLoadingInterceptor; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.reactive.logging.impl.Log; +import org.hibernate.reactive.logging.impl.LoggerFactory; + +import java.lang.invoke.MethodHandles; + +/** + * Reactive version of {@link LazyAttributeLoadingInterceptor}. + * + * It throws a {@link org.hibernate.LazyInitializationException} when a lazy attribute + * is not fetched using {@link org.hibernate.reactive.mutiny.Mutiny#fetch(Object)} + * or {@link org.hibernate.reactive.stage.Stage#fetch(Object)} but transparently + */ +public class ReactiveLazyAttributeLoadingInterceptor extends LazyAttributeLoadingInterceptor { + + private static final Log LOG = LoggerFactory.make( Log.class, MethodHandles.lookup() ); + + public ReactiveLazyAttributeLoadingInterceptor( + EntityRelatedState entityMeta, + Object identifier, + SharedSessionContractImplementor session) { + super( entityMeta, identifier, session ); + } + + @Override + protected Object handleRead(Object target, String attributeName, Object value) { + if ( !isAttributeLoaded( attributeName ) ) { + throw LOG.lazyFieldInitializationException( attributeName, getEntityName() ); + } + return value; + } +} diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/bythecode/spi/ReactiveBytecodeEnhancementMetadataPojoImplAdapter.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/bythecode/spi/ReactiveBytecodeEnhancementMetadataPojoImplAdapter.java new file mode 100644 index 000000000..5c8b94b2d --- /dev/null +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/bythecode/spi/ReactiveBytecodeEnhancementMetadataPojoImplAdapter.java @@ -0,0 +1,107 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.bythecode.spi; + +import org.hibernate.boot.Metadata; +import org.hibernate.bytecode.enhance.spi.interceptor.BytecodeLazyAttributeInterceptor; +import org.hibernate.bytecode.enhance.spi.interceptor.EnhancementAsProxyLazinessInterceptor; +import org.hibernate.bytecode.enhance.spi.interceptor.LazyAttributeLoadingInterceptor; +import org.hibernate.bytecode.enhance.spi.interceptor.LazyAttributesMetadata; +import org.hibernate.bytecode.internal.BytecodeEnhancementMetadataPojoImpl; +import org.hibernate.bytecode.spi.NotInstrumentedException; +import org.hibernate.engine.spi.EntityKey; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.mapping.PersistentClass; +import org.hibernate.reactive.bythecode.enhance.spi.interceptor.ReactiveEnhancementAsProxyLazinessInterceptor; +import org.hibernate.reactive.bythecode.enhance.spi.internal.ReactiveLazyAttributeLoadingInterceptor; +import org.hibernate.type.CompositeType; + +import java.util.Set; + +import static org.hibernate.engine.internal.ManagedTypeHelper.isPersistentAttributeInterceptableType; + +/** + * Extends {@link BytecodeEnhancementMetadataPojoImpl} to inject Reactive versions of {@link BytecodeLazyAttributeInterceptor} + */ +public class ReactiveBytecodeEnhancementMetadataPojoImplAdapter extends BytecodeEnhancementMetadataPojoImpl { + + public static BytecodeEnhancementMetadataPojoImpl from( + PersistentClass persistentClass, + Set identifierAttributeNames, + CompositeType nonAggregatedCidMapper, + boolean collectionsInDefaultFetchGroupEnabled, + Metadata metadata) { + final Class mappedClass = persistentClass.getMappedClass(); + final boolean enhancedForLazyLoading = isPersistentAttributeInterceptableType( mappedClass ); + final LazyAttributesMetadata lazyAttributesMetadata = enhancedForLazyLoading + ? LazyAttributesMetadata.from( persistentClass, true, collectionsInDefaultFetchGroupEnabled, metadata ) + : LazyAttributesMetadata.nonEnhanced( persistentClass.getEntityName() ); + + return new ReactiveBytecodeEnhancementMetadataPojoImplAdapter( + persistentClass.getEntityName(), + mappedClass, + identifierAttributeNames, + nonAggregatedCidMapper, + enhancedForLazyLoading, + lazyAttributesMetadata + ); + } + + ReactiveBytecodeEnhancementMetadataPojoImplAdapter(String entityName, Class mappedClass, Set identifierAttributeNames, CompositeType nonAggregatedCidMapper, boolean enhancedForLazyLoading, LazyAttributesMetadata lazyAttributesMetadata) { + super( + entityName, + mappedClass, + identifierAttributeNames, + nonAggregatedCidMapper, + enhancedForLazyLoading, + lazyAttributesMetadata + ); + } + + @Override + public LazyAttributeLoadingInterceptor injectInterceptor( + Object entity, + Object identifier, + SharedSessionContractImplementor session) throws NotInstrumentedException { + if ( !isEnhancedForLazyLoading() ) { + throw new NotInstrumentedException( "Entity class [" + getEntityClass() + .getName() + "] is not enhanced for lazy loading" ); + } + + if ( !getEntityClass().isInstance( entity ) ) { + throw new IllegalArgumentException( + String.format( + "Passed entity instance [%s] is not of expected type [%s]", + entity, + getEntityName() + ) + ); + } + final LazyAttributeLoadingInterceptor interceptor = new ReactiveLazyAttributeLoadingInterceptor( + getLazyAttributeLoadingInterceptorState(), + identifier, + session + ); + + injectInterceptor( entity, interceptor, session ); + + return interceptor; + } + + @Override + public void injectEnhancedEntityAsProxyInterceptor( + Object entity, + EntityKey entityKey, + SharedSessionContractImplementor session) { + final EnhancementAsProxyLazinessInterceptor.EntityRelatedState meta = + getEnhancementAsProxyLazinessInterceptorMetastate( session ); + injectInterceptor( + entity, + new ReactiveEnhancementAsProxyLazinessInterceptor( meta, entityKey, session ), + session + ); + } +} diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/impl/ReactiveEntityUpdateAction.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/impl/ReactiveEntityUpdateAction.java index 4a6b5e149..4d212ee72 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/impl/ReactiveEntityUpdateAction.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/engine/impl/ReactiveEntityUpdateAction.java @@ -94,7 +94,7 @@ public CompletionStage reactiveExecute() throws HibernateException { .thenAccept( v -> { handleDeleted( entry ); updateCacheItem( persister, ck, entry ); - handleNaturalIdResolutions( persister, session, id ); + handleNaturalIdSharedResolutions( id, persister, session.getPersistenceContext() ); postUpdate(); final StatisticsImplementor statistics = session.getFactory().getStatistics(); diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveLoadEventListener.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveLoadEventListener.java index 9804a02f6..4f3d53e58 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveLoadEventListener.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveLoadEventListener.java @@ -27,7 +27,6 @@ import org.hibernate.event.spi.EventSource; import org.hibernate.event.spi.LoadEvent; import org.hibernate.event.spi.LoadEventListener; -import org.hibernate.loader.internal.CacheLoadHelper; import org.hibernate.metamodel.mapping.AttributeMapping; import org.hibernate.metamodel.mapping.AttributeMappingsList; import org.hibernate.metamodel.mapping.CompositeIdentifierMapping; @@ -48,7 +47,7 @@ import static org.hibernate.engine.internal.ManagedTypeHelper.asPersistentAttributeInterceptable; import static org.hibernate.engine.internal.ManagedTypeHelper.isPersistentAttributeInterceptable; import static org.hibernate.loader.internal.CacheLoadHelper.loadFromSecondLevelCache; -import static org.hibernate.loader.internal.CacheLoadHelper.loadFromSessionCache; +import static org.hibernate.reactive.loader.internal.ReactiveCacheLoadHelper.loadFromSessionCache; import static org.hibernate.pretty.MessageHelper.infoString; import static org.hibernate.proxy.HibernateProxy.extractLazyInitializer; import static org.hibernate.reactive.session.impl.SessionUtil.checkEntityFound; @@ -653,15 +652,17 @@ private CompletionStage doLoad( return nullFuture(); } else { - final CacheLoadHelper.PersistenceContextEntry persistenceContextEntry = - loadFromSessionCache( keyToLoad, event.getLockOptions(), options, event.getSession() ); - final Object entity = persistenceContextEntry.entity(); - if ( entity != null ) { - return persistenceContextEntry.isManaged() ? initializeIfNecessary( entity ) : nullFuture(); - } - else { - return loadFromCacheOrDatasource( event, persister, keyToLoad ); - } + return loadFromSessionCache( keyToLoad, event.getLockOptions(), options, event.getSession() ).thenCompose( + persistenceContextEntry -> { + final Object entity = persistenceContextEntry.entity(); + if ( entity != null ) { + return persistenceContextEntry.isManaged() ? initializeIfNecessary( entity ) : nullFuture(); + } + else { + return loadFromCacheOrDatasource( event, persister, keyToLoad ); + } + } + ); } } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveRefreshEventListener.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveRefreshEventListener.java index ba3edda4a..82964597b 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveRefreshEventListener.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/event/impl/DefaultReactiveRefreshEventListener.java @@ -200,7 +200,7 @@ private static void evictEntity(Object entity, EntityPersister persister, Object ); final SoftLock lock = cache.lockItem( source, ck, previousVersion ); cache.remove(source, ck ); - source.getActionQueue().registerProcess( (success, session) -> cache.unlockItem( session, ck, lock ) ); + source.getActionQueue().registerCallback( (success, session) -> cache.unlockItem( session, ck, lock ) ); } } @@ -314,7 +314,7 @@ private void evictCachedCollections(Type[] types, Object id, EventSource source) ); final SoftLock lock = cache.lockItem( source, ck, null ); cache.remove( source, ck ); - actionQueue.registerProcess( (success, session) -> cache.unlockItem( session, ck, lock ) ); + actionQueue.registerCallback( (success, session) -> cache.unlockItem( session, ck, lock ) ); } } else if ( type.isComponentType() ) { diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/generator/values/internal/ReactiveGeneratedValuesHelper.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/generator/values/internal/ReactiveGeneratedValuesHelper.java index e842adf11..eeb9065a1 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/generator/values/internal/ReactiveGeneratedValuesHelper.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/generator/values/internal/ReactiveGeneratedValuesHelper.java @@ -5,27 +5,50 @@ */ package org.hibernate.reactive.generator.values.internal; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletionStage; + import org.hibernate.HibernateException; import org.hibernate.Internal; import org.hibernate.dialect.Dialect; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.generator.EventType; +import org.hibernate.generator.Generator; +import org.hibernate.generator.GeneratorCreationContext; import org.hibernate.generator.values.GeneratedValueBasicResultBuilder; import org.hibernate.generator.values.GeneratedValues; import org.hibernate.generator.values.GeneratedValuesMutationDelegate; import org.hibernate.generator.values.internal.GeneratedValuesHelper; import org.hibernate.generator.values.internal.GeneratedValuesImpl; import org.hibernate.generator.values.internal.GeneratedValuesMappingProducer; -import org.hibernate.id.IdentifierGeneratorHelper; +import org.hibernate.id.CompositeNestedGeneratedValueGenerator; +import org.hibernate.id.Configurable; +import org.hibernate.id.IdentifierGenerator; +import org.hibernate.id.SelectGenerator; +import org.hibernate.id.enhanced.DatabaseStructure; +import org.hibernate.id.enhanced.SequenceStructure; +import org.hibernate.id.enhanced.SequenceStyleGenerator; +import org.hibernate.id.enhanced.TableGenerator; +import org.hibernate.id.enhanced.TableStructure; import org.hibernate.id.insert.GetGeneratedKeysDelegate; import org.hibernate.id.insert.UniqueKeySelectingDelegate; -import org.hibernate.internal.CoreLogging; -import org.hibernate.internal.CoreMessageLogger; import org.hibernate.metamodel.mapping.ModelPart; +import org.hibernate.metamodel.spi.RuntimeModelCreationContext; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.pretty.MessageHelper; import org.hibernate.query.spi.QueryOptions; +import org.hibernate.reactive.id.ReactiveIdentifierGenerator; +import org.hibernate.reactive.id.impl.EmulatedSequenceReactiveIdentifierGenerator; +import org.hibernate.reactive.id.impl.ReactiveCompositeNestedGeneratedValueGenerator; +import org.hibernate.reactive.id.impl.ReactiveGeneratorWrapper; +import org.hibernate.reactive.id.impl.ReactiveSequenceIdentifierGenerator; +import org.hibernate.reactive.id.impl.TableReactiveIdentifierGenerator; import org.hibernate.reactive.id.insert.ReactiveInsertReturningDelegate; +import org.hibernate.reactive.logging.impl.Log; import org.hibernate.reactive.sql.exec.spi.ReactiveRowProcessingState; import org.hibernate.reactive.sql.exec.spi.ReactiveValuesResultSet; import org.hibernate.reactive.sql.results.internal.ReactiveDirectResultSetAccess; @@ -41,15 +64,10 @@ import org.hibernate.sql.results.jdbc.spi.JdbcValuesSourceProcessingOptions; import org.hibernate.type.descriptor.WrapperOptions; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; -import java.util.ArrayList; -import java.util.List; -import java.util.concurrent.CompletionStage; - +import static java.lang.invoke.MethodHandles.lookup; import static org.hibernate.generator.values.internal.GeneratedValuesHelper.noCustomSql; import static org.hibernate.internal.NaturalIdHelper.getNaturalIdPropertyNames; +import static org.hibernate.reactive.logging.impl.LoggerFactory.make; import static org.hibernate.reactive.sql.results.spi.ReactiveListResultsConsumer.UniqueSemantic.NONE; /** @@ -57,7 +75,7 @@ */ @Internal public class ReactiveGeneratedValuesHelper { - private static final CoreMessageLogger LOG = CoreLogging.messageLogger( IdentifierGeneratorHelper.class ); + private static final Log LOG = make( Log.class, lookup() ); /** * @@ -225,4 +243,53 @@ public boolean shouldReturnProxies() { return results.get( 0 ); } ); } + + public static Generator augmentWithReactiveGenerator( + Generator generator, + GeneratorCreationContext creationContext, + RuntimeModelCreationContext runtimeModelCreationContext) { + if ( generator instanceof SequenceStyleGenerator sequenceStyleGenerator) { + final DatabaseStructure structure = sequenceStyleGenerator.getDatabaseStructure(); + if ( structure instanceof TableStructure ) { + return initialize( (IdentifierGenerator) generator, new EmulatedSequenceReactiveIdentifierGenerator( (TableStructure) structure, runtimeModelCreationContext ), creationContext ); + } + if ( structure instanceof SequenceStructure ) { + return initialize( (IdentifierGenerator) generator, new ReactiveSequenceIdentifierGenerator( structure, runtimeModelCreationContext ), creationContext ); + } + throw LOG.unknownStructureType(); + } + if ( generator instanceof TableGenerator tableGenerator ) { + return initialize( + (IdentifierGenerator) generator, + new TableReactiveIdentifierGenerator( tableGenerator, runtimeModelCreationContext ), + creationContext + ); + } + if ( generator instanceof SelectGenerator ) { + throw LOG.selectGeneratorIsNotSupportedInHibernateReactive(); + } + if ( generator instanceof CompositeNestedGeneratedValueGenerator compositeNestedGeneratedValueGenerator ) { + final ReactiveCompositeNestedGeneratedValueGenerator reactiveCompositeNestedGeneratedValueGenerator = new ReactiveCompositeNestedGeneratedValueGenerator( + compositeNestedGeneratedValueGenerator, + creationContext, + runtimeModelCreationContext + ); + return initialize( + (IdentifierGenerator) generator, + reactiveCompositeNestedGeneratedValueGenerator, + creationContext + ); + } + //nothing to do + return generator; + } + + private static Generator initialize( + IdentifierGenerator idGenerator, + ReactiveIdentifierGenerator reactiveIdGenerator, + GeneratorCreationContext creationContext) { + ( (Configurable) reactiveIdGenerator ).initialize( creationContext.getSqlStringGenerationContext() ); + return new ReactiveGeneratorWrapper( reactiveIdGenerator, idGenerator ); + } + } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/ReactiveIdentifierGenerator.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/ReactiveIdentifierGenerator.java index 7121ea5c4..83ae3290a 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/ReactiveIdentifierGenerator.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/ReactiveIdentifierGenerator.java @@ -6,6 +6,7 @@ package org.hibernate.reactive.id; import org.hibernate.Incubating; +import org.hibernate.generator.EventType; import org.hibernate.generator.Generator; import org.hibernate.id.IdentifierGenerator; import org.hibernate.reactive.session.ReactiveConnectionSupplier; @@ -33,4 +34,8 @@ public interface ReactiveIdentifierGenerator extends Generator { * @param session the reactive session */ CompletionStage generate(ReactiveConnectionSupplier session, Object entity); + + default CompletionStage generate(ReactiveConnectionSupplier session, Object owner, Object currentValue, EventType eventType) { + return generate( session, owner ); + } } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/impl/ReactiveCompositeNestedGeneratedValueGenerator.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/impl/ReactiveCompositeNestedGeneratedValueGenerator.java new file mode 100644 index 000000000..1da85ecc1 --- /dev/null +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/impl/ReactiveCompositeNestedGeneratedValueGenerator.java @@ -0,0 +1,130 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.id.impl; + +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.generator.BeforeExecutionGenerator; +import org.hibernate.generator.Generator; +import org.hibernate.generator.GeneratorCreationContext; +import org.hibernate.id.CompositeNestedGeneratedValueGenerator; +import org.hibernate.id.IdentifierGenerationException; +import org.hibernate.mapping.Component; +import org.hibernate.metamodel.spi.RuntimeModelCreationContext; +import org.hibernate.reactive.generator.values.internal.ReactiveGeneratedValuesHelper; +import org.hibernate.reactive.id.ReactiveIdentifierGenerator; +import org.hibernate.reactive.session.ReactiveConnectionSupplier; + +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CompletionStage; + +import static org.hibernate.generator.EventType.INSERT; +import static org.hibernate.reactive.util.impl.CompletionStages.completedFuture; +import static org.hibernate.reactive.util.impl.CompletionStages.loop; + +public class ReactiveCompositeNestedGeneratedValueGenerator extends CompositeNestedGeneratedValueGenerator implements + ReactiveIdentifierGenerator { + + public ReactiveCompositeNestedGeneratedValueGenerator( + CompositeNestedGeneratedValueGenerator generator, + GeneratorCreationContext creationContext, + RuntimeModelCreationContext runtimeModelCreationContext) { + super( + generator.getGenerationContextLocator(), + generator.getCompositeType(), + reactivePlans( generator, creationContext, runtimeModelCreationContext ) + ); + } + + private static List reactivePlans( + CompositeNestedGeneratedValueGenerator generator, + GeneratorCreationContext creationContext, + RuntimeModelCreationContext runtimeModelCreationContext) { + final List plans = new ArrayList<>(); + for ( GenerationPlan plan : generator.getGenerationPlans() ) { + final GenerationPlan reactivePlane = new Component.ValueGenerationPlan( + (BeforeExecutionGenerator) ReactiveGeneratedValuesHelper.augmentWithReactiveGenerator( + plan.getGenerator(), + creationContext, + runtimeModelCreationContext + ), + plan.getInjector(), + plan.getPropertyIndex() + ); + plans.add( reactivePlane ); + } + return plans; + } + + @Override + public CompletionStage generate(ReactiveConnectionSupplier reactiveConnectionSupplier, Object object) { + SharedSessionContractImplementor session = (SharedSessionContractImplementor) reactiveConnectionSupplier; + final Object context = getGenerationContextLocator().locateGenerationContext( session, object ); + + final List generatedValues = getCompositeType().isMutable() + ? null + : new ArrayList<>( getGenerationPlans().size() ); + return loop( getGenerationPlans(), generationPlan -> generateIdentifier( + reactiveConnectionSupplier, + object, + generationPlan, + session, + generatedValues, + context + ) ) + .thenCompose( v -> handleGeneratedValues( generatedValues, context, session ) ); + } + + private CompletionStage generateIdentifier( + ReactiveConnectionSupplier reactiveConnectionSupplier, + Object object, + GenerationPlan generationPlan, + SharedSessionContractImplementor session, + List generatedValues, + Object context) { + final Generator generator = generationPlan.getGenerator(); + if ( generator.generatedBeforeExecution( object, session ) ) { + if ( generator instanceof ReactiveIdentifierGenerator reactiveIdentifierGenerator ) { + return reactiveIdentifierGenerator + .generate( reactiveConnectionSupplier, object ) + .thenAccept( generated -> { + if ( generatedValues != null ) { + generatedValues.add( generated ); + } + else { + generationPlan.getInjector().set( context, generated ); + } + } ); + } + else { + final Object currentValue = generator.allowAssignedIdentifiers() + ? getCompositeType().getPropertyValue( context, generationPlan.getPropertyIndex(), session ) + : null; + return completedFuture( ( (BeforeExecutionGenerator) generator ) + .generate( session, object, currentValue, INSERT ) ); + } + } + else { + throw new IdentifierGenerationException( "Identity generation isn't supported for composite ids" ); + } + } + + private CompletionStage handleGeneratedValues( + List generatedValues, + Object context, + SharedSessionContractImplementor session) { + if ( generatedValues != null ) { + final Object[] values = getCompositeType().getPropertyValues( context ); + for ( int i = 0; i < generatedValues.size(); i++ ) { + values[getGenerationPlans().get( i ).getPropertyIndex()] = generatedValues.get( i ); + } + return completedFuture( getCompositeType().replacePropertyValues( context, values, session ) ); + } + else { + return completedFuture( context ); + } + } +} diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/insert/ReactiveInsertReturningDelegate.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/insert/ReactiveInsertReturningDelegate.java index 2b4063173..90b098652 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/insert/ReactiveInsertReturningDelegate.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/id/insert/ReactiveInsertReturningDelegate.java @@ -91,7 +91,7 @@ public TableMutationBuilder createTableMutationBuilder( return new TableInsertReturningBuilder( persister, tableReference, generatedColumns, sessionFactory ); } else { - return new TableUpdateReturningBuilder<>( persister, tableReference, generatedColumns, sessionFactory ); + return new TableUpdateReturningBuilder( persister, tableReference, generatedColumns, sessionFactory ); } } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/loader/ast/internal/DatabaseSnapshotExecutor.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/loader/ast/internal/DatabaseSnapshotExecutor.java index 33e8465b2..7a9b3e70f 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/loader/ast/internal/DatabaseSnapshotExecutor.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/loader/ast/internal/DatabaseSnapshotExecutor.java @@ -36,9 +36,9 @@ import org.hibernate.sql.ast.tree.select.QuerySpec; import org.hibernate.sql.ast.tree.select.SelectStatement; import org.hibernate.sql.exec.internal.BaseExecutionContext; +import org.hibernate.sql.exec.internal.JdbcOperationQuerySelect; import org.hibernate.sql.exec.internal.JdbcParameterBindingsImpl; import org.hibernate.sql.exec.internal.JdbcParameterImpl; -import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; import org.hibernate.sql.exec.spi.JdbcParameterBindings; import org.hibernate.sql.exec.spi.JdbcParametersList; import org.hibernate.sql.results.graph.DomainResult; diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/loader/ast/internal/ReactiveCollectionBatchLoaderArrayParam.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/loader/ast/internal/ReactiveCollectionBatchLoaderArrayParam.java index 0960a2ebc..40ec2ccb0 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/loader/ast/internal/ReactiveCollectionBatchLoaderArrayParam.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/loader/ast/internal/ReactiveCollectionBatchLoaderArrayParam.java @@ -27,10 +27,10 @@ import org.hibernate.reactive.util.impl.CompletionStages; import org.hibernate.sql.ast.tree.expression.JdbcParameter; import org.hibernate.sql.ast.tree.select.SelectStatement; +import org.hibernate.sql.exec.internal.JdbcOperationQuerySelect; import org.hibernate.sql.exec.internal.JdbcParameterBindingImpl; import org.hibernate.sql.exec.internal.JdbcParameterBindingsImpl; import org.hibernate.sql.exec.internal.JdbcParameterImpl; -import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; import org.hibernate.sql.exec.spi.JdbcParameterBindings; import org.hibernate.sql.exec.spi.JdbcParametersList; import org.hibernate.sql.results.internal.RowTransformerStandardImpl; diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/loader/ast/internal/ReactiveCollectionBatchLoaderInPredicate.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/loader/ast/internal/ReactiveCollectionBatchLoaderInPredicate.java index 230ba5ef4..0371e7285 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/loader/ast/internal/ReactiveCollectionBatchLoaderInPredicate.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/loader/ast/internal/ReactiveCollectionBatchLoaderInPredicate.java @@ -22,7 +22,7 @@ import org.hibernate.metamodel.mapping.PluralAttributeMapping; import org.hibernate.query.spi.QueryOptions; import org.hibernate.sql.ast.tree.select.SelectStatement; -import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; +import org.hibernate.sql.exec.internal.JdbcOperationQuerySelect; import org.hibernate.sql.exec.spi.JdbcParameterBindings; import org.hibernate.sql.exec.spi.JdbcParametersList; diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/loader/ast/internal/ReactiveCollectionLoaderSingleKey.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/loader/ast/internal/ReactiveCollectionLoaderSingleKey.java index 90edf7d0d..7d243df2c 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/loader/ast/internal/ReactiveCollectionLoaderSingleKey.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/loader/ast/internal/ReactiveCollectionLoaderSingleKey.java @@ -27,8 +27,8 @@ import org.hibernate.sql.ast.SqlAstTranslatorFactory; import org.hibernate.sql.ast.tree.select.SelectStatement; import org.hibernate.sql.exec.internal.BaseExecutionContext; +import org.hibernate.sql.exec.internal.JdbcOperationQuerySelect; import org.hibernate.sql.exec.internal.JdbcParameterBindingsImpl; -import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; import org.hibernate.sql.exec.spi.JdbcParameterBindings; import org.hibernate.sql.exec.spi.JdbcParametersList; import org.hibernate.sql.results.internal.RowTransformerStandardImpl; diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/loader/ast/internal/ReactiveCollectionLoaderSubSelectFetch.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/loader/ast/internal/ReactiveCollectionLoaderSubSelectFetch.java index cee82c475..b0c2403ae 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/loader/ast/internal/ReactiveCollectionLoaderSubSelectFetch.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/loader/ast/internal/ReactiveCollectionLoaderSubSelectFetch.java @@ -25,7 +25,7 @@ import org.hibernate.reactive.sql.exec.internal.StandardReactiveSelectExecutor; import org.hibernate.reactive.sql.results.spi.ReactiveListResultsConsumer; import org.hibernate.sql.ast.SqlAstTranslatorFactory; -import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; +import org.hibernate.sql.exec.internal.JdbcOperationQuerySelect; import org.hibernate.sql.results.graph.DomainResult; import org.hibernate.sql.results.internal.ResultsHelper; import org.hibernate.sql.results.internal.RowTransformerStandardImpl; diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/loader/ast/internal/ReactiveEntityBatchLoaderArrayParam.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/loader/ast/internal/ReactiveEntityBatchLoaderArrayParam.java index 04f971ffd..ebcd5c1d8 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/loader/ast/internal/ReactiveEntityBatchLoaderArrayParam.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/loader/ast/internal/ReactiveEntityBatchLoaderArrayParam.java @@ -26,8 +26,8 @@ import org.hibernate.reactive.loader.ast.spi.ReactiveSingleIdEntityLoader; import org.hibernate.sql.ast.tree.expression.JdbcParameter; import org.hibernate.sql.ast.tree.select.SelectStatement; +import org.hibernate.sql.exec.internal.JdbcOperationQuerySelect; import org.hibernate.sql.exec.internal.JdbcParameterImpl; -import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; import org.hibernate.sql.exec.spi.JdbcParameterBindings; import static org.hibernate.loader.ast.internal.MultiKeyLoadLogging.MULTI_KEY_LOAD_LOGGER; diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/loader/ast/internal/ReactiveEntityBatchLoaderInPredicate.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/loader/ast/internal/ReactiveEntityBatchLoaderInPredicate.java index 0cbfda1c3..f1deb7615 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/loader/ast/internal/ReactiveEntityBatchLoaderInPredicate.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/loader/ast/internal/ReactiveEntityBatchLoaderInPredicate.java @@ -24,7 +24,7 @@ import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.query.spi.QueryOptions; import org.hibernate.sql.ast.tree.select.SelectStatement; -import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; +import org.hibernate.sql.exec.internal.JdbcOperationQuerySelect; import org.hibernate.sql.exec.spi.JdbcParameterBindings; import org.hibernate.sql.exec.spi.JdbcParametersList; diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/loader/ast/internal/ReactiveLoaderHelper.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/loader/ast/internal/ReactiveLoaderHelper.java index 44c90bfdf..a4f1bb6f0 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/loader/ast/internal/ReactiveLoaderHelper.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/loader/ast/internal/ReactiveLoaderHelper.java @@ -9,22 +9,37 @@ import java.util.List; import java.util.concurrent.CompletionStage; +import org.hibernate.Hibernate; +import org.hibernate.LockMode; import org.hibernate.LockOptions; +import org.hibernate.ObjectDeletedException; +import org.hibernate.cache.spi.access.EntityDataAccess; +import org.hibernate.cache.spi.access.SoftLock; +import org.hibernate.engine.spi.EntityEntry; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.engine.spi.SubselectFetch; +import org.hibernate.event.monitor.spi.DiagnosticEvent; +import org.hibernate.event.monitor.spi.EventMonitor; +import org.hibernate.event.spi.EventSource; +import org.hibernate.internal.OptimisticLockHelper; +import org.hibernate.loader.LoaderLogging; import org.hibernate.metamodel.mapping.JdbcMapping; +import org.hibernate.pretty.MessageHelper; +import org.hibernate.reactive.persister.entity.impl.ReactiveEntityPersister; import org.hibernate.reactive.sql.exec.internal.StandardReactiveSelectExecutor; import org.hibernate.reactive.sql.results.spi.ReactiveListResultsConsumer; import org.hibernate.sql.ast.tree.expression.JdbcParameter; import org.hibernate.sql.ast.tree.select.SelectStatement; +import org.hibernate.sql.exec.internal.JdbcOperationQuerySelect; import org.hibernate.sql.exec.internal.JdbcParameterBindingImpl; import org.hibernate.sql.exec.internal.JdbcParameterBindingsImpl; -import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; import org.hibernate.sql.exec.spi.JdbcParameterBindings; import org.hibernate.sql.exec.spi.JdbcParametersList; import org.hibernate.sql.results.internal.RowTransformerStandardImpl; import static java.util.Objects.requireNonNull; +import static org.hibernate.reactive.util.impl.CompletionStages.supplyStage; +import static org.hibernate.reactive.util.impl.CompletionStages.voidFuture; /** * @see org.hibernate.loader.ast.internal.LoaderHelper @@ -92,4 +107,132 @@ public static CompletionStage> loadByArrayParameter( ReactiveListResultsConsumer.UniqueSemantic.FILTER ); } + + /** + * A Reactive implementation of {@link org.hibernate.loader.ast.internal.LoaderHelper#upgradeLock(Object, EntityEntry, LockOptions, SharedSessionContractImplementor)} + */ + public static CompletionStage upgradeLock( + Object object, + EntityEntry entry, + LockOptions lockOptions, + SharedSessionContractImplementor session) { + final LockMode requestedLockMode = lockOptions.getLockMode(); + if ( requestedLockMode.greaterThan( entry.getLockMode() ) ) { + // Request is for a more restrictive lock than the lock already held + final ReactiveEntityPersister persister = (ReactiveEntityPersister) entry.getPersister(); + + if ( entry.getStatus().isDeletedOrGone()) { + throw new ObjectDeletedException( + "attempted to lock a deleted instance", + entry.getId(), + persister.getEntityName() + ); + } + + if ( LoaderLogging.LOADER_LOGGER.isTraceEnabled() ) { + LoaderLogging.LOADER_LOGGER.tracef( + "Locking `%s( %s )` in `%s` lock-mode", + persister.getEntityName(), + entry.getId(), + requestedLockMode + ); + } + + final boolean cachingEnabled = persister.canWriteToCache(); + SoftLock lock = null; + Object ck = null; + try { + if ( cachingEnabled ) { + final EntityDataAccess cache = persister.getCacheAccessStrategy(); + ck = cache.generateCacheKey( entry.getId(), persister, session.getFactory(), session.getTenantIdentifier() ); + lock = cache.lockItem( session, ck, entry.getVersion() ); + } + + if ( persister.isVersioned() && entry.getVersion() == null ) { + // This should be an empty entry created for an uninitialized bytecode proxy + if ( !Hibernate.isPropertyInitialized( object, persister.getVersionMapping().getPartName() ) ) { + Hibernate.initialize( object ); + entry = session.getPersistenceContextInternal().getEntry( object ); + assert entry.getVersion() != null; + } + else { + throw new IllegalStateException( String.format( + "Trying to lock versioned entity %s but found null version", + MessageHelper.infoString( persister.getEntityName(), entry.getId() ) + ) ); + } + } + + if ( persister.isVersioned() && requestedLockMode == LockMode.PESSIMISTIC_FORCE_INCREMENT ) { + // todo : should we check the current isolation mode explicitly? + OptimisticLockHelper.forceVersionIncrement( object, entry, session ); + } + else if ( entry.isExistsInDatabase() ) { + final EventMonitor eventMonitor = session.getEventMonitor(); + final DiagnosticEvent entityLockEvent = eventMonitor.beginEntityLockEvent(); + return reactiveLock( object, entry, lockOptions, session, persister, eventMonitor, entityLockEvent, cachingEnabled, ck, lock ); + } + else { + // should only be possible for a stateful session + if ( session instanceof EventSource eventSource ) { + eventSource.forceFlush( entry ); + } + } + entry.setLockMode(requestedLockMode); + } + finally { + // the database now holds a lock + the object is flushed from the cache, + // so release the soft lock + if ( cachingEnabled ) { + persister.getCacheAccessStrategy().unlockItem( session, ck, lock ); + } + } + } + return voidFuture(); + } + + private static CompletionStage reactiveLock( + Object object, + EntityEntry entry, + LockOptions lockOptions, + SharedSessionContractImplementor session, + ReactiveEntityPersister persister, + EventMonitor eventMonitor, + DiagnosticEvent entityLockEvent, + boolean cachingEnabled, + Object ck, + SoftLock lock) { + return supplyStage( () -> supplyStage( () -> persister.reactiveLock( entry.getId(), entry.getVersion(), object, lockOptions, session ) ) + .whenComplete( (v, e) -> completeLockEvent( entry, lockOptions, session, persister, eventMonitor, entityLockEvent, cachingEnabled, ck, lock, e == null ) ) ) + .whenComplete( (v, e) -> { + if ( cachingEnabled ) { + persister.getCacheAccessStrategy().unlockItem( session, ck, lock ); + } + } ); + } + + private static void completeLockEvent( + EntityEntry entry, + LockOptions lockOptions, + SharedSessionContractImplementor session, + ReactiveEntityPersister persister, + EventMonitor eventMonitor, + DiagnosticEvent entityLockEvent, + boolean cachingEnabled, + Object ck, + SoftLock lock, + boolean succes) { + eventMonitor.completeEntityLockEvent( + entityLockEvent, + entry.getId(), + persister.getEntityName(), + lockOptions.getLockMode(), + succes, + session + ); + if ( cachingEnabled ) { + persister.getCacheAccessStrategy().unlockItem( session, ck, lock ); + } + } + } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/loader/ast/internal/ReactiveMultiIdEntityLoaderArrayParam.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/loader/ast/internal/ReactiveMultiIdEntityLoaderArrayParam.java index ef81c828e..dc9798f70 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/loader/ast/internal/ReactiveMultiIdEntityLoaderArrayParam.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/loader/ast/internal/ReactiveMultiIdEntityLoaderArrayParam.java @@ -34,10 +34,10 @@ import org.hibernate.reactive.sql.results.spi.ReactiveListResultsConsumer; import org.hibernate.sql.ast.tree.expression.JdbcParameter; import org.hibernate.sql.ast.tree.select.SelectStatement; +import org.hibernate.sql.exec.internal.JdbcOperationQuerySelect; import org.hibernate.sql.exec.internal.JdbcParameterBindingImpl; import org.hibernate.sql.exec.internal.JdbcParameterBindingsImpl; import org.hibernate.sql.exec.internal.JdbcParameterImpl; -import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; import org.hibernate.sql.exec.spi.JdbcParameterBindings; import org.hibernate.sql.exec.spi.JdbcParametersList; import org.hibernate.sql.results.internal.RowTransformerStandardImpl; diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/loader/ast/internal/ReactiveMultiIdEntityLoaderStandard.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/loader/ast/internal/ReactiveMultiIdEntityLoaderStandard.java index 5661c3493..dec1485d7 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/loader/ast/internal/ReactiveMultiIdEntityLoaderStandard.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/loader/ast/internal/ReactiveMultiIdEntityLoaderStandard.java @@ -39,8 +39,8 @@ import org.hibernate.reactive.util.impl.CompletionStages; import org.hibernate.sql.ast.SqlAstTranslatorFactory; import org.hibernate.sql.ast.tree.select.SelectStatement; +import org.hibernate.sql.exec.internal.JdbcOperationQuerySelect; import org.hibernate.sql.exec.internal.JdbcParameterBindingsImpl; -import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; import org.hibernate.sql.exec.spi.JdbcParameterBindings; import org.hibernate.sql.exec.spi.JdbcParametersList; import org.hibernate.sql.results.internal.RowTransformerStandardImpl; diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/loader/ast/internal/ReactiveMultiKeyLoadChunker.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/loader/ast/internal/ReactiveMultiKeyLoadChunker.java index e5ec58e5d..f5f4b2dc8 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/loader/ast/internal/ReactiveMultiKeyLoadChunker.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/loader/ast/internal/ReactiveMultiKeyLoadChunker.java @@ -12,9 +12,9 @@ import org.hibernate.reactive.sql.exec.internal.StandardReactiveSelectExecutor; import org.hibernate.reactive.sql.results.spi.ReactiveListResultsConsumer; import org.hibernate.sql.ast.tree.select.SelectStatement; +import org.hibernate.sql.exec.internal.JdbcOperationQuerySelect; import org.hibernate.sql.exec.internal.JdbcParameterBindingsImpl; import org.hibernate.sql.exec.spi.ExecutionContext; -import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; import org.hibernate.sql.exec.spi.JdbcParameterBindings; import org.hibernate.sql.exec.spi.JdbcParametersList; import org.hibernate.sql.results.internal.RowTransformerStandardImpl; diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/loader/ast/internal/ReactiveNaturalIdLoaderDelegate.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/loader/ast/internal/ReactiveNaturalIdLoaderDelegate.java index 08b83cd47..af8c98899 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/loader/ast/internal/ReactiveNaturalIdLoaderDelegate.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/loader/ast/internal/ReactiveNaturalIdLoaderDelegate.java @@ -39,9 +39,9 @@ import org.hibernate.sql.ast.tree.select.QuerySpec; import org.hibernate.sql.ast.tree.select.SelectStatement; import org.hibernate.sql.exec.internal.BaseExecutionContext; +import org.hibernate.sql.exec.internal.JdbcOperationQuerySelect; import org.hibernate.sql.exec.internal.JdbcParameterBindingsImpl; import org.hibernate.sql.exec.spi.Callback; -import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; import org.hibernate.sql.exec.spi.JdbcParameterBindings; import org.hibernate.sql.exec.spi.JdbcParametersList; import org.hibernate.sql.results.graph.DomainResult; diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/loader/ast/internal/ReactiveSingleUniqueKeyEntityLoaderStandard.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/loader/ast/internal/ReactiveSingleUniqueKeyEntityLoaderStandard.java index 09490cdad..99bbbd100 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/loader/ast/internal/ReactiveSingleUniqueKeyEntityLoaderStandard.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/loader/ast/internal/ReactiveSingleUniqueKeyEntityLoaderStandard.java @@ -31,9 +31,9 @@ import org.hibernate.sql.ast.SqlAstTranslatorFactory; import org.hibernate.sql.ast.tree.select.SelectStatement; import org.hibernate.sql.exec.internal.BaseExecutionContext; +import org.hibernate.sql.exec.internal.JdbcOperationQuerySelect; import org.hibernate.sql.exec.internal.JdbcParameterBindingsImpl; import org.hibernate.sql.exec.spi.Callback; -import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; import org.hibernate.sql.exec.spi.JdbcParameterBindings; import org.hibernate.sql.exec.spi.JdbcParametersList; diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/loader/internal/ReactiveCacheLoadHelper.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/loader/internal/ReactiveCacheLoadHelper.java new file mode 100644 index 000000000..1e6f3eddd --- /dev/null +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/loader/internal/ReactiveCacheLoadHelper.java @@ -0,0 +1,46 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.loader.internal; + +import org.hibernate.LockOptions; +import org.hibernate.engine.spi.EntityEntry; +import org.hibernate.engine.spi.EntityKey; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.event.spi.LoadEventListener; +import org.hibernate.loader.internal.CacheLoadHelper; +import org.hibernate.loader.internal.CacheLoadHelper.PersistenceContextEntry.EntityStatus; + +import java.util.concurrent.CompletionStage; + +import static org.hibernate.loader.internal.CacheLoadHelper.PersistenceContextEntry.EntityStatus.MANAGED; +import static org.hibernate.reactive.loader.ast.internal.ReactiveLoaderHelper.upgradeLock; +import static org.hibernate.reactive.util.impl.CompletionStages.completedFuture; + +/** + * A reactive implementation of {@link CacheLoadHelper} + */ +public class ReactiveCacheLoadHelper { + + public static CompletionStage loadFromSessionCache( + EntityKey keyToLoad, + LockOptions lockOptions, + LoadEventListener.LoadType options, + SharedSessionContractImplementor session) { + final Object old = session.getEntityUsingInterceptor( keyToLoad ); + EntityStatus entityStatus = MANAGED; + if ( old != null ) { + // this object was already loaded + final EntityEntry oldEntry = session.getPersistenceContext().getEntry( old ); + entityStatus = CacheLoadHelper.entityStatus( keyToLoad, options, session, oldEntry, old ); + if ( entityStatus == MANAGED ) { + return upgradeLock( old, oldEntry, lockOptions, session ) + .thenApply(v -> new CacheLoadHelper.PersistenceContextEntry( old, MANAGED ) ); + } + } + return completedFuture( new CacheLoadHelper.PersistenceContextEntry( old, entityStatus ) ); + } + +} diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/logging/impl/Log.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/logging/impl/Log.java index 4f205492e..24246aacf 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/logging/impl/Log.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/logging/impl/Log.java @@ -7,6 +7,7 @@ +import java.sql.SQLException; import java.sql.SQLWarning; import jakarta.persistence.PersistenceException; @@ -266,6 +267,16 @@ public interface Log extends BasicLogger { @Message(id = 83, value = "Unexpected request of a non reactive connection") HibernateException unexpectedConnectionRequest(); + @Message(id = 84, value = "The application requested a JDBC connection, but Hibernate Reactive doesn't use JDBC. This could be caused by a bug or the use of an unsupported feature in Hibernate Reactive") + SQLException notUsingJdbc(); + + @Message(id = 85, value = "Reactive sessions do not support transparent lazy fetching - use Stage.fetch() or Mutiny.fetch() (property '%1$S' of entity '%2$s' was not loaded)") + LazyInitializationException lazyFieldInitializationException(String fieldName, String entityName); + + @LogMessage(level = ERROR) + @Message(id = 86, value = "Error closing reactive connection") + void errorClosingConnection(@Cause Throwable throwable); + // Same method that exists in CoreMessageLogger @LogMessage(level = WARN) @Message(id = 104, value = "firstResult/maxResults specified with collection fetch; applying in memory!" ) @@ -324,5 +335,4 @@ public interface Log extends BasicLogger { " This is probably due to an operation failing fast due to the transaction being marked for rollback.", id = 520) void jdbcExceptionThrownWithTransactionRolledBack(@Cause JDBCException e); - } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/metamodel/internal/ReactiveEntityInstantiatorPojoOptimized.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/metamodel/internal/ReactiveEntityInstantiatorPojoOptimized.java new file mode 100644 index 000000000..42edfad31 --- /dev/null +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/metamodel/internal/ReactiveEntityInstantiatorPojoOptimized.java @@ -0,0 +1,42 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.metamodel.internal; + +import org.hibernate.bytecode.spi.ReflectionOptimizer; +import org.hibernate.mapping.PersistentClass; +import org.hibernate.metamodel.internal.EntityInstantiatorPojoOptimized; +import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.reactive.bythecode.enhance.spi.internal.ReactiveLazyAttributeLoadingInterceptor; +import org.hibernate.type.descriptor.java.JavaType; + +import static org.hibernate.engine.internal.ManagedTypeHelper.asPersistentAttributeInterceptable; + +/** + * Extends {@link EntityInstantiatorPojoOptimized} to apply a {@link ReactiveLazyAttributeLoadingInterceptor} + */ +public class ReactiveEntityInstantiatorPojoOptimized extends EntityInstantiatorPojoOptimized { + + public ReactiveEntityInstantiatorPojoOptimized( + EntityPersister persister, + PersistentClass persistentClass, + JavaType javaType, + ReflectionOptimizer.InstantiationOptimizer instantiationOptimizer) { + super( persister, persistentClass, javaType, instantiationOptimizer ); + } + + @Override + protected Object applyInterception(Object entity) { + if ( isApplyBytecodeInterception() ) { + asPersistentAttributeInterceptable( entity ) + .$$_hibernate_setInterceptor( new ReactiveLazyAttributeLoadingInterceptor( + getLoadingInterceptorState(), + null, + null + ) ); + } + return entity; + } +} diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/metamodel/internal/ReactiveEntityInstantiatorPojoStandard.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/metamodel/internal/ReactiveEntityInstantiatorPojoStandard.java new file mode 100644 index 000000000..7653fdad6 --- /dev/null +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/metamodel/internal/ReactiveEntityInstantiatorPojoStandard.java @@ -0,0 +1,41 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.metamodel.internal; + +import org.hibernate.mapping.PersistentClass; +import org.hibernate.metamodel.internal.EntityInstantiatorPojoStandard; +import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.reactive.bythecode.enhance.spi.internal.ReactiveLazyAttributeLoadingInterceptor; +import org.hibernate.type.descriptor.java.JavaType; + +import static org.hibernate.engine.internal.ManagedTypeHelper.asPersistentAttributeInterceptable; + +/** + * Extends {@link EntityInstantiatorPojoStandard} to apply a {@link ReactiveLazyAttributeLoadingInterceptor} + */ +public class ReactiveEntityInstantiatorPojoStandard extends EntityInstantiatorPojoStandard { + + public ReactiveEntityInstantiatorPojoStandard( + EntityPersister persister, + PersistentClass persistentClass, + JavaType javaType) { + super( persister, persistentClass, javaType ); + } + + @Override + protected Object applyInterception(Object entity) { + if ( isApplyBytecodeInterception() ) { + asPersistentAttributeInterceptable( entity ) + .$$_hibernate_setInterceptor( new ReactiveLazyAttributeLoadingInterceptor( + getLoadingInterceptorState(), + null, + null + ) ); + } + return entity; + + } +} diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/metamodel/internal/ReactiveEntityRepresentationStrategyPojoStandard.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/metamodel/internal/ReactiveEntityRepresentationStrategyPojoStandard.java new file mode 100644 index 000000000..34e34e189 --- /dev/null +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/metamodel/internal/ReactiveEntityRepresentationStrategyPojoStandard.java @@ -0,0 +1,43 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.metamodel.internal; + +import org.hibernate.bytecode.spi.ReflectionOptimizer; +import org.hibernate.mapping.PersistentClass; +import org.hibernate.metamodel.internal.EntityRepresentationStrategyPojoStandard; +import org.hibernate.metamodel.spi.EntityInstantiator; +import org.hibernate.metamodel.spi.RuntimeModelCreationContext; +import org.hibernate.persister.entity.EntityPersister; + +/** + * Extends {@link EntityRepresentationStrategyPojoStandard} + * to create {@link ReactiveEntityInstantiatorPojoOptimized} and {@link ReactiveEntityInstantiatorPojoStandard} + */ +public class ReactiveEntityRepresentationStrategyPojoStandard extends EntityRepresentationStrategyPojoStandard { + + public ReactiveEntityRepresentationStrategyPojoStandard( + PersistentClass bootDescriptor, + EntityPersister runtimeDescriptor, + RuntimeModelCreationContext creationContext) { + super( bootDescriptor, runtimeDescriptor, creationContext ); + } + + @Override + protected EntityInstantiator determineInstantiator(PersistentClass bootDescriptor, EntityPersister persister) { + final ReflectionOptimizer reflectionOptimizer = getReflectionOptimizer(); + if ( reflectionOptimizer != null && reflectionOptimizer.getInstantiationOptimizer() != null ) { + return new ReactiveEntityInstantiatorPojoOptimized( + persister, + bootDescriptor, + getMappedJavaType(), + reflectionOptimizer.getInstantiationOptimizer() + ); + } + else { + return new ReactiveEntityInstantiatorPojoStandard( persister, bootDescriptor, getMappedJavaType() ); + } + } +} diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/metamodel/mapping/internal/ReactiveEmbeddedIdentifierMappingImpl.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/metamodel/mapping/internal/ReactiveEmbeddedIdentifierMappingImpl.java index 3f5244fa9..204a23d3a 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/metamodel/mapping/internal/ReactiveEmbeddedIdentifierMappingImpl.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/metamodel/mapping/internal/ReactiveEmbeddedIdentifierMappingImpl.java @@ -13,6 +13,7 @@ import org.hibernate.metamodel.mapping.EmbeddableMappingType; import org.hibernate.metamodel.mapping.JdbcMapping; import org.hibernate.metamodel.mapping.internal.EmbeddedIdentifierMappingImpl; +import org.hibernate.metamodel.mapping.internal.ToOneAttributeMapping; import org.hibernate.reactive.sql.results.graph.embeddable.internal.ReactiveEmbeddableFetchImpl; import org.hibernate.spi.NavigablePath; import org.hibernate.sql.ast.spi.SqlSelection; @@ -20,6 +21,7 @@ import org.hibernate.sql.results.graph.DomainResultCreationState; import org.hibernate.sql.results.graph.Fetch; import org.hibernate.sql.results.graph.FetchParent; +import org.hibernate.sql.results.graph.Fetchable; public class ReactiveEmbeddedIdentifierMappingImpl extends AbstractCompositeIdentifierMapping { @@ -104,4 +106,13 @@ public String getSqlAliasStem() { public String getFetchableName() { return delegate.getFetchableName(); } + + @Override + public Fetchable getFetchable(int position) { + Fetchable fetchable = delegate.getFetchable( position ); + if ( fetchable instanceof ToOneAttributeMapping ) { + return new ReactiveToOneAttributeMapping( (ToOneAttributeMapping) fetchable ); + } + return fetchable; + } } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/metamodel/mapping/internal/ReactiveRuntimeModelCreationContext.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/metamodel/mapping/internal/ReactiveRuntimeModelCreationContext.java index 876312b47..2f3719890 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/metamodel/mapping/internal/ReactiveRuntimeModelCreationContext.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/metamodel/mapping/internal/ReactiveRuntimeModelCreationContext.java @@ -7,6 +7,7 @@ import java.util.Map; +import org.hibernate.boot.model.relational.Database; import org.hibernate.boot.model.relational.SqlStringGenerationContext; import org.hibernate.boot.spi.BootstrapContext; import org.hibernate.boot.spi.MetadataImplementor; @@ -16,29 +17,37 @@ import org.hibernate.engine.jdbc.spi.JdbcServices; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.generator.Generator; +import org.hibernate.generator.GeneratorCreationContext; import org.hibernate.mapping.GeneratorSettings; import org.hibernate.mapping.PersistentClass; +import org.hibernate.mapping.Property; +import org.hibernate.mapping.RootClass; +import org.hibernate.mapping.SimpleValue; +import org.hibernate.mapping.Value; import org.hibernate.metamodel.spi.MappingMetamodelImplementor; import org.hibernate.metamodel.spi.RuntimeModelCreationContext; -import org.hibernate.persister.entity.EntityPersister; import org.hibernate.query.sqm.function.SqmFunctionRegistry; -import org.hibernate.reactive.tuple.entity.ReactiveEntityMetamodel; +import org.hibernate.reactive.boot.spi.ReactiveBootstrapContextAdapter; +import org.hibernate.reactive.logging.impl.Log; import org.hibernate.service.ServiceRegistry; -import org.hibernate.tuple.entity.EntityMetamodel; +import org.hibernate.type.Type; import org.hibernate.type.descriptor.java.spi.JavaTypeRegistry; import org.hibernate.type.spi.TypeConfiguration; +import static java.lang.invoke.MethodHandles.lookup; +import static org.hibernate.reactive.generator.values.internal.ReactiveGeneratedValuesHelper.augmentWithReactiveGenerator; +import static org.hibernate.reactive.logging.impl.LoggerFactory.make; + public class ReactiveRuntimeModelCreationContext implements RuntimeModelCreationContext { + private static final Log LOG = make( Log.class, lookup() ); + private final RuntimeModelCreationContext delegate; + private final BootstrapContext bootstrapContext; public ReactiveRuntimeModelCreationContext(RuntimeModelCreationContext delegate) { this.delegate = delegate; - } - - @Override - public EntityMetamodel createEntityMetamodel(PersistentClass persistentClass, EntityPersister persister) { - return new ReactiveEntityMetamodel( persistentClass, persister, delegate ); + bootstrapContext = new ReactiveBootstrapContextAdapter( delegate.getBootstrapContext() ); } @Override @@ -48,7 +57,7 @@ public SessionFactoryImplementor getSessionFactory() { @Override public BootstrapContext getBootstrapContext() { - return delegate.getBootstrapContext(); + return bootstrapContext; } @Override @@ -125,4 +134,91 @@ public Map getGenerators() { public GeneratorSettings getGeneratorSettings() { return delegate.getGeneratorSettings(); } + + @Override + public Generator getOrCreateIdGenerator(String rootName, PersistentClass persistentClass){ + final Generator existing = getGenerators().get( rootName ); + if ( existing != null ) { + return existing; + } + else { + final SimpleValue identifier = (SimpleValue) persistentClass.getIdentifier(); + final Generator idgenerator = augmentWithReactiveGenerator( + identifier.createGenerator( + getDialect(), + persistentClass.getRootClass(), + persistentClass.getIdentifierProperty(), + getGeneratorSettings() + ), + new IdGeneratorCreationContext( + persistentClass.getRootClass(), + persistentClass.getIdentifierProperty(), + getGeneratorSettings(), + identifier, + this + ), + this ); + getGenerators().put( rootName, idgenerator ); + return idgenerator; + } + } + + private record IdGeneratorCreationContext( + RootClass rootClass, + Property property, + GeneratorSettings defaults, + SimpleValue identifier, + RuntimeModelCreationContext buildingContext) implements GeneratorCreationContext { + + @Override + public Database getDatabase() { + return buildingContext.getBootModel().getDatabase(); + } + + @Override + public ServiceRegistry getServiceRegistry() { + return buildingContext.getBootstrapContext().getServiceRegistry(); + } + + @Override + public SqlStringGenerationContext getSqlStringGenerationContext() { + return defaults.getSqlStringGenerationContext(); + } + + @Override + public String getDefaultCatalog() { + return defaults.getDefaultCatalog(); + } + + @Override + public String getDefaultSchema() { + return defaults.getDefaultSchema(); + } + + @Override + public RootClass getRootClass() { + return rootClass; + } + + @Override + public PersistentClass getPersistentClass() { + return rootClass; + } + + @Override + public Property getProperty() { + return property; + } + + @Override + public Value getValue() { + return identifier; + } + + @Override + public Type getType() { + return identifier.getType(); + } + } + } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/metamodel/spi/ReactiveManagedTypeRepresentationResolver.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/metamodel/spi/ReactiveManagedTypeRepresentationResolver.java new file mode 100644 index 000000000..8641cf728 --- /dev/null +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/metamodel/spi/ReactiveManagedTypeRepresentationResolver.java @@ -0,0 +1,40 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.metamodel.spi; + +import org.hibernate.mapping.PersistentClass; +import org.hibernate.metamodel.internal.EntityRepresentationStrategyMap; +import org.hibernate.metamodel.internal.ManagedTypeRepresentationResolverStandard; +import org.hibernate.metamodel.spi.EntityRepresentationStrategy; +import org.hibernate.metamodel.spi.RuntimeModelCreationContext; +import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.reactive.metamodel.internal.ReactiveEntityRepresentationStrategyPojoStandard; + +/** + * Extends {@link ManagedTypeRepresentationResolverStandard} to create a {@link ReactiveEntityRepresentationStrategyPojoStandard} + */ +public class ReactiveManagedTypeRepresentationResolver extends ManagedTypeRepresentationResolverStandard { + + public static final ManagedTypeRepresentationResolverStandard INSTANCE = new ReactiveManagedTypeRepresentationResolver(); + + @Override + public EntityRepresentationStrategy resolveStrategy( + PersistentClass bootDescriptor, + EntityPersister runtimeDescriptor, + RuntimeModelCreationContext creationContext) { + if ( bootDescriptor.getMappedClass() == null ) { // i.e. RepresentationMode.MAP; + return new EntityRepresentationStrategyMap( bootDescriptor, creationContext ); + } + else { + return new ReactiveEntityRepresentationStrategyPojoStandard( + bootDescriptor, + runtimeDescriptor, + creationContext + ); + } + } + +} diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/mutiny/Mutiny.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/mutiny/Mutiny.java index bee3a5c1f..c8d1dfd5e 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/mutiny/Mutiny.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/mutiny/Mutiny.java @@ -5,6 +5,11 @@ */ package org.hibernate.reactive.mutiny; +import java.lang.invoke.MethodHandles; +import java.util.List; +import java.util.function.BiFunction; +import java.util.function.Function; + import org.hibernate.Cache; import org.hibernate.CacheMode; import org.hibernate.Filter; @@ -44,10 +49,6 @@ import jakarta.persistence.criteria.CriteriaUpdate; import jakarta.persistence.metamodel.Attribute; import jakarta.persistence.metamodel.Metamodel; -import java.lang.invoke.MethodHandles; -import java.util.List; -import java.util.function.BiFunction; -import java.util.function.Function; import static org.hibernate.engine.internal.ManagedTypeHelper.asPersistentAttributeInterceptable; import static org.hibernate.engine.internal.ManagedTypeHelper.isPersistentAttributeInterceptable; @@ -1971,6 +1972,59 @@ interface Transaction { */ interface SessionFactory extends AutoCloseable { + /** + * Obtain a new {@link Session reactive session}, the main + * interaction point between the user's program and Hibernate + * Reactive. + *

+ * The underlying database connection is obtained lazily + * when the returned {@link Session} needs to access the + * database. + *

+ * The client must close the session using {@link Session#close()}. + */ + @Incubating + Session createSession(); + + /** + * Obtain a new {@link Session reactive session}. + *

+ * The underlying database connection is obtained lazily + * when the returned {@link Session} needs to access the + * database. + *

+ * The client must close the session using {@link Session#close()}. + * @param tenantId the id of the tenant + */ + @Incubating + Session createSession(String tenantId); + + /** + * Obtain a new {@link StatelessSession reactive stateless session}. + *

+ * The underlying database connection is obtained lazily + * when the returned {@link StatelessSession} needs to access the + * database. + *

+ * The client must close the session using {@link Session#close()}. + */ + @Incubating + StatelessSession createStatelessSession(); + + /** + * Obtain a new {@link StatelessSession reactive stateless session}. + *

+ * The underlying database connection is obtained lazily + * when the returned {@link StatelessSession} needs to access the + * database. + *

+ * The client must close the session using {@link Session#close()}. + * + * @param tenantId the id of the tenant + */ + @Incubating + StatelessSession createStatelessSession(String tenantId); + /** * Obtain a new {@link Session reactive session} {@link Uni}, the main * interaction point between the user's program and Hibernate diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/mutiny/impl/MutinySessionFactoryImpl.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/mutiny/impl/MutinySessionFactoryImpl.java index 46a71b0b8..1959872ad 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/mutiny/impl/MutinySessionFactoryImpl.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/mutiny/impl/MutinySessionFactoryImpl.java @@ -13,8 +13,11 @@ import java.util.function.Supplier; import org.hibernate.Cache; -import org.hibernate.internal.SessionCreationOptions; +import org.hibernate.engine.creation.internal.SessionBuilderImpl; +import org.hibernate.engine.creation.internal.SessionCreationOptions; +import org.hibernate.engine.spi.SessionImplementor; import org.hibernate.internal.SessionFactoryImpl; +import org.hibernate.internal.SessionImpl; import org.hibernate.query.criteria.HibernateCriteriaBuilder; import org.hibernate.reactive.common.spi.Implementor; import org.hibernate.reactive.context.Context; @@ -25,6 +28,8 @@ import org.hibernate.reactive.mutiny.Mutiny; import org.hibernate.reactive.pool.ReactiveConnection; import org.hibernate.reactive.pool.ReactiveConnectionPool; +import org.hibernate.reactive.session.ReactiveSession; +import org.hibernate.reactive.session.ReactiveStatelessSession; import org.hibernate.reactive.session.impl.ReactiveSessionImpl; import org.hibernate.reactive.session.impl.ReactiveStatelessSessionImpl; import org.hibernate.service.ServiceRegistry; @@ -85,21 +90,48 @@ public Context getContext() { return context; } + @Override + public Mutiny.Session createSession() { + return createSession( getTenantIdentifier( options() ) ); + } + + @Override + public Mutiny.Session createSession(String tenantId) { + final SessionCreationOptions options = options(); + ReactiveConnectionPool pool = delegate.getServiceRegistry().getService( ReactiveConnectionPool.class ); + ReactiveSession sessionImpl = new ReactiveSessionImpl( delegate, options, pool.getProxyConnection( tenantId ) ); + return new MutinySessionImpl( sessionImpl, this ); + } + + @Override + public Mutiny.StatelessSession createStatelessSession() { + return createStatelessSession( getTenantIdentifier( options() ) ); + } + + @Override + public Mutiny.StatelessSession createStatelessSession(String tenantId) { + final SessionCreationOptions options = options(); + ReactiveConnectionPool pool = delegate.getServiceRegistry().getService( ReactiveConnectionPool.class ); + ReactiveStatelessSession sessionImpl = new ReactiveStatelessSessionImpl( delegate, options, pool.getProxyConnection( tenantId ) ); + return new MutinyStatelessSessionImpl( sessionImpl, this ); + } + @Override public Uni openSession() { SessionCreationOptions options = options(); - return uni( () -> connection( options.getTenantIdentifier() ) ) - .chain( reactiveConnection -> create( reactiveConnection, - () -> new ReactiveSessionImpl( delegate, options, reactiveConnection ) ) ) - .map( s -> new MutinySessionImpl(s, this) ); + return uni( () -> connection( getTenantIdentifier( options ) ) ) + .chain( reactiveConnection -> create( + reactiveConnection, + () -> new ReactiveSessionImpl( delegate, options, reactiveConnection ) + ) ) + .map( s -> new MutinySessionImpl( s, this ) ); } @Override public Uni openSession(String tenantId) { return uni( () -> connection( tenantId ) ) - .chain( reactiveConnection -> create( reactiveConnection, - () -> new ReactiveSessionImpl( delegate, options( tenantId ), reactiveConnection ) ) ) - .map( s -> new MutinySessionImpl(s, this) ); + .chain( reactiveConnection -> create( reactiveConnection, () -> new ReactiveSessionImpl( delegate, options( tenantId ), reactiveConnection ) ) ) + .map( s -> new MutinySessionImpl( s, this ) ); } /** @@ -118,27 +150,41 @@ private static Uni close(ReactiveConnection connection) { @Override public Uni openStatelessSession() { SessionCreationOptions options = options(); - return uni( () -> connection( options.getTenantIdentifier() ) ) - .chain( reactiveConnection -> create( reactiveConnection, - () -> new ReactiveStatelessSessionImpl( delegate, options, reactiveConnection ) ) ) - .map( s -> new MutinyStatelessSessionImpl(s, this) ); + return uni( () -> connection( getTenantIdentifier( options ) ) ) + .chain( reactiveConnection -> create( + reactiveConnection, + () -> new ReactiveStatelessSessionImpl( delegate, options, reactiveConnection ) + ) ) + .map( s -> new MutinyStatelessSessionImpl( s, this ) ); } @Override public Uni openStatelessSession(String tenantId) { return uni( () -> connection( tenantId ) ) - .chain( reactiveConnection -> create( reactiveConnection, - () -> new ReactiveStatelessSessionImpl( delegate, options( tenantId ), reactiveConnection ) ) ) + .chain( reactiveConnection -> create( + reactiveConnection, + () -> new ReactiveStatelessSessionImpl( delegate, options( tenantId ), reactiveConnection ) + ) ) .map( s -> new MutinyStatelessSessionImpl( s, this ) ); } private SessionCreationOptions options() { - return new SessionFactoryImpl.SessionBuilderImpl( delegate ); + return new SessionBuilderImpl( delegate ) { + @Override + protected SessionImplementor createSession() { + return new SessionImpl( delegate, this ); + } + }; } private SessionCreationOptions options(String tenantIdentifier) { - return new SessionFactoryImpl.SessionBuilderImpl( delegate ) - .tenantIdentifier( tenantIdentifier ); + SessionBuilderImpl sessionBuilder = new SessionBuilderImpl( delegate ) { + @Override + protected SessionImplementor createSession() { + return new SessionImpl( delegate, this ); + } + }; + return (SessionCreationOptions) sessionBuilder.tenantIdentifier( tenantIdentifier ); } private CompletionStage connection(String tenantId) { @@ -177,8 +223,8 @@ public Uni withSession(String tenantId, Function> Objects.requireNonNull( tenantId, "parameter 'tenantId' is required" ); Objects.requireNonNull( work, "parameter 'work' is required" ); Context.Key key = new MultitenantKey<>( contextKeyForSession, tenantId ); - Mutiny.Session current = context.get(key); - if ( current!=null && current.isOpen() ) { + Mutiny.Session current = context.get( key ); + if ( current != null && current.isOpen() ) { LOG.debugf( "Reusing existing open Mutiny.Session which was found in the current Vert.x context for current tenant '%s'", tenantId ); return work.apply( current ); } @@ -214,11 +260,11 @@ public Uni withStatelessSession(String tenantId, Function Uni withSession( + private Uni withSession( Uni sessionUni, Function> work, Context.Key contextKey) { @@ -233,25 +279,25 @@ private Uni withSession( @Override public Uni withTransaction(BiFunction> work) { Objects.requireNonNull( work, "parameter 'work' is required" ); - return withSession( s -> s.withTransaction( t -> work.apply(s, t) ) ); + return withSession( s -> s.withTransaction( t -> work.apply( s, t ) ) ); } @Override public Uni withStatelessTransaction(BiFunction> work) { Objects.requireNonNull( work, "parameter 'work' is required" ); - return withStatelessSession( s -> s.withTransaction( t -> work.apply(s, t) ) ); + return withStatelessSession( s -> s.withTransaction( t -> work.apply( s, t ) ) ); } @Override public Uni withTransaction(String tenantId, BiFunction> work) { Objects.requireNonNull( work, "parameter 'work' is required" ); - return withSession( tenantId, s -> s.withTransaction( t -> work.apply(s, t) ) ); + return withSession( tenantId, s -> s.withTransaction( t -> work.apply( s, t ) ) ); } @Override public Uni withStatelessTransaction(String tenantId, BiFunction> work) { Objects.requireNonNull( work, "parameter 'work' is required" ); - return withStatelessSession( tenantId, s -> s.withTransaction( t -> work.apply(s, t) ) ); + return withStatelessSession( tenantId, s -> s.withTransaction( t -> work.apply( s, t ) ) ); } @Override @@ -283,4 +329,9 @@ public void close() { public boolean isOpen() { return delegate.isOpen(); } + + private String getTenantIdentifier(SessionCreationOptions options) { + return options.getTenantIdentifierValue() == null ? null : delegate.getTenantIdentifierJavaType().toString( + options.getTenantIdentifierValue() ); + } } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/mutiny/impl/MutinySessionImpl.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/mutiny/impl/MutinySessionImpl.java index c80be96ed..5f4ea40f4 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/mutiny/impl/MutinySessionImpl.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/mutiny/impl/MutinySessionImpl.java @@ -221,7 +221,7 @@ public SelectionQuery createNativeQuery(String queryString, ResultSetMapp @Override public Uni find(Class entityClass, Object primaryKey) { - return uni( () -> delegate.reactiveFind( entityClass, primaryKey, null, null ) ); + return uni( () -> delegate.reactiveFind( entityClass, primaryKey ) ); } @Override @@ -236,7 +236,7 @@ public Uni find(Class entityClass, Identifier id) { @Override public Uni find(Class entityClass, Object primaryKey, LockMode lockMode) { - return uni( () -> delegate.reactiveFind( entityClass, primaryKey, new LockOptions( lockMode ), null ) ); + return uni( () -> delegate.reactiveFind( entityClass, primaryKey, lockMode, null ) ); } @Override @@ -252,7 +252,7 @@ public Uni find(Class entityClass, Object primaryKey, LockOptions lock @Override public Uni find(EntityGraph entityGraph, Object id) { Class entityClass = ( (RootGraph) entityGraph ).getGraphedType().getJavaType(); - return uni( () -> delegate.reactiveFind( entityClass, id, null, entityGraph ) ); + return uni( () -> delegate.reactiveFind( entityClass, id, (LockMode) null, entityGraph ) ); } @Override @@ -297,7 +297,7 @@ public Uni refresh(Object entity) { @Override public Uni refresh(Object entity, LockMode lockMode) { - return uni( () -> delegate.reactiveRefresh( entity, new LockOptions( lockMode ) ) ); + return uni( () -> delegate.reactiveRefresh( entity, lockMode ) ); } @Override @@ -317,7 +317,7 @@ public Uni refreshAll(Object... entity) { @Override public Uni lock(Object entity, LockMode lockMode) { - return uni( () -> delegate.reactiveLock( entity, new LockOptions( lockMode ) ) ); + return uni( () -> delegate.reactiveLock( entity, lockMode ) ); } @Override diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveAbstractEntityPersister.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveAbstractEntityPersister.java index 0ebebe603..1031f8b17 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveAbstractEntityPersister.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveAbstractEntityPersister.java @@ -30,7 +30,6 @@ import org.hibernate.bytecode.enhance.spi.interceptor.LazyAttributesMetadata; import org.hibernate.bytecode.spi.BytecodeEnhancementMetadata; import org.hibernate.collection.spi.PersistentCollection; -import org.hibernate.engine.internal.ManagedTypeHelper; import org.hibernate.engine.spi.EntityEntry; import org.hibernate.engine.spi.EntityKey; import org.hibernate.engine.spi.LoadQueryInfluencers; @@ -69,18 +68,19 @@ import org.hibernate.sql.ast.tree.select.SelectStatement; import org.hibernate.sql.exec.spi.JdbcParametersList; import org.hibernate.type.BasicType; +import org.hibernate.type.Type; import jakarta.persistence.metamodel.Attribute; import static java.lang.invoke.MethodHandles.lookup; import static java.util.Collections.emptyMap; +import static org.hibernate.engine.internal.ManagedTypeHelper.asPersistentAttributeInterceptable; import static org.hibernate.generator.EventType.INSERT; import static org.hibernate.generator.EventType.UPDATE; import static org.hibernate.internal.util.collections.CollectionHelper.setOfSize; import static org.hibernate.pretty.MessageHelper.infoString; import static org.hibernate.reactive.logging.impl.LoggerFactory.make; import static org.hibernate.reactive.util.impl.CompletionStages.completedFuture; -import static org.hibernate.reactive.util.impl.CompletionStages.failedFuture; import static org.hibernate.reactive.util.impl.CompletionStages.logSqlException; import static org.hibernate.reactive.util.impl.CompletionStages.nullFuture; import static org.hibernate.reactive.util.impl.CompletionStages.voidFuture; @@ -106,6 +106,8 @@ */ public interface ReactiveAbstractEntityPersister extends ReactiveEntityPersister { + Log LOG = make( Log.class, lookup() ); + /** * A self-reference of type {@code AbstractEntityPersister}. * @@ -303,25 +305,25 @@ default CompletionStage reactiveGetCurrentVersion(Object id, SharedSessi return getReactiveConnection( session ) .selectJdbc( delegate().getVersionSelectString(), params ) - .thenCompose( resultSet -> currentVersion( session, resultSet ) ); + .thenApply( resultSet -> currentVersion( session, resultSet ) ); } - private CompletionStage currentVersion(SharedSessionContractImplementor session, ResultSet resultSet) { + private Object currentVersion(SharedSessionContractImplementor session, ResultSet resultSet) { try { if ( !resultSet.next() ) { - return nullFuture(); + return null; } if ( !isVersioned() ) { - return completedFuture( this ); + return this; } - return completedFuture( getVersionType() - .getJdbcMapping() - .getJdbcValueExtractor() - .extract( resultSet, 1, session ) ); + return getVersionType() + .getJdbcMapping() + .getJdbcValueExtractor() + .extract( resultSet, 1, session ); } catch (SQLException sqle) { //can never happen - return failedFuture( new JDBCException( "error reading version", sqle ) ); + throw new JDBCException( "error reading version", sqle ); } } @@ -375,32 +377,51 @@ default CompletionStage reactiveInitializeLazyPropertiesFromDatastore( EntityEntry entry, String fieldName, SharedSessionContractImplementor session) { + return isNonLazyPropertyName( fieldName ) + ? initLazyProperty( entity, id, entry, fieldName, session ) + : initLazyProperties( entity, id, entry, fieldName, session ); + } - if ( !hasLazyProperties() ) { - throw new AssertionFailure( "no lazy properties" ); - } + boolean isNonLazyPropertyName(String fieldName); - final PersistentAttributeInterceptor interceptor = ManagedTypeHelper.asPersistentAttributeInterceptable( entity ).$$_hibernate_getInterceptor(); - if ( interceptor == null ) { - throw new AssertionFailure( "Expecting bytecode interceptor to be non-null" ); - } + private CompletionStage initLazyProperty( + Object entity, + Object id, + EntityEntry entry, + String fieldName, + SharedSessionContractImplementor session) { + // An eager property can be lazy because of an applied EntityGraph + final int propertyIndex = getPropertyIndex( fieldName ); + final List partsToSelect = List.of( getAttributeMapping( propertyIndex ) ); + return reactiveGetOrCreateLazyLoadPlan( fieldName, partsToSelect ) + .load( id, session ) + .thenApply( results -> { + final Object result = results[0]; + initializeLazyProperty( entity, entry, result, propertyIndex, getPropertyTypes()[propertyIndex] ); + return result; + } ); + } - make( Log.class, lookup() ).tracef( "Initializing lazy properties from datastore (triggered for `%s`)", fieldName ); + ReactiveSingleIdArrayLoadPlan reactiveGetOrCreateLazyLoadPlan(String fieldName, List partsToSelect); - final String fetchGroup = getEntityPersister().getBytecodeEnhancementMetadata() - .getLazyAttributesMetadata() - .getFetchGroupName( fieldName ); - final List fetchGroupAttributeDescriptors = getEntityPersister().getBytecodeEnhancementMetadata() - .getLazyAttributesMetadata() - .getFetchGroupAttributeDescriptors( fetchGroup ); + private CompletionStage initLazyProperties( + Object entity, + Object id, + EntityEntry entry, + String fieldName, + SharedSessionContractImplementor session) { - @SuppressWarnings("deprecation") - Set initializedLazyAttributeNames = interceptor.getInitializedLazyAttributeNames(); + assert hasLazyProperties(); + LOG.tracef( "Initializing lazy properties from datastore (triggered for '%s')", fieldName ); - // FIXME: How do I pass this to the query? - Object[] arguments = PreparedStatementAdaptor.bind( - statement -> getIdentifierType().nullSafeSet( statement, id, 1, session ) - ); + final var interceptor = asPersistentAttributeInterceptable( entity ).$$_hibernate_getInterceptor(); + assert interceptor != null : "Expecting bytecode interceptor to be non-null"; + final Set initializedLazyAttributeNames = interceptor.getInitializedLazyAttributeNames(); + LOG.tracef( "Initializing lazy properties from datastore (triggered for `%s`)", fieldName ); + + final var lazyAttributesMetadata = getBytecodeEnhancementMetadata().getLazyAttributesMetadata(); + final String fetchGroup = lazyAttributesMetadata.getFetchGroupName( fieldName ); + final var fetchGroupAttributeDescriptors = lazyAttributesMetadata.getFetchGroupAttributeDescriptors( fetchGroup ); return reactiveGetSQLLazySelectLoadPlan( fetchGroup ) .load( id, session ) @@ -421,51 +442,55 @@ default CompletionStage initLazyProperty( PersistentAttributeInterceptor interceptor, List fetchGroupAttributeDescriptors, Set initializedLazyAttributeNames, - Object[] values) { // Load all the lazy properties that are in the same fetch group - CompletionStage resultStage = nullFuture(); - int i = 0; - for ( LazyAttributeDescriptor fetchGroupAttributeDescriptor : fetchGroupAttributeDescriptors ) { - if ( initializedLazyAttributeNames.contains( fetchGroupAttributeDescriptor.getName() ) ) { + Object[] results) { // Load all the lazy properties that are in the same fetch group + CompletionStage finalResultStage = nullFuture(); + final int[] i = { 0 }; + for ( var fetchGroupAttributeDescriptor : fetchGroupAttributeDescriptors ) { + final String attributeName = fetchGroupAttributeDescriptor.getName(); + final boolean previousInitialized = initializedLazyAttributeNames.contains( attributeName ); + final int index = i[0]++; + if ( previousInitialized ) { // Already initialized - if ( fetchGroupAttributeDescriptor.getName().equals( fieldName ) ) { - resultStage = completedFuture( entry.getLoadedValue( fetchGroupAttributeDescriptor.getName() ) ); + if ( attributeName.equals( fieldName ) ) { + finalResultStage = finalResultStage + .thenApply( finalResult -> entry.getLoadedValue( fetchGroupAttributeDescriptor.getName() ) ); } + // it's already been initialized (e.g. by a write) so we don't want to overwrite + // TODO: we should consider 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 continue; } - final Object selectedValue = values[i++]; - if ( selectedValue instanceof CompletionStage ) { + final Object result = results[index]; + if ( result instanceof CompletionStage ) { // This happens with a lazy one-to-one (bytecode enhancement enabled) - CompletionStage selectedValueStage = (CompletionStage) selectedValue; - resultStage = resultStage - .thenCompose( result -> selectedValueStage - .thenApply( selected -> { - final boolean set = initializeLazyProperty( - fieldName, - entity, - entry, - fetchGroupAttributeDescriptor.getLazyIndex(), - selected - ); - if ( set ) { - interceptor.attributeInitialized( fetchGroupAttributeDescriptor.getName() ); - return selected; - } - return result; - } ) - ); + final CompletionStage resultStage = (CompletionStage) result; + finalResultStage = finalResultStage.thenCompose( finalResult -> resultStage + .thenApply( value -> { + if ( initializeLazyProperty( fieldName, entity, entry, fetchGroupAttributeDescriptor, result ) ) { + interceptor.attributeInitialized( fetchGroupAttributeDescriptor.getName() ); + return value; + } + return finalResult; + } ) + ); } else { - final boolean set = initializeLazyProperty( fieldName, entity, entry, fetchGroupAttributeDescriptor.getLazyIndex(), selectedValue ); - if ( set ) { - resultStage = completedFuture( selectedValue ); + if ( initializeLazyProperty( fieldName, entity, entry, fetchGroupAttributeDescriptor, result ) ) { interceptor.attributeInitialized( fetchGroupAttributeDescriptor.getName() ); + finalResultStage = finalResultStage.thenApply( finalResult -> result ); } } } - return resultStage.thenApply( result -> { - make( Log.class, lookup() ).trace( "Done initializing lazy properties" ); + return finalResultStage.thenApply( result -> { + LOG.trace( "Done initializing lazy properties" ); return result; } ); } @@ -474,13 +499,9 @@ default CompletionStage reactiveInitializeEnhancedEntityUsedAsProxy( Object entity, String nameOfAttributeBeingAccessed, SharedSessionContractImplementor session) { - final BytecodeEnhancementMetadata enhancementMetadata = getEntityPersister().getBytecodeEnhancementMetadata(); final BytecodeLazyAttributeInterceptor currentInterceptor = enhancementMetadata.extractLazyInterceptor( entity ); - if ( currentInterceptor instanceof EnhancementAsProxyLazinessInterceptor ) { - final EnhancementAsProxyLazinessInterceptor proxyInterceptor = - (EnhancementAsProxyLazinessInterceptor) currentInterceptor; - + if ( currentInterceptor instanceof EnhancementAsProxyLazinessInterceptor proxyInterceptor ) { final EntityKey entityKey = proxyInterceptor.getEntityKey(); final Object identifier = entityKey.getIdentifier(); @@ -494,19 +515,17 @@ default CompletionStage reactiveInitializeEnhancedEntityUsedAsProxy( .handleEntityNotFound( entityKey.getEntityName(), identifier ); } + final LazyAttributeLoadingInterceptor interceptor = enhancementMetadata + .injectInterceptor( entity, identifier, session ); + if ( nameOfAttributeBeingAccessed == null ) { return null; } else { - final LazyAttributeLoadingInterceptor interceptor = enhancementMetadata - .injectInterceptor( entity, identifier, session ); return interceptor.readObject( - entity, - nameOfAttributeBeingAccessed, - interceptor.isAttributeLoaded( nameOfAttributeBeingAccessed ) - ? getPropertyValue( entity, nameOfAttributeBeingAccessed ) - : ( (LazyPropertyInitializer) this ) - .initializeLazyProperty( nameOfAttributeBeingAccessed, entity, session ) + entity, nameOfAttributeBeingAccessed, interceptor.isAttributeLoaded( nameOfAttributeBeingAccessed ) + ? getPropertyValue( entity, nameOfAttributeBeingAccessed ) + : ( (LazyPropertyInitializer) this ).initializeLazyProperty( nameOfAttributeBeingAccessed, entity, session ) ); } } ); @@ -528,13 +547,16 @@ private CompletionStage loadFromDatabaseOrCache( return completedFuture( loaded ); } } - return ( (ReactiveSingleIdEntityLoader) determineLoaderToUse( session ) ) - .load( identifier, entity, LockOptions.NONE, session ); + final LockOptions lockOptions = new LockOptions(); + return ( (ReactiveSingleIdEntityLoader) determineLoaderToUse( session, lockOptions ) ) + .load( identifier, entity, lockOptions, session ); } - SingleIdEntityLoader determineLoaderToUse(SharedSessionContractImplementor session); + boolean initializeLazyProperty(String fieldName, Object entity, EntityEntry entry, LazyAttributeDescriptor fetchGroupAttributeDescriptor, Object propValue); + + void initializeLazyProperty(Object entity, EntityEntry entry, Object propValue, int index, Type type); - boolean initializeLazyProperty(String fieldName, Object entity, EntityEntry entry, int lazyIndex, Object selectedValue); + SingleIdEntityLoader determineLoaderToUse(SharedSessionContractImplementor session, LockOptions lockOptions); Object initializeLazyProperty(String fieldName, Object entity, SharedSessionContractImplementor session); diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveAbstractPersisterDelegate.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveAbstractPersisterDelegate.java index c5d068db7..62ddf29f5 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveAbstractPersisterDelegate.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveAbstractPersisterDelegate.java @@ -11,6 +11,7 @@ import java.util.List; import java.util.Map; import java.util.concurrent.CompletionStage; +import java.util.concurrent.ConcurrentHashMap; import java.util.function.Supplier; import org.hibernate.FetchMode; @@ -23,6 +24,7 @@ import org.hibernate.generator.Generator; import org.hibernate.generator.values.GeneratedValues; import org.hibernate.id.IdentityGenerator; +import org.hibernate.loader.ast.internal.LoaderSelectBuilder; import org.hibernate.loader.ast.spi.BatchLoaderFactory; import org.hibernate.loader.ast.spi.EntityBatchLoader; import org.hibernate.loader.ast.spi.MultiIdLoadOptions; @@ -34,6 +36,7 @@ import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.mapping.EntityValuedModelPart; import org.hibernate.metamodel.mapping.ManagedMappingType; +import org.hibernate.metamodel.mapping.ModelPart; import org.hibernate.metamodel.mapping.SingularAttributeMapping; import org.hibernate.metamodel.mapping.internal.EmbeddedIdentifierMappingImpl; import org.hibernate.metamodel.mapping.internal.GeneratedValuesProcessor; @@ -48,6 +51,7 @@ import org.hibernate.query.named.NamedQueryMemento; import org.hibernate.reactive.loader.ast.internal.ReactiveMultiIdEntityLoaderArrayParam; import org.hibernate.reactive.loader.ast.internal.ReactiveMultiIdEntityLoaderStandard; +import org.hibernate.reactive.loader.ast.internal.ReactiveSingleIdArrayLoadPlan; import org.hibernate.reactive.loader.ast.internal.ReactiveSingleIdEntityLoaderProvidedQueryImpl; import org.hibernate.reactive.loader.ast.internal.ReactiveSingleIdEntityLoaderStandardImpl; import org.hibernate.reactive.loader.ast.internal.ReactiveSingleUniqueKeyEntityLoaderStandard; @@ -64,6 +68,8 @@ import org.hibernate.reactive.sql.results.internal.ReactiveEntityResultImpl; import org.hibernate.spi.NavigablePath; import org.hibernate.sql.ast.tree.from.TableGroup; +import org.hibernate.sql.ast.tree.select.SelectStatement; +import org.hibernate.sql.exec.spi.JdbcParametersList; import org.hibernate.sql.results.graph.DomainResult; import org.hibernate.sql.results.graph.DomainResultCreationState; import org.hibernate.sql.results.graph.Fetch; @@ -90,6 +96,7 @@ public class ReactiveAbstractPersisterDelegate { private final EntityPersister entityDescriptor; private Map> uniqueKeyLoadersNew; + private ConcurrentHashMap nonLazyPropertyLoadPlansByName; public ReactiveAbstractPersisterDelegate( final EntityPersister entityPersister, @@ -347,6 +354,61 @@ else if ( entityIdentifierMapping instanceof EmbeddedIdentifierMappingImpl embed } } + /* + * Same as AbstractEntityPersister#getOrCreateLazyLoadPlan + */ + public ReactiveSingleIdArrayLoadPlan getOrCreateLazyLoadPlan(String fieldName, List partsToSelect) { + var propertyLoadPlansByName = nonLazyPropertyLoadPlansByName; + if ( propertyLoadPlansByName == null ) { + propertyLoadPlansByName = new ConcurrentHashMap<>(); + final ReactiveSingleIdArrayLoadPlan newLazyLoanPlan = createLazyLoadPlan( partsToSelect ); + propertyLoadPlansByName.put( fieldName, newLazyLoanPlan ); + nonLazyPropertyLoadPlansByName = propertyLoadPlansByName; + return newLazyLoanPlan; + } + else { + final ReactiveSingleIdArrayLoadPlan lazyLoanPlan = nonLazyPropertyLoadPlansByName.get( fieldName ); + if ( lazyLoanPlan == null ) { + final ReactiveSingleIdArrayLoadPlan newLazyLoanPlan = createLazyLoadPlan( partsToSelect ); + nonLazyPropertyLoadPlansByName.put( fieldName, newLazyLoanPlan ); + return newLazyLoanPlan; + } + else { + return lazyLoanPlan; + } + } + } + + private ReactiveSingleIdArrayLoadPlan createLazyLoadPlan(List partsToSelect) { + if ( partsToSelect.isEmpty() ) { + // only one-to-one is lazily fetched + return null; + } + else { + final LockOptions lockOptions = new LockOptions(); + final JdbcParametersList.Builder jdbcParametersBuilder = JdbcParametersList.newBuilder(); + final SelectStatement select = LoaderSelectBuilder.createSelect( + entityDescriptor, + partsToSelect, + entityDescriptor.getIdentifierMapping(), + null, + 1, + new LoadQueryInfluencers( entityDescriptor.getFactory() ), + lockOptions, + jdbcParametersBuilder::add, + entityDescriptor.getFactory() + ); + return new ReactiveSingleIdArrayLoadPlan( + entityDescriptor, + entityDescriptor.getIdentifierMapping(), + select, + jdbcParametersBuilder.build(), + lockOptions, + entityDescriptor.getFactory() + ); + } + } + private static class ReactiveNonAggregatedIdentifierMappingImpl extends NonAggregatedIdentifierMappingImpl { public ReactiveNonAggregatedIdentifierMappingImpl( diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveEntityPersister.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveEntityPersister.java index 16d6237f2..0899bbbc1 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveEntityPersister.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveEntityPersister.java @@ -10,7 +10,7 @@ import org.hibernate.LockMode; import org.hibernate.LockOptions; -import org.hibernate.bytecode.BytecodeLogging; +import org.hibernate.bytecode.enhance.internal.BytecodeEnhancementLogging; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.generator.values.GeneratedValues; import org.hibernate.loader.ast.spi.MultiIdLoadOptions; @@ -183,7 +183,7 @@ static CompletionStage forceInitialize( Object entityId, String entityName, SharedSessionContractImplementor session) { - BytecodeLogging.LOGGER.tracef( + BytecodeEnhancementLogging.ENHANCEMENT_LOGGER.tracef( "EnhancementAsProxyLazinessInterceptor#forceInitialize : %s#%s -> %s )", entityName, entityId, diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveGeneratedValuesProcessor.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveGeneratedValuesProcessor.java index bb4ecef88..b28e8975b 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveGeneratedValuesProcessor.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveGeneratedValuesProcessor.java @@ -16,8 +16,8 @@ import org.hibernate.reactive.sql.exec.internal.StandardReactiveSelectExecutor; import org.hibernate.reactive.sql.results.spi.ReactiveListResultsConsumer; import org.hibernate.sql.ast.tree.select.SelectStatement; +import org.hibernate.sql.exec.internal.JdbcOperationQuerySelect; import org.hibernate.sql.exec.internal.JdbcParameterBindingsImpl; -import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; import org.hibernate.sql.exec.spi.JdbcParameterBindings; import org.hibernate.sql.exec.spi.JdbcParametersList; diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveJoinedSubclassEntityPersister.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveJoinedSubclassEntityPersister.java index 0182ef403..a9b8e8759 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveJoinedSubclassEntityPersister.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveJoinedSubclassEntityPersister.java @@ -6,12 +6,15 @@ package org.hibernate.reactive.persister.entity.impl; import java.util.List; +import java.util.Set; import java.util.concurrent.CompletionStage; import org.hibernate.FetchMode; import org.hibernate.HibernateException; import org.hibernate.LockMode; import org.hibernate.LockOptions; +import org.hibernate.bytecode.enhance.spi.interceptor.LazyAttributeDescriptor; +import org.hibernate.bytecode.spi.BytecodeEnhancementMetadata; import org.hibernate.cache.spi.access.EntityDataAccess; import org.hibernate.cache.spi.access.NaturalIdDataAccess; import org.hibernate.engine.spi.CascadeStyle; @@ -27,6 +30,7 @@ import org.hibernate.mapping.Property; import org.hibernate.metamodel.mapping.AttributeMapping; import org.hibernate.metamodel.mapping.ManagedMappingType; +import org.hibernate.metamodel.mapping.ModelPart; import org.hibernate.metamodel.mapping.NaturalIdMapping; import org.hibernate.metamodel.mapping.SingularAttributeMapping; import org.hibernate.metamodel.mapping.internal.MappingModelCreationProcess; @@ -39,6 +43,7 @@ import org.hibernate.persister.entity.mutation.InsertCoordinator; import org.hibernate.persister.entity.mutation.UpdateCoordinator; import org.hibernate.property.access.spi.PropertyAccess; +import org.hibernate.reactive.bythecode.spi.ReactiveBytecodeEnhancementMetadataPojoImplAdapter; import org.hibernate.reactive.loader.ast.internal.ReactiveSingleIdArrayLoadPlan; import org.hibernate.reactive.loader.ast.spi.ReactiveSingleUniqueKeyEntityLoader; import org.hibernate.reactive.logging.impl.Log; @@ -51,7 +56,9 @@ import org.hibernate.sql.ast.tree.from.TableGroup; import org.hibernate.sql.results.graph.DomainResult; import org.hibernate.sql.results.graph.DomainResultCreationState; +import org.hibernate.type.CompositeType; import org.hibernate.type.EntityType; +import org.hibernate.type.Type; import static java.lang.invoke.MethodHandles.lookup; import static org.hibernate.reactive.logging.impl.LoggerFactory.make; @@ -60,8 +67,7 @@ * An {@link ReactiveEntityPersister} backed by {@link JoinedSubclassEntityPersister} * and {@link ReactiveAbstractEntityPersister}. */ -public class ReactiveJoinedSubclassEntityPersister extends JoinedSubclassEntityPersister - implements ReactiveAbstractEntityPersister { +public class ReactiveJoinedSubclassEntityPersister extends JoinedSubclassEntityPersister implements ReactiveAbstractEntityPersister { private static final Log LOG = make( Log.class, lookup() ); @@ -76,6 +82,27 @@ public ReactiveJoinedSubclassEntityPersister( reactiveDelegate = new ReactiveAbstractPersisterDelegate( this, persistentClass, new ReactiveRuntimeModelCreationContext( creationContext ) ); } + @Override + public boolean initializeLazyProperty(String fieldName, Object entity, EntityEntry entry, LazyAttributeDescriptor fetchGroupAttributeDescriptor, Object propValue) { + return super.initializeLazyProperty( fieldName, entity, entry, fetchGroupAttributeDescriptor, propValue ); + } + + @Override + public void initializeLazyProperty(Object entity, EntityEntry entry, Object propValue, int index, Type type) { + super.initializeLazyProperty( entity, entry, propValue, index, type ); + } + + @Override + protected BytecodeEnhancementMetadata getBytecodeEnhancementMetadataPojo( + PersistentClass persistentClass, + RuntimeModelCreationContext creationContext, + Set idAttributeNames, + CompositeType nonAggregatedCidMapper, + boolean collectionsInDefaultFetchGroupEnabled) { + return ReactiveBytecodeEnhancementMetadataPojoImplAdapter + .from( persistentClass, idAttributeNames, nonAggregatedCidMapper, collectionsInDefaultFetchGroupEnabled, creationContext.getMetadata() ); + } + @Override protected SingleIdEntityLoader buildSingleIdEntityLoader() { return reactiveDelegate.buildSingleIdEntityLoader(); @@ -139,8 +166,8 @@ protected AttributeMapping buildPluralAttributeMapping( } @Override - public SingleIdEntityLoader determineLoaderToUse(SharedSessionContractImplementor session) { - return super.determineLoaderToUse( session ); + public SingleIdEntityLoader determineLoaderToUse(SharedSessionContractImplementor session, LockOptions lockOptions) { + return super.determineLoaderToUse( session, lockOptions ); } @Override @@ -389,4 +416,13 @@ public ReactiveSingleIdArrayLoadPlan reactiveGetSQLLazySelectLoadPlan(String fet return this.getLazyLoadPlanByFetchGroup( getSubclassPropertyNameClosure() ).get(fetchGroup ); } + @Override + public ReactiveSingleIdArrayLoadPlan reactiveGetOrCreateLazyLoadPlan(String fieldName, List partsToSelect) { + return reactiveDelegate.getOrCreateLazyLoadPlan( fieldName, partsToSelect ); + } + + @Override + public boolean isNonLazyPropertyName(String fieldName) { + return super.isNonLazyPropertyName( fieldName ); + } } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveSingleTableEntityPersister.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveSingleTableEntityPersister.java index 3006a0eae..ac9fb433f 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveSingleTableEntityPersister.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveSingleTableEntityPersister.java @@ -6,6 +6,7 @@ package org.hibernate.reactive.persister.entity.impl; import java.util.List; +import java.util.Set; import java.util.concurrent.CompletionStage; import java.util.function.Supplier; @@ -13,6 +14,8 @@ import org.hibernate.HibernateException; import org.hibernate.LockMode; import org.hibernate.LockOptions; +import org.hibernate.bytecode.enhance.spi.interceptor.LazyAttributeDescriptor; +import org.hibernate.bytecode.spi.BytecodeEnhancementMetadata; import org.hibernate.cache.spi.access.EntityDataAccess; import org.hibernate.cache.spi.access.NaturalIdDataAccess; import org.hibernate.engine.spi.CascadeStyle; @@ -30,6 +33,7 @@ import org.hibernate.metamodel.mapping.AttributeMapping; import org.hibernate.metamodel.mapping.EntityIdentifierMapping; import org.hibernate.metamodel.mapping.ManagedMappingType; +import org.hibernate.metamodel.mapping.ModelPart; import org.hibernate.metamodel.mapping.NaturalIdMapping; import org.hibernate.metamodel.mapping.SingularAttributeMapping; import org.hibernate.metamodel.mapping.internal.MappingModelCreationProcess; @@ -42,6 +46,7 @@ import org.hibernate.persister.entity.mutation.InsertCoordinator; import org.hibernate.persister.entity.mutation.UpdateCoordinator; import org.hibernate.property.access.spi.PropertyAccess; +import org.hibernate.reactive.bythecode.spi.ReactiveBytecodeEnhancementMetadataPojoImplAdapter; import org.hibernate.reactive.generator.values.GeneratedValuesMutationDelegateAdaptor; import org.hibernate.reactive.loader.ast.internal.ReactiveSingleIdArrayLoadPlan; import org.hibernate.reactive.loader.ast.spi.ReactiveSingleUniqueKeyEntityLoader; @@ -55,7 +60,9 @@ import org.hibernate.sql.ast.tree.from.TableGroup; import org.hibernate.sql.results.graph.DomainResult; import org.hibernate.sql.results.graph.DomainResultCreationState; +import org.hibernate.type.CompositeType; import org.hibernate.type.EntityType; +import org.hibernate.type.Type; import static java.lang.invoke.MethodHandles.lookup; import static org.hibernate.reactive.logging.impl.LoggerFactory.make; @@ -80,14 +87,35 @@ public ReactiveSingleTableEntityPersister( reactiveDelegate = new ReactiveAbstractPersisterDelegate( this, persistentClass, new ReactiveRuntimeModelCreationContext( creationContext ) ); } + @Override + public boolean initializeLazyProperty(String fieldName, Object entity, EntityEntry entry, LazyAttributeDescriptor fetchGroupAttributeDescriptor, Object propValue) { + return super.initializeLazyProperty( fieldName, entity, entry, fetchGroupAttributeDescriptor, propValue ); + } + + @Override + public void initializeLazyProperty(Object entity, EntityEntry entry, Object propValue, int index, Type type) { + super.initializeLazyProperty( entity, entry, propValue, index, type ); + } + + @Override + protected BytecodeEnhancementMetadata getBytecodeEnhancementMetadataPojo( + PersistentClass persistentClass, + RuntimeModelCreationContext creationContext, + Set idAttributeNames, + CompositeType nonAggregatedCidMapper, + boolean collectionsInDefaultFetchGroupEnabled) { + return ReactiveBytecodeEnhancementMetadataPojoImplAdapter + .from( persistentClass, idAttributeNames, nonAggregatedCidMapper, collectionsInDefaultFetchGroupEnabled, creationContext.getMetadata() ); + } + @Override public GeneratedValuesMutationDelegate createInsertDelegate() { return ReactiveAbstractEntityPersister.super.createReactiveInsertDelegate(); } @Override - public SingleIdEntityLoader determineLoaderToUse(SharedSessionContractImplementor session) { - return super.determineLoaderToUse( session ); + public SingleIdEntityLoader determineLoaderToUse(SharedSessionContractImplementor session, LockOptions lockOptions) { + return super.determineLoaderToUse( session, lockOptions ); } @Override @@ -209,11 +237,6 @@ protected AttributeMapping buildPluralAttributeMapping( ); } - @Override - public boolean initializeLazyProperty(String fieldName, Object entity, EntityEntry entry, int lazyIndex, Object selectedValue) { - return super.initializeLazyProperty(fieldName, entity, entry, lazyIndex, selectedValue); - } - @Override public Object initializeLazyPropertiesFromDatastore(final Object entity, final Object id, final EntityEntry entry, final String fieldName, final SharedSessionContractImplementor session) { return reactiveInitializeLazyPropertiesFromDatastore( entity, id, entry, fieldName, session ); @@ -444,4 +467,14 @@ public ReactiveSingleIdArrayLoadPlan reactiveGetSQLLazySelectLoadPlan(String fet public NaturalIdMapping generateNaturalIdMapping(MappingModelCreationProcess creationProcess, PersistentClass bootEntityDescriptor) { return ReactiveAbstractEntityPersister.super.generateNaturalIdMapping(creationProcess, bootEntityDescriptor); } + + @Override + public ReactiveSingleIdArrayLoadPlan reactiveGetOrCreateLazyLoadPlan(String fieldName, List partsToSelect) { + return reactiveDelegate.getOrCreateLazyLoadPlan( fieldName, partsToSelect ); + } + + @Override + public boolean isNonLazyPropertyName(String fieldName) { + return super.isNonLazyPropertyName( fieldName ); + } } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveUnionSubclassEntityPersister.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveUnionSubclassEntityPersister.java index 52871e30f..320f2d75e 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveUnionSubclassEntityPersister.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/persister/entity/impl/ReactiveUnionSubclassEntityPersister.java @@ -7,6 +7,7 @@ import java.lang.invoke.MethodHandles; import java.util.List; +import java.util.Set; import java.util.concurrent.CompletionStage; import org.hibernate.FetchMode; @@ -14,6 +15,8 @@ import org.hibernate.LockMode; import org.hibernate.LockOptions; import org.hibernate.MappingException; +import org.hibernate.bytecode.enhance.spi.interceptor.LazyAttributeDescriptor; +import org.hibernate.bytecode.spi.BytecodeEnhancementMetadata; import org.hibernate.cache.spi.access.EntityDataAccess; import org.hibernate.cache.spi.access.NaturalIdDataAccess; import org.hibernate.engine.spi.CascadeStyle; @@ -30,6 +33,7 @@ import org.hibernate.mapping.Property; import org.hibernate.metamodel.mapping.AttributeMapping; import org.hibernate.metamodel.mapping.ManagedMappingType; +import org.hibernate.metamodel.mapping.ModelPart; import org.hibernate.metamodel.mapping.NaturalIdMapping; import org.hibernate.metamodel.mapping.SingularAttributeMapping; import org.hibernate.metamodel.mapping.internal.MappingModelCreationProcess; @@ -42,6 +46,7 @@ import org.hibernate.persister.entity.mutation.InsertCoordinator; import org.hibernate.persister.entity.mutation.UpdateCoordinator; import org.hibernate.property.access.spi.PropertyAccess; +import org.hibernate.reactive.bythecode.spi.ReactiveBytecodeEnhancementMetadataPojoImplAdapter; import org.hibernate.reactive.loader.ast.internal.ReactiveSingleIdArrayLoadPlan; import org.hibernate.reactive.loader.ast.spi.ReactiveSingleUniqueKeyEntityLoader; import org.hibernate.reactive.logging.impl.Log; @@ -55,7 +60,9 @@ import org.hibernate.sql.ast.tree.from.TableGroup; import org.hibernate.sql.results.graph.DomainResult; import org.hibernate.sql.results.graph.DomainResultCreationState; +import org.hibernate.type.CompositeType; import org.hibernate.type.EntityType; +import org.hibernate.type.Type; /** * An {@link ReactiveEntityPersister} backed by {@link UnionSubclassEntityPersister} @@ -76,6 +83,27 @@ public ReactiveUnionSubclassEntityPersister( reactiveDelegate = new ReactiveAbstractPersisterDelegate( this, persistentClass, new ReactiveRuntimeModelCreationContext( creationContext ) ); } + @Override + public boolean initializeLazyProperty(String fieldName, Object entity, EntityEntry entry, LazyAttributeDescriptor fetchGroupAttributeDescriptor, Object propValue) { + return super.initializeLazyProperty( fieldName, entity, entry, fetchGroupAttributeDescriptor, propValue ); + } + + @Override + public void initializeLazyProperty(Object entity, EntityEntry entry, Object propValue, int index, Type type) { + super.initializeLazyProperty( entity, entry, propValue, index, type ); + } + + @Override + protected BytecodeEnhancementMetadata getBytecodeEnhancementMetadataPojo( + PersistentClass persistentClass, + RuntimeModelCreationContext creationContext, + Set idAttributeNames, + CompositeType nonAggregatedCidMapper, + boolean collectionsInDefaultFetchGroupEnabled) { + return ReactiveBytecodeEnhancementMetadataPojoImplAdapter + .from( persistentClass, idAttributeNames, nonAggregatedCidMapper, collectionsInDefaultFetchGroupEnabled, creationContext.getMetadata() ); + } + @Override protected SingleIdEntityLoader buildSingleIdEntityLoader() { return reactiveDelegate.buildSingleIdEntityLoader(); @@ -139,8 +167,8 @@ protected AttributeMapping buildPluralAttributeMapping( } @Override - public SingleIdEntityLoader determineLoaderToUse(SharedSessionContractImplementor session) { - return super.determineLoaderToUse( session ); + public SingleIdEntityLoader determineLoaderToUse(SharedSessionContractImplementor session, LockOptions lockOptions) { + return super.determineLoaderToUse( session, lockOptions ); } @Override @@ -184,11 +212,6 @@ public Generator getGenerator() throws HibernateException { return reactiveDelegate.reactive( super.getGenerator() ); } - @Override - public boolean initializeLazyProperty(String fieldName, Object entity, EntityEntry entry, int lazyIndex, Object selectedValue) { - return super.initializeLazyProperty( fieldName, entity, entry, lazyIndex, selectedValue ); - } - /** * @see #insertReactive(Object[], Object, SharedSessionContractImplementor) */ @@ -415,4 +438,14 @@ protected ReactiveSingleUniqueKeyEntityLoader getReactiveUniqueKeyLoader public ReactiveSingleIdArrayLoadPlan reactiveGetSQLLazySelectLoadPlan(String fetchGroup) { return this.getLazyLoadPlanByFetchGroup( getSubclassPropertyNameClosure() ).get(fetchGroup ); } + + @Override + public ReactiveSingleIdArrayLoadPlan reactiveGetOrCreateLazyLoadPlan(String fieldName, List partsToSelect) { + return reactiveDelegate.getOrCreateLazyLoadPlan( fieldName, partsToSelect ); + } + + @Override + public boolean isNonLazyPropertyName(String fieldName) { + return super.isNonLazyPropertyName( fieldName ); + } } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/BatchingConnection.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/BatchingConnection.java index ce7ee013d..e467e9d26 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/BatchingConnection.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/BatchingConnection.java @@ -51,6 +51,11 @@ public DatabaseMetadata getDatabaseMetadata() { return delegate.getDatabaseMetadata(); } + @Override + public boolean isTransactionInProgress() { + return delegate.isTransactionInProgress(); + } + @Override public ReactiveConnection withBatchSize(int batchSize) { if ( batchSize <= 1 ) { @@ -104,9 +109,8 @@ public CompletionStage update( return voidFuture(); } else { - CompletionStage lastBatch = executeBatch(); - newBatch( sql, paramValues, expectation ); - return lastBatch; + return executeBatch() + .thenAccept( v -> newBatch( sql, paramValues, expectation ) ); } } } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/ReactiveConnection.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/ReactiveConnection.java index 10e304116..193b3395e 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/ReactiveConnection.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/ReactiveConnection.java @@ -30,6 +30,8 @@ @Incubating public interface ReactiveConnection { + boolean isTransactionInProgress(); + @FunctionalInterface interface Expectation { void verifyOutcome(int rowCount, int batchPosition, String sql); diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/ReactiveConnectionPool.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/ReactiveConnectionPool.java index ec1bdbd52..ad259652b 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/ReactiveConnectionPool.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/ReactiveConnectionPool.java @@ -37,12 +37,25 @@ */ @Incubating public interface ReactiveConnectionPool extends Service { + /** + * Obtain a lazily-initializing reactive connection. The + * actual connection might be made when the returned + * instance if {@link ReactiveConnection} is first used. + */ + ReactiveConnection getProxyConnection(); /** * Obtain a reactive connection, returning the connection - * via a {@link CompletionStage}. + * via a {@link CompletionStage} and overriding the default + * {@link SqlExceptionHelper} for the pool. */ - CompletionStage getConnection(); + ReactiveConnection getProxyConnection(SqlExceptionHelper sqlExceptionHelper); + + /** + * Obtain a reactive connection for the given tenant id, + * returning the connection via a {@link CompletionStage}. + */ + ReactiveConnection getProxyConnection(String tenantId); /** * Obtain a reactive connection, returning the connection @@ -57,6 +70,12 @@ public interface ReactiveConnectionPool extends Service { */ CompletionStage getConnection(String tenantId); + /** + * Obtain a reactive connection, returning the connection + * via a {@link CompletionStage}. + */ + CompletionStage getConnection(); + /** * Obtain a reactive connection for the given tenant id, * returning the connection via a {@link CompletionStage} diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/impl/OracleParameters.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/impl/OracleParameters.java deleted file mode 100644 index 2f4dd34c1..000000000 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/impl/OracleParameters.java +++ /dev/null @@ -1,15 +0,0 @@ -/* Hibernate, Relational Persistence for Idiomatic Java - * - * SPDX-License-Identifier: Apache-2.0 - * Copyright: Red Hat Inc. and Hibernate Authors - */ -package org.hibernate.reactive.pool.impl; - -public class OracleParameters extends Parameters { - - public static final OracleParameters INSTANCE = new OracleParameters(); - - private OracleParameters() { - super( ":" ); - } -} diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/impl/Parameters.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/impl/Parameters.java deleted file mode 100644 index d30826846..000000000 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/impl/Parameters.java +++ /dev/null @@ -1,153 +0,0 @@ -/* Hibernate, Relational Persistence for Idiomatic Java - * - * SPDX-License-Identifier: Apache-2.0 - * Copyright: Red Hat Inc. and Hibernate Authors - */ -package org.hibernate.reactive.pool.impl; - -import java.util.function.IntConsumer; - -import org.hibernate.dialect.CockroachDialect; -import org.hibernate.dialect.Dialect; -import org.hibernate.dialect.PostgreSQLDialect; -import org.hibernate.dialect.SQLServerDialect; - -/** - * Some databases have a different parameter syntax, which - * the Vert.x {@link io.vertx.sqlclient.SqlClient} does not abstract. - * This class converts JDBC/ODBC-style {@code ?} parameters generated - * by Hibernate ORM to the native format. - */ -public abstract class Parameters { - - private final String paramPrefix; - - private static final Parameters NO_PARSING = new Parameters( null ) { - @Override - public String process(String sql) { - return sql; - } - - @Override - public String process(String sql, int parameterCount) { - return sql; - } - }; - - protected Parameters(String paramPrefix) { - this.paramPrefix = paramPrefix; - } - - public static Parameters instance(Dialect dialect) { - if ( dialect instanceof PostgreSQLDialect || dialect instanceof CockroachDialect ) { - return PostgresParameters.INSTANCE; - } - if ( dialect instanceof SQLServerDialect ) { - return SQLServerParameters.INSTANCE; - } - return NO_PARSING; - } - - public static boolean isProcessingNotRequired(String sql) { - return sql == null - // There aren't any parameters - || sql.indexOf( '?' ) == -1; - } - - public String process(String sql) { - if ( isProcessingNotRequired( sql ) ) { - return sql; - } - return new Parser( sql, paramPrefix ).result(); - } - - /** - * Replace all JDBC-style {@code ?} parameters with Postgres-style - * {@code $n} parameters in the given SQL string. - */ - public String process(String sql, int parameterCount) { - if ( isProcessingNotRequired( sql ) ) { - return sql; - } - return new Parser( sql, parameterCount, paramPrefix ).result(); - } - - private static class Parser { - - private boolean inString; - private boolean inQuoted; - private boolean inSqlComment; - private boolean inCComment; - private boolean escaped; - private int count = 0; - private StringBuilder result; - private int previous; - - private Parser(String sql, String paramPrefix) { - this( sql, 10, paramPrefix ); - } - - private Parser(String sql, int parameterCount, final String paramPrefix) { - result = new StringBuilder( sql.length() + parameterCount ); - // We aren't using lambdas or method reference because of a bug in the JVM: - // https://bugs.openjdk.java.net/browse/JDK-8161588 - // Please, don't change this unless you've tested it with Quarkus - sql.codePoints().forEach( new IntConsumer() { - @Override - public void accept(int codePoint) { - if ( escaped ) { - escaped = false; - } - else { - switch ( codePoint ) { - case '\\': - escaped = true; - return; - case '"': - if ( !inString && !inSqlComment && !inCComment ) { - inQuoted = !inQuoted; - } - break; - case '\'': - if ( !inQuoted && !inSqlComment && !inCComment ) { - inString = !inString; - } - break; - case '-': - if ( !inQuoted && !inString && !inCComment && previous == '-' ) { - inSqlComment = true; - } - break; - case '\n': - inSqlComment = false; - break; - case '*': - if ( !inQuoted && !inString && !inSqlComment && previous == '/' ) { - inCComment = true; - } - break; - case '/': - if ( previous == '*' ) { - inCComment = false; - } - break; - //TODO: $$-quoted strings - case '?': - if ( !inQuoted && !inString ) { - result.append( paramPrefix ).append( ++count ); - previous = '?'; - return; - } - } - } - previous = codePoint; - result.appendCodePoint( codePoint ); - } - } ); - } - - public String result() { - return result.toString(); - } - } -} diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/impl/PostgresParameters.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/impl/PostgresParameters.java deleted file mode 100644 index b53e37839..000000000 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/impl/PostgresParameters.java +++ /dev/null @@ -1,15 +0,0 @@ -/* Hibernate, Relational Persistence for Idiomatic Java - * - * SPDX-License-Identifier: Apache-2.0 - * Copyright: Red Hat Inc. and Hibernate Authors - */ -package org.hibernate.reactive.pool.impl; - -public class PostgresParameters extends Parameters { - - public static final PostgresParameters INSTANCE = new PostgresParameters(); - - private PostgresParameters() { - super( "$" ); - } -} diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/impl/SQLServerParameters.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/impl/SQLServerParameters.java deleted file mode 100644 index 232c001c1..000000000 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/impl/SQLServerParameters.java +++ /dev/null @@ -1,15 +0,0 @@ -/* Hibernate, Relational Persistence for Idiomatic Java - * - * SPDX-License-Identifier: Apache-2.0 - * Copyright: Red Hat Inc. and Hibernate Authors - */ -package org.hibernate.reactive.pool.impl; - -public class SQLServerParameters extends Parameters { - - public static final SQLServerParameters INSTANCE = new SQLServerParameters(); - - private SQLServerParameters() { - super( "@P" ); - } -} diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/impl/SqlClientConnection.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/impl/SqlClientConnection.java index 3e8647f6c..a58c19d06 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/impl/SqlClientConnection.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/impl/SqlClientConnection.java @@ -73,6 +73,11 @@ public DatabaseMetadata getDatabaseMetadata() { return client().databaseMetadata(); } + @Override + public boolean isTransactionInProgress() { + return connection.transaction() != null; + } + @Override public CompletionStage update(String sql, Object[] paramValues) { translateNulls( paramValues ); diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/impl/SqlClientPool.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/impl/SqlClientPool.java index 5e2a59517..89ba43e7e 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/impl/SqlClientPool.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/pool/impl/SqlClientPool.java @@ -7,10 +7,12 @@ import java.sql.ResultSet; import java.sql.SQLException; +import java.util.List; import java.util.Objects; import java.util.concurrent.CompletableFuture; import java.util.concurrent.CompletionStage; import java.util.function.Consumer; +import java.util.function.Supplier; import org.hibernate.engine.jdbc.internal.FormatStyle; import org.hibernate.engine.jdbc.spi.SqlExceptionHelper; @@ -26,8 +28,11 @@ import io.vertx.sqlclient.RowSet; import io.vertx.sqlclient.SqlConnection; import io.vertx.sqlclient.Tuple; +import io.vertx.sqlclient.spi.DatabaseMetadata; +import static org.hibernate.reactive.util.impl.CompletionStages.completedFuture; import static org.hibernate.reactive.util.impl.CompletionStages.rethrow; +import static org.hibernate.reactive.util.impl.CompletionStages.voidFuture; /** * A pool of reactive connections backed by a supplier of @@ -72,6 +77,7 @@ public abstract class SqlClientPool implements ReactiveConnectionPool { * subclasses which support multitenancy. * * @param tenantId the id of the tenant + * * @throws UnsupportedOperationException if multitenancy is not supported * @see ReactiveConnectionPool#getConnection(String) */ @@ -79,6 +85,23 @@ protected Pool getTenantPool(String tenantId) { throw new UnsupportedOperationException( "multitenancy not supported by built-in SqlClientPool" ); } + @Override + public ReactiveConnection getProxyConnection() { + return new ProxyConnection( this::getConnection ); + } + + @Override + public ReactiveConnection getProxyConnection(String tenantId) { + return tenantId == null + ? new ProxyConnection( this::getConnection ) + : new ProxyConnection( () -> getConnection( tenantId ) ); + } + + @Override + public ReactiveConnection getProxyConnection(SqlExceptionHelper sqlExceptionHelper) { + return new ProxyConnection( () -> getConnection( sqlExceptionHelper ) ); + } + @Override public CompletionStage getConnection() { return getConnectionFromPool( getPool() ); @@ -143,10 +166,13 @@ private T convertException(T rows, String sql, Throwable sqlException) { if ( sqlException == null ) { return rows; } - if ( sqlException instanceof DatabaseException ) { - DatabaseException de = (DatabaseException) sqlException; + if ( sqlException instanceof DatabaseException de ) { sqlException = getSqlExceptionHelper() - .convert( new SQLException( de.getMessage(), de.getSqlState(), de.getErrorCode() ), "error executing SQL statement", sql ); + .convert( + new SQLException( de.getMessage(), de.getSqlState(), de.getErrorCode() ), + "error executing SQL statement", + sql + ); } return rethrow( sqlException ); } @@ -186,4 +212,155 @@ private SqlClientConnection newConnection(SqlConnection connection) { private SqlClientConnection newConnection(SqlConnection connection, SqlExceptionHelper sqlExceptionHelper) { return new SqlClientConnection( connection, getPool(), getSqlStatementLogger(), sqlExceptionHelper ); } + + private static class ProxyConnection implements ReactiveConnection { + private final Supplier> connectionSupplier; + private Integer batchSize; + private ReactiveConnection connection; + + public ProxyConnection(Supplier> connectionSupplier) { + this.connectionSupplier = connectionSupplier; + } + + /** + * @return the existing {@link ReactiveConnection}, or open a new one + */ + CompletionStage connection() { + if ( connection == null ) { + return connectionSupplier.get() + .thenApply( conn -> { + if ( batchSize != null ) { + conn.withBatchSize( batchSize ); + } + connection = conn; + return connection; + } ); + } + return completedFuture( connection ); + } + + @Override + public boolean isTransactionInProgress() { + return connection != null && connection.isTransactionInProgress(); + } + + @Override + public DatabaseMetadata getDatabaseMetadata() { + Objects.requireNonNull( connection, "Database metadata not available until the connection is opened" ); + return connection.getDatabaseMetadata(); + } + + @Override + public CompletionStage execute(String sql) { + return connection().thenCompose( conn -> conn.execute( sql ) ); + } + + @Override + public CompletionStage executeOutsideTransaction(String sql) { + return connection().thenCompose( conn -> conn.executeOutsideTransaction( sql ) ); + } + + @Override + public CompletionStage executeUnprepared(String sql) { + return connection().thenCompose( conn -> conn.executeUnprepared( sql ) ); + } + + @Override + public CompletionStage update(String sql) { + return connection().thenCompose( conn -> conn.update( sql ) ); + } + + @Override + public CompletionStage update(String sql, Object[] paramValues) { + return connection().thenCompose( conn -> conn.update( sql, paramValues ) ); + } + + @Override + public CompletionStage update(String sql, Object[] paramValues, boolean allowBatching, Expectation expectation) { + return connection().thenCompose( conn -> conn.update( sql, paramValues, allowBatching, expectation ) ); + } + + @Override + public CompletionStage update(String sql, List paramValues) { + return connection().thenCompose( conn -> conn.update( sql, paramValues ) ); + } + + @Override + public CompletionStage select(String sql) { + return connection().thenCompose( conn -> conn.select( sql ) ); + } + + @Override + public CompletionStage select(String sql, Object[] paramValues) { + return connection().thenCompose( conn -> conn.select( sql ) ); + } + + @Override + public CompletionStage selectJdbc(String sql, Object[] paramValues) { + return connection().thenCompose( conn -> conn.selectJdbc( sql, paramValues ) ); + } + + @Override + public CompletionStage insertAndSelectIdentifier( + String sql, + Object[] paramValues, + Class idClass, + String idColumnName) { + return connection().thenCompose( conn -> conn + .insertAndSelectIdentifier( sql, paramValues, idClass, idColumnName ) ); + } + + @Override + public CompletionStage insertAndSelectIdentifierAsResultSet( + String sql, + Object[] paramValues, + Class idClass, + String idColumnName) { + return connection().thenCompose( conn -> conn + .insertAndSelectIdentifierAsResultSet( sql, paramValues, idClass, idColumnName ) ); + } + + @Override + public CompletionStage selectIdentifier(String sql, Object[] paramValues, Class idClass) { + return connection().thenCompose( conn -> conn.selectIdentifier( sql, paramValues, idClass ) ); + } + + @Override + public CompletionStage beginTransaction() { + return connection().thenCompose( ReactiveConnection::beginTransaction ); + } + + @Override + public CompletionStage commitTransaction() { + return connection().thenCompose( ReactiveConnection::commitTransaction ); + } + + @Override + public CompletionStage rollbackTransaction() { + return connection().thenCompose( ReactiveConnection::rollbackTransaction ); + } + + @Override + public ReactiveConnection withBatchSize(int batchSize) { + if ( connection == null ) { + this.batchSize = batchSize; + } + else { + connection = connection.withBatchSize( batchSize ); + } + return this; + } + + @Override + public CompletionStage executeBatch() { + return connection().thenCompose( ReactiveConnection::executeBatch ); + } + + @Override + public CompletionStage close() { + return connection != null + ? connection.close().thenAccept( v -> connection = null ) + : voidFuture(); + } + } } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/provider/impl/ReactiveServiceInitiators.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/provider/impl/ReactiveServiceInitiators.java index 2343a9644..13b0ae6b8 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/provider/impl/ReactiveServiceInitiators.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/provider/impl/ReactiveServiceInitiators.java @@ -22,6 +22,7 @@ import org.hibernate.engine.jdbc.internal.SqlStatementLoggerInitiator; import org.hibernate.engine.jndi.internal.JndiServiceInitiator; import org.hibernate.event.internal.EntityCopyObserverFactoryInitiator; +import org.hibernate.internal.util.cache.InternalCacheFactoryInitiator; import org.hibernate.persister.internal.PersisterFactoryInitiator; import org.hibernate.property.access.internal.PropertyAccessStrategyResolverInitiator; import org.hibernate.reactive.context.impl.VertxContextInitiator; @@ -161,6 +162,9 @@ private static List> buildInitialServiceInitiatorLis // Custom for Hibernate Reactive: BatchLoaderFactory serviceInitiators.add( ReactiveBatchLoaderFactoryInitiator.INSTANCE ); + // [standard] InternalCacheFactoryService + serviceInitiators.add( InternalCacheFactoryInitiator.INSTANCE ); + // --- end of services defined by Hibernate ORM // --- custom ones follow: diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/provider/service/MySqlReactiveInformationExtractorImpl.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/provider/service/MySqlReactiveInformationExtractorImpl.java index 0504e44ee..b6a9920e8 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/provider/service/MySqlReactiveInformationExtractorImpl.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/provider/service/MySqlReactiveInformationExtractorImpl.java @@ -64,6 +64,15 @@ protected T processPrimaryKeysResultSet( throw new UnsupportedOperationException(); } + @Override + protected T processPrimaryKeysResultSet( + String catalogFilter, + String schemaFilter, + String tableName, + ExtractionContext.ResultSetProcessor processor) throws SQLException { + throw new UnsupportedOperationException(); + } + @Override protected T processCatalogsResultSet(ExtractionContext.ResultSetProcessor processor) throws SQLException { // MySQL does not implement information_schema.information_schema_catalog_name @@ -193,4 +202,5 @@ protected T processImportedKeysResultSet( return getExtractionContext().getQueryResults( sb.toString(), parameters.toArray(), processor ); } + } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/provider/service/NoJdbcConnectionProvider.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/provider/service/NoJdbcConnectionProvider.java index 5c3380f65..a02c3e152 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/provider/service/NoJdbcConnectionProvider.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/provider/service/NoJdbcConnectionProvider.java @@ -6,10 +6,14 @@ package org.hibernate.reactive.provider.service; import org.hibernate.engine.jdbc.connections.spi.ConnectionProvider; +import org.hibernate.reactive.logging.impl.Log; import java.sql.Connection; import java.sql.SQLException; +import static java.lang.invoke.MethodHandles.lookup; +import static org.hibernate.reactive.logging.impl.LoggerFactory.make; + /** * A dummy Hibernate {@link ConnectionProvider} throws an * exception if a JDBC connection is requested. @@ -17,12 +21,13 @@ * @author Gavin King */ public class NoJdbcConnectionProvider implements ConnectionProvider { + private static final Log LOG = make( Log.class, lookup() ); public static final NoJdbcConnectionProvider INSTANCE = new NoJdbcConnectionProvider(); @Override public Connection getConnection() throws SQLException { - throw new SQLException( "Not using JDBC" ); + throw LOG.notUsingJdbc(); } @Override diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/provider/service/NoJdbcConnectionProviderInitiator.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/provider/service/NoJdbcConnectionProviderInitiator.java index a57f72b16..6e32d5e7f 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/provider/service/NoJdbcConnectionProviderInitiator.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/provider/service/NoJdbcConnectionProviderInitiator.java @@ -7,7 +7,7 @@ import org.hibernate.boot.registry.StandardServiceInitiator; import org.hibernate.engine.jdbc.connections.internal.ConnectionProviderInitiator; -import org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProviderImpl; +import org.hibernate.engine.jdbc.connections.internal.DriverManagerConnectionProvider; import org.hibernate.engine.jdbc.connections.spi.ConnectionProvider; import org.hibernate.service.spi.ServiceRegistryImplementor; @@ -28,7 +28,7 @@ public class NoJdbcConnectionProviderInitiator implements StandardServiceInitiat @Override public ConnectionProvider initiateService(Map configurationValues, ServiceRegistryImplementor registry) { ConnectionProvider provider = ConnectionProviderInitiator.INSTANCE.initiateService(configurationValues, registry); - if (provider instanceof DriverManagerConnectionProviderImpl) { + if ( provider instanceof DriverManagerConnectionProvider ) { return NoJdbcConnectionProvider.INSTANCE; } return provider; diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/provider/service/NoJdbcEnvironmentInitiator.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/provider/service/NoJdbcEnvironmentInitiator.java index 0cf59c413..cf3de29fb 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/provider/service/NoJdbcEnvironmentInitiator.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/provider/service/NoJdbcEnvironmentInitiator.java @@ -10,6 +10,7 @@ import org.hibernate.engine.jdbc.connections.spi.DatabaseConnectionInfo; import org.hibernate.engine.jdbc.dialect.spi.DialectFactory; import org.hibernate.engine.jdbc.dialect.spi.DialectResolutionInfo; +import org.hibernate.engine.jdbc.env.JdbcMetadataOnBoot; import org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentImpl; import org.hibernate.engine.jdbc.env.internal.JdbcEnvironmentInitiator; import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment; @@ -77,6 +78,7 @@ protected JdbcEnvironmentImpl getJdbcEnvironmentWithDefaults( @Override protected JdbcEnvironmentImpl getJdbcEnvironmentUsingJdbcMetadata( + JdbcMetadataOnBoot jdbcMetadataAccess, Map configurationValues, ServiceRegistryImplementor registry, DialectFactory dialectFactory, diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/provider/service/OracleSqlReactiveInformationExtractorImpl.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/provider/service/OracleSqlReactiveInformationExtractorImpl.java index d85b74d1e..bf3d7a1ea 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/provider/service/OracleSqlReactiveInformationExtractorImpl.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/provider/service/OracleSqlReactiveInformationExtractorImpl.java @@ -11,13 +11,10 @@ import java.util.List; import org.hibernate.boot.model.naming.Identifier; -import org.hibernate.internal.CoreLogging; -import org.hibernate.internal.CoreMessageLogger; import org.hibernate.tool.schema.extract.spi.ExtractionContext; public class OracleSqlReactiveInformationExtractorImpl extends AbstractReactiveInformationSchemaBasedExtractorImpl { - private static final CoreMessageLogger LOG = CoreLogging.messageLogger( PostgreSqlReactiveInformationExtractorImpl.class ); public OracleSqlReactiveInformationExtractorImpl(ExtractionContext extractionContext) { super( extractionContext ); @@ -190,12 +187,45 @@ protected String getResultSetIsNullableLabel() { @Override protected int dataTypeCode(String typeName) { - // ORACLE only supports "float" sql type for double precision - // so return code for double for both double and float column types - if ( typeName.equalsIgnoreCase( "float" ) || - typeName.toLowerCase().startsWith( "double" ) ) { - return Types.DOUBLE; - } - return super.dataTypeCode( typeName ); + return switch ( typeName.toLowerCase() ) { + // ORACLE only supports "float" sql type for double precision + // so return code for double for both double and float column types + case "float", "double", "binary_double" -> Types.DOUBLE; + case "timestamp" -> Types.TIMESTAMP; + case "timestamp with time zone", "timestamp with local time zone" -> Types.TIMESTAMP_WITH_TIMEZONE; + case "clob" -> Types.CLOB; + case "blob" -> Types.BLOB; + case "raw" -> Types.VARBINARY; + case "long raw" -> Types.LONGVARBINARY; + case "ref cursor" -> Types.REF_CURSOR; + case "number" -> Types.NUMERIC; + case "date" -> Types.DATE; + case "nvarchar2" -> Types.NVARCHAR; + case "varchar2" -> Types.VARCHAR; + default -> 0; + }; + } + + @Override + protected T processPrimaryKeysResultSet( + String catalogFilter, + String schemaFilter, + String tableName, + ExtractionContext.ResultSetProcessor processor) throws SQLException { + final StringBuilder sb = new StringBuilder() + .append( "SELECT NULL AS table_cat, " ) + .append( "c.owner AS table_schem, " ) + .append( "c.table_name, " ) + .append( "c.column_name, " ) + .append( "c.position AS key_seq, " ) + .append( "c.constraint_name AS pk_name " ) + .append( "FROM all_cons_columns c, all_constraints k " ) + .append( "WHERE k.constraint_type = 'P' AND k.table_name = :1 AND k.owner like :2 escape '/' ") + .append( "AND k.constraint_name = c.constraint_name AND k.table_name = c.table_name AND k.owner = c.owner ORDER BY column_name"); + + List parameterValues = new ArrayList<>(2); + parameterValues.add( tableName ); + parameterValues.add( schemaFilter == null ? "%" : schemaFilter ); + return getExtractionContext().getQueryResults( sb.toString(), parameterValues.toArray(), processor ); } } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/provider/service/PostgreSqlReactiveInformationExtractorImpl.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/provider/service/PostgreSqlReactiveInformationExtractorImpl.java index 62fff7ecc..8f0d285cd 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/provider/service/PostgreSqlReactiveInformationExtractorImpl.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/provider/service/PostgreSqlReactiveInformationExtractorImpl.java @@ -12,8 +12,6 @@ import java.util.List; import org.hibernate.boot.model.naming.Identifier; -import org.hibernate.internal.CoreLogging; -import org.hibernate.internal.CoreMessageLogger; import org.hibernate.tool.schema.extract.spi.ExtractionContext; import org.hibernate.type.SqlTypes; @@ -26,8 +24,6 @@ */ public class PostgreSqlReactiveInformationExtractorImpl extends AbstractReactiveInformationSchemaBasedExtractorImpl { - private static final CoreMessageLogger LOG = CoreLogging.messageLogger( PostgreSqlReactiveInformationExtractorImpl.class ); - public PostgreSqlReactiveInformationExtractorImpl(ExtractionContext extractionContext) { super( extractionContext ); } @@ -42,6 +38,14 @@ protected T processPrimaryKeysResultSet( throw new UnsupportedOperationException(); } + @Override + protected T processPrimaryKeysResultSet( + String catalogFilter, + String schemaFilter, + String tableName, + ExtractionContext.ResultSetProcessor processor) throws SQLException { + throw new UnsupportedOperationException(); + } @Override protected T processIndexInfoResultSet( String catalog, diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/provider/service/SqlServerReactiveInformationExtractorImpl.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/provider/service/SqlServerReactiveInformationExtractorImpl.java index eea7fc477..10d290388 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/provider/service/SqlServerReactiveInformationExtractorImpl.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/provider/service/SqlServerReactiveInformationExtractorImpl.java @@ -162,6 +162,15 @@ protected T processPrimaryKeysResultSet( throw new UnsupportedOperationException(); } + @Override + protected T processPrimaryKeysResultSet( + String catalogFilter, + String schemaFilter, + String tableName, + ExtractionContext.ResultSetProcessor processor) throws SQLException { + throw new UnsupportedOperationException(); + } + @Override protected T processIndexInfoResultSet( String catalog, diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sql/internal/ReactiveNativeNonSelectQueryPlan.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sql/internal/ReactiveNativeNonSelectQueryPlan.java index be5541c5d..c5bd2e849 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sql/internal/ReactiveNativeNonSelectQueryPlan.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sql/internal/ReactiveNativeNonSelectQueryPlan.java @@ -5,7 +5,6 @@ */ package org.hibernate.reactive.query.sql.internal; - import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -20,12 +19,11 @@ import org.hibernate.query.sql.spi.ParameterOccurrence; import org.hibernate.query.sqm.internal.SqmJdbcExecutionContextAdapter; import org.hibernate.reactive.engine.spi.ReactiveSharedSessionContractImplementor; -import org.hibernate.reactive.pool.impl.Parameters; import org.hibernate.reactive.query.sql.spi.ReactiveNonSelectQueryPlan; import org.hibernate.reactive.sql.exec.internal.StandardReactiveJdbcMutationExecutor; +import org.hibernate.sql.exec.internal.JdbcOperationQueryMutationNative; import org.hibernate.sql.exec.internal.JdbcParameterBindingsImpl; import org.hibernate.sql.exec.spi.JdbcOperationQueryMutation; -import org.hibernate.sql.exec.spi.JdbcOperationQueryMutationNative; import org.hibernate.sql.exec.spi.JdbcParameterBinder; import org.hibernate.sql.exec.spi.JdbcParameterBindings; @@ -71,10 +69,8 @@ public CompletionStage executeReactiveUpdate(DomainQueryExecutionContex } final SQLQueryParser parser = new SQLQueryParser( sql, null, session.getFactory() ); - - Parameters parameters = Parameters.instance( session.getDialect() ); final JdbcOperationQueryMutation jdbcMutation = new JdbcOperationQueryMutationNative( - parameters.process( parser.process() ), + parser.process(), jdbcParameterBinders, affectedTableNames ); diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sql/internal/ReactiveNativeQueryImpl.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sql/internal/ReactiveNativeQueryImpl.java index 6ca5b2024..d58ae57a9 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sql/internal/ReactiveNativeQueryImpl.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sql/internal/ReactiveNativeQueryImpl.java @@ -142,19 +142,14 @@ private ReactiveSelectQueryPlan reactiveSelectPlan() { private ReactiveNonSelectQueryPlan reactiveNonSelectPlan() { final QueryInterpretationCache.Key cacheKey = generateNonSelectInterpretationsKey(); if ( cacheKey != null ) { - NonSelectQueryPlan queryPlan = getSession().getFactory().getQueryEngine() - .getInterpretationCache().getNonSelectQueryPlan( cacheKey ); + NonSelectQueryPlan queryPlan = getSession().getFactory().getQueryEngine().getInterpretationCache().getNonSelectQueryPlan( cacheKey ); if ( queryPlan != null ) { return (ReactiveNonSelectQueryPlan) queryPlan; } } - final String sqlString = expandParameterLists(); - ReactiveNonSelectQueryPlan queryPlan = new ReactiveNativeNonSelectQueryPlan( - sqlString, - getQuerySpaces(), - getParameterOccurrences() - ); + final String sqlString = expandParameterLists( 1 ); + ReactiveNonSelectQueryPlan queryPlan = new ReactiveNativeNonSelectQueryPlan( sqlString, getQuerySpaces(), getParameterOccurrences() ); if ( cacheKey != null ) { getSession().getFactory().getQueryEngine().getInterpretationCache() .cacheNonSelectQueryPlan( cacheKey, queryPlan ); diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sql/internal/ReactiveNativeSelectQueryPlanImpl.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sql/internal/ReactiveNativeSelectQueryPlanImpl.java index f1eca36df..037f4d17d 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sql/internal/ReactiveNativeSelectQueryPlanImpl.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sql/internal/ReactiveNativeSelectQueryPlanImpl.java @@ -11,7 +11,6 @@ import java.util.Set; import java.util.concurrent.CompletionStage; -import org.hibernate.dialect.Dialect; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.query.results.ResultSetMapping; import org.hibernate.query.spi.DomainQueryExecutionContext; @@ -23,13 +22,12 @@ import org.hibernate.query.sql.spi.ParameterOccurrence; import org.hibernate.query.sqm.internal.SqmJdbcExecutionContextAdapter; import org.hibernate.reactive.engine.spi.ReactiveSharedSessionContractImplementor; -import org.hibernate.reactive.pool.impl.Parameters; import org.hibernate.reactive.query.internal.ReactiveResultSetMappingProcessor; import org.hibernate.reactive.query.spi.ReactiveNativeSelectQueryPlan; import org.hibernate.reactive.sql.exec.internal.StandardReactiveSelectExecutor; import org.hibernate.reactive.sql.results.spi.ReactiveListResultsConsumer; +import org.hibernate.sql.exec.internal.JdbcOperationQuerySelect; import org.hibernate.sql.exec.internal.JdbcParameterBindingsImpl; -import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; import org.hibernate.sql.exec.spi.JdbcParameterBinder; import org.hibernate.sql.exec.spi.JdbcParameterBindings; import org.hibernate.sql.results.jdbc.spi.JdbcValuesMappingProducer; @@ -61,8 +59,7 @@ public ReactiveNativeSelectQueryPlanImpl( resultSetMapping.addAffectedTableNames( affectedTableNames, sessionFactory ); } this.affectedTableNames = affectedTableNames; - Dialect dialect = sessionFactory.getJdbcServices().getDialect(); - this.sql = Parameters.instance( dialect ).process( parser.process() ); + this.sql = parser.process(); this.parameterList = parameterList; } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sql/spi/ReactiveNamedSqmQueryMemento.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sql/spi/ReactiveNamedSqmQueryMemento.java index 388488b26..aae4316a3 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sql/spi/ReactiveNamedSqmQueryMemento.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sql/spi/ReactiveNamedSqmQueryMemento.java @@ -20,7 +20,7 @@ import org.hibernate.query.sqm.spi.NamedSqmQueryMemento; import org.hibernate.query.sqm.tree.SqmStatement; import org.hibernate.query.sqm.tree.select.SqmSelectStatement; -import org.hibernate.reactive.query.sqm.internal.ReactiveQuerySqmImpl; +import org.hibernate.reactive.query.sqm.internal.ReactiveSqmQueryImpl; import org.hibernate.reactive.query.sqm.internal.ReactiveSqmSelectionQueryImpl; /** @@ -49,10 +49,10 @@ public SqmQueryImplementor toQuery(SharedSessionContractImplementor session) public SqmQueryImplementor toQuery(SharedSessionContractImplementor session, Class resultType) { // A bit of a hack, I'm sure that if we have a better look at this we can avoid the instanceof if ( delegate instanceof NamedHqlQueryMementoImpl ) { - return new ReactiveQuerySqmImpl<>( (NamedHqlQueryMementoImpl) delegate, resultType, session ); + return new ReactiveSqmQueryImpl<>( (NamedHqlQueryMementoImpl) delegate, resultType, session ); } if ( delegate instanceof NamedCriteriaQueryMementoImpl ) { - return new ReactiveQuerySqmImpl<>( (NamedCriteriaQueryMementoImpl) delegate, resultType, session ); + return new ReactiveSqmQueryImpl<>( (NamedCriteriaQueryMementoImpl) delegate, resultType, session ); } else { throw new UnsupportedOperationException( "NamedSqmQueryMemento not recognized: " + delegate.getClass() ); diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/ReactiveSqmSelectionQuery.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/ReactiveSqmSelectionQuery.java index 0794ae435..a43d917e9 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/ReactiveSqmSelectionQuery.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/ReactiveSqmSelectionQuery.java @@ -24,7 +24,7 @@ /** * @see org.hibernate.query.sqm.SqmSelectionQuery */ -public interface ReactiveSqmSelectionQuery extends ReactiveSelectionQuery, SqmQuery { +public interface ReactiveSqmSelectionQuery extends ReactiveSelectionQuery, SqmQuery { @Override ReactiveSqmSelectionQuery setParameter(String name, Object value); diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/internal/ConcreteSqmSelectReactiveQueryPlan.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/internal/ConcreteSqmSelectReactiveQueryPlan.java index 7c7cfd062..3c69c0d74 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/internal/ConcreteSqmSelectReactiveQueryPlan.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/internal/ConcreteSqmSelectReactiveQueryPlan.java @@ -6,45 +6,30 @@ package org.hibernate.reactive.query.sqm.internal; import java.util.List; -import java.util.Map; import java.util.concurrent.CompletionStage; -import java.util.function.Supplier; import org.hibernate.ScrollMode; -import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment; -import org.hibernate.engine.jdbc.spi.JdbcServices; -import org.hibernate.engine.spi.SessionFactoryImplementor; -import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.engine.spi.SubselectFetch; -import org.hibernate.metamodel.mapping.MappingModelExpressible; +import org.hibernate.internal.util.MutableObject; import org.hibernate.query.Query; import org.hibernate.query.spi.DomainQueryExecutionContext; -import org.hibernate.query.spi.QueryEngine; import org.hibernate.query.spi.QueryOptions; -import org.hibernate.query.spi.QueryParameterImplementor; import org.hibernate.query.spi.ScrollableResultsImplementor; +import org.hibernate.query.sqm.internal.CacheableSqmInterpretation; import org.hibernate.query.sqm.internal.ConcreteSqmSelectQueryPlan; import org.hibernate.query.sqm.internal.DomainParameterXref; -import org.hibernate.query.sqm.internal.SqmUtil; -import org.hibernate.query.sqm.spi.SqmParameterMappingModelResolutionAccess; -import org.hibernate.query.sqm.sql.SqmTranslation; -import org.hibernate.query.sqm.sql.SqmTranslator; -import org.hibernate.query.sqm.sql.SqmTranslatorFactory; -import org.hibernate.query.sqm.tree.expression.SqmParameter; import org.hibernate.query.sqm.tree.select.SqmSelectStatement; import org.hibernate.reactive.engine.spi.ReactiveSharedSessionContractImplementor; import org.hibernate.reactive.query.sqm.spi.ReactiveSelectQueryPlan; import org.hibernate.reactive.sql.exec.internal.StandardReactiveSelectExecutor; import org.hibernate.reactive.sql.results.spi.ReactiveListResultsConsumer; import org.hibernate.reactive.sql.results.spi.ReactiveResultsConsumer; -import org.hibernate.sql.ast.SqlAstTranslator; -import org.hibernate.sql.ast.SqlAstTranslatorFactory; -import org.hibernate.sql.ast.spi.FromClauseAccess; +import org.hibernate.reactive.util.impl.CompletionStages; import org.hibernate.sql.ast.tree.expression.Expression; import org.hibernate.sql.ast.tree.select.SelectStatement; -import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; import org.hibernate.sql.exec.spi.JdbcParameterBindings; import org.hibernate.sql.exec.spi.JdbcParametersList; +import org.hibernate.sql.exec.spi.JdbcSelect; import org.hibernate.sql.results.internal.TupleMetadata; import org.hibernate.sql.results.spi.RowTransformer; @@ -68,7 +53,7 @@ public class ConcreteSqmSelectReactiveQueryPlan extends ConcreteSqmSelectQuer private final SqmSelectStatement sqm; private final DomainParameterXref domainParameterXref; - private volatile CacheableSqmInterpretation cacheableSqmInterpretation; + private volatile CacheableSqmInterpretation cacheableSqmInterpretation; public ConcreteSqmSelectReactiveQueryPlan( SqmSelectStatement sqm, @@ -91,27 +76,46 @@ private static CompletionStage> listInterpreter( String hql, DomainParameterXref domainParameterXref, DomainQueryExecutionContext executionContext, - CacheableSqmInterpretation sqmInterpretation, + CacheableSqmInterpretation sqmInterpretation, JdbcParameterBindings jdbcParameterBindings, RowTransformer rowTransformer) { final ReactiveSharedSessionContractImplementor session = (ReactiveSharedSessionContractImplementor) executionContext.getSession(); - final JdbcOperationQuerySelect jdbcSelect = sqmInterpretation.getJdbcSelect(); - // I'm using a supplier so that the whenComplete at the end will catch any errors, like a finally-block - Supplier fetchHandlerSupplier = () -> SubselectFetch - .createRegistrationHandler( session.getPersistenceContext().getBatchFetchQueue(), sqmInterpretation.selectStatement, JdbcParametersList.empty(), jdbcParameterBindings ); - return completedFuture( fetchHandlerSupplier ) - .thenApply( Supplier::get ) - .thenCompose( subSelectFetchKeyHandler -> session + final JdbcSelect jdbcSelect = sqmInterpretation.jdbcOperation(); + + return CompletionStages + .supplyStage( () -> { + final var subSelectFetchKeyHandler = SubselectFetch.createRegistrationHandler( + session.getPersistenceContext() + .getBatchFetchQueue(), + sqmInterpretation.statement(), + JdbcParametersList.empty(), + jdbcParameterBindings + ); + return session .reactiveAutoFlushIfRequired( jdbcSelect.getAffectedTableNames() ) - .thenCompose( required -> StandardReactiveSelectExecutor.INSTANCE - .list( jdbcSelect, - jdbcParameterBindings, - ConcreteSqmSelectQueryPlan.listInterpreterExecutionContext( hql, executionContext, jdbcSelect, subSelectFetchKeyHandler ), - rowTransformer, - ReactiveListResultsConsumer.UniqueSemantic.ALLOW - ) - ) - ) + .thenCompose( required -> { + final Expression fetchExpression = sqmInterpretation.statement() + .getQueryPart() + .getFetchClauseExpression(); + final int resultCountEstimate = fetchExpression != null + ? interpretIntExpression( fetchExpression, jdbcParameterBindings ) + : -1; + return StandardReactiveSelectExecutor.INSTANCE.list( + jdbcSelect, + jdbcParameterBindings, + ConcreteSqmSelectQueryPlan.listInterpreterExecutionContext( + hql, + executionContext, + jdbcSelect, + subSelectFetchKeyHandler + ), + rowTransformer, + (Class) executionContext.getResultType(), + ReactiveListResultsConsumer.UniqueSemantic.ALLOW, + resultCountEstimate + ); + } ); + } ) .whenComplete( (rs, t) -> domainParameterXref.clearExpansions() ); } @@ -119,43 +123,47 @@ private static CompletionStage executeQueryInterpreter( String hql, DomainParameterXref domainParameterXref, DomainQueryExecutionContext executionContext, - CacheableSqmInterpretation sqmInterpretation, + CacheableSqmInterpretation sqmInterpretation, JdbcParameterBindings jdbcParameterBindings, RowTransformer rowTransformer, ReactiveResultsConsumer resultsConsumer) { final ReactiveSharedSessionContractImplementor session = (ReactiveSharedSessionContractImplementor) executionContext.getSession(); - final JdbcOperationQuerySelect jdbcSelect = sqmInterpretation.getJdbcSelect(); - // I'm using a supplier so that the whenComplete at the end will catch any errors, like a finally-block - Supplier fetchHandlerSupplier = () -> SubselectFetch - .createRegistrationHandler( session.getPersistenceContext().getBatchFetchQueue(), sqmInterpretation.selectStatement, JdbcParametersList.empty(), jdbcParameterBindings ); - return completedFuture( fetchHandlerSupplier ) - .thenApply( Supplier::get ) - .thenCompose( subSelectFetchKeyHandler -> session - .reactiveAutoFlushIfRequired( jdbcSelect.getAffectedTableNames() ) - .thenCompose( required -> StandardReactiveSelectExecutor.INSTANCE - .executeQuery( - jdbcSelect, - jdbcParameterBindings, - ConcreteSqmSelectQueryPlan.listInterpreterExecutionContext( - hql, - executionContext, - jdbcSelect, - subSelectFetchKeyHandler - ), - rowTransformer, - null, - resultCountEstimate( sqmInterpretation, jdbcParameterBindings ), - resultsConsumer - ) - ) - ) + final JdbcSelect jdbcSelect = sqmInterpretation.jdbcOperation(); + return CompletionStages + .supplyStage( () -> { + final var subSelectFetchKeyHandler = SubselectFetch.createRegistrationHandler( + session.getPersistenceContext() + .getBatchFetchQueue(), + sqmInterpretation.statement(), + JdbcParametersList.empty(), + jdbcParameterBindings + ); + return session + .reactiveAutoFlushIfRequired( jdbcSelect.getAffectedTableNames() ) + .thenCompose( required -> StandardReactiveSelectExecutor.INSTANCE + .executeQuery( + jdbcSelect, + jdbcParameterBindings, + ConcreteSqmSelectQueryPlan.listInterpreterExecutionContext( + hql, + executionContext, + jdbcSelect, + subSelectFetchKeyHandler + ), + rowTransformer, + null, + resultCountEstimate( sqmInterpretation, jdbcParameterBindings ), + resultsConsumer + ) + ); + }) .whenComplete( (rs, t) -> domainParameterXref.clearExpansions() ); } private static int resultCountEstimate( - CacheableSqmInterpretation sqmInterpretation, + CacheableSqmInterpretation sqmInterpretation, JdbcParameterBindings jdbcParameterBindings) { - final Expression fetchExpression = sqmInterpretation.selectStatement.getQueryPart() + final Expression fetchExpression = sqmInterpretation.statement().getQueryPart() .getFetchClauseExpression(); return fetchExpression != null ? interpretIntExpression( fetchExpression, jdbcParameterBindings ) @@ -191,16 +199,16 @@ private CompletionStage withCacheableSqmInterpretation(DomainQueryExec // to protect access. However, synchronized is much simpler here. We will verify // during throughput testing whether this is an issue and consider changes then - CacheableSqmInterpretation localCopy = cacheableSqmInterpretation; + CacheableSqmInterpretation localCopy = cacheableSqmInterpretation; JdbcParameterBindings jdbcParameterBindings = null; if ( localCopy == null ) { synchronized ( this ) { localCopy = cacheableSqmInterpretation; if ( localCopy == null ) { - localCopy = buildCacheableSqmInterpretation( sqm, domainParameterXref, executionContext ); - jdbcParameterBindings = localCopy.firstParameterBindings; - localCopy.firstParameterBindings = null; + final MutableObject mutableValue = new MutableObject<>(); + localCopy = buildInterpretation( sqm, domainParameterXref, executionContext, mutableValue ); + jdbcParameterBindings = mutableValue.get(); cacheableSqmInterpretation = localCopy; } } @@ -208,15 +216,15 @@ private CompletionStage withCacheableSqmInterpretation(DomainQueryExec else { // If the translation depends on parameter bindings, or it isn't compatible with the current query options, // we have to rebuild the JdbcSelect, which is still better than having to translate from SQM to SQL AST again - if ( localCopy.jdbcSelect.dependsOnParameterBindings() ) { + if ( localCopy.jdbcOperation().dependsOnParameterBindings() ) { jdbcParameterBindings = createJdbcParameterBindings( localCopy, executionContext ); } // If the translation depends on the limit or lock options, we have to rebuild the JdbcSelect // We could avoid this by putting the lock options into the cache key - if ( !localCopy.jdbcSelect.isCompatibleWith( jdbcParameterBindings, executionContext.getQueryOptions() ) ) { - localCopy = buildCacheableSqmInterpretation( sqm, domainParameterXref, executionContext ); - jdbcParameterBindings = localCopy.firstParameterBindings; - localCopy.firstParameterBindings = null; + if ( !localCopy.jdbcOperation().isCompatibleWith( jdbcParameterBindings, executionContext.getQueryOptions() ) ) { + final MutableObject mutableValue = new MutableObject<>(); + localCopy = buildInterpretation( sqm, domainParameterXref, executionContext, mutableValue ); + jdbcParameterBindings = mutableValue.get(); cacheableSqmInterpretation = localCopy; } } @@ -228,130 +236,12 @@ private CompletionStage withCacheableSqmInterpretation(DomainQueryExec return interpreter.interpret( context, executionContext, localCopy, jdbcParameterBindings ); } - // Copy and paste from ORM - private JdbcParameterBindings createJdbcParameterBindings(CacheableSqmInterpretation sqmInterpretation, DomainQueryExecutionContext executionContext) { - return SqmUtil.createJdbcParameterBindings( - executionContext.getQueryParameterBindings(), - domainParameterXref, - sqmInterpretation.getJdbcParamsXref(), - new SqmParameterMappingModelResolutionAccess() { - //this is pretty ugly! - @Override @SuppressWarnings("unchecked") - public MappingModelExpressible getResolvedMappingModelType(SqmParameter parameter) { - return (MappingModelExpressible) sqmInterpretation.getSqmParameterMappingModelTypes().get(parameter); - } - }, - executionContext.getSession() - ); - } - - private static CacheableSqmInterpretation buildCacheableSqmInterpretation( - SqmSelectStatement sqm, - DomainParameterXref domainParameterXref, - DomainQueryExecutionContext executionContext) { - final SharedSessionContractImplementor session = executionContext.getSession(); - final SessionFactoryImplementor sessionFactory = session.getFactory(); - final QueryEngine queryEngine = sessionFactory.getQueryEngine(); - - final SqmTranslatorFactory sqmTranslatorFactory = queryEngine.getSqmTranslatorFactory(); - - final SqmTranslator sqmConverter = sqmTranslatorFactory.createSelectTranslator( - sqm, - executionContext.getQueryOptions(), - domainParameterXref, - executionContext.getQueryParameterBindings(), - executionContext.getSession().getLoadQueryInfluencers(), - sessionFactory.getSqlTranslationEngine(), - true - ); - -// tableGroupAccess = sqmConverter.getFromClauseAccess(); - final SqmTranslation sqmInterpretation = sqmConverter.translate(); - final FromClauseAccess tableGroupAccess = sqmConverter.getFromClauseAccess(); - final JdbcServices jdbcServices = sessionFactory.getJdbcServices(); - final JdbcEnvironment jdbcEnvironment = jdbcServices.getJdbcEnvironment(); - final SqlAstTranslatorFactory sqlAstTranslatorFactory = jdbcEnvironment.getSqlAstTranslatorFactory(); - final SqlAstTranslator selectTranslator = sqlAstTranslatorFactory - .buildSelectTranslator( sessionFactory, sqmInterpretation.getSqlAst() ); - final Map, Map, List>> jdbcParamsXref = SqmUtil - .generateJdbcParamsXref( domainParameterXref, sqmInterpretation::getJdbcParamsBySqmParam ); - final JdbcParameterBindings jdbcParameterBindings = SqmUtil.createJdbcParameterBindings( - executionContext.getQueryParameterBindings(), - domainParameterXref, - jdbcParamsXref, - new SqmParameterMappingModelResolutionAccess() { - @Override @SuppressWarnings("unchecked") - public MappingModelExpressible getResolvedMappingModelType(SqmParameter parameter) { - return (MappingModelExpressible) sqmInterpretation.getSqmParameterMappingModelTypeResolutions().get(parameter); - } - }, - session - ); - - final JdbcOperationQuerySelect jdbcSelect = selectTranslator.translate( - jdbcParameterBindings, - executionContext.getQueryOptions() - ); - - return new CacheableSqmInterpretation( - sqmInterpretation.getSqlAst(), - jdbcSelect, - tableGroupAccess, - jdbcParamsXref, - sqmInterpretation.getSqmParameterMappingModelTypeResolutions(), - jdbcParameterBindings - ); - } - private interface SqmInterpreter { CompletionStage interpret( X context, DomainQueryExecutionContext executionContext, - CacheableSqmInterpretation sqmInterpretation, + CacheableSqmInterpretation sqmInterpretation, JdbcParameterBindings jdbcParameterBindings); } - private static class CacheableSqmInterpretation { - private final SelectStatement selectStatement; - private final JdbcOperationQuerySelect jdbcSelect; - private final FromClauseAccess tableGroupAccess; - private final Map, Map, List>> jdbcParamsXref; - private final Map, MappingModelExpressible> sqmParameterMappingModelTypes; - private transient JdbcParameterBindings firstParameterBindings; - - CacheableSqmInterpretation( - SelectStatement selectStatement, - JdbcOperationQuerySelect jdbcSelect, - FromClauseAccess tableGroupAccess, - Map, Map, List>> jdbcParamsXref, - Map, MappingModelExpressible> sqmParameterMappingModelTypes, - JdbcParameterBindings firstParameterBindings) { - this.selectStatement = selectStatement; - this.jdbcSelect = jdbcSelect; - this.tableGroupAccess = tableGroupAccess; - this.jdbcParamsXref = jdbcParamsXref; - this.sqmParameterMappingModelTypes = sqmParameterMappingModelTypes; - this.firstParameterBindings = firstParameterBindings; - } - - SelectStatement getSelectStatement() { - return selectStatement; - } - - JdbcOperationQuerySelect getJdbcSelect() { - return jdbcSelect; - } - - FromClauseAccess getTableGroupAccess() { - return tableGroupAccess; - } - - Map, Map, List>> getJdbcParamsXref() { - return jdbcParamsXref; - } - - public Map, MappingModelExpressible> getSqmParameterMappingModelTypes() { - return sqmParameterMappingModelTypes; - } - } } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/internal/ReactiveAbstractMultiTableMutationQueryPlan.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/internal/ReactiveAbstractMultiTableMutationQueryPlan.java new file mode 100644 index 000000000..a1759ccbc --- /dev/null +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/internal/ReactiveAbstractMultiTableMutationQueryPlan.java @@ -0,0 +1,35 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.query.sqm.internal; + +import org.hibernate.action.internal.BulkOperationCleanupAction; +import org.hibernate.query.spi.DomainQueryExecutionContext; +import org.hibernate.query.sqm.internal.AbstractMultiTableMutationQueryPlan; +import org.hibernate.query.sqm.internal.DomainParameterXref; +import org.hibernate.query.sqm.tree.SqmDmlStatement; +import org.hibernate.reactive.query.sql.spi.ReactiveNonSelectQueryPlan; +import org.hibernate.reactive.query.sqm.mutation.internal.ReactiveHandler; + +import java.util.concurrent.CompletionStage; + +public abstract class ReactiveAbstractMultiTableMutationQueryPlan, F> + extends AbstractMultiTableMutationQueryPlan implements ReactiveNonSelectQueryPlan { + + public ReactiveAbstractMultiTableMutationQueryPlan( + S statement, + DomainParameterXref domainParameterXref, + F strategy) { + super( statement, domainParameterXref, strategy ); + } + + @Override + public CompletionStage executeReactiveUpdate(DomainQueryExecutionContext context) { + BulkOperationCleanupAction.schedule( context.getSession(), getStatement() ); + final Interpretation interpretation = getInterpretation( context ); + return ((ReactiveHandler)interpretation.handler()).reactiveExecute( interpretation.jdbcParameterBindings(), context ); + } + +} diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/internal/ReactiveMultiTableDeleteQueryPlan.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/internal/ReactiveMultiTableDeleteQueryPlan.java index 90bdf6c49..30a77ba2d 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/internal/ReactiveMultiTableDeleteQueryPlan.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/internal/ReactiveMultiTableDeleteQueryPlan.java @@ -5,35 +5,32 @@ */ package org.hibernate.reactive.query.sqm.internal; -import java.util.concurrent.CompletionStage; - -import org.hibernate.action.internal.BulkOperationCleanupAction; import org.hibernate.query.spi.DomainQueryExecutionContext; import org.hibernate.query.sqm.internal.DomainParameterXref; +import org.hibernate.query.sqm.mutation.spi.MultiTableHandlerBuildResult; +import org.hibernate.query.sqm.mutation.spi.SqmMultiTableMutationStrategy; import org.hibernate.query.sqm.tree.delete.SqmDeleteStatement; -import org.hibernate.reactive.query.sql.spi.ReactiveNonSelectQueryPlan; import org.hibernate.reactive.query.sqm.mutation.spi.ReactiveSqmMultiTableMutationStrategy; /** * @see org.hibernate.query.sqm.internal.MultiTableDeleteQueryPlan */ -public class ReactiveMultiTableDeleteQueryPlan implements ReactiveNonSelectQueryPlan { - private final SqmDeleteStatement sqmDelete; - private final DomainParameterXref domainParameterXref; - private final ReactiveSqmMultiTableMutationStrategy deleteStrategy; +public class ReactiveMultiTableDeleteQueryPlan + extends ReactiveAbstractMultiTableMutationQueryPlan, SqmMultiTableMutationStrategy> { public ReactiveMultiTableDeleteQueryPlan( SqmDeleteStatement sqmDelete, DomainParameterXref domainParameterXref, ReactiveSqmMultiTableMutationStrategy deleteStrategy) { - this.sqmDelete = sqmDelete; - this.domainParameterXref = domainParameterXref; - this.deleteStrategy = deleteStrategy; + super( sqmDelete, domainParameterXref, deleteStrategy ); } @Override - public CompletionStage executeReactiveUpdate(DomainQueryExecutionContext executionContext) { - BulkOperationCleanupAction.schedule( executionContext.getSession(), sqmDelete ); - return deleteStrategy.reactiveExecuteDelete( sqmDelete, domainParameterXref, executionContext ); + protected MultiTableHandlerBuildResult buildHandler( + SqmDeleteStatement statement, + DomainParameterXref domainParameterXref, + SqmMultiTableMutationStrategy strategy, + DomainQueryExecutionContext context) { + return strategy.buildHandler( statement, domainParameterXref, context ); } } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/internal/ReactiveMultiTableInsertQueryPlan.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/internal/ReactiveMultiTableInsertQueryPlan.java index ed0e68b01..502f4f53c 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/internal/ReactiveMultiTableInsertQueryPlan.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/internal/ReactiveMultiTableInsertQueryPlan.java @@ -5,35 +5,32 @@ */ package org.hibernate.reactive.query.sqm.internal; -import java.util.concurrent.CompletionStage; - -import org.hibernate.action.internal.BulkOperationCleanupAction; import org.hibernate.query.spi.DomainQueryExecutionContext; import org.hibernate.query.sqm.internal.DomainParameterXref; +import org.hibernate.query.sqm.mutation.spi.MultiTableHandlerBuildResult; +import org.hibernate.query.sqm.mutation.spi.SqmMultiTableInsertStrategy; import org.hibernate.query.sqm.tree.insert.SqmInsertStatement; -import org.hibernate.reactive.query.sql.spi.ReactiveNonSelectQueryPlan; import org.hibernate.reactive.query.sqm.mutation.spi.ReactiveSqmMultiTableInsertStrategy; /** * @see org.hibernate.query.sqm.internal.MultiTableInsertQueryPlan */ -public class ReactiveMultiTableInsertQueryPlan implements ReactiveNonSelectQueryPlan { - private final SqmInsertStatement sqmInsert; - private final DomainParameterXref domainParameterXref; - private final ReactiveSqmMultiTableInsertStrategy mutationStrategy; +public class ReactiveMultiTableInsertQueryPlan + extends ReactiveAbstractMultiTableMutationQueryPlan, SqmMultiTableInsertStrategy> { public ReactiveMultiTableInsertQueryPlan( SqmInsertStatement sqmInsert, DomainParameterXref domainParameterXref, ReactiveSqmMultiTableInsertStrategy mutationStrategy) { - this.sqmInsert = sqmInsert; - this.domainParameterXref = domainParameterXref; - this.mutationStrategy = mutationStrategy; + super( sqmInsert, domainParameterXref, mutationStrategy ); } @Override - public CompletionStage executeReactiveUpdate(DomainQueryExecutionContext executionContext) { - BulkOperationCleanupAction.schedule( executionContext.getSession(), sqmInsert ); - return mutationStrategy.reactiveExecuteInsert( sqmInsert, domainParameterXref, executionContext ); + protected MultiTableHandlerBuildResult buildHandler( + SqmInsertStatement statement, + DomainParameterXref domainParameterXref, + SqmMultiTableInsertStrategy strategy, + DomainQueryExecutionContext context) { + return strategy.buildHandler( statement, domainParameterXref, context ); } } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/internal/ReactiveMultiTableUpdateQueryPlan.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/internal/ReactiveMultiTableUpdateQueryPlan.java index 605186079..0715e06b5 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/internal/ReactiveMultiTableUpdateQueryPlan.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/internal/ReactiveMultiTableUpdateQueryPlan.java @@ -5,35 +5,31 @@ */ package org.hibernate.reactive.query.sqm.internal; -import java.util.concurrent.CompletionStage; - -import org.hibernate.action.internal.BulkOperationCleanupAction; import org.hibernate.query.spi.DomainQueryExecutionContext; import org.hibernate.query.sqm.internal.DomainParameterXref; +import org.hibernate.query.sqm.mutation.spi.MultiTableHandlerBuildResult; +import org.hibernate.query.sqm.mutation.spi.SqmMultiTableMutationStrategy; import org.hibernate.query.sqm.tree.update.SqmUpdateStatement; -import org.hibernate.reactive.query.sql.spi.ReactiveNonSelectQueryPlan; -import org.hibernate.reactive.query.sqm.mutation.spi.ReactiveSqmMultiTableMutationStrategy; /** * @see org.hibernate.query.sqm.internal.MultiTableUpdateQueryPlan */ -public class ReactiveMultiTableUpdateQueryPlan implements ReactiveNonSelectQueryPlan { - private final SqmUpdateStatement sqmUpdate; - private final DomainParameterXref domainParameterXref; - private final ReactiveSqmMultiTableMutationStrategy mutationStrategy; +public class ReactiveMultiTableUpdateQueryPlan + extends ReactiveAbstractMultiTableMutationQueryPlan, SqmMultiTableMutationStrategy> { public ReactiveMultiTableUpdateQueryPlan( SqmUpdateStatement sqmUpdate, DomainParameterXref domainParameterXref, - ReactiveSqmMultiTableMutationStrategy mutationStrategy) { - this.sqmUpdate = sqmUpdate; - this.domainParameterXref = domainParameterXref; - this.mutationStrategy = mutationStrategy; + SqmMultiTableMutationStrategy mutationStrategy) { + super( sqmUpdate, domainParameterXref, mutationStrategy ); } @Override - public CompletionStage executeReactiveUpdate(DomainQueryExecutionContext executionContext) { - BulkOperationCleanupAction.schedule( executionContext.getSession(), sqmUpdate ); - return mutationStrategy.reactiveExecuteUpdate( sqmUpdate, domainParameterXref, executionContext ); + protected MultiTableHandlerBuildResult buildHandler( + SqmUpdateStatement statement, + DomainParameterXref domainParameterXref, + SqmMultiTableMutationStrategy strategy, + DomainQueryExecutionContext context) { + return strategy.buildHandler( statement, domainParameterXref, context ); } } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/internal/ReactiveSimpleDeleteQueryPlan.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/internal/ReactiveSimpleDeleteQueryPlan.java index eb055a38a..83113911b 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/internal/ReactiveSimpleDeleteQueryPlan.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/internal/ReactiveSimpleDeleteQueryPlan.java @@ -5,215 +5,89 @@ */ package org.hibernate.reactive.query.sqm.internal; -import java.sql.PreparedStatement; -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.concurrent.CompletionStage; - import org.hibernate.action.internal.BulkOperationCleanupAction; -import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.engine.spi.SharedSessionContractImplementor; -import org.hibernate.metamodel.mapping.EntityMappingType; -import org.hibernate.metamodel.mapping.ForeignKeyDescriptor; -import org.hibernate.metamodel.mapping.MappingModelExpressible; -import org.hibernate.metamodel.mapping.SoftDeleteMapping; -import org.hibernate.metamodel.mapping.internal.MappingModelCreationHelper; +import org.hibernate.persister.entity.EntityPersister; import org.hibernate.query.spi.DomainQueryExecutionContext; -import org.hibernate.query.spi.QueryParameterImplementor; +import org.hibernate.query.sqm.internal.CacheableSqmInterpretation; import org.hibernate.query.sqm.internal.DomainParameterXref; import org.hibernate.query.sqm.internal.SimpleDeleteQueryPlan; import org.hibernate.query.sqm.internal.SqmJdbcExecutionContextAdapter; -import org.hibernate.query.sqm.internal.SqmUtil; -import org.hibernate.query.sqm.spi.SqmParameterMappingModelResolutionAccess; -import org.hibernate.query.sqm.sql.SqmTranslation; -import org.hibernate.query.sqm.sql.SqmTranslator; import org.hibernate.query.sqm.tree.delete.SqmDeleteStatement; -import org.hibernate.query.sqm.tree.expression.SqmParameter; +import org.hibernate.reactive.logging.impl.Log; +import org.hibernate.reactive.logging.impl.LoggerFactory; import org.hibernate.reactive.query.sql.spi.ReactiveNonSelectQueryPlan; -import org.hibernate.reactive.query.sqm.mutation.internal.ReactiveSqmMutationStrategyHelper; import org.hibernate.reactive.sql.exec.internal.StandardReactiveJdbcMutationExecutor; -import org.hibernate.spi.NavigablePath; -import org.hibernate.sql.ast.SqlAstTranslator; -import org.hibernate.sql.ast.tree.AbstractUpdateOrDeleteStatement; +import org.hibernate.reactive.util.impl.CompletionStages; import org.hibernate.sql.ast.tree.MutationStatement; -import org.hibernate.sql.ast.tree.expression.Expression; -import org.hibernate.sql.ast.tree.from.MutatingTableReferenceGroupWrapper; -import org.hibernate.sql.ast.tree.from.NamedTableReference; -import org.hibernate.sql.ast.tree.predicate.InSubQueryPredicate; -import org.hibernate.sql.ast.tree.select.QuerySpec; -import org.hibernate.sql.ast.tree.update.Assignment; -import org.hibernate.sql.ast.tree.update.UpdateStatement; +import org.hibernate.sql.exec.spi.ExecutionContext; import org.hibernate.sql.exec.spi.JdbcOperationQueryMutation; import org.hibernate.sql.exec.spi.JdbcParameterBindings; -import org.hibernate.sql.exec.spi.JdbcParametersList; -import org.hibernate.sql.results.internal.SqlSelectionImpl; -import static org.hibernate.query.sqm.internal.SqmJdbcExecutionContextAdapter.usingLockingAndPaging; - -public class ReactiveSimpleDeleteQueryPlan extends SimpleDeleteQueryPlan implements ReactiveNonSelectQueryPlan { - private final EntityMappingType entityDescriptor; - private final SqmDeleteStatement sqmDelete; - private final DomainParameterXref domainParameterXref; +import java.lang.invoke.MethodHandles; +import java.util.concurrent.CompletionStage; - private JdbcOperationQueryMutation jdbcOperation; +public class ReactiveSimpleDeleteQueryPlan + extends SimpleDeleteQueryPlan implements ReactiveNonSelectQueryPlan { - private SqmTranslation sqmInterpretation; - private Map, Map, List>> jdbcParamsXref; + private static final Log LOG = LoggerFactory.make( Log.class, MethodHandles.lookup() ); public ReactiveSimpleDeleteQueryPlan( - EntityMappingType entityDescriptor, + EntityPersister entityDescriptor, SqmDeleteStatement sqmDelete, DomainParameterXref domainParameterXref) { super( entityDescriptor, sqmDelete, domainParameterXref ); - this.entityDescriptor = entityDescriptor; - this.sqmDelete = sqmDelete; - this.domainParameterXref = domainParameterXref; } @Override - protected SqlAstTranslator createTranslator(DomainQueryExecutionContext executionContext) { - final SessionFactoryImplementor factory = executionContext.getSession().getFactory(); - final SqmTranslator translator = factory.getQueryEngine().getSqmTranslatorFactory().createMutationTranslator( - sqmDelete, - executionContext.getQueryOptions(), - domainParameterXref, - executionContext.getQueryParameterBindings(), - executionContext.getSession().getLoadQueryInfluencers(), - factory.getSqlTranslationEngine() - ); - - sqmInterpretation = (SqmTranslation) translator.translate(); - - this.jdbcParamsXref = SqmUtil.generateJdbcParamsXref( - domainParameterXref, - sqmInterpretation::getJdbcParamsBySqmParam - ); - - return factory.getJdbcServices() - .getJdbcEnvironment() - .getSqlAstTranslatorFactory() - .buildMutationTranslator( factory, createDeleteAst() ); + protected int execute(CacheableSqmInterpretation sqmInterpretation, JdbcParameterBindings jdbcParameterBindings, ExecutionContext executionContext) { + throw LOG.nonReactiveMethodCall( "executeReactiveUpdate" ); } - // Copy and paste from superclass - private MutationStatement createDeleteAst() { - final MutationStatement ast; - if ( entityDescriptor.getSoftDeleteMapping() == null ) { - ast = sqmInterpretation.getSqlAst(); - } - else { - final AbstractUpdateOrDeleteStatement sqlDeleteAst = sqmInterpretation.getSqlAst(); - final NamedTableReference targetTable = sqlDeleteAst.getTargetTable(); - final SoftDeleteMapping columnMapping = getEntityDescriptor().getSoftDeleteMapping(); - final Assignment assignment = columnMapping.createSoftDeleteAssignment( targetTable ); - - ast = new UpdateStatement( - targetTable, - Collections.singletonList( assignment ), - sqlDeleteAst.getRestriction() - ); - } - return ast; + @Override + public CompletionStage executeReactiveUpdate(DomainQueryExecutionContext context) { + BulkOperationCleanupAction.schedule( context.getSession(), getStatement() ); + final Interpretation interpretation = getInterpretation( context ); + final ExecutionContext executionContext = SqmJdbcExecutionContextAdapter.omittingLockingAndPaging( context ); + return executeReactive( interpretation.interpretation(), interpretation.jdbcParameterBindings(), executionContext ); } - @Override - public CompletionStage executeReactiveUpdate(DomainQueryExecutionContext executionContext) { - BulkOperationCleanupAction.schedule( executionContext.getSession(), sqmDelete ); + protected CompletionStage executeReactive(CacheableSqmInterpretation sqmInterpretation, JdbcParameterBindings jdbcParameterBindings, ExecutionContext executionContext) { final SharedSessionContractImplementor session = executionContext.getSession(); - final SessionFactoryImplementor factory = session.getFactory(); - SqlAstTranslator sqlAstTranslator = null; - if ( jdbcOperation == null ) { - sqlAstTranslator = createTranslator( executionContext ); - } - - final JdbcParameterBindings jdbcParameterBindings = SqmUtil.createJdbcParameterBindings( - executionContext.getQueryParameterBindings(), - domainParameterXref, - jdbcParamsXref, - new SqmParameterMappingModelResolutionAccess() { - @Override @SuppressWarnings("unchecked") - public MappingModelExpressible getResolvedMappingModelType(SqmParameter parameter) { - return (MappingModelExpressible) sqmInterpretation.getSqmParameterMappingModelTypeResolutions().get(parameter); - } - }, - session + return CompletionStages.loop(getCollectionTableDeletes(), delete -> + executeReactive( delete, jdbcParameterBindings, executionContext.getSession(), executionContext ) + ).thenCompose( v -> CompletionStages.loop( + getCollectionTableDeletes(), delete -> + executeReactive( + delete, + jdbcParameterBindings, + executionContext.getSession(), + executionContext + ) + ) + .thenCompose( unused -> executeReactive( + sqmInterpretation.jdbcOperation(), + jdbcParameterBindings, + session, + executionContext + ) ) ); - - if ( jdbcOperation != null - && !jdbcOperation.isCompatibleWith( jdbcParameterBindings, executionContext.getQueryOptions() ) ) { - sqlAstTranslator = createTranslator( executionContext ); - } - - if ( sqlAstTranslator != null ) { - jdbcOperation = sqlAstTranslator.translate( jdbcParameterBindings, executionContext.getQueryOptions() ); - } - - final boolean missingRestriction = sqmDelete.getWhereClause() == null - || sqmDelete.getWhereClause().getPredicate() == null; - if ( missingRestriction ) { - assert domainParameterXref.getSqmParameterCount() == 0; - assert jdbcParamsXref.isEmpty(); - } - - final SqmJdbcExecutionContextAdapter executionContextAdapter = usingLockingAndPaging( executionContext ); - - return ReactiveSqmMutationStrategyHelper.cleanUpCollectionTables( - entityDescriptor, - (tableReference, attributeMapping) -> { - if ( missingRestriction ) { - return null; - } - - final ForeignKeyDescriptor fkDescriptor = attributeMapping.getKeyDescriptor(); - final Expression fkColumnExpression = MappingModelCreationHelper.buildColumnReferenceExpression( - new MutatingTableReferenceGroupWrapper( - new NavigablePath( attributeMapping.getRootPathName() ), - attributeMapping, - (NamedTableReference) tableReference - ), - fkDescriptor.getKeyPart(), - null, - factory - ); - - final QuerySpec matchingIdSubQuery = new QuerySpec( false ); - - final MutatingTableReferenceGroupWrapper tableGroup = new MutatingTableReferenceGroupWrapper( - new NavigablePath( attributeMapping.getRootPathName() ), - attributeMapping, - sqmInterpretation.getSqlAst().getTargetTable() - ); - final Expression fkTargetColumnExpression = MappingModelCreationHelper.buildColumnReferenceExpression( - tableGroup, - fkDescriptor.getTargetPart(), - sqmInterpretation.getSqlExpressionResolver(), - factory - ); - matchingIdSubQuery.getSelectClause() - .addSqlSelection( new SqlSelectionImpl( 0, fkTargetColumnExpression ) ); - - matchingIdSubQuery.getFromClause().addRoot( - tableGroup - ); - - matchingIdSubQuery.applyPredicate( sqmInterpretation.getSqlAst().getRestriction() ); - - return new InSubQueryPredicate( fkColumnExpression, matchingIdSubQuery, false ); - }, - ( missingRestriction ? JdbcParameterBindings.NO_BINDINGS : jdbcParameterBindings ), - executionContextAdapter - ) - .thenCompose( unused -> StandardReactiveJdbcMutationExecutor.INSTANCE - .executeReactive( - jdbcOperation, - jdbcParameterBindings, - session.getJdbcCoordinator().getStatementPreparer()::prepareStatement, - ReactiveSimpleDeleteQueryPlan::doNothing, - executionContextAdapter ) - ); } - private static void doNothing(Integer i, PreparedStatement ps) { + private static CompletionStage executeReactive( + JdbcOperationQueryMutation delete, + JdbcParameterBindings jdbcParameterBindings, + SharedSessionContractImplementor executionContext, + ExecutionContext executionContext1) { + return StandardReactiveJdbcMutationExecutor.INSTANCE.executeReactive( + delete, + jdbcParameterBindings, + sql -> executionContext + .getJdbcCoordinator() + .getStatementPreparer() + .prepareStatement( sql ), + (integer, preparedStatement) -> {}, + executionContext1 + ); } } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/internal/ReactiveSimpleInsertQueryPlan.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/internal/ReactiveSimpleInsertQueryPlan.java deleted file mode 100644 index bf67d16ea..000000000 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/internal/ReactiveSimpleInsertQueryPlan.java +++ /dev/null @@ -1,121 +0,0 @@ -/* Hibernate, Relational Persistence for Idiomatic Java - * - * SPDX-License-Identifier: Apache-2.0 - * Copyright: Red Hat Inc. and Hibernate Authors - */ -package org.hibernate.reactive.query.sqm.internal; - - -import java.util.List; -import java.util.Map; -import java.util.concurrent.CompletionStage; - -import org.hibernate.action.internal.BulkOperationCleanupAction; -import org.hibernate.engine.spi.SessionFactoryImplementor; -import org.hibernate.engine.spi.SharedSessionContractImplementor; -import org.hibernate.metamodel.mapping.MappingModelExpressible; -import org.hibernate.query.spi.DomainQueryExecutionContext; -import org.hibernate.query.spi.QueryParameterImplementor; -import org.hibernate.query.sqm.internal.DomainParameterXref; -import org.hibernate.query.sqm.internal.SqmJdbcExecutionContextAdapter; -import org.hibernate.query.sqm.internal.SqmUtil; -import org.hibernate.query.sqm.spi.SqmParameterMappingModelResolutionAccess; -import org.hibernate.query.sqm.sql.SqmTranslation; -import org.hibernate.query.sqm.tree.expression.SqmParameter; -import org.hibernate.query.sqm.tree.insert.SqmInsertStatement; -import org.hibernate.reactive.query.sql.spi.ReactiveNonSelectQueryPlan; -import org.hibernate.reactive.sql.exec.internal.StandardReactiveJdbcMutationExecutor; -import org.hibernate.sql.ast.SqlAstTranslator; -import org.hibernate.sql.ast.tree.MutationStatement; -import org.hibernate.sql.exec.spi.JdbcOperationQueryMutation; -import org.hibernate.sql.exec.spi.JdbcParameterBindings; -import org.hibernate.sql.exec.spi.JdbcParametersList; - -/** - * @see org.hibernate.query.sqm.internal.SimpleInsertQueryPlan - */ -public class ReactiveSimpleInsertQueryPlan implements ReactiveNonSelectQueryPlan { - - private final SqmInsertStatement sqmInsert; - - private final DomainParameterXref domainParameterXref; - - private Map, MappingModelExpressible> paramTypeResolutions; - - private JdbcOperationQueryMutation jdbcInsert; - private Map, Map, List>> jdbcParamsXref; - - public ReactiveSimpleInsertQueryPlan( - SqmInsertStatement sqmInsert, - DomainParameterXref domainParameterXref) { - this.sqmInsert = sqmInsert; - this.domainParameterXref = domainParameterXref; - } - - @Override - public CompletionStage executeReactiveUpdate(DomainQueryExecutionContext executionContext) { - BulkOperationCleanupAction.schedule( executionContext.getSession(), sqmInsert ); - final SharedSessionContractImplementor session = executionContext.getSession(); - SqlAstTranslator insertTranslator = jdbcInsert == null - ? createInsertTranslator( executionContext ) - : null; - - final JdbcParameterBindings jdbcParameterBindings = SqmUtil.createJdbcParameterBindings( - executionContext.getQueryParameterBindings(), - domainParameterXref, - jdbcParamsXref, - new SqmParameterMappingModelResolutionAccess() { - @Override @SuppressWarnings("unchecked") - public MappingModelExpressible getResolvedMappingModelType(SqmParameter parameter) { - return (MappingModelExpressible) paramTypeResolutions.get(parameter); - } - }, - session - ); - - if ( jdbcInsert != null && !jdbcInsert.isCompatibleWith( jdbcParameterBindings, executionContext.getQueryOptions() ) ) { - insertTranslator = createInsertTranslator( executionContext ); - } - - if ( insertTranslator != null ) { - jdbcInsert = insertTranslator.translate( jdbcParameterBindings, executionContext.getQueryOptions() ); - } - - return StandardReactiveJdbcMutationExecutor.INSTANCE - .executeReactive( - jdbcInsert, - jdbcParameterBindings, - session.getJdbcCoordinator().getStatementPreparer()::prepareStatement, - (i, ps) -> {}, - SqmJdbcExecutionContextAdapter.omittingLockingAndPaging( executionContext ) - ); - } - - // Copied from Hibernate ORM SimpleInsertQueryPlan#createInsertTranslator - private SqlAstTranslator createInsertTranslator(DomainQueryExecutionContext executionContext) { - final SessionFactoryImplementor factory = executionContext.getSession().getFactory(); - - final SqmTranslation sqmInterpretation = factory.getQueryEngine().getSqmTranslatorFactory() - .createMutationTranslator( - sqmInsert, - executionContext.getQueryOptions(), - domainParameterXref, - executionContext.getQueryParameterBindings(), - executionContext.getSession().getLoadQueryInfluencers(), - factory.getSqlTranslationEngine() - ) - .translate(); - - this.jdbcParamsXref = SqmUtil.generateJdbcParamsXref( - domainParameterXref, - sqmInterpretation::getJdbcParamsBySqmParam - ); - - this.paramTypeResolutions = sqmInterpretation.getSqmParameterMappingModelTypeResolutions(); - - return factory.getJdbcServices() - .getJdbcEnvironment() - .getSqlAstTranslatorFactory() - .buildMutationTranslator( factory, sqmInterpretation.getSqlAst() ); - } -} diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/internal/ReactiveSimpleNonSelectQueryPlan.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/internal/ReactiveSimpleNonSelectQueryPlan.java new file mode 100644 index 000000000..1a2a942f5 --- /dev/null +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/internal/ReactiveSimpleNonSelectQueryPlan.java @@ -0,0 +1,56 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.query.sqm.internal; + +import org.hibernate.action.internal.BulkOperationCleanupAction; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.query.spi.DomainQueryExecutionContext; +import org.hibernate.query.sqm.internal.CacheableSqmInterpretation; +import org.hibernate.query.sqm.internal.DomainParameterXref; +import org.hibernate.query.sqm.internal.SimpleNonSelectQueryPlan; +import org.hibernate.query.sqm.internal.SqmJdbcExecutionContextAdapter; +import org.hibernate.query.sqm.tree.SqmDmlStatement; +import org.hibernate.reactive.query.sql.spi.ReactiveNonSelectQueryPlan; +import org.hibernate.reactive.sql.exec.internal.StandardReactiveJdbcMutationExecutor; +import org.hibernate.sql.ast.tree.MutationStatement; +import org.hibernate.sql.exec.spi.ExecutionContext; +import org.hibernate.sql.exec.spi.JdbcOperationQueryMutation; +import org.hibernate.sql.exec.spi.JdbcParameterBindings; + +import java.util.concurrent.CompletionStage; + +public class ReactiveSimpleNonSelectQueryPlan extends + SimpleNonSelectQueryPlan implements ReactiveNonSelectQueryPlan { + + public ReactiveSimpleNonSelectQueryPlan( + SqmDmlStatement statement, + DomainParameterXref domainParameterXref) { + super( statement, domainParameterXref ); + } + + @Override + public CompletionStage executeReactiveUpdate(DomainQueryExecutionContext context) { + BulkOperationCleanupAction.schedule( context.getSession(), getStatement() ); + final Interpretation interpretation = getInterpretation( context ); + final ExecutionContext executionContext = SqmJdbcExecutionContextAdapter.omittingLockingAndPaging( context ); + return executeReactive( interpretation.interpretation(), interpretation.jdbcParameterBindings(), executionContext ); + } + + protected CompletionStage executeReactive(CacheableSqmInterpretation sqmInterpretation, JdbcParameterBindings jdbcParameterBindings, ExecutionContext executionContext) { + final SharedSessionContractImplementor session = executionContext.getSession(); + return StandardReactiveJdbcMutationExecutor.INSTANCE.executeReactive( + sqmInterpretation.jdbcOperation(), + jdbcParameterBindings, + sql -> session + .getJdbcCoordinator() + .getStatementPreparer() + .prepareStatement( sql ), + (integer, preparedStatement) -> { + }, + executionContext + ); + } +} diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/internal/ReactiveSimpleUpdateQueryPlan.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/internal/ReactiveSimpleUpdateQueryPlan.java deleted file mode 100644 index f49f848f5..000000000 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/internal/ReactiveSimpleUpdateQueryPlan.java +++ /dev/null @@ -1,117 +0,0 @@ -/* Hibernate, Relational Persistence for Idiomatic Java - * - * SPDX-License-Identifier: Apache-2.0 - * Copyright: Red Hat Inc. and Hibernate Authors - */ -package org.hibernate.reactive.query.sqm.internal; - -import java.util.List; -import java.util.Map; -import java.util.concurrent.CompletionStage; - -import org.hibernate.action.internal.BulkOperationCleanupAction; -import org.hibernate.engine.spi.SessionFactoryImplementor; -import org.hibernate.engine.spi.SharedSessionContractImplementor; -import org.hibernate.metamodel.mapping.MappingModelExpressible; -import org.hibernate.query.spi.DomainQueryExecutionContext; -import org.hibernate.query.spi.QueryEngine; -import org.hibernate.query.spi.QueryParameterImplementor; -import org.hibernate.query.sqm.internal.DomainParameterXref; -import org.hibernate.query.sqm.internal.SqmJdbcExecutionContextAdapter; -import org.hibernate.query.sqm.internal.SqmUtil; -import org.hibernate.query.sqm.spi.SqmParameterMappingModelResolutionAccess; -import org.hibernate.query.sqm.sql.SqmTranslation; -import org.hibernate.query.sqm.sql.SqmTranslator; -import org.hibernate.query.sqm.sql.SqmTranslatorFactory; -import org.hibernate.query.sqm.tree.expression.SqmParameter; -import org.hibernate.query.sqm.tree.update.SqmUpdateStatement; -import org.hibernate.reactive.query.sql.spi.ReactiveNonSelectQueryPlan; -import org.hibernate.reactive.sql.exec.internal.StandardReactiveJdbcMutationExecutor; -import org.hibernate.sql.ast.SqlAstTranslator; -import org.hibernate.sql.ast.spi.FromClauseAccess; -import org.hibernate.sql.ast.tree.MutationStatement; -import org.hibernate.sql.exec.spi.JdbcOperationQueryMutation; -import org.hibernate.sql.exec.spi.JdbcParameterBindings; -import org.hibernate.sql.exec.spi.JdbcParametersList; - - -/** - * @see org.hibernate.query.sqm.internal.SimpleUpdateQueryPlan - */ -public class ReactiveSimpleUpdateQueryPlan implements ReactiveNonSelectQueryPlan { - - private final SqmUpdateStatement sqmUpdate; - private final DomainParameterXref domainParameterXref; - - private JdbcOperationQueryMutation jdbcUpdate; - private FromClauseAccess tableGroupAccess; - private Map, Map, List>> jdbcParamsXref; - private Map, MappingModelExpressible> sqmParamMappingTypeResolutions; - - public ReactiveSimpleUpdateQueryPlan(SqmUpdateStatement sqmUpdate, DomainParameterXref domainParameterXref) { - this.sqmUpdate = sqmUpdate; - this.domainParameterXref = domainParameterXref; - } - - @Override - public CompletionStage executeReactiveUpdate(DomainQueryExecutionContext executionContext) { - BulkOperationCleanupAction.schedule( executionContext.getSession(), sqmUpdate ); - final SharedSessionContractImplementor session = executionContext.getSession(); - SqlAstTranslator updateTranslator = jdbcUpdate == null - ? createUpdateTranslator( executionContext ) - : null; - final JdbcParameterBindings jdbcParameterBindings = SqmUtil.createJdbcParameterBindings( - executionContext.getQueryParameterBindings(), - domainParameterXref, - jdbcParamsXref, - new SqmParameterMappingModelResolutionAccess() { - @Override @SuppressWarnings("unchecked") - public MappingModelExpressible getResolvedMappingModelType(SqmParameter parameter) { - return (MappingModelExpressible) sqmParamMappingTypeResolutions.get(parameter); - } - }, - session - ); - - if ( jdbcUpdate != null && !jdbcUpdate.isCompatibleWith( jdbcParameterBindings, executionContext.getQueryOptions() ) ) { - updateTranslator = createUpdateTranslator( executionContext ); - } - - if ( updateTranslator != null ) { - jdbcUpdate = updateTranslator.translate( jdbcParameterBindings, executionContext.getQueryOptions() ); - } - - return StandardReactiveJdbcMutationExecutor.INSTANCE - .executeReactive( - jdbcUpdate, - jdbcParameterBindings, - session.getJdbcCoordinator().getStatementPreparer()::prepareStatement, - (i, ps) -> {}, - SqmJdbcExecutionContextAdapter.omittingLockingAndPaging( executionContext ) - ); - } - - // I can probably change ORM to reuse this - private SqlAstTranslator createUpdateTranslator(DomainQueryExecutionContext executionContext) { - final SessionFactoryImplementor factory = executionContext.getSession().getFactory(); - final QueryEngine queryEngine = factory.getQueryEngine(); - - final SqmTranslatorFactory translatorFactory = queryEngine.getSqmTranslatorFactory(); - final SqmTranslator translator = translatorFactory.createMutationTranslator( - sqmUpdate, - executionContext.getQueryOptions(), - domainParameterXref, - executionContext.getQueryParameterBindings(), - executionContext.getSession().getLoadQueryInfluencers(), - factory.getSqlTranslationEngine() - ); - - final SqmTranslation sqmInterpretation = translator.translate(); - tableGroupAccess = sqmInterpretation.getFromClauseAccess(); - this.jdbcParamsXref = SqmUtil - .generateJdbcParamsXref( domainParameterXref, sqmInterpretation::getJdbcParamsBySqmParam ); - this.sqmParamMappingTypeResolutions = sqmInterpretation.getSqmParameterMappingModelTypeResolutions(); - return factory.getJdbcServices().getJdbcEnvironment().getSqlAstTranslatorFactory() - .buildMutationTranslator( factory, sqmInterpretation.getSqlAst() ); - } -} diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/internal/ReactiveQuerySqmImpl.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/internal/ReactiveSqmQueryImpl.java similarity index 80% rename from hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/internal/ReactiveQuerySqmImpl.java rename to hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/internal/ReactiveSqmQueryImpl.java index 5242ab747..b0bc9f823 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/internal/ReactiveQuerySqmImpl.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/internal/ReactiveSqmQueryImpl.java @@ -44,11 +44,14 @@ import org.hibernate.query.spi.HqlInterpretation; import org.hibernate.query.spi.QueryInterpretationCache; import org.hibernate.query.spi.QueryOptions; -import org.hibernate.query.sqm.internal.QuerySqmImpl; import org.hibernate.query.sqm.internal.SqmInterpretationsKey; +import org.hibernate.query.sqm.internal.SqmQueryImpl; +import org.hibernate.query.sqm.tree.SqmCopyContext; import org.hibernate.query.sqm.tree.SqmStatement; import org.hibernate.query.sqm.tree.delete.SqmDeleteStatement; import org.hibernate.query.sqm.tree.insert.SqmInsertStatement; +import org.hibernate.query.sqm.tree.insert.SqmInsertValuesStatement; +import org.hibernate.query.sqm.tree.insert.SqmValues; import org.hibernate.query.sqm.tree.select.SqmSelectStatement; import org.hibernate.query.sqm.tree.update.SqmUpdateStatement; import org.hibernate.reactive.logging.impl.Log; @@ -71,15 +74,15 @@ import jakarta.persistence.metamodel.Type; /** - * A reactive {@link QuerySqmImpl} + * A reactive {@link SqmQueryImpl} */ -public class ReactiveQuerySqmImpl extends QuerySqmImpl implements ReactiveSqmQueryImplementor { +public class ReactiveSqmQueryImpl extends SqmQueryImpl implements ReactiveSqmQueryImplementor { private static final Log LOG = LoggerFactory.make( Log.class, MethodHandles.lookup() ); private final ReactiveAbstractSelectionQuery selectionQueryDelegate; - public ReactiveQuerySqmImpl( + public ReactiveSqmQueryImpl( NamedHqlQueryMementoImpl memento, Class expectedResultType, SharedSessionContractImplementor session) { @@ -87,7 +90,7 @@ public ReactiveQuerySqmImpl( this.selectionQueryDelegate = createSelectionQueryDelegate( session ); } - public ReactiveQuerySqmImpl( + public ReactiveSqmQueryImpl( NamedCriteriaQueryMementoImpl memento, Class resultType, SharedSessionContractImplementor session) { @@ -95,7 +98,7 @@ public ReactiveQuerySqmImpl( this.selectionQueryDelegate = createSelectionQueryDelegate( session ); } - public ReactiveQuerySqmImpl( + public ReactiveSqmQueryImpl( String hql, HqlInterpretation hqlInterpretation, Class resultType, @@ -104,7 +107,7 @@ public ReactiveQuerySqmImpl( this.selectionQueryDelegate = createSelectionQueryDelegate( session ); } - public ReactiveQuerySqmImpl( + public ReactiveSqmQueryImpl( SqmStatement criteria, Class resultType, SharedSessionContractImplementor session) { @@ -415,13 +418,13 @@ private ReactiveNonSelectQueryPlan buildUpdateQueryPlan() { final ReactiveSqmMultiTableMutationStrategy multiTableStrategy = (ReactiveSqmMultiTableMutationStrategy) entityDescriptor.getSqmMultiTableMutationStrategy(); return multiTableStrategy == null - ? new ReactiveSimpleUpdateQueryPlan( sqmUpdate, getDomainParameterXref() ) + ? new ReactiveSimpleNonSelectQueryPlan( sqmUpdate, getDomainParameterXref() ) : new ReactiveMultiTableUpdateQueryPlan( sqmUpdate, getDomainParameterXref(), multiTableStrategy ); } private ReactiveNonSelectQueryPlan buildInsertQueryPlan() { //noinspection rawtypes - final SqmInsertStatement sqmInsert = (SqmInsertStatement) getSqmStatement(); + final SqmInsertStatement sqmInsert = (SqmInsertStatement) getSqmStatement(); final String entityNameToInsert = sqmInsert.getTarget().getModel().getHibernateEntityName(); final EntityPersister persister = getSessionFactory() @@ -438,87 +441,101 @@ private ReactiveNonSelectQueryPlan buildInsertQueryPlan() { } } } - if ( !useMultiTableInsert ) { - return new ReactiveSimpleInsertQueryPlan( sqmInsert, getDomainParameterXref() ); - } - else { + if ( useMultiTableInsert ) { return new ReactiveMultiTableInsertQueryPlan( sqmInsert, getDomainParameterXref(), (ReactiveSqmMultiTableInsertStrategy) persister.getSqmMultiTableInsertStrategy() ); } + else if ( sqmInsert instanceof SqmInsertValuesStatement insertValues + && insertValues.getValuesList().size() != 1 + && !getSessionFactory().getJdbcServices().getDialect().supportsValuesListForInsert() ) { + // Split insert-values queries if the dialect doesn't support values lists + final List valuesList = insertValues.getValuesList(); + final ReactiveNonSelectQueryPlan[] planParts = new ReactiveNonSelectQueryPlan[valuesList.size()]; + for ( int i = 0; i < valuesList.size(); i++ ) { + final SqmInsertValuesStatement subInsert = + insertValues.copyWithoutValues( SqmCopyContext.simpleContext() ); + subInsert.values( valuesList.get( i ) ); + planParts[i] = new ReactiveSimpleNonSelectQueryPlan( subInsert, getDomainParameterXref() ); + } + + return new ReactiveAggregatedNonSelectQueryPlan( planParts ); + } + + return new ReactiveSimpleNonSelectQueryPlan( sqmInsert, getDomainParameterXref() ); } // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // QueryOptions @Override - public ReactiveQuerySqmImpl setHint(String hintName, Object value) { + public ReactiveSqmQueryImpl setHint(String hintName, Object value) { super.setHint( hintName, value ); return this; } @Override - public ReactiveQuerySqmImpl addQueryHint(String hint) { + public ReactiveSqmQueryImpl addQueryHint(String hint) { super.addQueryHint( hint ); return this; } @Override - public ReactiveQuerySqmImpl setLockOptions(LockOptions lockOptions) { + public ReactiveSqmQueryImpl setLockOptions(LockOptions lockOptions) { super.setLockOptions( lockOptions ); return this; } @Override - public ReactiveQuerySqmImpl setLockMode(String alias, LockMode lockMode) { + public ReactiveSqmQueryImpl setLockMode(String alias, LockMode lockMode) { super.setLockMode( alias, lockMode ); return this; } @Override - public ReactiveQuerySqmImpl setTupleTransformer(TupleTransformer transformer) { + public ReactiveSqmQueryImpl setTupleTransformer(TupleTransformer transformer) { throw new UnsupportedOperationException(); } @Override - public ReactiveQuerySqmImpl setResultListTransformer(ResultListTransformer transformer) { + public ReactiveSqmQueryImpl setResultListTransformer(ResultListTransformer transformer) { super.setResultListTransformer( transformer ); return this; } @Override @Deprecated - public ReactiveQuerySqmImpl setResultTransformer(ResultTransformer transformer) { + public ReactiveSqmQueryImpl setResultTransformer(ResultTransformer transformer) { throw new UnsupportedOperationException(); } @Override - public ReactiveQuerySqmImpl setMaxResults(int maxResult) { + public ReactiveSqmQueryImpl setMaxResults(int maxResult) { super.setMaxResults( maxResult ); return this; } @Override - public ReactiveQuerySqmImpl setFirstResult(int startPosition) { + public ReactiveSqmQueryImpl setFirstResult(int startPosition) { super.setFirstResult( startPosition ); return this; } @Override - public ReactiveQuerySqmImpl setHibernateFlushMode(FlushMode flushMode) { + public ReactiveSqmQueryImpl setHibernateFlushMode(FlushMode flushMode) { super.setHibernateFlushMode( flushMode ); return this; } @Override - public ReactiveQuerySqmImpl setFlushMode(FlushModeType flushMode) { + public ReactiveSqmQueryImpl setFlushMode(FlushModeType flushMode) { super.setFlushMode( flushMode ); return this; } @Override - public ReactiveQuerySqmImpl setLockMode(LockModeType lockMode) { + public ReactiveSqmQueryImpl setLockMode(LockModeType lockMode) { super.setLockMode( lockMode ); return this; } @@ -527,313 +544,313 @@ public ReactiveQuerySqmImpl setLockMode(LockModeType lockMode) { // covariance @Override - public ReactiveQuerySqmImpl applyGraph(RootGraph graph, GraphSemantic semantic) { + public ReactiveSqmQueryImpl applyGraph(RootGraph graph, GraphSemantic semantic) { super.applyGraph( graph, semantic ); return this; } @Override - public ReactiveQuerySqmImpl applyLoadGraph(RootGraph graph) { + public ReactiveSqmQueryImpl applyLoadGraph(RootGraph graph) { super.applyLoadGraph( graph ); return this; } @Override - public ReactiveQuerySqmImpl applyFetchGraph(RootGraph graph) { + public ReactiveSqmQueryImpl applyFetchGraph(RootGraph graph) { super.applyFetchGraph( graph ); return this; } @Override - public ReactiveQuerySqmImpl setComment(String comment) { + public ReactiveSqmQueryImpl setComment(String comment) { super.setComment( comment ); return this; } @Override - public ReactiveQuerySqmImpl setCacheMode(CacheMode cacheMode) { + public ReactiveSqmQueryImpl setCacheMode(CacheMode cacheMode) { super.setCacheMode( cacheMode ); return this; } @Override - public ReactiveQuerySqmImpl setCacheRetrieveMode(CacheRetrieveMode cacheRetrieveMode) { + public ReactiveSqmQueryImpl setCacheRetrieveMode(CacheRetrieveMode cacheRetrieveMode) { super.setCacheRetrieveMode( cacheRetrieveMode ); return this; } @Override - public ReactiveQuerySqmImpl setCacheStoreMode(CacheStoreMode cacheStoreMode) { + public ReactiveSqmQueryImpl setCacheStoreMode(CacheStoreMode cacheStoreMode) { super.setCacheStoreMode( cacheStoreMode ); return this; } @Override - public ReactiveQuerySqmImpl setCacheable(boolean cacheable) { + public ReactiveSqmQueryImpl setCacheable(boolean cacheable) { super.setCacheable( cacheable ); return this; } @Override - public ReactiveQuerySqmImpl setCacheRegion(String cacheRegion) { + public ReactiveSqmQueryImpl setCacheRegion(String cacheRegion) { super.setCacheRegion( cacheRegion ); return this; } @Override - public ReactiveQuerySqmImpl setHibernateLockMode(LockMode lockMode) { + public ReactiveSqmQueryImpl setHibernateLockMode(LockMode lockMode) { super.setHibernateLockMode( lockMode ); return this; } @Override - public ReactiveQuerySqmImpl setTimeout(int timeout) { + public ReactiveSqmQueryImpl setTimeout(int timeout) { super.setTimeout( timeout ); return this; } @Override - public ReactiveQuerySqmImpl setFetchSize(int fetchSize) { + public ReactiveSqmQueryImpl setFetchSize(int fetchSize) { super.setFetchSize( fetchSize ); return this; } @Override - public ReactiveQuerySqmImpl setReadOnly(boolean readOnly) { + public ReactiveSqmQueryImpl setReadOnly(boolean readOnly) { super.setReadOnly( readOnly ); return this; } @Override - public ReactiveQuerySqmImpl setProperties(Object bean) { + public ReactiveSqmQueryImpl setProperties(Object bean) { super.setProperties( bean ); return this; } @Override - public ReactiveQuerySqmImpl setProperties(Map bean) { + public ReactiveSqmQueryImpl setProperties(Map bean) { super.setProperties( bean ); return this; } @Override - public ReactiveQuerySqmImpl setParameter(String name, Object value) { + public ReactiveSqmQueryImpl setParameter(String name, Object value) { super.setParameter( name, value ); return this; } @Override - public

ReactiveQuerySqmImpl setParameter(String name, P value, Class

javaType) { + public

ReactiveSqmQueryImpl setParameter(String name, P value, Class

javaType) { super.setParameter( name, value, javaType ); return this; } @Override - public

ReactiveQuerySqmImpl setParameter(String name, P value, Type

type) { + public

ReactiveSqmQueryImpl setParameter(String name, P value, Type

type) { super.setParameter( name, value, type ); return this; } @Override - public ReactiveQuerySqmImpl setParameter(String name, Instant value, TemporalType temporalType) { + public ReactiveSqmQueryImpl setParameter(String name, Instant value, TemporalType temporalType) { super.setParameter( name, value, temporalType ); return this; } @Override - public ReactiveQuerySqmImpl setParameter(int position, Object value) { + public ReactiveSqmQueryImpl setParameter(int position, Object value) { super.setParameter( position, value ); return this; } @Override - public

ReactiveQuerySqmImpl setParameter(int position, P value, Class

javaType) { + public

ReactiveSqmQueryImpl setParameter(int position, P value, Class

javaType) { super.setParameter( position, value, javaType ); return this; } @Override - public

ReactiveQuerySqmImpl setParameter(int position, P value, Type

type) { + public

ReactiveSqmQueryImpl setParameter(int position, P value, Type

type) { super.setParameter( position, value, type ); return this; } @Override - public ReactiveQuerySqmImpl setParameter(int position, Instant value, TemporalType temporalType) { + public ReactiveSqmQueryImpl setParameter(int position, Instant value, TemporalType temporalType) { super.setParameter( position, value, temporalType ); return this; } @Override - public

ReactiveQuerySqmImpl setParameter(QueryParameter

parameter, P value) { + public

ReactiveSqmQueryImpl setParameter(QueryParameter

parameter, P value) { super.setParameter( parameter, value ); return this; } @Override - public

ReactiveQuerySqmImpl setParameter(QueryParameter

parameter, P value, Class

javaType) { + public

ReactiveSqmQueryImpl setParameter(QueryParameter

parameter, P value, Class

javaType) { super.setParameter( parameter, value, javaType ); return this; } @Override - public

ReactiveQuerySqmImpl setParameter(QueryParameter

parameter, P value, Type

type) { + public

ReactiveSqmQueryImpl setParameter(QueryParameter

parameter, P value, Type

type) { super.setParameter( parameter, value, type ); return this; } @Override - public

ReactiveQuerySqmImpl setParameter(Parameter

parameter, P value) { + public

ReactiveSqmQueryImpl setParameter(Parameter

parameter, P value) { super.setParameter( parameter, value ); return this; } @Override - public ReactiveQuerySqmImpl setParameter(Parameter param, Calendar value, TemporalType temporalType) { + public ReactiveSqmQueryImpl setParameter(Parameter param, Calendar value, TemporalType temporalType) { super.setParameter( param, value, temporalType ); return this; } @Override - public ReactiveQuerySqmImpl setParameter(Parameter param, Date value, TemporalType temporalType) { + public ReactiveSqmQueryImpl setParameter(Parameter param, Date value, TemporalType temporalType) { super.setParameter( param, value, temporalType ); return this; } @Override - public ReactiveQuerySqmImpl setParameter(String name, Calendar value, TemporalType temporalType) { + public ReactiveSqmQueryImpl setParameter(String name, Calendar value, TemporalType temporalType) { super.setParameter( name, value, temporalType ); return this; } @Override - public ReactiveQuerySqmImpl setParameter(String name, Date value, TemporalType temporalType) { + public ReactiveSqmQueryImpl setParameter(String name, Date value, TemporalType temporalType) { super.setParameter( name, value, temporalType ); return this; } @Override - public ReactiveQuerySqmImpl setParameter(int position, Calendar value, TemporalType temporalType) { + public ReactiveSqmQueryImpl setParameter(int position, Calendar value, TemporalType temporalType) { super.setParameter( position, value, temporalType ); return this; } @Override - public ReactiveQuerySqmImpl setParameter(int position, Date value, TemporalType temporalType) { + public ReactiveSqmQueryImpl setParameter(int position, Date value, TemporalType temporalType) { super.setParameter( position, value, temporalType ); return this; } @Override - public ReactiveQuerySqmImpl setParameterList(String name, Collection values) { + public ReactiveSqmQueryImpl setParameterList(String name, Collection values) { super.setParameterList( name, values ); return this; } @Override - public

ReactiveQuerySqmImpl setParameterList(String name, Collection values, Class

javaType) { + public

ReactiveSqmQueryImpl setParameterList(String name, Collection values, Class

javaType) { super.setParameterList( name, values, javaType ); return this; } @Override - public

ReactiveQuerySqmImpl setParameterList(String name, Collection values, Type

type) { + public

ReactiveSqmQueryImpl setParameterList(String name, Collection values, Type

type) { super.setParameterList( name, values, type ); return this; } @Override - public ReactiveQuerySqmImpl setParameterList(String name, Object[] values) { + public ReactiveSqmQueryImpl setParameterList(String name, Object[] values) { super.setParameterList( name, values ); return this; } @Override - public

ReactiveQuerySqmImpl setParameterList(String name, P[] values, Class

javaType) { + public

ReactiveSqmQueryImpl setParameterList(String name, P[] values, Class

javaType) { super.setParameterList( name, values, javaType ); return this; } @Override - public

ReactiveQuerySqmImpl setParameterList(String name, P[] values, Type

type) { + public

ReactiveSqmQueryImpl setParameterList(String name, P[] values, Type

type) { super.setParameterList( name, values, type ); return this; } @Override - public ReactiveQuerySqmImpl setParameterList(int position, Collection values) { + public ReactiveSqmQueryImpl setParameterList(int position, Collection values) { super.setParameterList( position, values ); return this; } @Override - public

ReactiveQuerySqmImpl setParameterList(int position, Collection values, Class

javaType) { + public

ReactiveSqmQueryImpl setParameterList(int position, Collection values, Class

javaType) { super.setParameterList( position, values, javaType ); return this; } @Override - public

ReactiveQuerySqmImpl setParameterList(int position, Collection values, Type

type) { + public

ReactiveSqmQueryImpl setParameterList(int position, Collection values, Type

type) { super.setParameterList( position, values, type ); return this; } @Override - public ReactiveQuerySqmImpl setParameterList(int position, Object[] values) { + public ReactiveSqmQueryImpl setParameterList(int position, Object[] values) { super.setParameterList( position, values ); return this; } @Override - public

ReactiveQuerySqmImpl setParameterList(int position, P[] values, Class

javaType) { + public

ReactiveSqmQueryImpl setParameterList(int position, P[] values, Class

javaType) { super.setParameterList( position, values, javaType ); return this; } @Override - public

ReactiveQuerySqmImpl setParameterList(int position, P[] values, Type

type) { + public

ReactiveSqmQueryImpl setParameterList(int position, P[] values, Type

type) { super.setParameterList( position, values, type ); return this; } @Override - public

ReactiveQuerySqmImpl setParameterList(QueryParameter

parameter, Collection values) { + public

ReactiveSqmQueryImpl setParameterList(QueryParameter

parameter, Collection values) { super.setParameterList( parameter, values ); return this; } @Override - public

ReactiveQuerySqmImpl setParameterList(QueryParameter

parameter, Collection values, Class

javaType) { + public

ReactiveSqmQueryImpl setParameterList(QueryParameter

parameter, Collection values, Class

javaType) { super.setParameterList( parameter, values, javaType ); return this; } @Override - public

ReactiveQuerySqmImpl setParameterList(QueryParameter

parameter, Collection values, Type

type) { + public

ReactiveSqmQueryImpl setParameterList(QueryParameter

parameter, Collection values, Type

type) { super.setParameterList( parameter, values, type ); return this; } @Override - public

ReactiveQuerySqmImpl setParameterList(QueryParameter

parameter, P[] values) { + public

ReactiveSqmQueryImpl setParameterList(QueryParameter

parameter, P[] values) { super.setParameterList( parameter, values ); return this; } @Override - public

ReactiveQuerySqmImpl setParameterList(QueryParameter

parameter, P[] values, Class

javaType) { + public

ReactiveSqmQueryImpl setParameterList(QueryParameter

parameter, P[] values, Class

javaType) { super.setParameterList( parameter, values, javaType ); return this; } @Override - public

ReactiveQuerySqmImpl setParameterList(QueryParameter

parameter, P[] values, Type

type) { + public

ReactiveSqmQueryImpl setParameterList(QueryParameter

parameter, P[] values, Type

type) { super.setParameterList( parameter, values, type ); return this; } @Override - public ReactiveQuerySqmImpl setFollowOnLocking(boolean enable) { + public ReactiveSqmQueryImpl setFollowOnLocking(boolean enable) { super.setFollowOnLocking( enable ); return this; } @@ -844,7 +861,7 @@ public void applyGraph(RootGraphImplementor graph, GraphSemantic semantic) { } @Override - public ReactiveQuerySqmImpl enableFetchProfile(String profileName) { + public ReactiveSqmQueryImpl enableFetchProfile(String profileName) { selectionQueryDelegate.enableFetchProfile( profileName ); return this; } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/ReactiveHandler.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/ReactiveHandler.java index d23ae3225..cab920f73 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/ReactiveHandler.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/ReactiveHandler.java @@ -5,14 +5,30 @@ */ package org.hibernate.reactive.query.sqm.mutation.internal; +import java.lang.invoke.MethodHandles; import java.util.concurrent.CompletionStage; import org.hibernate.query.spi.DomainQueryExecutionContext; +import org.hibernate.query.sqm.mutation.internal.Handler; +import org.hibernate.reactive.logging.impl.Log; +import org.hibernate.reactive.logging.impl.LoggerFactory; +import org.hibernate.sql.exec.spi.JdbcParameterBindings; /** * @see org.hibernate.query.sqm.mutation.internal.Handler */ -public interface ReactiveHandler { +public interface ReactiveHandler extends Handler { + Log LOG = LoggerFactory.make( Log.class, MethodHandles.lookup() ); + + @Override + default int execute(DomainQueryExecutionContext executionContext) { + throw LOG.nonReactiveMethodCall( "reactiveExecute" ); + } + + @Override + default int execute(JdbcParameterBindings jdbcParameterBindings, DomainQueryExecutionContext executionContext) { + throw LOG.nonReactiveMethodCall( "reactiveExecute" ); + } /** * Execute the multi-table update or delete indicated by the SQM AST @@ -22,5 +38,17 @@ public interface ReactiveHandler { * * @return The "number of rows affected" count */ - CompletionStage reactiveExecute(DomainQueryExecutionContext executionContext); + default CompletionStage reactiveExecute(DomainQueryExecutionContext executionContext){ + return reactiveExecute( createJdbcParameterBindings( executionContext ), executionContext ); + } + + /** + * Execute the multi-table update or delete indicated by the SQM AST + * passed in when this Handler was created. + * + * @param jdbcParameterBindings The parameter bindings for JDBC parameters + * @param executionContext Contextual information needed for execution + * @return The "number of rows affected" count + */ + CompletionStage reactiveExecute(JdbcParameterBindings jdbcParameterBindings, DomainQueryExecutionContext executionContext); } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/ReactiveSqmMutationStrategyHelper.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/ReactiveSqmMutationStrategyHelper.java deleted file mode 100644 index b2e36a296..000000000 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/ReactiveSqmMutationStrategyHelper.java +++ /dev/null @@ -1,161 +0,0 @@ -/* Hibernate, Relational Persistence for Idiomatic Java - * - * SPDX-License-Identifier: Apache-2.0 - * Copyright: Red Hat Inc. and Hibernate Authors - */ -package org.hibernate.reactive.query.sqm.mutation.internal; - -import java.sql.PreparedStatement; -import java.util.concurrent.CompletionStage; -import java.util.function.BiFunction; - -import org.hibernate.engine.jdbc.spi.JdbcServices; -import org.hibernate.engine.spi.SessionFactoryImplementor; -import org.hibernate.metamodel.mapping.EntityMappingType; -import org.hibernate.metamodel.mapping.PluralAttributeMapping; -import org.hibernate.metamodel.mapping.internal.EmbeddedAttributeMapping; -import org.hibernate.reactive.sql.exec.internal.StandardReactiveJdbcMutationExecutor; -import org.hibernate.reactive.util.impl.CompletionStages; -import org.hibernate.reactive.util.impl.CompletionStages.Completable; -import org.hibernate.sql.ast.tree.delete.DeleteStatement; -import org.hibernate.sql.ast.tree.from.NamedTableReference; -import org.hibernate.sql.ast.tree.from.TableReference; -import org.hibernate.sql.ast.tree.predicate.Predicate; -import org.hibernate.sql.exec.spi.ExecutionContext; -import org.hibernate.sql.exec.spi.JdbcOperationQueryMutation; -import org.hibernate.sql.exec.spi.JdbcParameterBindings; - -import static org.hibernate.reactive.util.impl.CompletionStages.failedFuture; -import static org.hibernate.reactive.util.impl.CompletionStages.voidFuture; - -/** - * @see org.hibernate.query.sqm.mutation.internal.SqmMutationStrategyHelper - */ -public class ReactiveSqmMutationStrategyHelper { - - private ReactiveSqmMutationStrategyHelper() { - } - - public static CompletionStage cleanUpCollectionTables( - EntityMappingType entityDescriptor, - BiFunction restrictionProducer, - JdbcParameterBindings jdbcParameterBindings, - ExecutionContext executionContext) { - if ( !entityDescriptor.getEntityPersister().hasCollections() ) { - // none to clean-up - return voidFuture(); - } - - try { - final Completable stage = new Completable<>(); - entityDescriptor - .visitSubTypeAttributeMappings( attributeMapping -> { - if ( attributeMapping instanceof PluralAttributeMapping ) { - cleanUpCollectionTable( - (PluralAttributeMapping) attributeMapping, - restrictionProducer, - jdbcParameterBindings, - executionContext - ).handle( stage::complete ); - } - else if ( attributeMapping instanceof EmbeddedAttributeMapping ) { - cleanUpCollectionTables( - (EmbeddedAttributeMapping) attributeMapping, - restrictionProducer, - jdbcParameterBindings, - executionContext - ).handle( stage::complete ); - } - else { - stage.complete( null, null ); - } - } - ); - return stage.getStage(); - } - catch (Throwable throwable) { - return failedFuture( throwable ); - } - } - - private static CompletionStage cleanUpCollectionTables( - EmbeddedAttributeMapping attributeMapping, - BiFunction restrictionProducer, - JdbcParameterBindings jdbcParameterBindings, - ExecutionContext executionContext) { - try { - final Completable completable = new Completable<>(); - attributeMapping.visitSubParts( - modelPart -> { - if ( modelPart instanceof PluralAttributeMapping ) { - cleanUpCollectionTable( - (PluralAttributeMapping) modelPart, - restrictionProducer, - jdbcParameterBindings, - executionContext - ).handle( completable::complete ); - } - else if ( modelPart instanceof EmbeddedAttributeMapping ) { - cleanUpCollectionTables( - (EmbeddedAttributeMapping) modelPart, - restrictionProducer, - jdbcParameterBindings, - executionContext - ).handle( completable::complete ); - } - }, - null - ); - return completable.getStage(); - } - catch (Throwable throwable) { - return failedFuture( throwable ); - } - } - - private static CompletionStage cleanUpCollectionTable( - PluralAttributeMapping attributeMapping, - BiFunction restrictionProducer, - JdbcParameterBindings jdbcParameterBindings, - ExecutionContext executionContext) { - final String separateCollectionTable = attributeMapping.getSeparateCollectionTable(); - if ( separateCollectionTable == null ) { - // one-to-many - update the matching rows in the associated table setting the fk column(s) to null - // not yet implemented - do nothing - return voidFuture(); - } - - final SessionFactoryImplementor sessionFactory = executionContext.getSession().getFactory(); - final JdbcServices jdbcServices = sessionFactory.getJdbcServices(); - - // element-collection or many-to-many - delete the collection-table row - - final NamedTableReference tableReference = new NamedTableReference( - separateCollectionTable, - DeleteStatement.DEFAULT_ALIAS, - true - ); - - final DeleteStatement sqlAstDelete = new DeleteStatement( - tableReference, - restrictionProducer.apply( tableReference, attributeMapping ) - ); - - JdbcOperationQueryMutation jdbcDelete = jdbcServices.getJdbcEnvironment() - .getSqlAstTranslatorFactory() - .buildMutationTranslator( sessionFactory, sqlAstDelete ) - .translate( jdbcParameterBindings, executionContext.getQueryOptions() ); - return StandardReactiveJdbcMutationExecutor.INSTANCE - .executeReactive( - jdbcDelete, - jdbcParameterBindings, - executionContext.getSession().getJdbcCoordinator().getStatementPreparer()::prepareStatement, - ReactiveSqmMutationStrategyHelper::doNothing, - executionContext - ) - .thenCompose( CompletionStages::voidFuture ); - } - - private static void doNothing(Integer i, PreparedStatement ps) { - } -} diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/cte/ReactiveAbstractCteMutationHandler.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/cte/ReactiveAbstractCteMutationHandler.java index 806b75718..cba5e09aa 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/cte/ReactiveAbstractCteMutationHandler.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/cte/ReactiveAbstractCteMutationHandler.java @@ -5,189 +5,49 @@ */ package org.hibernate.reactive.query.sqm.mutation.internal.cte; -import java.util.ArrayList; -import java.util.Collections; -import java.util.IdentityHashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.CompletionStage; - import org.hibernate.LockMode; import org.hibernate.LockOptions; -import org.hibernate.engine.jdbc.spi.JdbcServices; -import org.hibernate.engine.spi.SessionFactoryImplementor; -import org.hibernate.metamodel.mapping.EntityMappingType; -import org.hibernate.metamodel.mapping.MappingModelExpressible; -import org.hibernate.metamodel.mapping.SqlExpressible; import org.hibernate.query.spi.DomainQueryExecutionContext; -import org.hibernate.query.sqm.internal.DomainParameterXref; import org.hibernate.query.sqm.internal.SqmJdbcExecutionContextAdapter; -import org.hibernate.query.sqm.internal.SqmUtil; -import org.hibernate.query.sqm.mutation.internal.MatchingIdSelectionHelper; -import org.hibernate.query.sqm.mutation.internal.MultiTableSqmMutationConverter; -import org.hibernate.query.sqm.mutation.internal.cte.AbstractCteMutationHandler; -import org.hibernate.query.sqm.mutation.internal.cte.CteMutationStrategy; -import org.hibernate.query.sqm.spi.SqmParameterMappingModelResolutionAccess; -import org.hibernate.query.sqm.tree.SqmDeleteOrUpdateStatement; -import org.hibernate.query.sqm.tree.expression.SqmParameter; import org.hibernate.reactive.engine.spi.ReactiveSharedSessionContractImplementor; import org.hibernate.reactive.query.sqm.mutation.spi.ReactiveAbstractMutationHandler; import org.hibernate.reactive.sql.exec.internal.StandardReactiveSelectExecutor; import org.hibernate.reactive.sql.results.spi.ReactiveListResultsConsumer; -import org.hibernate.sql.ast.SqlAstTranslator; -import org.hibernate.sql.ast.tree.cte.CteContainer; -import org.hibernate.sql.ast.tree.cte.CteMaterialization; -import org.hibernate.sql.ast.tree.cte.CteStatement; -import org.hibernate.sql.ast.tree.cte.CteTable; -import org.hibernate.sql.ast.tree.cte.CteTableGroup; -import org.hibernate.sql.ast.tree.expression.Expression; -import org.hibernate.sql.ast.tree.expression.JdbcParameter; -import org.hibernate.sql.ast.tree.from.NamedTableReference; -import org.hibernate.sql.ast.tree.predicate.Predicate; -import org.hibernate.sql.ast.tree.select.QuerySpec; -import org.hibernate.sql.ast.tree.select.SelectStatement; -import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; +import org.hibernate.sql.exec.internal.JdbcOperationQuerySelect; import org.hibernate.sql.exec.spi.JdbcParameterBindings; -import org.hibernate.sql.results.graph.DomainResult; -import org.hibernate.sql.results.graph.basic.BasicResult; -import org.hibernate.sql.results.internal.SqlSelectionImpl; + +import java.util.concurrent.CompletionStage; /** * @see org.hibernate.query.sqm.mutation.internal.cte.AbstractCteMutationHandler */ public interface ReactiveAbstractCteMutationHandler extends ReactiveAbstractMutationHandler { - CteTable getCteTable(); - - DomainParameterXref getDomainParameterXref(); - - CteMutationStrategy getStrategy(); - - void addDmlCtes( - CteContainer statement, - CteStatement idSelectCte, - MultiTableSqmMutationConverter sqmConverter, - Map, List> parameterResolutions, - SessionFactoryImplementor factory); - /** - * @see org.hibernate.query.sqm.mutation.internal.cte.AbstractCteMutationHandler#execute(DomainQueryExecutionContext) + * @see org.hibernate.query.sqm.mutation.internal.cte.AbstractCteMutationHandler#execute(JdbcParameterBindings, DomainQueryExecutionContext) */ @Override - default CompletionStage reactiveExecute(DomainQueryExecutionContext executionContext) { - final SqmDeleteOrUpdateStatement sqmMutationStatement = getSqmDeleteOrUpdateStatement(); - final SessionFactoryImplementor factory = executionContext.getSession().getFactory(); - final EntityMappingType entityDescriptor = getEntityDescriptor(); - final String explicitDmlTargetAlias; - // We need an alias because we try to acquire a WRITE lock for these rows in the CTE - if ( sqmMutationStatement.getTarget().getExplicitAlias() == null ) { - explicitDmlTargetAlias = "dml_target"; - } - else { - explicitDmlTargetAlias = sqmMutationStatement.getTarget().getExplicitAlias(); - } - - final MultiTableSqmMutationConverter sqmConverter = new MultiTableSqmMutationConverter( - entityDescriptor, - sqmMutationStatement, - sqmMutationStatement.getTarget(), - explicitDmlTargetAlias, - getDomainParameterXref(), - executionContext.getQueryOptions(), - executionContext.getSession().getLoadQueryInfluencers(), - executionContext.getQueryParameterBindings(), - factory.getSqlTranslationEngine() - ); - final Map, List> parameterResolutions; - if ( getDomainParameterXref().getSqmParameterCount() == 0 ) { - parameterResolutions = Collections.emptyMap(); - } - else { - parameterResolutions = new IdentityHashMap<>(); - } - - final Predicate restriction = sqmConverter.visitWhereClause( sqmMutationStatement.getWhereClause() ); - sqmConverter.pruneTableGroupJoins(); - - final CteStatement idSelectCte = new CteStatement( - getCteTable(), - MatchingIdSelectionHelper.generateMatchingIdSelectStatement( - entityDescriptor, - sqmMutationStatement, - true, - restriction, - sqmConverter, - executionContext - ), - // The id-select cte will be reused multiple times - CteMaterialization.MATERIALIZED - ); - - // Create the main query spec that will return the count of - final QuerySpec querySpec = new QuerySpec( true, 1 ); - final List> domainResults = new ArrayList<>( 1 ); - final SelectStatement statement = new SelectStatement( querySpec, domainResults ); - final JdbcServices jdbcServices = factory.getJdbcServices(); - final SqlAstTranslator translator = jdbcServices.getJdbcEnvironment() - .getSqlAstTranslatorFactory() - .buildSelectTranslator( factory, statement ); - - final Expression count = createCountStar( factory, sqmConverter ); - domainResults.add( - new BasicResult<>( - 0, - null, - ( (SqlExpressible) count ).getJdbcMapping() - ) - ); - querySpec.getSelectClause().addSqlSelection( new SqlSelectionImpl( 0, count ) ); - querySpec.getFromClause().addRoot( - new CteTableGroup( - new NamedTableReference( - idSelectCte.getCteTable().getTableExpression(), - AbstractCteMutationHandler.CTE_TABLE_IDENTIFIER - ) - ) - ); - - // Add all CTEs - statement.addCteStatement( idSelectCte ); - addDmlCtes( statement, idSelectCte, sqmConverter, parameterResolutions, factory ); - - final JdbcParameterBindings jdbcParameterBindings = SqmUtil.createJdbcParameterBindings( - executionContext.getQueryParameterBindings(), - getDomainParameterXref(), - SqmUtil.generateJdbcParamsXref( getDomainParameterXref(), sqmConverter ), - new SqmParameterMappingModelResolutionAccess() { - @Override @SuppressWarnings("unchecked") - public MappingModelExpressible getResolvedMappingModelType(SqmParameter parameter) { - return (MappingModelExpressible) sqmConverter.getSqmParameterMappingModelExpressibleResolutions().get( parameter ); - } - }, - executionContext.getSession() - ); + default CompletionStage reactiveExecute( + JdbcParameterBindings jdbcParameterBindings, + DomainQueryExecutionContext executionContext){ final LockOptions lockOptions = executionContext.getQueryOptions().getLockOptions(); - final LockMode lockMode = lockOptions.getAliasSpecificLockMode( explicitDmlTargetAlias ); // Acquire a WRITE lock for the rows that are about to be modified - lockOptions.setAliasSpecificLockMode( explicitDmlTargetAlias, LockMode.WRITE ); - final JdbcOperationQuerySelect select = translator.translate( - jdbcParameterBindings, - executionContext.getQueryOptions() - ); - lockOptions.setAliasSpecificLockMode( explicitDmlTargetAlias, lockMode ); - + lockOptions.setLockMode( LockMode.WRITE ); return ( (ReactiveSharedSessionContractImplementor) executionContext.getSession() ) - .reactiveAutoFlushIfRequired( select.getAffectedTableNames() ) - .thenCompose( v -> StandardReactiveSelectExecutor.INSTANCE.list( - select, - jdbcParameterBindings, - SqmJdbcExecutionContextAdapter.omittingLockingAndPaging( executionContext ), - row -> row[0], - ReactiveListResultsConsumer.UniqueSemantic.NONE - ) - .thenApply( list -> ( (Number) list.get( 0 ) ).intValue() ) + .reactiveAutoFlushIfRequired( getSelect().getAffectedTableNames() ) + .thenCompose( v -> StandardReactiveSelectExecutor.INSTANCE + .list( + getSelect(), + jdbcParameterBindings, + SqmJdbcExecutionContextAdapter.omittingLockingAndPaging( executionContext ), + row -> row[0], + null, + ReactiveListResultsConsumer.UniqueSemantic.NONE, + 1 + ) + .thenApply( list -> ( (Number) list.get( 0 ) ).intValue() ) ); } - Expression createCountStar(SessionFactoryImplementor factory, MultiTableSqmMutationConverter sqmConverter); + JdbcOperationQuerySelect getSelect(); } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/cte/ReactiveCteDeleteHandler.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/cte/ReactiveCteDeleteHandler.java index a1f555253..5aaf573a2 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/cte/ReactiveCteDeleteHandler.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/cte/ReactiveCteDeleteHandler.java @@ -5,60 +5,37 @@ */ package org.hibernate.reactive.query.sqm.mutation.internal.cte; -import java.lang.invoke.MethodHandles; -import java.util.List; -import java.util.Map; import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.internal.util.MutableObject; import org.hibernate.query.spi.DomainQueryExecutionContext; import org.hibernate.query.sqm.internal.DomainParameterXref; -import org.hibernate.query.sqm.mutation.internal.MultiTableSqmMutationConverter; import org.hibernate.query.sqm.mutation.internal.cte.CteDeleteHandler; import org.hibernate.query.sqm.mutation.internal.cte.CteMutationStrategy; import org.hibernate.query.sqm.tree.delete.SqmDeleteStatement; -import org.hibernate.query.sqm.tree.expression.SqmParameter; -import org.hibernate.reactive.logging.impl.Log; -import org.hibernate.reactive.logging.impl.LoggerFactory; -import org.hibernate.sql.ast.tree.cte.CteContainer; -import org.hibernate.sql.ast.tree.cte.CteStatement; import org.hibernate.sql.ast.tree.cte.CteTable; -import org.hibernate.sql.ast.tree.expression.Expression; -import org.hibernate.sql.ast.tree.expression.JdbcParameter; +import org.hibernate.sql.exec.internal.JdbcOperationQuerySelect; +import org.hibernate.sql.exec.spi.JdbcParameterBindings; /** * @see org.hibernate.query.sqm.mutation.internal.cte.CteDeleteHandler */ public class ReactiveCteDeleteHandler extends CteDeleteHandler implements ReactiveAbstractCteMutationHandler { - private static final Log LOG = LoggerFactory.make( Log.class, MethodHandles.lookup() ); - protected ReactiveCteDeleteHandler( CteTable cteTable, SqmDeleteStatement sqmDeleteStatement, DomainParameterXref domainParameterXref, CteMutationStrategy strategy, - SessionFactoryImplementor sessionFactory) { - super( cteTable, sqmDeleteStatement, domainParameterXref, strategy, sessionFactory ); - } - - @Override - public void addDmlCtes( - CteContainer statement, - CteStatement idSelectCte, - MultiTableSqmMutationConverter sqmConverter, - Map, List> parameterResolutions, - SessionFactoryImplementor factory) { - super.addDmlCtes( statement, idSelectCte, sqmConverter, parameterResolutions, factory ); - } - - @Override - public int execute(DomainQueryExecutionContext executionContext) { - throw LOG.nonReactiveMethodCall( "reactiveExecute" ); + SessionFactoryImplementor sessionFactory, + DomainQueryExecutionContext context, + MutableObject firstJdbcParameterBindingsConsumer) { + super( cteTable, sqmDeleteStatement, domainParameterXref, strategy, sessionFactory, context, firstJdbcParameterBindingsConsumer ); } @Override - public Expression createCountStar(SessionFactoryImplementor factory, MultiTableSqmMutationConverter sqmConverter) { - return super.createCountStar( factory, sqmConverter ); + public JdbcOperationQuerySelect getSelect() { + return super.getSelect(); } } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/cte/ReactiveCteInsertHandler.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/cte/ReactiveCteInsertHandler.java index 603563e0f..9361df67f 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/cte/ReactiveCteInsertHandler.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/cte/ReactiveCteInsertHandler.java @@ -5,93 +5,35 @@ */ package org.hibernate.reactive.query.sqm.mutation.internal.cte; -import java.lang.invoke.MethodHandles; -import java.util.AbstractMap; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.concurrent.CompletionStage; - -import org.hibernate.dialect.temptable.TemporaryTable; -import org.hibernate.engine.jdbc.spi.JdbcServices; -import org.hibernate.engine.spi.SessionFactoryImplementor; -import org.hibernate.id.BulkInsertionCapableIdentifierGenerator; -import org.hibernate.id.OptimizableGenerator; -import org.hibernate.id.enhanced.Optimizer; -import org.hibernate.internal.util.collections.Stack; -import org.hibernate.metamodel.mapping.BasicValuedMapping; -import org.hibernate.metamodel.mapping.MappingModelExpressible; -import org.hibernate.metamodel.mapping.SqlExpressible; -import org.hibernate.persister.entity.EntityPersister; -import org.hibernate.query.results.internal.TableGroupImpl; +import org.hibernate.internal.util.MutableObject; import org.hibernate.query.spi.DomainQueryExecutionContext; -import org.hibernate.query.sqm.BinaryArithmeticOperator; -import org.hibernate.query.sqm.ComparisonOperator; import org.hibernate.query.sqm.internal.DomainParameterXref; import org.hibernate.query.sqm.internal.SqmJdbcExecutionContextAdapter; -import org.hibernate.query.sqm.internal.SqmUtil; -import org.hibernate.query.sqm.mutation.internal.MultiTableSqmMutationConverter; -import org.hibernate.query.sqm.mutation.internal.SqmInsertStrategyHelper; import org.hibernate.query.sqm.mutation.internal.cte.CteInsertHandler; -import org.hibernate.query.sqm.spi.SqmParameterMappingModelResolutionAccess; -import org.hibernate.query.sqm.sql.BaseSqmToSqlAstConverter; -import org.hibernate.query.sqm.sql.internal.SqmPathInterpretation; -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; -import org.hibernate.query.sqm.tree.insert.SqmValues; import org.hibernate.reactive.engine.spi.ReactiveSharedSessionContractImplementor; import org.hibernate.reactive.logging.impl.Log; import org.hibernate.reactive.logging.impl.LoggerFactory; import org.hibernate.reactive.query.sqm.mutation.internal.ReactiveHandler; import org.hibernate.reactive.sql.exec.internal.StandardReactiveSelectExecutor; import org.hibernate.reactive.sql.results.spi.ReactiveListResultsConsumer; -import org.hibernate.spi.NavigablePath; -import org.hibernate.sql.ast.SqlAstJoinType; -import org.hibernate.sql.ast.SqlAstTranslator; -import org.hibernate.sql.ast.spi.SqlAstProcessingState; -import org.hibernate.sql.ast.tree.Statement; -import org.hibernate.sql.ast.tree.cte.CteColumn; -import org.hibernate.sql.ast.tree.cte.CteMaterialization; -import org.hibernate.sql.ast.tree.cte.CteStatement; import org.hibernate.sql.ast.tree.cte.CteTable; -import org.hibernate.sql.ast.tree.cte.CteTableGroup; -import org.hibernate.sql.ast.tree.expression.BinaryArithmeticExpression; -import org.hibernate.sql.ast.tree.expression.ColumnReference; -import org.hibernate.sql.ast.tree.expression.Expression; -import org.hibernate.sql.ast.tree.expression.QueryLiteral; -import org.hibernate.sql.ast.tree.expression.SelfRenderingSqlFragmentExpression; -import org.hibernate.sql.ast.tree.from.NamedTableReference; -import org.hibernate.sql.ast.tree.from.TableGroup; -import org.hibernate.sql.ast.tree.from.TableGroupJoin; -import org.hibernate.sql.ast.tree.from.ValuesTableGroup; -import org.hibernate.sql.ast.tree.insert.InsertSelectStatement; -import org.hibernate.sql.ast.tree.insert.Values; -import org.hibernate.sql.ast.tree.predicate.ComparisonPredicate; -import org.hibernate.sql.ast.tree.select.QueryPart; -import org.hibernate.sql.ast.tree.select.QuerySpec; -import org.hibernate.sql.ast.tree.select.SelectStatement; -import org.hibernate.sql.ast.tree.update.Assignment; -import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; import org.hibernate.sql.exec.spi.JdbcParameterBindings; -import org.hibernate.sql.results.graph.DomainResult; -import org.hibernate.sql.results.graph.basic.BasicResult; -import org.hibernate.sql.results.internal.SqlSelectionImpl; + +import java.lang.invoke.MethodHandles; +import java.util.concurrent.CompletionStage; public class ReactiveCteInsertHandler extends CteInsertHandler implements ReactiveHandler { private static final Log LOG = LoggerFactory.make( Log.class, MethodHandles.lookup() ); - private final SessionFactoryImplementor sessionFactory; - public ReactiveCteInsertHandler( CteTable cteTable, SqmInsertStatement sqmStatement, DomainParameterXref domainParameterXref, - SessionFactoryImplementor sessionFactory) { - super( cteTable, sqmStatement, domainParameterXref, sessionFactory ); - this.sessionFactory = sessionFactory; + DomainQueryExecutionContext context, + MutableObject firstJdbcParameterBindingsConsumer) { + super( cteTable, sqmStatement, domainParameterXref, context, firstJdbcParameterBindingsConsumer ); } @Override @@ -99,470 +41,23 @@ public int execute(DomainQueryExecutionContext executionContext) { throw LOG.nonReactiveMethodCall( "reactiveExecute" ); } - // Pretty much a copy and paste of the method in the super class - // We should refactor this @Override - public CompletionStage reactiveExecute(DomainQueryExecutionContext executionContext) { - final SqmInsertStatement sqmInsertStatement = getSqmStatement(); - final SessionFactoryImplementor factory = executionContext.getSession().getFactory(); - final EntityPersister entityDescriptor = getEntityDescriptor().getEntityPersister(); - final String explicitDmlTargetAlias; - if ( sqmInsertStatement.getTarget().getExplicitAlias() == null ) { - explicitDmlTargetAlias = "dml_target"; - } - else { - explicitDmlTargetAlias = sqmInsertStatement.getTarget().getExplicitAlias(); - } - - final MultiTableSqmMutationConverter sqmConverter = new MultiTableSqmMutationConverter( - entityDescriptor, - sqmInsertStatement, - sqmInsertStatement.getTarget(), - explicitDmlTargetAlias, - getDomainParameterXref(), - executionContext.getQueryOptions(), - executionContext.getSession().getLoadQueryInfluencers(), - executionContext.getQueryParameterBindings(), - factory.getSqlTranslationEngine() - ); - final TableGroup insertingTableGroup = sqmConverter.getMutatingTableGroup(); - - // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // visit the insertion target using our special converter, collecting - // information about the target paths - - final int size = getSqmStatement().getInsertionTargetPaths().size(); - final List, Assignment>> targetPathColumns = new ArrayList<>( size ); - final List targetPathCteColumns = new ArrayList<>( size ); - final NamedTableReference entityTableReference = new NamedTableReference( - getCteTable().getTableExpression(), - TemporaryTable.DEFAULT_ALIAS, - true - ); - final InsertSelectStatement insertStatement = new InsertSelectStatement( entityTableReference ); - - final BaseSqmToSqlAstConverter.AdditionalInsertValues additionalInsertValues = sqmConverter.visitInsertionTargetPaths( - (assignable, columnReferences) -> { - final SqmPathInterpretation pathInterpretation = (SqmPathInterpretation) assignable; - final int offset = CteTable.determineModelPartStartIndex( - entityDescriptor, - pathInterpretation.getExpressionType() - ); - if ( offset == -1 ) { - throw new IllegalStateException( "Couldn't find matching cte column for: " + ( (Expression) assignable ).getExpressionType() ); - } - final int end = offset + pathInterpretation.getExpressionType().getJdbcTypeCount(); - // Find a matching cte table column and set that at the current index - final List columns = getCteTable().getCteColumns().subList( offset, end ); - insertStatement.addTargetColumnReferences( columnReferences ); - targetPathCteColumns.addAll( columns ); - targetPathColumns.add( - new AbstractMap.SimpleEntry<>( - columns, - new Assignment( - assignable, - (Expression) assignable - ) - ) - ); - }, - sqmInsertStatement, - entityDescriptor, - insertingTableGroup - ); - - final boolean assignsId = targetPathCteColumns.contains( getCteTable().getCteColumns().get( 0 ) ); - - // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - // Create the statement that represent the source for the entity cte - - final Stack processingStateStack = sqmConverter.getProcessingStateStack(); - final SqlAstProcessingState oldState = processingStateStack.pop(); - final Statement queryStatement; - if ( sqmInsertStatement instanceof SqmInsertSelectStatement ) { - final QueryPart queryPart = sqmConverter.visitQueryPart( ( (SqmInsertSelectStatement) sqmInsertStatement ).getSelectQueryPart() ); - queryPart.visitQuerySpecs( - querySpec -> { - // This returns true if the insertion target uses a sequence with an optimizer - // in which case we will fill the row_number column instead of the id column - if ( additionalInsertValues.applySelections( querySpec, sessionFactory ) ) { - final CteColumn rowNumberColumn = getCteTable().getCteColumns() - .get( getCteTable().getCteColumns().size() - 1 ); - final ColumnReference columnReference = new ColumnReference( - (String) null, - rowNumberColumn.getColumnExpression(), - false, - null, - rowNumberColumn.getJdbcMapping() - ); - insertStatement.getTargetColumns().set( - insertStatement.getTargetColumns().size() - 1, - columnReference - ); - targetPathCteColumns.set( - targetPathCteColumns.size() - 1, - rowNumberColumn - ); - } - if ( !assignsId && entityDescriptor.getGenerator().generatedOnExecution() ) { - querySpec.getSelectClause().addSqlSelection( - new SqlSelectionImpl( - 0, - SqmInsertStrategyHelper.createRowNumberingExpression( - querySpec, - sessionFactory - ) - ) - ); - } - } - ); - queryStatement = new SelectStatement( queryPart ); - } - else { - final List sqmValuesList = ( (SqmInsertValuesStatement) sqmInsertStatement ).getValuesList(); - final List valuesList = new ArrayList<>( sqmValuesList.size() ); - for ( SqmValues sqmValues : sqmValuesList ) { - final Values values = sqmConverter.visitValues( sqmValues ); - additionalInsertValues.applyValues( values ); - valuesList.add( values ); - } - final QuerySpec querySpec = new QuerySpec( true ); - final NavigablePath navigablePath = new NavigablePath( entityDescriptor.getRootPathName() ); - final List columnNames = new ArrayList<>( targetPathColumns.size() ); - final String valuesAlias = insertingTableGroup.getPrimaryTableReference().getIdentificationVariable(); - for ( Map.Entry, Assignment> entry : targetPathColumns ) { - for ( ColumnReference columnReference : entry.getValue().getAssignable().getColumnReferences() ) { - columnNames.add( columnReference.getColumnExpression() ); - querySpec.getSelectClause().addSqlSelection( - new SqlSelectionImpl( - 0, - columnReference.getQualifier().equals( valuesAlias ) - ? columnReference - : new ColumnReference( - valuesAlias, - columnReference.getColumnExpression(), - false, - null, - columnReference.getJdbcMapping() - ) - ) - ); - } - } - final ValuesTableGroup valuesTableGroup = new ValuesTableGroup( - navigablePath, - entityDescriptor.getEntityPersister(), - valuesList, - insertingTableGroup.getPrimaryTableReference().getIdentificationVariable(), - columnNames, - true, - factory - ); - querySpec.getFromClause().addRoot( valuesTableGroup ); - queryStatement = new SelectStatement( querySpec ); - } - processingStateStack.push( oldState ); - sqmConverter.pruneTableGroupJoins(); - - if ( !assignsId && entityDescriptor.getGenerator().generatedOnExecution() ) { - // Add the row number to the assignments - final CteColumn rowNumberColumn = getCteTable().getCteColumns() - .get( getCteTable().getCteColumns().size() - 1 ); - final ColumnReference columnReference = new ColumnReference( - (String) null, - rowNumberColumn.getColumnExpression(), - false, - null, - rowNumberColumn.getJdbcMapping() - ); - insertStatement.getTargetColumns().add( columnReference ); - targetPathCteColumns.add( rowNumberColumn ); - } - - final CteTable entityCteTable = createCteTable( getCteTable(), targetPathCteColumns ); - - // Create the main query spec that will return the count of rows - final QuerySpec querySpec = new QuerySpec( true, 1 ); - final List> domainResults = new ArrayList<>( 1 ); - final SelectStatement statement = new SelectStatement( querySpec, domainResults ); - - final CteStatement entityCte; - if ( additionalInsertValues.requiresRowNumberIntermediate() ) { - final CteTable fullEntityCteTable = getCteTable(); - final String baseTableName = "base_" + entityCteTable.getTableExpression(); - final CteStatement baseEntityCte = new CteStatement( - entityCteTable.withName( baseTableName ), - queryStatement, - // The query cte will be reused multiple times - CteMaterialization.MATERIALIZED - ); - statement.addCteStatement( baseEntityCte ); - - final CteColumn rowNumberColumn = fullEntityCteTable.getCteColumns().get( - fullEntityCteTable.getCteColumns().size() - 1 - ); - final ColumnReference rowNumberColumnReference = new ColumnReference( - "e", - rowNumberColumn.getColumnExpression(), - false, - null, - rowNumberColumn.getJdbcMapping() - ); - final CteColumn idColumn = fullEntityCteTable.getCteColumns().get( 0 ); - final BasicValuedMapping idType = (BasicValuedMapping) idColumn.getJdbcMapping(); - final Optimizer optimizer = ( (OptimizableGenerator) entityDescriptor.getGenerator() ).getOptimizer(); - final BasicValuedMapping integerType = (BasicValuedMapping) rowNumberColumn.getJdbcMapping(); - final Expression rowNumberMinusOneModuloIncrement = new BinaryArithmeticExpression( - new BinaryArithmeticExpression( - rowNumberColumnReference, - BinaryArithmeticOperator.SUBTRACT, - new QueryLiteral<>( - 1, - (BasicValuedMapping) rowNumberColumn.getJdbcMapping() - ), - integerType - ), - BinaryArithmeticOperator.MODULO, - new QueryLiteral<>( - optimizer.getIncrementSize(), - integerType - ), - integerType - ); - - // Create the CTE that fetches a new sequence value for the row numbers that need it - { - final QuerySpec rowsWithSequenceQuery = new QuerySpec( true ); - rowsWithSequenceQuery.getFromClause().addRoot( - new CteTableGroup( new NamedTableReference( baseTableName, "e" ) ) - ); - rowsWithSequenceQuery.getSelectClause().addSqlSelection( - new SqlSelectionImpl( - 0, - rowNumberColumnReference - ) - ); - final String fragment = ( (BulkInsertionCapableIdentifierGenerator) entityDescriptor.getGenerator() ) - .determineBulkInsertionIdentifierGenerationSelectFragment( - sessionFactory.getSqlStringGenerationContext() - ); - rowsWithSequenceQuery.getSelectClause().addSqlSelection( - new SqlSelectionImpl( - 1, - new SelfRenderingSqlFragmentExpression( fragment ) - ) - ); - rowsWithSequenceQuery.applyPredicate( - new ComparisonPredicate( - rowNumberMinusOneModuloIncrement, - ComparisonOperator.EQUAL, - new QueryLiteral<>( - 0, - integerType - ) - ) - ); - final CteTable rowsWithSequenceCteTable = new CteTable( - ROW_NUMBERS_WITH_SEQUENCE_VALUE, - List.of( rowNumberColumn, idColumn ) - ); - final SelectStatement rowsWithSequenceStatement = new SelectStatement( rowsWithSequenceQuery ); - final CteStatement rowsWithSequenceCte = new CteStatement( - rowsWithSequenceCteTable, - rowsWithSequenceStatement, - // The query cte will be reused multiple times - CteMaterialization.MATERIALIZED - ); - statement.addCteStatement( rowsWithSequenceCte ); - } - - // Create the CTE that represents the entity cte - { - final QuerySpec entityQuery = new QuerySpec( true ); - final NavigablePath navigablePath = new NavigablePath( baseTableName ); - final TableGroup baseTableGroup = new TableGroupImpl( - navigablePath, - null, - new NamedTableReference( baseTableName, "e" ), - null - ); - final TableGroup rowsWithSequenceTableGroup = new CteTableGroup( - new NamedTableReference( - ROW_NUMBERS_WITH_SEQUENCE_VALUE, - "t" - ) - ); - baseTableGroup.addTableGroupJoin( - new TableGroupJoin( - rowsWithSequenceTableGroup.getNavigablePath(), - SqlAstJoinType.LEFT, - rowsWithSequenceTableGroup, - new ComparisonPredicate( - new BinaryArithmeticExpression( - rowNumberColumnReference, - BinaryArithmeticOperator.SUBTRACT, - rowNumberMinusOneModuloIncrement, - integerType - ), - ComparisonOperator.EQUAL, - new ColumnReference( - "t", - rowNumberColumn.getColumnExpression(), - false, - null, - rowNumberColumn.getJdbcMapping() - ) - ) - ) - ); - entityQuery.getFromClause().addRoot( baseTableGroup ); - entityQuery.getSelectClause().addSqlSelection( - new SqlSelectionImpl( - 0, - new BinaryArithmeticExpression( - new ColumnReference( - "t", - idColumn.getColumnExpression(), - false, - null, - idColumn.getJdbcMapping() - ), - BinaryArithmeticOperator.ADD, - new BinaryArithmeticExpression( - rowNumberColumnReference, - BinaryArithmeticOperator.SUBTRACT, - new ColumnReference( - "t", - rowNumberColumn.getColumnExpression(), - false, - null, - rowNumberColumn.getJdbcMapping() - ), - integerType - ), - idType - ) - ) - ); - final CteTable finalEntityCteTable; - if ( targetPathCteColumns.contains( getCteTable().getCteColumns().get( 0 ) ) ) { - finalEntityCteTable = entityCteTable; - } - else { - targetPathCteColumns.add( 0, getCteTable().getCteColumns().get( 0 ) ); - finalEntityCteTable = createCteTable( getCteTable(), targetPathCteColumns ); - } - final List cteColumns = finalEntityCteTable.getCteColumns(); - for ( int i = 1; i < cteColumns.size(); i++ ) { - final CteColumn cteColumn = cteColumns.get( i ); - entityQuery.getSelectClause().addSqlSelection( - new SqlSelectionImpl( - i, - new ColumnReference( - "e", - cteColumn.getColumnExpression(), - false, - null, - cteColumn.getJdbcMapping() - ) - ) - ); - } - - final SelectStatement entityStatement = new SelectStatement( entityQuery ); - entityCte = new CteStatement( - finalEntityCteTable, - entityStatement, - // The query cte will be reused multiple times - CteMaterialization.MATERIALIZED - ); - statement.addCteStatement( entityCte ); - } - } - else if ( !assignsId && entityDescriptor.getGenerator().generatedOnExecution() ) { - final String baseTableName = "base_" + entityCteTable.getTableExpression(); - final CteStatement baseEntityCte = new CteStatement( - entityCteTable.withName( baseTableName ), - queryStatement, - // The query cte will be reused multiple times - CteMaterialization.MATERIALIZED - ); - statement.addCteStatement( baseEntityCte ); - targetPathCteColumns.add( 0, getCteTable().getCteColumns().get( 0 ) ); - final CteTable finalEntityCteTable = createCteTable( getCteTable(), targetPathCteColumns ); - final QuerySpec finalQuerySpec = new QuerySpec( true ); - final SelectStatement finalQueryStatement = new SelectStatement( finalQuerySpec ); - entityCte = new CteStatement( - finalEntityCteTable, - finalQueryStatement, - // The query cte will be reused multiple times - CteMaterialization.MATERIALIZED - ); - } - else { - entityCte = new CteStatement( - entityCteTable, - queryStatement, - // The query cte will be reused multiple times - CteMaterialization.MATERIALIZED - ); - statement.addCteStatement( entityCte ); - } - - // Add all CTEs - final String baseInsertCte = addDmlCtes( - statement, - entityCte, - targetPathColumns, - assignsId, - sqmConverter, - factory - ); - - final Expression count = createCountStar( factory, sqmConverter ); - domainResults - .add( new BasicResult<>( 0, null, ( (SqlExpressible) count ).getJdbcMapping() ) ); - querySpec.getSelectClause().addSqlSelection( new SqlSelectionImpl( 0, count ) ); - querySpec.getFromClause().addRoot( - new CteTableGroup( - new NamedTableReference( - // We want to return the insertion count of the base table - baseInsertCte, - CTE_TABLE_IDENTIFIER - ) - ) - ); - - // Execute the statement - final JdbcServices jdbcServices = factory.getJdbcServices(); - final SqlAstTranslator translator = jdbcServices.getJdbcEnvironment() - .getSqlAstTranslatorFactory() - .buildSelectTranslator( factory, statement ); - final JdbcParameterBindings jdbcParameterBindings = SqmUtil.createJdbcParameterBindings( - executionContext.getQueryParameterBindings(), - getDomainParameterXref(), - SqmUtil.generateJdbcParamsXref( getDomainParameterXref(), sqmConverter ), - new SqmParameterMappingModelResolutionAccess() { - @Override - @SuppressWarnings("unchecked") - public MappingModelExpressible getResolvedMappingModelType(SqmParameter parameter) { - return (MappingModelExpressible) sqmConverter.getSqmParameterMappingModelExpressibleResolutions() - .get( parameter ); - } - }, - executionContext.getSession() - ); - final JdbcOperationQuerySelect select = translator.translate( jdbcParameterBindings, executionContext.getQueryOptions() ); - return ( (ReactiveSharedSessionContractImplementor) executionContext.getSession() ) - .reactiveAutoFlushIfRequired( select.getAffectedTableNames() ) - .thenCompose( v -> StandardReactiveSelectExecutor.INSTANCE.list( - select, + public CompletionStage reactiveExecute( + JdbcParameterBindings jdbcParameterBindings, + DomainQueryExecutionContext context) { + return ( (ReactiveSharedSessionContractImplementor) context.getSession() ) + .reactiveAutoFlushIfRequired( getSelect().getAffectedTableNames() ) + .thenCompose( v -> StandardReactiveSelectExecutor.INSTANCE + .list( + getSelect(), jdbcParameterBindings, - SqmJdbcExecutionContextAdapter.omittingLockingAndPaging( executionContext ), + SqmJdbcExecutionContextAdapter.omittingLockingAndPaging( context ), row -> row[0], - ReactiveListResultsConsumer.UniqueSemantic.NONE + null, + ReactiveListResultsConsumer.UniqueSemantic.NONE, + 1 ) - .thenApply( list -> ( (Number) list.get( 0 ) ).intValue() ) ); + .thenApply( list -> ( (Number) list.get( 0 ) ).intValue() ) + ); } } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/cte/ReactiveCteInsertStrategy.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/cte/ReactiveCteInsertStrategy.java index d7259ebb1..e0a0e10fd 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/cte/ReactiveCteInsertStrategy.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/cte/ReactiveCteInsertStrategy.java @@ -5,16 +5,18 @@ */ package org.hibernate.reactive.query.sqm.mutation.internal.cte; -import java.util.concurrent.CompletionStage; - +import org.hibernate.internal.util.MutableObject; import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.spi.RuntimeModelCreationContext; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.query.spi.DomainQueryExecutionContext; import org.hibernate.query.sqm.internal.DomainParameterXref; +import org.hibernate.query.sqm.mutation.internal.InsertHandler; import org.hibernate.query.sqm.mutation.internal.cte.CteInsertStrategy; +import org.hibernate.query.sqm.mutation.spi.MultiTableHandlerBuildResult; import org.hibernate.query.sqm.tree.insert.SqmInsertStatement; import org.hibernate.reactive.query.sqm.mutation.spi.ReactiveSqmMultiTableInsertStrategy; +import org.hibernate.sql.exec.spi.JdbcParameterBindings; public class ReactiveCteInsertStrategy extends CteInsertStrategy implements ReactiveSqmMultiTableInsertStrategy { @@ -31,11 +33,16 @@ public ReactiveCteInsertStrategy( } @Override - public CompletionStage reactiveExecuteInsert( - SqmInsertStatement sqmInsertStatement, - DomainParameterXref domainParameterXref, - DomainQueryExecutionContext context) { - return new ReactiveCteInsertHandler( getEntityCteTable(), sqmInsertStatement, domainParameterXref, getSessionFactory() ) - .reactiveExecute( context ); + public MultiTableHandlerBuildResult buildHandler(SqmInsertStatement sqmInsertStatement, DomainParameterXref domainParameterXref, DomainQueryExecutionContext context) { + final MutableObject firstJdbcParameterBindings = new MutableObject<>(); + final InsertHandler multiTableHandler = new ReactiveCteInsertHandler( + getEntityCteTable(), + sqmInsertStatement, + domainParameterXref, + context, + firstJdbcParameterBindings + ); + return new MultiTableHandlerBuildResult( multiTableHandler, firstJdbcParameterBindings.get() ); } + } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/cte/ReactiveCteMutationStrategy.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/cte/ReactiveCteMutationStrategy.java index 6da230aad..445cfe2f6 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/cte/ReactiveCteMutationStrategy.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/cte/ReactiveCteMutationStrategy.java @@ -5,17 +5,21 @@ */ package org.hibernate.reactive.query.sqm.mutation.internal.cte; -import java.util.concurrent.CompletionStage; - +import org.hibernate.internal.util.MutableObject; import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.spi.RuntimeModelCreationContext; import org.hibernate.persister.entity.EntityPersister; import org.hibernate.query.spi.DomainQueryExecutionContext; import org.hibernate.query.sqm.internal.DomainParameterXref; import org.hibernate.query.sqm.mutation.internal.cte.CteMutationStrategy; +import org.hibernate.query.sqm.mutation.spi.MultiTableHandler; +import org.hibernate.query.sqm.mutation.spi.MultiTableHandlerBuildResult; +import org.hibernate.query.sqm.tree.SqmDeleteOrUpdateStatement; import org.hibernate.query.sqm.tree.delete.SqmDeleteStatement; import org.hibernate.query.sqm.tree.update.SqmUpdateStatement; +import org.hibernate.reactive.query.sqm.mutation.internal.ReactiveHandler; import org.hibernate.reactive.query.sqm.mutation.spi.ReactiveSqmMultiTableMutationStrategy; +import org.hibernate.sql.exec.spi.JdbcParameterBindings; public class ReactiveCteMutationStrategy extends CteMutationStrategy implements ReactiveSqmMultiTableMutationStrategy { @@ -28,22 +32,53 @@ public ReactiveCteMutationStrategy(EntityPersister rootDescriptor, RuntimeModelC } @Override - public CompletionStage reactiveExecuteUpdate( - SqmUpdateStatement sqmUpdateStatement, - DomainParameterXref domainParameterXref, - DomainQueryExecutionContext context) { - checkMatch( sqmUpdateStatement ); - return new ReactiveCteUpdateHandler( getIdCteTable(), sqmUpdateStatement, domainParameterXref, this, getSessionFactory() ) - .reactiveExecute( context ); + public MultiTableHandlerBuildResult buildHandler(SqmDeleteOrUpdateStatement sqmStatement, DomainParameterXref domainParameterXref, DomainQueryExecutionContext context) { + final MutableObject firstJdbcParameterBindings = new MutableObject<>(); + final MultiTableHandler multiTableHandler = sqmStatement instanceof SqmDeleteStatement sqmDelete + ? buildHandler( sqmDelete, domainParameterXref, context, firstJdbcParameterBindings) + : buildHandler( (SqmUpdateStatement) sqmStatement, domainParameterXref, context, firstJdbcParameterBindings ); + return new MultiTableHandlerBuildResult( multiTableHandler, firstJdbcParameterBindings.get() ); } @Override - public CompletionStage reactiveExecuteDelete( - SqmDeleteStatement sqmDeleteStatement, - DomainParameterXref domainParameterXref, - DomainQueryExecutionContext context) { - checkMatch( sqmDeleteStatement ); - return new ReactiveCteDeleteHandler( getIdCteTable(), sqmDeleteStatement, domainParameterXref, this, getSessionFactory() ) - .reactiveExecute( context ); + public ReactiveHandler buildHandler(SqmDeleteStatement sqmDelete, DomainParameterXref domainParameterXref, DomainQueryExecutionContext context, MutableObject firstJdbcParameterBindingsConsumer) { + checkMatch( sqmDelete ); + if ( getRootDescriptor().getSoftDeleteMapping() != null ) { + return new ReactiveCteSoftDeleteHandler( + getIdCteTable(), + sqmDelete, + domainParameterXref, + this, + getSessionFactory(), + context, + firstJdbcParameterBindingsConsumer + ); + } + else { + return new ReactiveCteDeleteHandler( + getIdCteTable(), + sqmDelete, + domainParameterXref, + this, + getSessionFactory(), + context, + firstJdbcParameterBindingsConsumer + ); + } } + + @Override + public MultiTableHandler buildHandler(SqmUpdateStatement sqmUpdate, DomainParameterXref domainParameterXref, DomainQueryExecutionContext context, MutableObject firstJdbcParameterBindingsConsumer) { + checkMatch( sqmUpdate ); + return new ReactiveCteUpdateHandler( + getIdCteTable(), + sqmUpdate, + domainParameterXref, + this, + getSessionFactory(), + context, + firstJdbcParameterBindingsConsumer + ); + } + } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/cte/ReactiveCteSoftDeleteHandler.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/cte/ReactiveCteSoftDeleteHandler.java new file mode 100644 index 000000000..bf42b0dfb --- /dev/null +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/cte/ReactiveCteSoftDeleteHandler.java @@ -0,0 +1,43 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.query.sqm.mutation.internal.cte; + +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.internal.util.MutableObject; +import org.hibernate.query.spi.DomainQueryExecutionContext; +import org.hibernate.query.sqm.internal.DomainParameterXref; +import org.hibernate.query.sqm.mutation.internal.cte.CteMutationStrategy; +import org.hibernate.query.sqm.mutation.internal.cte.CteSoftDeleteHandler; +import org.hibernate.query.sqm.tree.delete.SqmDeleteStatement; +import org.hibernate.sql.ast.tree.cte.CteTable; +import org.hibernate.sql.exec.internal.JdbcOperationQuerySelect; +import org.hibernate.sql.exec.spi.JdbcParameterBindings; + +public class ReactiveCteSoftDeleteHandler extends CteSoftDeleteHandler implements ReactiveAbstractCteMutationHandler { + protected ReactiveCteSoftDeleteHandler( + CteTable cteTable, + SqmDeleteStatement sqmDeleteStatement, + DomainParameterXref domainParameterXref, + CteMutationStrategy strategy, + SessionFactoryImplementor sessionFactory, + DomainQueryExecutionContext context, + MutableObject firstJdbcParameterBindingsConsumer) { + super( + cteTable, + sqmDeleteStatement, + domainParameterXref, + strategy, + sessionFactory, + context, + firstJdbcParameterBindingsConsumer + ); + } + + @Override + public JdbcOperationQuerySelect getSelect() { + return super.getSelect(); + } +} diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/cte/ReactiveCteUpdateHandler.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/cte/ReactiveCteUpdateHandler.java index 0730ff33e..811f9d436 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/cte/ReactiveCteUpdateHandler.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/cte/ReactiveCteUpdateHandler.java @@ -5,59 +5,35 @@ */ package org.hibernate.reactive.query.sqm.mutation.internal.cte; -import java.lang.invoke.MethodHandles; -import java.util.List; -import java.util.Map; - import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.internal.util.MutableObject; import org.hibernate.query.spi.DomainQueryExecutionContext; import org.hibernate.query.sqm.internal.DomainParameterXref; -import org.hibernate.query.sqm.mutation.internal.MultiTableSqmMutationConverter; import org.hibernate.query.sqm.mutation.internal.cte.CteMutationStrategy; import org.hibernate.query.sqm.mutation.internal.cte.CteUpdateHandler; -import org.hibernate.query.sqm.tree.expression.SqmParameter; import org.hibernate.query.sqm.tree.update.SqmUpdateStatement; -import org.hibernate.reactive.logging.impl.Log; -import org.hibernate.reactive.logging.impl.LoggerFactory; -import org.hibernate.sql.ast.tree.cte.CteContainer; -import org.hibernate.sql.ast.tree.cte.CteStatement; import org.hibernate.sql.ast.tree.cte.CteTable; -import org.hibernate.sql.ast.tree.expression.Expression; -import org.hibernate.sql.ast.tree.expression.JdbcParameter; +import org.hibernate.sql.exec.internal.JdbcOperationQuerySelect; +import org.hibernate.sql.exec.spi.JdbcParameterBindings; /** * @see CteUpdateHandler */ public class ReactiveCteUpdateHandler extends CteUpdateHandler implements ReactiveAbstractCteMutationHandler { - private static final Log LOG = LoggerFactory.make( Log.class, MethodHandles.lookup() ); - public ReactiveCteUpdateHandler( CteTable cteTable, SqmUpdateStatement sqmStatement, DomainParameterXref domainParameterXref, CteMutationStrategy strategy, - SessionFactoryImplementor sessionFactory) { - super( cteTable, sqmStatement, domainParameterXref, strategy, sessionFactory ); - } - - @Override - public void addDmlCtes( - CteContainer statement, - CteStatement idSelectCte, - MultiTableSqmMutationConverter sqmConverter, - Map, List> parameterResolutions, - SessionFactoryImplementor factory) { - super.addDmlCtes( statement, idSelectCte, sqmConverter, parameterResolutions, factory ); - } - - @Override - public int execute(DomainQueryExecutionContext executionContext) { - throw LOG.nonReactiveMethodCall( "reactiveExecute" ); + SessionFactoryImplementor sessionFactory, + DomainQueryExecutionContext context, + MutableObject firstJdbcParameterBindingsConsumer) { + super( cteTable, sqmStatement, domainParameterXref, strategy, sessionFactory, context, firstJdbcParameterBindingsConsumer ); } @Override - public Expression createCountStar(SessionFactoryImplementor factory, MultiTableSqmMutationConverter sqmConverter) { - return super.createCountStar( factory, sqmConverter ); + public JdbcOperationQuerySelect getSelect() { + return super.getSelect(); } } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/cte/ReactiveInsertExecutionDelegate.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/cte/ReactiveInsertExecutionDelegate.java deleted file mode 100644 index 33acc38f6..000000000 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/cte/ReactiveInsertExecutionDelegate.java +++ /dev/null @@ -1,68 +0,0 @@ -/* Hibernate, Relational Persistence for Idiomatic Java - * - * SPDX-License-Identifier: Apache-2.0 - * Copyright: Red Hat Inc. and Hibernate Authors - */ -package org.hibernate.reactive.query.sqm.mutation.internal.cte; - -import java.util.List; -import java.util.Map; -import java.util.concurrent.CompletionStage; -import java.util.function.Function; - -import org.hibernate.dialect.temptable.TemporaryTable; -import org.hibernate.engine.spi.SharedSessionContractImplementor; -import org.hibernate.query.spi.DomainQueryExecutionContext; -import org.hibernate.query.sqm.internal.DomainParameterXref; -import org.hibernate.query.sqm.mutation.internal.MultiTableSqmMutationConverter; -import org.hibernate.query.sqm.mutation.internal.temptable.InsertExecutionDelegate; -import org.hibernate.query.sqm.mutation.spi.AfterUseAction; -import org.hibernate.reactive.query.sqm.mutation.internal.temptable.ReactiveTableBasedInsertHandler; -import org.hibernate.sql.ast.tree.expression.JdbcParameter; -import org.hibernate.sql.ast.tree.from.TableGroup; -import org.hibernate.sql.ast.tree.from.TableReference; -import org.hibernate.sql.ast.tree.insert.ConflictClause; -import org.hibernate.sql.ast.tree.insert.InsertSelectStatement; -import org.hibernate.sql.ast.tree.update.Assignment; -import org.hibernate.sql.exec.spi.ExecutionContext; - -/** - * @see InsertExecutionDelegate - */ -public class ReactiveInsertExecutionDelegate extends InsertExecutionDelegate implements ReactiveTableBasedInsertHandler.ReactiveExecutionDelegate { - - public ReactiveInsertExecutionDelegate( - MultiTableSqmMutationConverter sqmConverter, - TemporaryTable entityTable, - AfterUseAction afterUseAction, - Function sessionUidAccess, - DomainParameterXref domainParameterXref, - TableGroup insertingTableGroup, - Map tableReferenceByAlias, - List assignments, - InsertSelectStatement insertStatement, - ConflictClause conflictClause, - JdbcParameter sessionUidParameter, - DomainQueryExecutionContext executionContext) { - super( - sqmConverter, - entityTable, - afterUseAction, - sessionUidAccess, - domainParameterXref, - insertingTableGroup, - tableReferenceByAlias, - assignments, - insertStatement, - conflictClause, - sessionUidParameter, - executionContext - ); - } - - @Override - public CompletionStage reactiveExecute(ExecutionContext executionContext) { - // FIXME: Why is this null? - return null; - } -} diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveExecuteWithTemporaryTableHelper.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveExecuteWithTemporaryTableHelper.java index e8f7bbea6..164403171 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveExecuteWithTemporaryTableHelper.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveExecuteWithTemporaryTableHelper.java @@ -7,6 +7,8 @@ import java.lang.invoke.MethodHandles; import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; import java.util.UUID; import java.util.concurrent.CompletionStage; import java.util.function.Function; @@ -16,42 +18,38 @@ import org.hibernate.boot.TempTableDdlTransactionHandling; import org.hibernate.dialect.Dialect; import org.hibernate.dialect.temptable.TemporaryTable; -import org.hibernate.dialect.temptable.TemporaryTableColumn; +import org.hibernate.dialect.temptable.TemporaryTableSessionUidColumn; +import org.hibernate.dialect.temptable.TemporaryTableStrategy; import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment; +import org.hibernate.engine.jdbc.spi.JdbcCoordinator; import org.hibernate.engine.jdbc.spi.JdbcServices; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.engine.spi.SharedSessionContractImplementor; -import org.hibernate.metamodel.mapping.BasicValuedMapping; import org.hibernate.metamodel.mapping.EntityMappingType; import org.hibernate.metamodel.mapping.ModelPart; -import org.hibernate.query.sqm.ComparisonOperator; -import org.hibernate.query.sqm.mutation.internal.MultiTableSqmMutationConverter; +import org.hibernate.query.sqm.mutation.internal.temptable.ExecuteWithTemporaryTableHelper; import org.hibernate.query.sqm.mutation.spi.AfterUseAction; import org.hibernate.query.sqm.mutation.spi.BeforeUseAction; import org.hibernate.reactive.logging.impl.Log; import org.hibernate.reactive.logging.impl.LoggerFactory; +import org.hibernate.reactive.pool.ReactiveConnection; import org.hibernate.reactive.query.sqm.mutation.internal.temptable.ReactiveTemporaryTableHelper.TemporaryTableCreationWork; import org.hibernate.reactive.session.ReactiveConnectionSupplier; import org.hibernate.reactive.sql.exec.internal.StandardReactiveJdbcMutationExecutor; -import org.hibernate.spi.NavigablePath; +import org.hibernate.reactive.util.impl.CompletionStages; import org.hibernate.sql.ast.SqlAstJoinType; import org.hibernate.sql.ast.SqlAstTranslatorFactory; -import org.hibernate.sql.ast.tree.expression.ColumnReference; -import org.hibernate.sql.ast.tree.expression.QueryLiteral; -import org.hibernate.sql.ast.tree.from.NamedTableReference; -import org.hibernate.sql.ast.tree.from.StandardTableGroup; -import org.hibernate.sql.ast.tree.from.TableGroup; -import org.hibernate.sql.ast.tree.from.TableReference; +import org.hibernate.sql.ast.tree.expression.JdbcParameter; import org.hibernate.sql.ast.tree.insert.InsertSelectStatement; -import org.hibernate.sql.ast.tree.predicate.ComparisonPredicate; -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.exec.spi.ExecutionContext; import org.hibernate.sql.exec.spi.JdbcOperationQueryMutation; import org.hibernate.sql.exec.spi.JdbcParameterBindings; -import org.hibernate.sql.results.internal.SqlSelectionImpl; import static org.hibernate.reactive.query.sqm.mutation.internal.temptable.ReactiveTemporaryTableHelper.cleanTemporaryTableRows; +import static org.hibernate.reactive.util.impl.CompletionStages.failedFuture; +import static org.hibernate.reactive.util.impl.CompletionStages.falseFuture; import static org.hibernate.reactive.util.impl.CompletionStages.voidFuture; /** @@ -64,79 +62,6 @@ public final class ReactiveExecuteWithTemporaryTableHelper { private ReactiveExecuteWithTemporaryTableHelper() { } - public static CompletionStage saveMatchingIdsIntoIdTable( - MultiTableSqmMutationConverter sqmConverter, - Predicate suppliedPredicate, - TemporaryTable idTable, - Function sessionUidAccess, - JdbcParameterBindings jdbcParameterBindings, - ExecutionContext executionContext) { - - final TableGroup mutatingTableGroup = sqmConverter.getMutatingTableGroup(); - - assert mutatingTableGroup.getModelPart() instanceof EntityMappingType; - final EntityMappingType mutatingEntityDescriptor = (EntityMappingType) mutatingTableGroup.getModelPart(); - - final NamedTableReference idTableReference = new NamedTableReference( - idTable.getTableExpression(), - InsertSelectStatement.DEFAULT_ALIAS - ); - final InsertSelectStatement idTableInsert = new InsertSelectStatement( idTableReference ); - - for ( int i = 0; i < idTable.getColumns().size(); i++ ) { - final TemporaryTableColumn column = idTable.getColumns().get( i ); - idTableInsert.addTargetColumnReferences( - new ColumnReference( - idTableReference, - column.getColumnName(), - // id columns cannot be formulas and cannot have custom read and write expressions - false, - null, - column.getJdbcMapping() - ) - ); - } - - final QuerySpec matchingIdSelection = new QuerySpec( true, 1 ); - idTableInsert.setSourceSelectStatement( matchingIdSelection ); - - matchingIdSelection.getFromClause().addRoot( mutatingTableGroup ); - - mutatingEntityDescriptor.getIdentifierMapping().forEachSelectable( - (selectionIndex, selection) -> { - final TableReference tableReference = mutatingTableGroup.resolveTableReference( - mutatingTableGroup.getNavigablePath(), - selection.getContainingTableExpression() - ); - matchingIdSelection.getSelectClause().addSqlSelection( - new SqlSelectionImpl( - selectionIndex + 1, - sqmConverter.getSqlExpressionResolver().resolveSqlExpression( - tableReference, - selection - ) - ) - ); - } - ); - - if ( idTable.getSessionUidColumn() != null ) { - final int jdbcPosition = matchingIdSelection.getSelectClause().getSqlSelections().size(); - matchingIdSelection.getSelectClause().addSqlSelection( - new SqlSelectionImpl( - jdbcPosition, - new QueryLiteral<>( - UUID.fromString( sessionUidAccess.apply( executionContext.getSession() ) ), - (BasicValuedMapping) idTable.getSessionUidColumn().getJdbcMapping() - ) - ) - ); - } - - matchingIdSelection.applyPredicate( suppliedPredicate ); - return saveIntoTemporaryTable( idTableInsert, jdbcParameterBindings, executionContext ); - } - public static CompletionStage saveIntoTemporaryTable( InsertSelectStatement temporaryTableInsert, JdbcParameterBindings jdbcParameterBindings, @@ -150,12 +75,14 @@ public static CompletionStage saveIntoTemporaryTable( // Acquire a WRITE lock for the rows that are about to be modified lockOptions.setLockMode( LockMode.WRITE ); // Visit the table joins and reset the lock mode if we encounter OUTER joins that are not supported - if ( temporaryTableInsert.getSourceSelectStatement() != null + final QueryPart sourceSelectStatement = temporaryTableInsert.getSourceSelectStatement(); + if ( sourceSelectStatement != null && !jdbcEnvironment.getDialect().supportsOuterJoinForUpdate() ) { - temporaryTableInsert.getSourceSelectStatement().visitQuerySpecs( + sourceSelectStatement.visitQuerySpecs( querySpec -> querySpec.getFromClause().visitTableJoins( tableJoin -> { - if ( tableJoin.getJoinType() != SqlAstJoinType.INNER ) { + if ( tableJoin.isInitialized() + && tableJoin.getJoinType() != SqlAstJoinType.INNER ) { lockOptions.setLockMode( lockMode ); } } @@ -166,131 +93,66 @@ public static CompletionStage saveIntoTemporaryTable( .translate( jdbcParameterBindings, executionContext.getQueryOptions() ); lockOptions.setLockMode( lockMode ); + return saveIntoTemporaryTable(jdbcInsert, jdbcParameterBindings, executionContext); + } + + public static CompletionStage saveIntoTemporaryTable( + JdbcOperationQueryMutation jdbcInsert, + JdbcParameterBindings jdbcParameterBindings, + ExecutionContext executionContext) { return StandardReactiveJdbcMutationExecutor.INSTANCE .executeReactive( jdbcInsert, jdbcParameterBindings, - executionContext.getSession().getJdbcCoordinator().getStatementPreparer()::prepareStatement, - ReactiveExecuteWithTemporaryTableHelper::doNothing, + sql -> executionContext.getSession().getJdbcCoordinator() + .getStatementPreparer().prepareStatement( sql ), + (integer, preparedStatement) -> {}, executionContext - ); } public static QuerySpec createIdTableSelectQuerySpec( TemporaryTable idTable, - Function sessionUidAccess, + JdbcParameter sessionUidParameter, EntityMappingType entityDescriptor, ExecutionContext executionContext) { - return createIdTableSelectQuerySpec( idTable, null, sessionUidAccess, entityDescriptor, executionContext ); + return createIdTableSelectQuerySpec( idTable, null, sessionUidParameter, entityDescriptor, executionContext ); } public static QuerySpec createIdTableSelectQuerySpec( TemporaryTable idTable, ModelPart fkModelPart, - Function sessionUidAccess, + JdbcParameter sessionUidParameter, EntityMappingType entityDescriptor, ExecutionContext executionContext) { - final QuerySpec querySpec = new QuerySpec( false ); - - final NamedTableReference idTableReference = new NamedTableReference( - idTable.getTableExpression(), - TemporaryTable.DEFAULT_ALIAS, - true - ); - final TableGroup idTableGroup = new StandardTableGroup( - true, - new NavigablePath( idTableReference.getTableExpression() ), - entityDescriptor, - null, - idTableReference, - null, - executionContext.getSession().getFactory() - ); - - querySpec.getFromClause().addRoot( idTableGroup ); - - applyIdTableSelections( querySpec, idTableReference, idTable, fkModelPart ); - applyIdTableRestrictions( querySpec, idTableReference, idTable, sessionUidAccess, executionContext ); - - return querySpec; + return ExecuteWithTemporaryTableHelper.createIdTableSelectQuerySpec( idTable, fkModelPart, sessionUidParameter, entityDescriptor, executionContext ); } - // TODO: I think we can reuse the method in ExecuteWithTemporaryTableHelper - private static void applyIdTableSelections( - QuerySpec querySpec, - TableReference tableReference, - TemporaryTable idTable, - ModelPart fkModelPart) { - if ( fkModelPart == null ) { - final int size = idTable.getEntityDescriptor().getIdentifierMapping().getJdbcTypeCount(); - for ( int i = 0; i < size; i++ ) { - final TemporaryTableColumn temporaryTableColumn = idTable.getColumns().get( i ); - if ( temporaryTableColumn != idTable.getSessionUidColumn() ) { - querySpec.getSelectClause().addSqlSelection( - new SqlSelectionImpl( - i, - new ColumnReference( - tableReference, - temporaryTableColumn.getColumnName(), - false, - null, - temporaryTableColumn.getJdbcMapping() - ) - ) - ); - } - } - } - else { - fkModelPart.forEachSelectable( - (i, selectableMapping) -> querySpec.getSelectClause() - .addSqlSelection( new SqlSelectionImpl( - i, - new ColumnReference( - tableReference, - selectableMapping.getSelectionExpression(), - false, - null, - selectableMapping.getJdbcMapping() - ) - ) - ) - ); - } + @Deprecated(forRemoval = true, since = "3.1") + public static CompletionStage performBeforeTemporaryTableUseActions( + TemporaryTable temporaryTable, + ExecutionContext executionContext) { + return performBeforeTemporaryTableUseActions( + temporaryTable, + executionContext.getSession().getDialect().getTemporaryTableBeforeUseAction(), + executionContext + ).thenCompose( CompletionStages::voidFuture ); } - private static void applyIdTableRestrictions( - QuerySpec querySpec, - TableReference idTableReference, - TemporaryTable idTable, - Function sessionUidAccess, + public static CompletionStage performBeforeTemporaryTableUseActions( + TemporaryTable temporaryTable, + TemporaryTableStrategy temporaryTableStrategy, ExecutionContext executionContext) { - if ( idTable.getSessionUidColumn() != null ) { - querySpec.applyPredicate( - new ComparisonPredicate( - new ColumnReference( - idTableReference, - idTable.getSessionUidColumn().getColumnName(), - false, null, - idTable.getSessionUidColumn().getJdbcMapping() - ), - ComparisonOperator.EQUAL, - new QueryLiteral<>( - UUID.fromString( sessionUidAccess.apply( executionContext.getSession() ) ), - (BasicValuedMapping) idTable.getSessionUidColumn().getJdbcMapping() - ) - ) - ); - } + return performBeforeTemporaryTableUseActions( temporaryTable, temporaryTableStrategy.getTemporaryTableBeforeUseAction(), executionContext ); } - public static CompletionStage performBeforeTemporaryTableUseActions( + public static CompletionStage performBeforeTemporaryTableUseActions( TemporaryTable temporaryTable, + BeforeUseAction beforeUseAction, ExecutionContext executionContext) { final SessionFactoryImplementor factory = executionContext.getSession().getFactory(); final Dialect dialect = factory.getJdbcServices().getDialect(); - if ( dialect.getTemporaryTableBeforeUseAction() == BeforeUseAction.CREATE ) { + if ( beforeUseAction == BeforeUseAction.CREATE ) { final TemporaryTableCreationWork temporaryTableCreationWork = new TemporaryTableCreationWork( temporaryTable, factory ); final TempTableDdlTransactionHandling ddlTransactionHandling = dialect.getTemporaryTableDdlTransactionHandling(); if ( ddlTransactionHandling == TempTableDdlTransactionHandling.NONE ) { @@ -298,7 +160,7 @@ public static CompletionStage performBeforeTemporaryTableUseActions( } throw LOG.notYetImplemented(); } - return voidFuture(); + return falseFuture(); } public static CompletionStage performAfterTemporaryTableUseActions( @@ -308,14 +170,51 @@ public static CompletionStage performAfterTemporaryTableUseActions( ExecutionContext executionContext) { final SessionFactoryImplementor factory = executionContext.getSession().getFactory(); final Dialect dialect = factory.getJdbcServices().getDialect(); - switch ( afterUseAction ) { - case CLEAN: - return cleanTemporaryTableRows( temporaryTable, dialect.getTemporaryTableExporter(), sessionUidAccess, executionContext.getSession() ); - case DROP: - return dropAction( temporaryTable, executionContext, factory, dialect ); - default: - return voidFuture(); + return switch ( afterUseAction ) { + case CLEAN -> cleanTemporaryTableRows( temporaryTable, dialect.getTemporaryTableExporter(), sessionUidAccess, executionContext.getSession() ); + case DROP -> dropAction( temporaryTable, executionContext, factory, dialect ); + default -> voidFuture(); + }; + } + + public static CompletionStage loadInsertedRowNumbers( + String sqlSelect, + TemporaryTable temporaryTable, + Function sessionUidAccess, + int rows, + ExecutionContext executionContext) { + final TemporaryTableSessionUidColumn sessionUidColumn = temporaryTable.getSessionUidColumn(); + final SharedSessionContractImplementor session = executionContext.getSession(); + final JdbcCoordinator jdbcCoordinator = session.getJdbcCoordinator(); + PreparedStatement preparedStatement = null; + preparedStatement = jdbcCoordinator.getStatementPreparer().prepareStatement( sqlSelect ); + Object[] parameters = new Object[1]; + if ( sessionUidColumn != null ) { + parameters[0] = UUID.fromString( sessionUidAccess.apply( session ) ); + } + final Integer[] rowNumbers = new Integer[rows]; + return reactiveConnection(session).selectJdbc( sqlSelect, parameters ) + .thenApply( resultSet -> getRowNumbers( rows, resultSet, rowNumbers ) ); + } + + private static Integer[] getRowNumbers(int rows, ResultSet resultSet, Integer[] rowNumbers) { + int rowIndex = 0; + try { + while ( resultSet.next() ) { + rowNumbers[rowIndex++] = resultSet.getInt( 1 ); + } + return rowNumbers; + } + catch ( IndexOutOfBoundsException e ) { + throw new IllegalArgumentException( "Expected " + rows + " to be inserted but found more", e ); } + catch ( SQLException ex ) { + throw new IllegalStateException( ex ); + } + } + + private static ReactiveConnection reactiveConnection(SharedSessionContractImplementor session) { + return ( (ReactiveConnectionSupplier) session ).getReactiveConnection(); } private static CompletionStage dropAction( @@ -327,10 +226,11 @@ private static CompletionStage dropAction( if ( ddlTransactionHandling == TempTableDdlTransactionHandling.NONE ) { return new ReactiveTemporaryTableHelper .TemporaryTableDropWork( temporaryTable, factory ) - .reactiveExecute( ( (ReactiveConnectionSupplier) executionContext.getSession() ).getReactiveConnection() ); + .reactiveExecute( ( (ReactiveConnectionSupplier) executionContext.getSession() ).getReactiveConnection() ) + .thenCompose( CompletionStages::voidFuture ); } - throw LOG.notYetImplemented(); + return failedFuture( LOG.notYetImplemented() ); } private static void doNothing(Integer integer, PreparedStatement preparedStatement) { diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveGlobalTemporaryTableInsertStrategy.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveGlobalTemporaryTableInsertStrategy.java index 2f7f7bb37..9ca1dfce8 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveGlobalTemporaryTableInsertStrategy.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveGlobalTemporaryTableInsertStrategy.java @@ -6,16 +6,19 @@ package org.hibernate.reactive.query.sqm.mutation.internal.temptable; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; import org.hibernate.engine.jdbc.connections.spi.JdbcConnectionAccess; import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.internal.util.MutableObject; import org.hibernate.metamodel.mapping.internal.MappingModelCreationProcess; import org.hibernate.query.spi.DomainQueryExecutionContext; import org.hibernate.query.sqm.internal.DomainParameterXref; +import org.hibernate.query.sqm.mutation.internal.InsertHandler; import org.hibernate.query.sqm.mutation.internal.temptable.GlobalTemporaryTableStrategy; +import org.hibernate.query.sqm.mutation.spi.MultiTableHandlerBuildResult; import org.hibernate.query.sqm.tree.insert.SqmInsertStatement; import org.hibernate.reactive.query.sqm.mutation.spi.ReactiveSqmMultiTableInsertStrategy; +import org.hibernate.sql.exec.spi.JdbcParameterBindings; /** * @see org.hibernate.query.sqm.mutation.internal.temptable.GlobalTemporaryTableInsertStrategy @@ -48,23 +51,23 @@ public void release(SessionFactoryImplementor sessionFactory, JdbcConnectionAcce } @Override - public CompletionStage reactiveExecuteInsert( - SqmInsertStatement sqmInsertStatement, - DomainParameterXref domainParameterXref, - DomainQueryExecutionContext context) { - return tableCreatedStage.thenCompose( v -> new ReactiveTableBasedInsertHandler( + public MultiTableHandlerBuildResult buildHandler(SqmInsertStatement sqmInsertStatement, DomainParameterXref domainParameterXref, DomainQueryExecutionContext context) { + final MutableObject firstJdbcParameterBindings = new MutableObject<>(); + final InsertHandler multiTableHandler = new ReactiveTableBasedInsertHandler( sqmInsertStatement, domainParameterXref, getTemporaryTable(), - getSessionFactory().getJdbcServices().getDialect().getTemporaryTableAfterUseAction(), + getTemporaryTableStrategy(), + false, // generally a global temp table should already track a Connection-specific uid, // but just in case a particular env needs it... - ReactiveGlobalTemporaryTableStrategy::sessionIdentifier, - getSessionFactory() - ).reactiveExecute( context ) ); + session -> session.getSessionIdentifier().toString(), + context, + firstJdbcParameterBindings + ); + return new MultiTableHandlerBuildResult( multiTableHandler, firstJdbcParameterBindings.get() ); } - @Override public boolean isPrepared() { return prepared; diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveGlobalTemporaryTableMutationStrategy.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveGlobalTemporaryTableMutationStrategy.java index 66b001520..0995dc70e 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveGlobalTemporaryTableMutationStrategy.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveGlobalTemporaryTableMutationStrategy.java @@ -6,22 +6,28 @@ package org.hibernate.reactive.query.sqm.mutation.internal.temptable; import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; import org.hibernate.engine.jdbc.connections.spi.JdbcConnectionAccess; import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.internal.util.MutableObject; import org.hibernate.metamodel.mapping.internal.MappingModelCreationProcess; +import org.hibernate.persister.entity.EntityPersister; import org.hibernate.query.spi.DomainQueryExecutionContext; import org.hibernate.query.sqm.internal.DomainParameterXref; +import org.hibernate.query.sqm.mutation.internal.temptable.GlobalTemporaryTableMutationStrategy; import org.hibernate.query.sqm.mutation.internal.temptable.GlobalTemporaryTableStrategy; +import org.hibernate.query.sqm.mutation.spi.MultiTableHandler; +import org.hibernate.query.sqm.mutation.spi.MultiTableHandlerBuildResult; +import org.hibernate.query.sqm.tree.SqmDeleteOrUpdateStatement; import org.hibernate.query.sqm.tree.delete.SqmDeleteStatement; import org.hibernate.query.sqm.tree.update.SqmUpdateStatement; import org.hibernate.reactive.query.sqm.mutation.spi.ReactiveSqmMultiTableMutationStrategy; +import org.hibernate.sql.exec.spi.JdbcParameterBindings; /** * @see org.hibernate.query.sqm.mutation.internal.temptable.GlobalTemporaryTableMutationStrategy */ -public class ReactiveGlobalTemporaryTableMutationStrategy extends GlobalTemporaryTableStrategy +public class ReactiveGlobalTemporaryTableMutationStrategy extends GlobalTemporaryTableMutationStrategy implements ReactiveGlobalTemporaryTableStrategy, ReactiveSqmMultiTableMutationStrategy { private final CompletableFuture tableCreatedStage = new CompletableFuture<>(); @@ -50,35 +56,70 @@ public void release(SessionFactoryImplementor sessionFactory, JdbcConnectionAcce } @Override - public CompletionStage reactiveExecuteUpdate( - SqmUpdateStatement sqmUpdateStatement, + public MultiTableHandlerBuildResult buildHandler(SqmDeleteOrUpdateStatement sqmStatement, DomainParameterXref domainParameterXref, DomainQueryExecutionContext context) { + final MutableObject firstJdbcParameterBindings = new MutableObject<>(); + final MultiTableHandler multiTableHandler = sqmStatement instanceof SqmDeleteStatement sqmDelete + ? buildHandler( sqmDelete, domainParameterXref, context, firstJdbcParameterBindings ) + : buildHandler( (SqmUpdateStatement) sqmStatement, domainParameterXref, context, firstJdbcParameterBindings ); + return new MultiTableHandlerBuildResult( multiTableHandler, firstJdbcParameterBindings.get() ); + } + + @Override + public MultiTableHandler buildHandler( + SqmUpdateStatement sqmUpdate, DomainParameterXref domainParameterXref, - DomainQueryExecutionContext context) { - return tableCreatedStage - .thenCompose( v -> new ReactiveTableBasedUpdateHandler( - sqmUpdateStatement, - domainParameterXref, - getTemporaryTable(), - getSessionFactory().getJdbcServices().getDialect().getTemporaryTableAfterUseAction(), - ReactivePersistentTableStrategy::sessionIdentifier, - getSessionFactory() - ).reactiveExecute( context ) ); + DomainQueryExecutionContext context, + MutableObject firstJdbcParameterBindingsConsumer) { + return new ReactiveTableBasedUpdateHandler( + sqmUpdate, + domainParameterXref, + getTemporaryTable(), + getTemporaryTableStrategy(), + false, + // generally a global temp table should already track a Connection-specific uid, + // but just in case a particular env needs it... + session -> session.getSessionIdentifier().toString(), + context, + firstJdbcParameterBindingsConsumer + ); } @Override - public CompletionStage reactiveExecuteDelete( - SqmDeleteStatement sqmDeleteStatement, + public MultiTableHandler buildHandler( + SqmDeleteStatement sqmDelete, DomainParameterXref domainParameterXref, - DomainQueryExecutionContext context) { - return tableCreatedStage - .thenCompose( v -> new ReactiveTableBasedDeleteHandler( - sqmDeleteStatement, - domainParameterXref, - getTemporaryTable(), - getSessionFactory().getJdbcServices().getDialect().getTemporaryTableAfterUseAction(), - ReactiveGlobalTemporaryTableStrategy::sessionIdentifier, - getSessionFactory() - ).reactiveExecute( context ) ); + DomainQueryExecutionContext context, + MutableObject firstJdbcParameterBindingsConsumer) { + final EntityPersister rootDescriptor = context.getSession().getFactory().getMappingMetamodel() + .getEntityDescriptor( sqmDelete.getRoot().getEntityName() ); + if ( rootDescriptor.getSoftDeleteMapping() != null ) { + return new ReactiveTableBasedSoftDeleteHandler( + sqmDelete, + domainParameterXref, + getTemporaryTable(), + getTemporaryTableStrategy(), + false, + // generally a global temp table should already track a Connection-specific uid, + // but just in case a particular env needs it... + session -> session.getSessionIdentifier().toString(), + context, + firstJdbcParameterBindingsConsumer + ); + } + else { + return new ReactiveTableBasedDeleteHandler( + sqmDelete, + domainParameterXref, + getTemporaryTable(), + getTemporaryTableStrategy(), + false, + // generally a global temp table should already track a Connection-specific uid, + // but just in case a particular env needs it... + session -> session.getSessionIdentifier().toString(), + context, + firstJdbcParameterBindingsConsumer + ); + } } @Override diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveLocalTemporaryTableInsertStrategy.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveLocalTemporaryTableInsertStrategy.java index ae2298ddd..21a9b3ced 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveLocalTemporaryTableInsertStrategy.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveLocalTemporaryTableInsertStrategy.java @@ -5,15 +5,15 @@ */ package org.hibernate.reactive.query.sqm.mutation.internal.temptable; -import java.util.concurrent.CompletionStage; - -import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.internal.util.MutableObject; import org.hibernate.query.spi.DomainQueryExecutionContext; import org.hibernate.query.sqm.internal.DomainParameterXref; import org.hibernate.query.sqm.mutation.internal.temptable.LocalTemporaryTableInsertStrategy; -import org.hibernate.query.sqm.mutation.spi.AfterUseAction; +import org.hibernate.query.sqm.mutation.spi.MultiTableHandler; +import org.hibernate.query.sqm.mutation.spi.MultiTableHandlerBuildResult; import org.hibernate.query.sqm.tree.insert.SqmInsertStatement; import org.hibernate.reactive.query.sqm.mutation.spi.ReactiveSqmMultiTableInsertStrategy; +import org.hibernate.sql.exec.spi.JdbcParameterBindings; public class ReactiveLocalTemporaryTableInsertStrategy extends LocalTemporaryTableInsertStrategy implements ReactiveSqmMultiTableInsertStrategy { @@ -23,27 +23,21 @@ public ReactiveLocalTemporaryTableInsertStrategy(LocalTemporaryTableInsertStrate } @Override - public CompletionStage reactiveExecuteInsert( - SqmInsertStatement sqmInsertStatement, - DomainParameterXref domainParameterXref, - DomainQueryExecutionContext context) { - return new ReactiveTableBasedInsertHandler( + public MultiTableHandlerBuildResult buildHandler(SqmInsertStatement sqmInsertStatement, DomainParameterXref domainParameterXref, DomainQueryExecutionContext context) { + final MutableObject firstJdbcParameterBindings = new MutableObject<>(); + final MultiTableHandler multiTableHandler = new ReactiveTableBasedInsertHandler( sqmInsertStatement, domainParameterXref, getTemporaryTable(), - afterUserAction(), - ReactiveLocalTemporaryTableInsertStrategy::throwUnexpectedCallToSessionUIDError, - getSessionFactory() - ).reactiveExecute( context ); - } - - private static String throwUnexpectedCallToSessionUIDError(SharedSessionContractImplementor session) { - throw new UnsupportedOperationException( "Unexpected call to access Session uid" ); + getTemporaryTableStrategy(), + isDropIdTables(), + session -> { + throw new UnsupportedOperationException( "Unexpected call to access Session uid" ); + }, + context, + firstJdbcParameterBindings + ); + return new MultiTableHandlerBuildResult( multiTableHandler, firstJdbcParameterBindings.get() ); } - private AfterUseAction afterUserAction() { - return isDropIdTables() - ? AfterUseAction.DROP - : getSessionFactory().getJdbcServices().getDialect().getTemporaryTableAfterUseAction(); - } } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveLocalTemporaryTableMutationStrategy.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveLocalTemporaryTableMutationStrategy.java index fe07871eb..3e26378d9 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveLocalTemporaryTableMutationStrategy.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveLocalTemporaryTableMutationStrategy.java @@ -5,16 +5,16 @@ */ package org.hibernate.reactive.query.sqm.mutation.internal.temptable; -import java.util.concurrent.CompletionStage; - -import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.internal.util.MutableObject; +import org.hibernate.persister.entity.EntityPersister; import org.hibernate.query.spi.DomainQueryExecutionContext; import org.hibernate.query.sqm.internal.DomainParameterXref; import org.hibernate.query.sqm.mutation.internal.temptable.LocalTemporaryTableMutationStrategy; -import org.hibernate.query.sqm.mutation.spi.AfterUseAction; +import org.hibernate.query.sqm.mutation.spi.MultiTableHandler; import org.hibernate.query.sqm.tree.delete.SqmDeleteStatement; import org.hibernate.query.sqm.tree.update.SqmUpdateStatement; import org.hibernate.reactive.query.sqm.mutation.spi.ReactiveSqmMultiTableMutationStrategy; +import org.hibernate.sql.exec.spi.JdbcParameterBindings; public class ReactiveLocalTemporaryTableMutationStrategy extends LocalTemporaryTableMutationStrategy implements ReactiveSqmMultiTableMutationStrategy { @@ -23,44 +23,61 @@ public ReactiveLocalTemporaryTableMutationStrategy(LocalTemporaryTableMutationSt super( mutationStrategy.getTemporaryTable(), mutationStrategy.getSessionFactory() ); } - private static String throwUnexpectedAccessToSessionUID(SharedSessionContractImplementor session) { - // Should probably go in the LOG - throw new UnsupportedOperationException( "Unexpected call to access Session uid" ); - } - @Override - public CompletionStage reactiveExecuteUpdate( + public MultiTableHandler buildHandler( SqmUpdateStatement sqmUpdate, DomainParameterXref domainParameterXref, - DomainQueryExecutionContext context) { + DomainQueryExecutionContext context, + MutableObject firstJdbcParameterBindingsConsumer) { return new ReactiveTableBasedUpdateHandler( sqmUpdate, domainParameterXref, getTemporaryTable(), - afterUseAction(), - ReactiveLocalTemporaryTableMutationStrategy::throwUnexpectedAccessToSessionUID, - getSessionFactory() - ).reactiveExecute( context ); + getTemporaryTableStrategy(), + isDropIdTables(), + session -> { + throw new UnsupportedOperationException( "Unexpected call to access Session uid" ); + }, + context, + firstJdbcParameterBindingsConsumer + ); } @Override - public CompletionStage reactiveExecuteDelete( + public MultiTableHandler buildHandler( SqmDeleteStatement sqmDelete, DomainParameterXref domainParameterXref, - DomainQueryExecutionContext context) { - return new ReactiveTableBasedDeleteHandler( - sqmDelete, - domainParameterXref, - getTemporaryTable(), - afterUseAction(), - ReactiveLocalTemporaryTableMutationStrategy::throwUnexpectedAccessToSessionUID, - getSessionFactory() - ).reactiveExecute( context ); - } - - private AfterUseAction afterUseAction() { - return isDropIdTables() - ? AfterUseAction.DROP - : getSessionFactory().getJdbcServices().getDialect().getTemporaryTableAfterUseAction(); + DomainQueryExecutionContext context, + MutableObject firstJdbcParameterBindingsConsumer) { + final EntityPersister rootDescriptor = context.getSession().getFactory().getMappingMetamodel() + .getEntityDescriptor( sqmDelete.getRoot().getEntityName() ); + if ( rootDescriptor.getSoftDeleteMapping() != null ) { + return new ReactiveTableBasedSoftDeleteHandler( + sqmDelete, + domainParameterXref, + getTemporaryTable(), + getTemporaryTableStrategy(), + isDropIdTables(), + session -> { + throw new UnsupportedOperationException( "Unexpected call to access Session uid" ); + }, + context, + firstJdbcParameterBindingsConsumer + ); + } + else { + return new ReactiveTableBasedDeleteHandler( + sqmDelete, + domainParameterXref, + getTemporaryTable(), + getTemporaryTableStrategy(), + isDropIdTables(), + session -> { + throw new UnsupportedOperationException( "Unexpected call to access Session uid" ); + }, + context, + firstJdbcParameterBindingsConsumer + ); + } } } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactivePersistentTableInsertStrategy.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactivePersistentTableInsertStrategy.java index 9a44a44cf..a5b0c36c3 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactivePersistentTableInsertStrategy.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactivePersistentTableInsertStrategy.java @@ -10,12 +10,16 @@ import org.hibernate.engine.jdbc.connections.spi.JdbcConnectionAccess; import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.internal.util.MutableObject; import org.hibernate.metamodel.mapping.internal.MappingModelCreationProcess; import org.hibernate.query.spi.DomainQueryExecutionContext; import org.hibernate.query.sqm.internal.DomainParameterXref; import org.hibernate.query.sqm.mutation.internal.temptable.PersistentTableInsertStrategy; +import org.hibernate.query.sqm.mutation.spi.MultiTableHandler; +import org.hibernate.query.sqm.mutation.spi.MultiTableHandlerBuildResult; import org.hibernate.query.sqm.tree.insert.SqmInsertStatement; import org.hibernate.reactive.query.sqm.mutation.spi.ReactiveSqmMultiTableInsertStrategy; +import org.hibernate.sql.exec.spi.JdbcParameterBindings; public class ReactivePersistentTableInsertStrategy extends PersistentTableInsertStrategy implements ReactivePersistentTableStrategy, ReactiveSqmMultiTableInsertStrategy { @@ -45,18 +49,19 @@ public void release(SessionFactoryImplementor sessionFactory, JdbcConnectionAcce } @Override - public CompletionStage reactiveExecuteInsert( - SqmInsertStatement sqmInsertStatement, - DomainParameterXref domainParameterXref, - DomainQueryExecutionContext context) { - return tableCreatedStage.thenCompose( v -> new ReactiveTableBasedInsertHandler( + public MultiTableHandlerBuildResult buildHandler(SqmInsertStatement sqmInsertStatement, DomainParameterXref domainParameterXref, DomainQueryExecutionContext context) { + final MutableObject firstJdbcParameterBindings = new MutableObject<>(); + final MultiTableHandler multiTableHandler = new ReactiveTableBasedInsertHandler( sqmInsertStatement, domainParameterXref, getTemporaryTable(), - getSessionFactory().getJdbcServices().getDialect().getTemporaryTableAfterUseAction(), - ReactivePersistentTableStrategy::sessionIdentifier, - getSessionFactory() - ).reactiveExecute( context ) ); + getTemporaryTableStrategy(), + false, + session -> session.getSessionIdentifier().toString(), + context, + firstJdbcParameterBindings + ); + return new MultiTableHandlerBuildResult( multiTableHandler, firstJdbcParameterBindings.get() ); } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactivePersistentTableMutationStrategy.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactivePersistentTableMutationStrategy.java index 57cd14f4b..76109c78e 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactivePersistentTableMutationStrategy.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactivePersistentTableMutationStrategy.java @@ -5,18 +5,22 @@ */ package org.hibernate.reactive.query.sqm.mutation.internal.temptable; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; - import org.hibernate.engine.jdbc.connections.spi.JdbcConnectionAccess; import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.internal.util.MutableObject; import org.hibernate.metamodel.mapping.internal.MappingModelCreationProcess; +import org.hibernate.persister.entity.EntityPersister; import org.hibernate.query.spi.DomainQueryExecutionContext; import org.hibernate.query.sqm.internal.DomainParameterXref; import org.hibernate.query.sqm.mutation.internal.temptable.PersistentTableMutationStrategy; +import org.hibernate.query.sqm.mutation.spi.MultiTableHandler; import org.hibernate.query.sqm.tree.delete.SqmDeleteStatement; import org.hibernate.query.sqm.tree.update.SqmUpdateStatement; import org.hibernate.reactive.query.sqm.mutation.spi.ReactiveSqmMultiTableMutationStrategy; +import org.hibernate.sql.exec.spi.JdbcParameterBindings; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; public class ReactivePersistentTableMutationStrategy extends PersistentTableMutationStrategy @@ -47,37 +51,56 @@ public void release(SessionFactoryImplementor sessionFactory, JdbcConnectionAcce } @Override - public CompletionStage reactiveExecuteUpdate( - SqmUpdateStatement sqmUpdateStatement, + public MultiTableHandler buildHandler( + SqmUpdateStatement sqmUpdate, DomainParameterXref domainParameterXref, - DomainQueryExecutionContext context) { - return tableCreatedStage - .thenCompose( v -> new ReactiveTableBasedUpdateHandler( - sqmUpdateStatement, - domainParameterXref, - getTemporaryTable(), - getSessionFactory().getJdbcServices().getDialect().getTemporaryTableAfterUseAction(), - ReactivePersistentTableStrategy::sessionIdentifier, - getSessionFactory() - ).reactiveExecute( context ) ); + DomainQueryExecutionContext context, + MutableObject firstJdbcParameterBindingsConsumer) { + return new ReactiveTableBasedUpdateHandler( + sqmUpdate, + domainParameterXref, + getTemporaryTable(), + getTemporaryTableStrategy(), + false, + session -> session.getSessionIdentifier().toString(), + context, + firstJdbcParameterBindingsConsumer + ); } @Override - public CompletionStage reactiveExecuteDelete( - SqmDeleteStatement sqmDeleteStatement, + public MultiTableHandler buildHandler( + SqmDeleteStatement sqmDelete, DomainParameterXref domainParameterXref, - DomainQueryExecutionContext context) { - return tableCreatedStage - .thenCompose( v -> new ReactiveTableBasedDeleteHandler( - sqmDeleteStatement, - domainParameterXref, - getTemporaryTable(), - getSessionFactory().getJdbcServices().getDialect().getTemporaryTableAfterUseAction(), - ReactivePersistentTableStrategy::sessionIdentifier, - getSessionFactory() - ).reactiveExecute( context ) ); + DomainQueryExecutionContext context, + MutableObject firstJdbcParameterBindingsConsumer) { + final EntityPersister rootDescriptor = context.getSession().getFactory().getMappingMetamodel() + .getEntityDescriptor( sqmDelete.getRoot().getEntityName() ); + if ( rootDescriptor.getSoftDeleteMapping() != null ) { + return new ReactiveTableBasedSoftDeleteHandler( + sqmDelete, + domainParameterXref, + getTemporaryTable(), + getTemporaryTableStrategy(), + false, + session -> session.getSessionIdentifier().toString(), + context, + firstJdbcParameterBindingsConsumer + ); + } + else { + return new ReactiveTableBasedDeleteHandler( + sqmDelete, + domainParameterXref, + getTemporaryTable(), + getTemporaryTableStrategy(), + false, + session -> session.getSessionIdentifier().toString(), + context, + firstJdbcParameterBindingsConsumer + ); + } } - @Override public CompletionStage getDropTableActionStage() { return tableDroppedStage; diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveRestrictedDeleteExecutionDelegate.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveRestrictedDeleteExecutionDelegate.java deleted file mode 100644 index 0e1097b63..000000000 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveRestrictedDeleteExecutionDelegate.java +++ /dev/null @@ -1,640 +0,0 @@ -/* Hibernate, Relational Persistence for Idiomatic Java - * - * SPDX-License-Identifier: Apache-2.0 - * Copyright: Red Hat Inc. and Hibernate Authors - */ -package org.hibernate.reactive.query.sqm.mutation.internal.temptable; - -import java.lang.invoke.MethodHandles; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Supplier; - -import org.hibernate.dialect.temptable.TemporaryTable; -import org.hibernate.engine.jdbc.spi.JdbcServices; -import org.hibernate.engine.spi.LoadQueryInfluencers; -import org.hibernate.engine.spi.SessionFactoryImplementor; -import org.hibernate.engine.spi.SharedSessionContractImplementor; -import org.hibernate.internal.util.MutableInteger; -import org.hibernate.metamodel.mapping.EntityMappingType; -import org.hibernate.metamodel.mapping.ForeignKeyDescriptor; -import org.hibernate.metamodel.mapping.MappingModelExpressible; -import org.hibernate.metamodel.mapping.SelectableConsumer; -import org.hibernate.metamodel.mapping.internal.MappingModelCreationHelper; -import org.hibernate.persister.entity.EntityPersister; -import org.hibernate.query.spi.DomainQueryExecutionContext; -import org.hibernate.query.spi.QueryOptions; -import org.hibernate.query.spi.QueryParameterBindings; -import org.hibernate.query.sqm.internal.DomainParameterXref; -import org.hibernate.query.sqm.internal.SqmJdbcExecutionContextAdapter; -import org.hibernate.query.sqm.internal.SqmUtil; -import org.hibernate.query.sqm.mutation.internal.MultiTableSqmMutationConverter; -import org.hibernate.query.sqm.mutation.internal.TableKeyExpressionCollector; -import org.hibernate.query.sqm.mutation.internal.temptable.ColumnReferenceCheckingSqlAstWalker; -import org.hibernate.query.sqm.mutation.internal.temptable.ExecuteWithoutIdTableHelper; -import org.hibernate.query.sqm.mutation.internal.temptable.RestrictedDeleteExecutionDelegate; -import org.hibernate.query.sqm.mutation.spi.AfterUseAction; -import org.hibernate.query.sqm.spi.SqmParameterMappingModelResolutionAccess; -import org.hibernate.query.sqm.tree.delete.SqmDeleteStatement; -import org.hibernate.query.sqm.tree.expression.SqmParameter; -import org.hibernate.reactive.logging.impl.Log; -import org.hibernate.reactive.logging.impl.LoggerFactory; -import org.hibernate.reactive.query.sqm.mutation.internal.ReactiveSqmMutationStrategyHelper; -import org.hibernate.reactive.sql.exec.internal.StandardReactiveJdbcMutationExecutor; -import org.hibernate.reactive.util.impl.CompletionStages; -import org.hibernate.reactive.util.impl.CompletionStages.Completable; -import org.hibernate.spi.NavigablePath; -import org.hibernate.sql.ast.spi.SqlExpressionResolver; -import org.hibernate.sql.ast.tree.delete.DeleteStatement; -import org.hibernate.sql.ast.tree.expression.ColumnReference; -import org.hibernate.sql.ast.tree.expression.Expression; -import org.hibernate.sql.ast.tree.expression.JdbcParameter; -import org.hibernate.sql.ast.tree.expression.SqlTuple; -import org.hibernate.sql.ast.tree.from.MutatingTableReferenceGroupWrapper; -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.UnionTableReference; -import org.hibernate.sql.ast.tree.predicate.InSubQueryPredicate; -import org.hibernate.sql.ast.tree.predicate.Predicate; -import org.hibernate.sql.ast.tree.predicate.PredicateCollector; -import org.hibernate.sql.ast.tree.select.QuerySpec; -import org.hibernate.sql.exec.spi.ExecutionContext; -import org.hibernate.sql.exec.spi.JdbcOperationQueryMutation; -import org.hibernate.sql.exec.spi.JdbcParameterBindings; - -import static org.hibernate.query.sqm.mutation.internal.temptable.ExecuteWithTemporaryTableHelper.createIdTableSelectQuerySpec; -import static org.hibernate.reactive.util.impl.CompletionStages.voidFuture; - -/** - * The reactive version of {@link RestrictedDeleteExecutionDelegate} - */ -// Basically a copy of RestrictedDeleteExecutionDelegate, we will probably need to refactor this code to avoid -// duplication -public class ReactiveRestrictedDeleteExecutionDelegate - implements ReactiveTableBasedDeleteHandler.ReactiveExecutionDelegate { - - private static final Log LOG = LoggerFactory.make( Log.class, MethodHandles.lookup() ); - - private final EntityMappingType entityDescriptor; - private final TemporaryTable idTable; - private final AfterUseAction afterUseAction; - private final SqmDeleteStatement sqmDelete; - private final DomainParameterXref domainParameterXref; - private final SessionFactoryImplementor sessionFactory; - - private final Function sessionUidAccess; - private final MultiTableSqmMutationConverter converter; - - public ReactiveRestrictedDeleteExecutionDelegate( - EntityMappingType entityDescriptor, - TemporaryTable idTable, - AfterUseAction afterUseAction, - SqmDeleteStatement sqmDelete, - DomainParameterXref domainParameterXref, - Function sessionUidAccess, - QueryOptions queryOptions, - LoadQueryInfluencers loadQueryInfluencers, - QueryParameterBindings queryParameterBindings, - SessionFactoryImplementor sessionFactory) { - this.entityDescriptor = entityDescriptor; - this.idTable = idTable; - this.afterUseAction = afterUseAction; - this.sqmDelete = sqmDelete; - this.domainParameterXref = domainParameterXref; - this.sessionUidAccess = sessionUidAccess; - this.sessionFactory = sessionFactory; - this.converter = new MultiTableSqmMutationConverter( - entityDescriptor, - sqmDelete, - sqmDelete.getTarget(), - domainParameterXref, - queryOptions, - loadQueryInfluencers, - queryParameterBindings, - sessionFactory.getSqlTranslationEngine() - ); - } - - @Override - public CompletionStage reactiveExecute(DomainQueryExecutionContext executionContext) { - final EntityPersister entityDescriptor = sessionFactory.getRuntimeMetamodels() - .getMappingMetamodel() - .getEntityDescriptor( sqmDelete.getTarget().getEntityName() ); - final String hierarchyRootTableName = entityDescriptor.getTableName(); - - final TableGroup deletingTableGroup = converter.getMutatingTableGroup(); - - final TableReference hierarchyRootTableReference = deletingTableGroup.resolveTableReference( - deletingTableGroup.getNavigablePath(), - hierarchyRootTableName - ); - assert hierarchyRootTableReference != null; - - // Use the converter to interpret the where-clause. We do this for 2 reasons: - // 1) the resolved Predicate is ultimately the base for applying restriction to the deletes - // 2) we also inspect each ColumnReference that is part of the where-clause to see which - // table it comes from. If all the referenced columns (if any at all) are from the root table - // we can perform all the deletes without using an id-table - final Predicate specifiedRestriction = converter.visitWhereClause( sqmDelete.getWhereClause() ); - - final PredicateCollector predicateCollector = new PredicateCollector( specifiedRestriction ); - entityDescriptor.applyBaseRestrictions( - predicateCollector, - deletingTableGroup, - true, - executionContext.getSession().getLoadQueryInfluencers().getEnabledFilters(), - false, - null, - converter - ); - - converter.pruneTableGroupJoins(); - final ColumnReferenceCheckingSqlAstWalker walker = new ColumnReferenceCheckingSqlAstWalker( - hierarchyRootTableReference.getIdentificationVariable() - ); - if ( predicateCollector.getPredicate() != null ) { - predicateCollector.getPredicate().accept( walker ); - } - - // We need an id table if we want to delete from an intermediate table to avoid FK violations - // The intermediate table has a FK to the root table, so we can't delete from the root table first - // Deleting from the intermediate table first also isn't possible, - // because that is the source for deletion in other tables, hence we need an id table - final boolean needsIdTable = !walker.isAllColumnReferencesFromIdentificationVariable() - || entityDescriptor != entityDescriptor.getRootEntityDescriptor(); - - final SqmJdbcExecutionContextAdapter executionContextAdapter = SqmJdbcExecutionContextAdapter.omittingLockingAndPaging( - executionContext ); - - if ( needsIdTable ) { - return executeWithIdTable( - predicateCollector.getPredicate(), - converter.getJdbcParamsBySqmParam(), - converter.getSqmParameterMappingModelExpressibleResolutions(), - executionContextAdapter - ); - } - else { - return executeWithoutIdTable( - predicateCollector.getPredicate(), - deletingTableGroup, - converter.getJdbcParamsBySqmParam(), - converter.getSqmParameterMappingModelExpressibleResolutions(), - converter.getSqlExpressionResolver(), - executionContextAdapter - ); - } - } - - private CompletionStage executeWithoutIdTable( - Predicate suppliedPredicate, - TableGroup tableGroup, - Map, List>> restrictionSqmParameterResolutions, - Map, MappingModelExpressible> paramTypeResolutions, - SqlExpressionResolver sqlExpressionResolver, - ExecutionContext executionContext) { - assert entityDescriptor == entityDescriptor.getRootEntityDescriptor(); - - final EntityPersister rootEntityPersister = entityDescriptor.getEntityPersister(); - final String rootTableName = rootEntityPersister.getTableName(); - final NamedTableReference rootTableReference = (NamedTableReference) tableGroup.resolveTableReference( - tableGroup.getNavigablePath(), - rootTableName - ); - - final QuerySpec matchingIdSubQuerySpec = ExecuteWithoutIdTableHelper.createIdMatchingSubQuerySpec( - tableGroup.getNavigablePath(), - rootTableReference, - suppliedPredicate, - rootEntityPersister, - sqlExpressionResolver, - sessionFactory - ); - - final JdbcParameterBindings jdbcParameterBindings = SqmUtil.createJdbcParameterBindings( - executionContext.getQueryParameterBindings(), - domainParameterXref, - SqmUtil.generateJdbcParamsXref( - domainParameterXref, - () -> restrictionSqmParameterResolutions - ), - new SqmParameterMappingModelResolutionAccess() { - @Override @SuppressWarnings("unchecked") - public MappingModelExpressible getResolvedMappingModelType(SqmParameter parameter) { - return (MappingModelExpressible) paramTypeResolutions.get(parameter); - } - }, - executionContext.getSession() - ); - - - CompletionStage cleanUpCollectionTablesStage = ReactiveSqmMutationStrategyHelper.cleanUpCollectionTables( - entityDescriptor, - (tableReference, attributeMapping) -> { - // No need for a predicate if there is no supplied predicate i.e. this is a full cleanup - if ( suppliedPredicate == null ) { - return null; - } - final ForeignKeyDescriptor fkDescriptor = attributeMapping.getKeyDescriptor(); - final QuerySpec idSelectFkSubQuery; - // todo (6.0): based on the location of the attribute mapping, we could prune the table group of the subquery - if ( fkDescriptor.getTargetPart().isEntityIdentifierMapping() ) { - idSelectFkSubQuery = matchingIdSubQuerySpec; - } - else { - idSelectFkSubQuery = ExecuteWithoutIdTableHelper.createIdMatchingSubQuerySpec( - tableGroup.getNavigablePath(), - rootTableReference, - suppliedPredicate, - rootEntityPersister, - sqlExpressionResolver, - sessionFactory - ); - } - return new InSubQueryPredicate( - MappingModelCreationHelper.buildColumnReferenceExpression( - new MutatingTableReferenceGroupWrapper( - new NavigablePath( attributeMapping.getRootPathName() ), - attributeMapping, - (NamedTableReference) tableReference - ), - fkDescriptor, - null, - sessionFactory - ), - idSelectFkSubQuery, - false - ); - - }, - jdbcParameterBindings, - executionContext - ); - - final CompletionStage[] deleteFromNonRootStages = new CompletionStage[] { voidFuture() }; - if ( rootTableReference instanceof UnionTableReference ) { - final MutableInteger rows = new MutableInteger(); - return cleanUpCollectionTablesStage - .thenCompose( v -> visitUnionTableReferences( - suppliedPredicate, - tableGroup, - sqlExpressionResolver, - executionContext, - matchingIdSubQuerySpec, - jdbcParameterBindings, - deleteFromNonRootStages, - rows - ) ) - .thenApply( o -> rows.get() ); - } - else { - entityDescriptor.visitConstraintOrderedTables( - (tableExpression, tableKeyColumnVisitationSupplier) -> { - if ( !tableExpression.equals( rootTableName ) ) { - final NamedTableReference tableReference = (NamedTableReference) tableGroup.getTableReference( - tableGroup.getNavigablePath(), - tableExpression, - true - ); - final QuerySpec idMatchingSubQuerySpec; - // No need for a predicate if there is no supplied predicate i.e. this is a full cleanup - idMatchingSubQuerySpec = suppliedPredicate == null ? null : matchingIdSubQuerySpec; - CompletableFuture future = new CompletableFuture<>(); - deleteFromNonRootStages[0] = deleteFromNonRootStages[0] - .thenCompose( v -> future ); - try { - deleteFromNonRootTableWithoutIdTable( - tableReference, - tableKeyColumnVisitationSupplier, - sqlExpressionResolver, - tableGroup, - idMatchingSubQuerySpec, - jdbcParameterBindings, - executionContext - ) - .thenCompose( CompletionStages::voidFuture ) - .whenComplete( (unused, throwable) -> { - if ( throwable == null ) { - future.complete( unused ); - } - else { - future.completeExceptionally( throwable ); - } - } ); - } - catch (Throwable t) { - future.completeExceptionally( t ); - } - } - } - ); - - return deleteFromNonRootStages[0] - .thenCompose( v -> deleteFromRootTableWithoutIdTable( - rootTableReference, - suppliedPredicate, - jdbcParameterBindings, - executionContext - ) ); - } - } - - private CompletionStage visitUnionTableReferences( - Predicate suppliedPredicate, - TableGroup tableGroup, - SqlExpressionResolver sqlExpressionResolver, - ExecutionContext executionContext, - QuerySpec matchingIdSubQuerySpec, - JdbcParameterBindings jdbcParameterBindings, - CompletionStage[] deleteFromNonRootStages, - MutableInteger rows) { - entityDescriptor.visitConstraintOrderedTables( - (tableExpression, tableKeyColumnVisitationSupplier) -> { - final NamedTableReference tableReference = new NamedTableReference( - tableExpression, - tableGroup.getPrimaryTableReference().getIdentificationVariable() - ); - final QuerySpec idMatchingSubQuerySpec; - // No need for a predicate if there is no supplied predicate i.e. this is a full cleanup - idMatchingSubQuerySpec = suppliedPredicate == null ? null : matchingIdSubQuerySpec; - CompletableFuture future = new CompletableFuture<>(); - deleteFromNonRootStages[0] = deleteFromNonRootStages[0] - .thenCompose( v -> future ); - deleteFromNonRootTableWithoutIdTable( - tableReference, - tableKeyColumnVisitationSupplier, - sqlExpressionResolver, - tableGroup, - idMatchingSubQuerySpec, - jdbcParameterBindings, - executionContext - ) - .thenAccept( rows::plus ) - .whenComplete( (unused, throwable) -> { - if ( throwable == null ) { - future.complete( unused ); - } - else { - future.completeExceptionally( throwable ); - } - } ); - } - ); - return deleteFromNonRootStages[0]; - } - - private CompletionStage deleteFromRootTableWithoutIdTable( - NamedTableReference rootTableReference, - Predicate predicate, - JdbcParameterBindings jdbcParameterBindings, - ExecutionContext executionContext) { - return executeSqlDelete( - new DeleteStatement( rootTableReference, predicate ), - jdbcParameterBindings, - executionContext - ); - } - - private CompletionStage deleteFromNonRootTableWithoutIdTable( - NamedTableReference targetTableReference, - Supplier> tableKeyColumnVisitationSupplier, - SqlExpressionResolver sqlExpressionResolver, - TableGroup rootTableGroup, - QuerySpec matchingIdSubQuerySpec, - JdbcParameterBindings jdbcParameterBindings, - ExecutionContext executionContext) { - assert targetTableReference != null; - LOG.tracef( "deleteFromNonRootTable - %s", targetTableReference.getTableExpression() ); - - final NamedTableReference deleteTableReference = new NamedTableReference( - targetTableReference.getTableExpression(), - DeleteStatement.DEFAULT_ALIAS, - true - ); - final Predicate tableDeletePredicate; - if ( matchingIdSubQuerySpec == null ) { - tableDeletePredicate = null; - } - else { - /* - * delete from sub_table - * where sub_id in ( - * select root_id from root_table - * where {predicate} - * ) - */ - - /* - * Create the `sub_id` reference as the LHS of the in-subquery predicate - */ - final List deletingTableColumnRefs = new ArrayList<>(); - tableKeyColumnVisitationSupplier.get().accept( - (columnIndex, selection) -> { - assert deleteTableReference.getTableReference( selection.getContainingTableExpression() ) != null; - - final Expression expression = sqlExpressionResolver.resolveSqlExpression( - deleteTableReference, - selection - ); - - deletingTableColumnRefs.add( (ColumnReference) expression ); - } - ); - - final Expression deletingTableColumnRefsExpression = deletingTableColumnRefs.size() == 1 - ? deletingTableColumnRefs.get( 0 ) - : new SqlTuple( deletingTableColumnRefs, entityDescriptor.getIdentifierMapping() ); - - tableDeletePredicate = new InSubQueryPredicate( - deletingTableColumnRefsExpression, - matchingIdSubQuerySpec, - false - ); - } - - final DeleteStatement sqlAstDelete = new DeleteStatement( deleteTableReference, tableDeletePredicate ); - return executeSqlDelete( - sqlAstDelete, - jdbcParameterBindings, - executionContext - ).thenApply( rows -> { - LOG.debugf( "deleteFromNonRootTable - `%s` : %s rows", targetTableReference, rows ); - return rows; - } ); - } - - private static CompletionStage executeSqlDelete( - DeleteStatement sqlAst, - JdbcParameterBindings jdbcParameterBindings, - ExecutionContext executionContext) { - final SessionFactoryImplementor factory = executionContext.getSession().getFactory(); - - final JdbcServices jdbcServices = factory.getJdbcServices(); - - final JdbcOperationQueryMutation jdbcDelete = jdbcServices.getJdbcEnvironment() - .getSqlAstTranslatorFactory() - .buildMutationTranslator( factory, sqlAst ) - .translate( jdbcParameterBindings, executionContext.getQueryOptions() ); - - return StandardReactiveJdbcMutationExecutor.INSTANCE - .executeReactive( - jdbcDelete, - jdbcParameterBindings, - sql -> executionContext.getSession() - .getJdbcCoordinator() - .getStatementPreparer() - .prepareStatement( sql ), - (integer, preparedStatement) -> {}, - executionContext - ); - } - - private CompletionStage executeWithIdTable( - Predicate predicate, - Map, List>> restrictionSqmParameterResolutions, - Map, MappingModelExpressible> paramTypeResolutions, - ExecutionContext executionContext) { - final JdbcParameterBindings jdbcParameterBindings = SqmUtil.createJdbcParameterBindings( - executionContext.getQueryParameterBindings(), - domainParameterXref, - SqmUtil.generateJdbcParamsXref( - domainParameterXref, - () -> restrictionSqmParameterResolutions - ), - new SqmParameterMappingModelResolutionAccess() { - @Override @SuppressWarnings("unchecked") - public MappingModelExpressible getResolvedMappingModelType(SqmParameter parameter) { - return (MappingModelExpressible) paramTypeResolutions.get(parameter); - } - }, - executionContext.getSession() - ); - - return ReactiveExecuteWithTemporaryTableHelper - .performBeforeTemporaryTableUseActions( idTable, executionContext ) - .thenCompose( v -> executeUsingIdTable( predicate, executionContext, jdbcParameterBindings ) - .handle( CompletionStages::handle ) - .thenCompose( resultHandler -> ReactiveExecuteWithTemporaryTableHelper - .performAfterTemporaryTableUseActions( - idTable, - sessionUidAccess, - afterUseAction, - executionContext - ) - .thenCompose( resultHandler::getResultAsCompletionStage ) - ) - ); - } - - private CompletionStage executeUsingIdTable( - Predicate predicate, - ExecutionContext executionContext, - JdbcParameterBindings jdbcParameterBindings) { - return ReactiveExecuteWithTemporaryTableHelper.saveMatchingIdsIntoIdTable( - converter, - predicate, - idTable, - sessionUidAccess, - jdbcParameterBindings, - executionContext ) - .thenCompose( rows -> { - final QuerySpec idTableIdentifierSubQuery = createIdTableSelectQuerySpec( - idTable, - sessionUidAccess, - entityDescriptor, - executionContext - ); - - return ReactiveSqmMutationStrategyHelper.cleanUpCollectionTables( - entityDescriptor, - (tableReference, attributeMapping) -> { - final ForeignKeyDescriptor fkDescriptor = attributeMapping.getKeyDescriptor(); - final QuerySpec idTableFkSubQuery = fkDescriptor.getTargetPart() - .isEntityIdentifierMapping() - ? idTableIdentifierSubQuery - : createIdTableSelectQuerySpec( idTable, fkDescriptor.getTargetPart(), sessionUidAccess, entityDescriptor, executionContext ); - return new InSubQueryPredicate( - MappingModelCreationHelper.buildColumnReferenceExpression( - new MutatingTableReferenceGroupWrapper( - new NavigablePath( attributeMapping.getRootPathName() ), - attributeMapping, - (NamedTableReference) tableReference - ), - fkDescriptor, - null, - sessionFactory - ), - idTableFkSubQuery, - false - ); - - }, - JdbcParameterBindings.NO_BINDINGS, - executionContext - ).thenCompose( unused -> visitConstraintOrderedTables( idTableIdentifierSubQuery, executionContext ) - .thenApply( v -> rows ) ); - } ); - } - - private CompletionStage visitConstraintOrderedTables( - QuerySpec idTableIdentifierSubQuery, - ExecutionContext executionContext) { - final Completable completable = new Completable<>(); - entityDescriptor - .visitConstraintOrderedTables( (tableExpression, tableKeyColumnVisitationSupplier) -> deleteFromTableUsingIdTable( - tableExpression, - tableKeyColumnVisitationSupplier, - idTableIdentifierSubQuery, - executionContext - ) - .handle( completable::complete ) - ); - return completable.getStage().thenCompose( CompletionStages::voidFuture ); - } - - private CompletionStage deleteFromTableUsingIdTable( - String tableExpression, - Supplier> tableKeyColumnVisitationSupplier, - QuerySpec idTableSubQuery, - ExecutionContext executionContext) { - LOG.tracef( "deleteFromTableUsingIdTable - %s", tableExpression ); - - final TableKeyExpressionCollector keyColumnCollector = new TableKeyExpressionCollector( entityDescriptor ); - final NamedTableReference targetTable = new NamedTableReference( - tableExpression, - DeleteStatement.DEFAULT_ALIAS, - true - ); - - tableKeyColumnVisitationSupplier.get().accept( - (columnIndex, selection) -> { - assert selection.getContainingTableExpression().equals( tableExpression ); - assert !selection.isFormula(); - assert selection.getCustomReadExpression() == null; - assert selection.getCustomWriteExpression() == null; - - keyColumnCollector - .apply( new ColumnReference( targetTable, selection ) ); - } - ); - - final InSubQueryPredicate predicate = new InSubQueryPredicate( - keyColumnCollector.buildKeyExpression(), - idTableSubQuery, - false - ); - - return executeSqlDelete( - new DeleteStatement( targetTable, predicate ), - JdbcParameterBindings.NO_BINDINGS, - executionContext - ); - } - -} diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveTableBasedDeleteHandler.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveTableBasedDeleteHandler.java index bb9ceff9c..66a6ee910 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveTableBasedDeleteHandler.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveTableBasedDeleteHandler.java @@ -6,67 +6,147 @@ package org.hibernate.reactive.query.sqm.mutation.internal.temptable; import java.lang.invoke.MethodHandles; +import java.util.UUID; import java.util.concurrent.CompletionStage; import java.util.function.Function; import org.hibernate.dialect.temptable.TemporaryTable; -import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.dialect.temptable.TemporaryTableStrategy; import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.internal.util.MutableObject; +import org.hibernate.persister.entity.UnionSubclassEntityPersister; import org.hibernate.query.spi.DomainQueryExecutionContext; +import org.hibernate.query.sqm.internal.CacheableSqmInterpretation; import org.hibernate.query.sqm.internal.DomainParameterXref; +import org.hibernate.query.sqm.internal.SqmJdbcExecutionContextAdapter; import org.hibernate.query.sqm.mutation.internal.temptable.TableBasedDeleteHandler; -import org.hibernate.query.sqm.mutation.spi.AfterUseAction; import org.hibernate.query.sqm.tree.delete.SqmDeleteStatement; import org.hibernate.reactive.logging.impl.Log; import org.hibernate.reactive.logging.impl.LoggerFactory; import org.hibernate.reactive.query.sqm.mutation.spi.ReactiveAbstractMutationHandler; +import org.hibernate.reactive.sql.exec.internal.StandardReactiveJdbcMutationExecutor; +import org.hibernate.reactive.util.impl.CompletionStages; +import org.hibernate.sql.ast.tree.expression.JdbcParameter; +import org.hibernate.sql.ast.tree.insert.InsertSelectStatement; +import org.hibernate.sql.exec.internal.JdbcParameterBindingImpl; +import org.hibernate.sql.exec.internal.JdbcParameterBindingsImpl; +import org.hibernate.sql.exec.spi.JdbcOperationQueryMutation; +import org.hibernate.sql.exec.spi.JdbcParameterBindings; + +import static org.hibernate.reactive.util.impl.CompletionStages.loop; public class ReactiveTableBasedDeleteHandler extends TableBasedDeleteHandler implements ReactiveAbstractMutationHandler { private static final Log LOG = LoggerFactory.make( Log.class, MethodHandles.lookup() ); - public interface ReactiveExecutionDelegate extends TableBasedDeleteHandler.ExecutionDelegate { - - @Override - default int execute(DomainQueryExecutionContext executionContext) { - throw LOG.nonReactiveMethodCall( "reactiveExecute" ); - } - - CompletionStage reactiveExecute(DomainQueryExecutionContext executionContext); - } - public ReactiveTableBasedDeleteHandler( SqmDeleteStatement sqmDeleteStatement, DomainParameterXref domainParameterXref, TemporaryTable idTable, - AfterUseAction afterUseAction, + TemporaryTableStrategy temporaryTableStrategy, + boolean forceDropAfterUse, Function sessionUidAccess, - SessionFactoryImplementor sessionFactory) { - super( sqmDeleteStatement, domainParameterXref, idTable, afterUseAction, sessionUidAccess, sessionFactory ); + DomainQueryExecutionContext context, + MutableObject firstJdbcParameterBindingsConsumer) { + super( + sqmDeleteStatement, + domainParameterXref, + idTable, + temporaryTableStrategy, + forceDropAfterUse, + sessionUidAccess, + context, + firstJdbcParameterBindingsConsumer + ); } @Override - public CompletionStage reactiveExecute(DomainQueryExecutionContext executionContext) { + public CompletionStage reactiveExecute(JdbcParameterBindings jdbcParameterBindings, DomainQueryExecutionContext context) { if ( LOG.isTraceEnabled() ) { LOG.tracef( "Starting multi-table delete execution - %s", - getSqmDeleteOrUpdateStatement().getRoot().getModel().getName() + getSqmStatement().getTarget().getModel().getName() + ); + } + final SqmJdbcExecutionContextAdapter executionContext = SqmJdbcExecutionContextAdapter.omittingLockingAndPaging( context ); + final CacheableSqmInterpretation idTableInsert = getIdTableInsert(); + final TemporaryTable idTable = getIdTable(); + final StandardReactiveJdbcMutationExecutor jdbcMutationExecutor = StandardReactiveJdbcMutationExecutor.INSTANCE; + if ( idTableInsert != null ) { + return ReactiveExecuteWithTemporaryTableHelper.performBeforeTemporaryTableUseActions( + idTable, + getTemporaryTableStrategy(), + executionContext + ).thenCompose( unused -> + ReactiveExecuteWithTemporaryTableHelper.saveIntoTemporaryTable( idTableInsert.jdbcOperation(), jdbcParameterBindings, executionContext ) + .thenCompose( rows -> executeDelete( jdbcParameterBindings, rows, executionContext, jdbcMutationExecutor ) ) + .handle( CompletionStages::handle ) + .thenCompose( handler -> ReactiveExecuteWithTemporaryTableHelper + .performAfterTemporaryTableUseActions( idTable, getSessionUidAccess(), getAfterUseAction(), executionContext ) + .thenCompose( v -> handler.getResultAsCompletionStage() ) ) + ); + } + else { + return loop(getCollectionTableDeletes() ,delete -> + reactiveExecute( jdbcParameterBindings, delete, jdbcMutationExecutor, executionContext ) + ).thenCompose( v -> { + final int[] rows = { 0 }; + return deleteRows( jdbcParameterBindings, jdbcMutationExecutor, executionContext, rows ) + .thenApply( vv -> rows[0] ); + } ); + } + } + + private CompletionStage deleteRows(JdbcParameterBindings jdbcParameterBindings, StandardReactiveJdbcMutationExecutor jdbcMutationExecutor, SqmJdbcExecutionContextAdapter executionContext, int[] rows) { + if ( getEntityDescriptor() instanceof UnionSubclassEntityPersister ) { + return loop( getDeletes(), delete -> reactiveExecute( jdbcParameterBindings, delete, jdbcMutationExecutor, executionContext ) + .thenApply( tot -> rows[0] += tot ) + ); + } + else { + return loop( getDeletes(), delete -> reactiveExecute( jdbcParameterBindings, delete, jdbcMutationExecutor, executionContext ) + .thenApply( tot -> rows[0] = tot ) + ); + } + } + + private CompletionStage executeDelete( + JdbcParameterBindings jdbcParameterBindings, + Integer rows, + SqmJdbcExecutionContextAdapter executionContext, + StandardReactiveJdbcMutationExecutor jdbcMutationExecutor) { + final JdbcParameterBindings sessionUidBindings = new JdbcParameterBindingsImpl( 1 ); + final JdbcParameter sessionUidParameter = getSessionUidParameter(); + if ( sessionUidParameter != null ) { + sessionUidBindings.addBinding( + sessionUidParameter, + new JdbcParameterBindingImpl( + sessionUidParameter.getExpressionType().getSingleJdbcMapping(), + UUID.fromString( getSessionUidAccess().apply( executionContext.getSession() ) ) + ) ); } - return resolveDelegate( executionContext ).reactiveExecute( executionContext ); + return loop( getCollectionTableDeletes(), delete -> + reactiveExecute( jdbcParameterBindings, delete, jdbcMutationExecutor, executionContext ) + ).thenCompose( v -> loop( getDeletes(), delete -> + reactiveExecute( jdbcParameterBindings, delete, jdbcMutationExecutor, executionContext ) + ) ).thenApply( v -> rows ); } - protected ReactiveExecutionDelegate resolveDelegate(DomainQueryExecutionContext executionContext) { - return new ReactiveRestrictedDeleteExecutionDelegate( - getEntityDescriptor(), - getIdTable(), - getAfterUseAction(), - getSqmDeleteOrUpdateStatement(), - getDomainParameterXref(), - getSessionUidAccess(), - executionContext.getQueryOptions(), - executionContext.getSession().getLoadQueryInfluencers(), - executionContext.getQueryParameterBindings(), - getSessionFactory() + private static CompletionStage reactiveExecute( + JdbcParameterBindings jdbcParameterBindings, + JdbcOperationQueryMutation delete, + StandardReactiveJdbcMutationExecutor jdbcMutationExecutor, + SqmJdbcExecutionContextAdapter executionContext) { + return jdbcMutationExecutor.executeReactive( + delete, + jdbcParameterBindings, + sql -> executionContext.getSession() + .getJdbcCoordinator() + .getStatementPreparer() + .prepareStatement( sql ), + (integer, preparedStatement) -> { + }, + executionContext ); } } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveTableBasedInsertHandler.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveTableBasedInsertHandler.java index d697758ed..194b8b598 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveTableBasedInsertHandler.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveTableBasedInsertHandler.java @@ -5,105 +5,398 @@ */ package org.hibernate.reactive.query.sqm.mutation.internal.temptable; -import java.lang.invoke.MethodHandles; -import java.util.List; -import java.util.Map; -import java.util.concurrent.CompletionStage; -import java.util.function.Function; - import org.hibernate.dialect.temptable.TemporaryTable; -import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.dialect.temptable.TemporaryTableStrategy; import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.generator.BeforeExecutionGenerator; +import org.hibernate.generator.Generator; +import org.hibernate.generator.values.GeneratedValuesMutationDelegate; +import org.hibernate.id.insert.Binder; +import org.hibernate.internal.util.MutableObject; +import org.hibernate.metamodel.mapping.BasicEntityIdentifierMapping; +import org.hibernate.metamodel.mapping.EntityIdentifierMapping; +import org.hibernate.metamodel.mapping.JdbcMapping; +import org.hibernate.persister.entity.EntityPersister; import org.hibernate.query.spi.DomainQueryExecutionContext; import org.hibernate.query.sqm.internal.DomainParameterXref; import org.hibernate.query.sqm.internal.SqmJdbcExecutionContextAdapter; -import org.hibernate.query.sqm.mutation.internal.MultiTableSqmMutationConverter; import org.hibernate.query.sqm.mutation.internal.temptable.TableBasedInsertHandler; -import org.hibernate.query.sqm.mutation.spi.AfterUseAction; import org.hibernate.query.sqm.tree.insert.SqmInsertStatement; +import org.hibernate.reactive.id.insert.ReactiveInsertGeneratedIdentifierDelegate; import org.hibernate.reactive.logging.impl.Log; import org.hibernate.reactive.logging.impl.LoggerFactory; import org.hibernate.reactive.query.sqm.mutation.internal.ReactiveHandler; -import org.hibernate.reactive.query.sqm.mutation.internal.cte.ReactiveInsertExecutionDelegate; +import org.hibernate.reactive.sql.exec.internal.StandardReactiveJdbcMutationExecutor; +import org.hibernate.reactive.sql.exec.internal.StandardReactiveSelectExecutor; +import org.hibernate.reactive.sql.results.spi.ReactiveListResultsConsumer; +import org.hibernate.reactive.util.impl.CompletionStages; import org.hibernate.sql.ast.tree.expression.JdbcParameter; -import org.hibernate.sql.ast.tree.from.TableGroup; -import org.hibernate.sql.ast.tree.from.TableReference; -import org.hibernate.sql.ast.tree.insert.ConflictClause; -import org.hibernate.sql.ast.tree.insert.InsertSelectStatement; -import org.hibernate.sql.ast.tree.update.Assignment; +import org.hibernate.sql.exec.internal.JdbcParameterBindingImpl; +import org.hibernate.sql.exec.internal.JdbcParameterBindingsImpl; import org.hibernate.sql.exec.spi.ExecutionContext; +import org.hibernate.sql.exec.spi.JdbcOperationQueryMutation; +import org.hibernate.sql.exec.spi.JdbcParameterBinder; +import org.hibernate.sql.exec.spi.JdbcParameterBindings; +import org.hibernate.type.descriptor.ValueBinder; -public class ReactiveTableBasedInsertHandler extends TableBasedInsertHandler implements ReactiveHandler { +import java.lang.invoke.MethodHandles; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.CompletionStage; +import java.util.function.Function; +import java.util.stream.IntStream; - private static final Log LOG = LoggerFactory.make( Log.class, MethodHandles.lookup() ); +import static org.hibernate.generator.EventType.INSERT; +import static org.hibernate.reactive.util.impl.CompletionStages.loop; - public interface ReactiveExecutionDelegate extends ExecutionDelegate { - @Override - default int execute(ExecutionContext executionContext) { - throw LOG.nonReactiveMethodCall( "reactiveExecute" ); - } +public class ReactiveTableBasedInsertHandler extends TableBasedInsertHandler implements ReactiveHandler { - CompletionStage reactiveExecute(ExecutionContext executionContext); - } + private static final Log LOG = LoggerFactory.make( Log.class, MethodHandles.lookup() ); public ReactiveTableBasedInsertHandler( SqmInsertStatement sqmInsert, DomainParameterXref domainParameterXref, TemporaryTable entityTable, - AfterUseAction afterUseAction, + TemporaryTableStrategy temporaryTableStrategy, + boolean forceDropAfterUse, Function sessionUidAccess, - SessionFactoryImplementor sessionFactory) { - super( sqmInsert, domainParameterXref, entityTable, afterUseAction, sessionUidAccess, sessionFactory ); + DomainQueryExecutionContext context, + MutableObject firstJdbcParameterBindingsConsumer) { + super( sqmInsert, domainParameterXref, entityTable, temporaryTableStrategy, forceDropAfterUse, sessionUidAccess, context, firstJdbcParameterBindingsConsumer ); } @Override - public CompletionStage reactiveExecute(DomainQueryExecutionContext executionContext) { + public CompletionStage reactiveExecute( + JdbcParameterBindings jdbcParameterBindings, + DomainQueryExecutionContext context) { if ( LOG.isTraceEnabled() ) { LOG.tracef( "Starting multi-table insert execution - %s", - getSqmInsertStatement().getTarget().getModel().getName() + getSqmStatement().getTarget().getModel().getName() ); } - final SqmJdbcExecutionContextAdapter executionContextAdapter = SqmJdbcExecutionContextAdapter - .omittingLockingAndPaging( executionContext ); - return resolveDelegate( executionContext ) - .reactiveExecute( executionContextAdapter ); + final SqmJdbcExecutionContextAdapter executionContext = SqmJdbcExecutionContextAdapter.omittingLockingAndPaging( context ); + // NOTE: we could get rid of using a temporary table if the expressions in Values are "stable". + // But that is a non-trivial optimization that requires more effort + // as we need to split out individual inserts if we have a non-bulk capable optimizer + return ReactiveExecuteWithTemporaryTableHelper.performBeforeTemporaryTableUseActions( + getEntityTable(), + getTemporaryTableStrategy(), + executionContext + ).thenCompose( createdTable -> + ReactiveExecuteWithTemporaryTableHelper.saveIntoTemporaryTable( + getTemporaryTableInsert().jdbcOperation(), + jdbcParameterBindings, + executionContext + ).thenCompose( rows -> { + if ( rows != 0 ) { + final JdbcParameterBindings sessionUidBindings = new JdbcParameterBindingsImpl( 1 ); + final JdbcParameter sessionUidParameter = getSessionUidParameter(); + if ( sessionUidParameter != null ) { + sessionUidBindings.addBinding( + sessionUidParameter, + new JdbcParameterBindingImpl( + sessionUidParameter.getExpressionType().getSingleJdbcMapping(), + UUID.fromString( getSessionUidAccess().apply( executionContext.getSession() ) ) + ) + ); + } + return insertRootTable( rows, createdTable, sessionUidBindings, executionContext ) + .thenCompose( insertedRows -> CompletionStages + .loop( + getNonRootTableInserts(), nonRootTableInsert -> + insertTable( nonRootTableInsert, sessionUidBindings, executionContext ) + ).thenApply( v -> insertedRows ) + ); + } + return CompletionStages.completedFuture( rows ); + } ) ) + .handle( CompletionStages::handle ) + .thenCompose( handler -> ReactiveExecuteWithTemporaryTableHelper + .performAfterTemporaryTableUseActions( + getEntityTable(), + getSessionUidAccess(), + getAfterUseAction(), + executionContext + ) + .thenCompose( v -> handler.getResultAsCompletionStage() ) + ); } - @Override - protected ReactiveExecutionDelegate resolveDelegate(DomainQueryExecutionContext executionContext) { - return (ReactiveExecutionDelegate) super.resolveDelegate( executionContext ); + private CompletionStage insertTable( + JdbcOperationQueryMutation nonRootTableInsert, + JdbcParameterBindings sessionUidBindings, + ExecutionContext executionContext) { + return StandardReactiveJdbcMutationExecutor.INSTANCE + .executeReactive( + nonRootTableInsert, + sessionUidBindings, + sql -> executionContext.getSession() + .getJdbcCoordinator() + .getStatementPreparer() + .prepareStatement( sql ), + (integer, preparedStatement) -> { + }, + executionContext + ).thenCompose( unused -> CompletionStages.voidFuture() ); } - @Override - protected ExecutionDelegate buildExecutionDelegate( - SqmInsertStatement sqmInsert, - MultiTableSqmMutationConverter sqmConverter, - TemporaryTable entityTable, - AfterUseAction afterUseAction, - Function sessionUidAccess, - DomainParameterXref domainParameterXref, - TableGroup insertingTableGroup, - Map tableReferenceByAlias, - List assignments, - InsertSelectStatement insertStatement, - ConflictClause conflictClause, - JdbcParameter sessionUidParameter, - DomainQueryExecutionContext executionContext) { - return new ReactiveInsertExecutionDelegate( - sqmConverter, - entityTable, - afterUseAction, - sessionUidAccess, - domainParameterXref, - insertingTableGroup, - tableReferenceByAlias, - assignments, - insertStatement, - conflictClause, - sessionUidParameter, - executionContext - ); + private CompletionStage insertRootTable( + int rows, + boolean rowNumberStartsAtOne, + JdbcParameterBindings sessionUidBindings, + SqmJdbcExecutionContextAdapter executionContext) { + final EntityPersister entityPersister = getEntityDescriptor().getEntityPersister(); + final Generator generator = entityPersister.getGenerator(); + final EntityIdentifierMapping identifierMapping = entityPersister.getIdentifierMapping(); + + final SharedSessionContractImplementor session = executionContext.getSession(); + final RootTableInserter rootTableInserter = getRootTableInserter(); + + if ( rootTableInserter.temporaryTableIdentitySelect() != null ) { + return StandardReactiveSelectExecutor.INSTANCE.list( + rootTableInserter.temporaryTableIdentitySelect(), + sessionUidBindings, + executionContext, + null, + null, + ReactiveListResultsConsumer.UniqueSemantic.NONE, + rows + ).thenApply( list -> { + Map entityTableToRootIdentity = new LinkedHashMap<>( list.size() ); + for ( Object o : list ) { + entityTableToRootIdentity.put( o, null ); + } + return entityTableToRootIdentity; + } ).thenCompose( entityTableToRootIdentity -> insertRootTable( + sessionUidBindings, + executionContext, + rootTableInserter, + entityPersister, + identifierMapping, + entityTableToRootIdentity, + session + ) ); + } + else { + final Map entityTableToRootIdentity = null; + + if ( rootTableInserter.temporaryTableIdUpdate() != null ) { + final BeforeExecutionGenerator beforeExecutionGenerator = (BeforeExecutionGenerator) generator; + final JdbcParameterBindings updateBindings = new JdbcParameterBindingsImpl( 3 ); + final JdbcParameter sessionUidParameter = getSessionUidParameter(); + if ( sessionUidParameter != null ) { + updateBindings.addBinding( + sessionUidParameter, + new JdbcParameterBindingImpl( + sessionUidParameter.getExpressionType().getSingleJdbcMapping(), + UUID.fromString( getSessionUidAccess().apply( session ) ) + ) + ); + } + final List parameterBinders = rootTableInserter.temporaryTableIdUpdate().getParameterBinders(); + final JdbcParameter rootIdentity = (JdbcParameter) parameterBinders.get( 0 ); + final JdbcParameter rowNumber = (JdbcParameter) parameterBinders.get( 1 ); + final BasicEntityIdentifierMapping basicIdentifierMapping = (BasicEntityIdentifierMapping) identifierMapping; + + if ( !rowNumberStartsAtOne ) { + return ReactiveExecuteWithTemporaryTableHelper.loadInsertedRowNumbers( + rootTableInserter.temporaryTableRowNumberSelectSql(), + getEntityTable(), + getSessionUidAccess(), + rows, + executionContext + ).thenCompose( rowNumbers -> + forEachRow( + rowNumbers, + executionContext, + updateBindings, + rowNumber, + rootIdentity, + basicIdentifierMapping, + beforeExecutionGenerator, + session, + rootTableInserter + ).thenCompose( v -> insertRootTable( + sessionUidBindings, + executionContext, + rootTableInserter, + entityPersister, + identifierMapping, + entityTableToRootIdentity, + session ) + ) + ); + } + else { + final Integer[] rowNumbers = IntStream.range( 1, rows + 1 ).boxed() + .toArray(Integer[]::new); + return forEachRow( + rowNumbers, + executionContext, + updateBindings, + rowNumber, + rootIdentity, + basicIdentifierMapping, + beforeExecutionGenerator, + session, + rootTableInserter + ).thenCompose( v -> insertRootTable( + sessionUidBindings, + executionContext, + rootTableInserter, + entityPersister, + identifierMapping, + entityTableToRootIdentity, + session) + ); + } + } + return insertRootTable( + sessionUidBindings, + executionContext, + rootTableInserter, + entityPersister, + identifierMapping, + entityTableToRootIdentity, + session + ); + } + } + + private static CompletionStage forEachRow( + Integer[] rowNumbers, + SqmJdbcExecutionContextAdapter executionContext, + JdbcParameterBindings updateBindings, + JdbcParameter rowNumber, + JdbcParameter rootIdentity, + BasicEntityIdentifierMapping basicIdentifierMapping, + BeforeExecutionGenerator beforeExecutionGenerator, + SharedSessionContractImplementor session, + RootTableInserter rootTableInserter) { + return loop( rowNumbers, rowNumberValue -> { + updateBindings.addBinding( + rowNumber, + new JdbcParameterBindingImpl( + rowNumber.getExpressionType().getSingleJdbcMapping(), + rowNumberValue + ) + ); + updateBindings.addBinding( + rootIdentity, + new JdbcParameterBindingImpl( + basicIdentifierMapping.getJdbcMapping(), + beforeExecutionGenerator.generate( session, null, null, INSERT ) + ) + ); + return StandardReactiveJdbcMutationExecutor.INSTANCE.executeReactive( + rootTableInserter.temporaryTableIdUpdate(), + updateBindings, + sql -> session + .getJdbcCoordinator() + .getStatementPreparer() + .prepareStatement( sql ), + (integer, preparedStatement) -> { + }, + executionContext + ).thenApply( updateCount -> { + assert updateCount == 1; + return updateCount; + } ); + } ); } + + private CompletionStage insertRootTable( + JdbcParameterBindings sessionUidBindings, + SqmJdbcExecutionContextAdapter executionContext, + RootTableInserter rootTableInserter, + EntityPersister entityPersister, + EntityIdentifierMapping identifierMapping, + Map entityTableToRootIdentity, + SharedSessionContractImplementor session ) { + if ( rootTableInserter.rootTableInsertWithReturningSql() != null ) { + final GeneratedValuesMutationDelegate insertDelegate = entityPersister.getEntityPersister().getInsertDelegate(); + final BasicEntityIdentifierMapping basicIdentifierMapping = (BasicEntityIdentifierMapping) identifierMapping; + // todo 7.0 : InsertGeneratedIdentifierDelegate will be removed once we're going to handle + // generated values within the jdbc insert operaetion itself + final ReactiveInsertGeneratedIdentifierDelegate identifierDelegate = (ReactiveInsertGeneratedIdentifierDelegate) insertDelegate; + final ValueBinder jdbcValueBinder = basicIdentifierMapping.getJdbcMapping().getJdbcValueBinder(); + return loop(entityTableToRootIdentity.entrySet() , entry -> + identifierDelegate.reactivePerformInsertReturning( + rootTableInserter.rootTableInsertWithReturningSql(), + session, + new Binder() { + @Override + public void bindValues(PreparedStatement ps) throws SQLException { + jdbcValueBinder.bind( ps, entry.getKey(), 1, session ); + final JdbcParameter sessionUidParameter = getSessionUidParameter(); + if ( sessionUidParameter != null ) { + sessionUidParameter.getParameterBinder().bindParameterValue( + ps, + 2, + sessionUidBindings, + executionContext + ); + } + } + + @Override + public Object getEntity() { + return null; + } + } + ).thenAccept( generatedValues -> { + entry.setValue( generatedValues.getGeneratedValue( identifierMapping ) ); + } ) + ).thenCompose( unused -> { + final JdbcParameterBindings updateBindings = new JdbcParameterBindingsImpl( 2 ); + + final List parameterBinders = rootTableInserter.temporaryTableIdentityUpdate() + .getParameterBinders(); + final JdbcParameter rootIdentity = (JdbcParameter) parameterBinders.get( 0 ); + final JdbcParameter entityIdentity = (JdbcParameter) parameterBinders.get( 1 ); + return loop(entityTableToRootIdentity.entrySet(), entry -> { + JdbcMapping jdbcMapping = basicIdentifierMapping.getJdbcMapping(); + updateBindings.addBinding( + entityIdentity, + new JdbcParameterBindingImpl( jdbcMapping, entry.getKey() ) + ); + updateBindings.addBinding( + rootIdentity, + new JdbcParameterBindingImpl( jdbcMapping, entry.getValue() ) + ); + return StandardReactiveJdbcMutationExecutor.INSTANCE.executeReactive( + rootTableInserter.temporaryTableIdentityUpdate(), + updateBindings, + sql -> session + .getJdbcCoordinator() + .getStatementPreparer() + .prepareStatement( sql ), + (integer, preparedStatement) -> { + }, + executionContext + ); + }).thenApply( v -> entityTableToRootIdentity.size() ); + }); + } + else { + return StandardReactiveJdbcMutationExecutor.INSTANCE.executeReactive( + rootTableInserter.rootTableInsert(), + sessionUidBindings, + sql -> session + .getJdbcCoordinator() + .getStatementPreparer() + .prepareStatement( sql ), + (integer, preparedStatement) -> { + }, + executionContext + ); + } + } + } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveTableBasedSoftDeleteHandler.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveTableBasedSoftDeleteHandler.java new file mode 100644 index 000000000..e0edb82e2 --- /dev/null +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveTableBasedSoftDeleteHandler.java @@ -0,0 +1,129 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.query.sqm.mutation.internal.temptable; + +import org.hibernate.dialect.temptable.TemporaryTable; +import org.hibernate.dialect.temptable.TemporaryTableStrategy; +import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.internal.util.MutableObject; +import org.hibernate.query.spi.DomainQueryExecutionContext; +import org.hibernate.query.sqm.internal.CacheableSqmInterpretation; +import org.hibernate.query.sqm.internal.DomainParameterXref; +import org.hibernate.query.sqm.internal.SqmJdbcExecutionContextAdapter; +import org.hibernate.query.sqm.mutation.internal.temptable.TableBasedSoftDeleteHandler; +import org.hibernate.query.sqm.tree.delete.SqmDeleteStatement; +import org.hibernate.reactive.logging.impl.Log; +import org.hibernate.reactive.logging.impl.LoggerFactory; +import org.hibernate.reactive.query.sqm.mutation.internal.ReactiveHandler; +import org.hibernate.reactive.sql.exec.internal.StandardReactiveJdbcMutationExecutor; +import org.hibernate.reactive.util.impl.CompletionStages; +import org.hibernate.sql.ast.tree.expression.JdbcParameter; +import org.hibernate.sql.ast.tree.insert.InsertSelectStatement; +import org.hibernate.sql.exec.internal.JdbcParameterBindingImpl; +import org.hibernate.sql.exec.internal.JdbcParameterBindingsImpl; +import org.hibernate.sql.exec.spi.JdbcOperationQueryMutation; +import org.hibernate.sql.exec.spi.JdbcParameterBindings; + +import java.lang.invoke.MethodHandles; +import java.util.UUID; +import java.util.concurrent.CompletionStage; +import java.util.function.Function; + +public class ReactiveTableBasedSoftDeleteHandler extends TableBasedSoftDeleteHandler implements ReactiveHandler { + private static final Log LOG = LoggerFactory.make( Log.class, MethodHandles.lookup() ); + + public ReactiveTableBasedSoftDeleteHandler( + SqmDeleteStatement sqmDelete, + DomainParameterXref domainParameterXref, + TemporaryTable idTable, + TemporaryTableStrategy temporaryTableStrategy, + boolean forceDropAfterUse, + Function sessionUidAccess, + DomainQueryExecutionContext context, + MutableObject firstJdbcParameterBindingsConsumer) { + super( + sqmDelete, + domainParameterXref, + idTable, + temporaryTableStrategy, + forceDropAfterUse, + sessionUidAccess, + context, + firstJdbcParameterBindingsConsumer + ); + } + + @Override + public CompletionStage reactiveExecute( + JdbcParameterBindings jdbcParameterBindings, + DomainQueryExecutionContext context) { + if ( LOG.isTraceEnabled() ) { + LOG.tracef( + "Starting multi-table delete execution - %s", + getSqmStatement().getTarget().getModel().getName() + ); + } + final SqmJdbcExecutionContextAdapter executionContext = SqmJdbcExecutionContextAdapter.omittingLockingAndPaging( context ); + StandardReactiveJdbcMutationExecutor jdbcMutationExecutor = StandardReactiveJdbcMutationExecutor.INSTANCE; + + final CacheableSqmInterpretation idTableInsert = getIdTableInsert(); + if ( idTableInsert != null ) { + return ReactiveExecuteWithTemporaryTableHelper.performBeforeTemporaryTableUseActions( + getIdTable(), + getTemporaryTableStrategy(), + executionContext + ).thenCompose( unused -> + ReactiveExecuteWithTemporaryTableHelper.saveIntoTemporaryTable( + idTableInsert.jdbcOperation(), + jdbcParameterBindings, + executionContext + ).thenCompose( rows -> { + final JdbcParameterBindings sessionUidBindings = new JdbcParameterBindingsImpl( 1 ); + final JdbcParameter sessionUidParameter = getSessionUidParameter(); + if ( sessionUidParameter != null ) { + sessionUidBindings.addBinding( + sessionUidParameter, + new JdbcParameterBindingImpl( + sessionUidParameter.getExpressionType().getSingleJdbcMapping(), + UUID.fromString( getSessionUidAccess().apply( executionContext.getSession() ) ) + ) + ); + } + return jdbcMutationExecutor.executeReactive( + getSoftDelete(), + sessionUidBindings, + sql -> executionContext.getSession() + .getJdbcCoordinator() + .getStatementPreparer() + .prepareStatement( sql ), + (integer, preparedStatement) -> {}, + executionContext + ).thenApply( u -> rows ); + } ) + .handle( CompletionStages::handle) + .thenCompose( handler -> ReactiveExecuteWithTemporaryTableHelper + .performAfterTemporaryTableUseActions( + getIdTable(), + getSessionUidAccess(), + getAfterUseAction(), + executionContext) + .thenCompose( v -> handler.getResultAsCompletionStage() ) ) + ); + } + else { + return jdbcMutationExecutor.executeReactive( + getSoftDelete(), + jdbcParameterBindings, + sql -> executionContext.getSession() + .getJdbcCoordinator() + .getStatementPreparer() + .prepareStatement( sql ), + (integer, preparedStatement) -> {}, + executionContext + ); + } + } +} diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveTableBasedUpdateHandler.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveTableBasedUpdateHandler.java index 7379f962e..d3ba6f6f3 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveTableBasedUpdateHandler.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveTableBasedUpdateHandler.java @@ -6,99 +6,117 @@ package org.hibernate.reactive.query.sqm.mutation.internal.temptable; import java.lang.invoke.MethodHandles; -import java.util.List; -import java.util.Map; import java.util.concurrent.CompletionStage; import java.util.function.Function; +import org.hibernate.AssertionFailure; import org.hibernate.dialect.temptable.TemporaryTable; -import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.dialect.temptable.TemporaryTableStrategy; import org.hibernate.engine.spi.SharedSessionContractImplementor; +import org.hibernate.internal.util.MutableObject; import org.hibernate.query.spi.DomainQueryExecutionContext; import org.hibernate.query.sqm.internal.DomainParameterXref; import org.hibernate.query.sqm.internal.SqmJdbcExecutionContextAdapter; -import org.hibernate.query.sqm.mutation.internal.MultiTableSqmMutationConverter; import org.hibernate.query.sqm.mutation.internal.temptable.TableBasedUpdateHandler; -import org.hibernate.query.sqm.mutation.spi.AfterUseAction; import org.hibernate.query.sqm.tree.update.SqmUpdateStatement; import org.hibernate.reactive.logging.impl.Log; import org.hibernate.reactive.logging.impl.LoggerFactory; import org.hibernate.reactive.query.sqm.mutation.spi.ReactiveAbstractMutationHandler; -import org.hibernate.sql.ast.tree.from.TableGroup; -import org.hibernate.sql.ast.tree.from.TableReference; -import org.hibernate.sql.ast.tree.predicate.Predicate; -import org.hibernate.sql.ast.tree.update.Assignment; +import org.hibernate.reactive.sql.exec.internal.StandardReactiveJdbcMutationExecutor; +import org.hibernate.reactive.util.impl.CompletionStages; import org.hibernate.sql.exec.spi.ExecutionContext; +import org.hibernate.sql.exec.spi.JdbcOperationQueryMutation; +import org.hibernate.sql.exec.spi.JdbcParameterBindings; + +import static org.hibernate.reactive.util.impl.CompletionStages.loop; +import static org.hibernate.reactive.util.impl.CompletionStages.voidFuture; public class ReactiveTableBasedUpdateHandler extends TableBasedUpdateHandler implements ReactiveAbstractMutationHandler { private static final Log LOG = LoggerFactory.make( Log.class, MethodHandles.lookup() ); - public interface ReactiveExecutionDelegate extends TableBasedUpdateHandler.ExecutionDelegate { - @Override - default int execute(ExecutionContext executionContext) { - throw LOG.nonReactiveMethodCall( "reactiveExecute" ); - } - - CompletionStage reactiveExecute(ExecutionContext executionContext); - } - public ReactiveTableBasedUpdateHandler( SqmUpdateStatement sqmUpdate, DomainParameterXref domainParameterXref, TemporaryTable idTable, - AfterUseAction afterUseAction, + TemporaryTableStrategy temporaryTableStrategy, + boolean forceDropAfterUse, Function sessionUidAccess, - SessionFactoryImplementor sessionFactory) { - super( sqmUpdate, domainParameterXref, idTable, afterUseAction, sessionUidAccess, sessionFactory ); + DomainQueryExecutionContext context, + MutableObject firstJdbcParameterBindingsConsumer) { + super( sqmUpdate, domainParameterXref, idTable, temporaryTableStrategy, forceDropAfterUse, sessionUidAccess, context, firstJdbcParameterBindingsConsumer ); } @Override - public int execute(DomainQueryExecutionContext executionContext) { - throw LOG.nonReactiveMethodCall( "reactiveExecute" ); - } - - @Override - public CompletionStage reactiveExecute(DomainQueryExecutionContext executionContext) { + public CompletionStage reactiveExecute(JdbcParameterBindings jdbcParameterBindings, DomainQueryExecutionContext context) { if ( LOG.isTraceEnabled() ) { LOG.tracef( "Starting multi-table update execution - %s", - getSqmDeleteOrUpdateStatement().getRoot().getModel().getName() + getSqmStatement().getTarget().getModel().getName() ); } - final SqmJdbcExecutionContextAdapter executionContextAdapter = SqmJdbcExecutionContextAdapter.omittingLockingAndPaging( executionContext ); - return resolveDelegate( executionContext ).reactiveExecute( executionContextAdapter ); + final SqmJdbcExecutionContextAdapter executionContext = SqmJdbcExecutionContextAdapter.omittingLockingAndPaging( context ); + return ReactiveExecuteWithTemporaryTableHelper.performBeforeTemporaryTableUseActions( + getIdTable(), + getTemporaryTableStrategy(), + executionContext + ).thenCompose( unused -> ReactiveExecuteWithTemporaryTableHelper + .saveIntoTemporaryTable( + getMatchingIdsIntoIdTableInsert().jdbcOperation(), + jdbcParameterBindings, + executionContext + ).thenCompose( rows -> loop(getTableUpdaters(), tableUpdater -> + updateTable( tableUpdater, rows, jdbcParameterBindings, executionContext ) ) + .thenApply(v -> rows)) + .handle( CompletionStages::handle) + .thenCompose( handler -> ReactiveExecuteWithTemporaryTableHelper + .performAfterTemporaryTableUseActions( getIdTable(), getSessionUidAccess(), getAfterUseAction(), executionContext ) + .thenCompose( v -> handler.getResultAsCompletionStage() )) + ); } - @Override - protected ReactiveExecutionDelegate resolveDelegate(DomainQueryExecutionContext executionContext) { - return (ReactiveExecutionDelegate) super.resolveDelegate( executionContext ); + private CompletionStage updateTable( + TableUpdater tableUpdater, + int expectedUpdateCount, + JdbcParameterBindings jdbcParameterBindings, + ExecutionContext executionContext) { + if ( tableUpdater == null ) { + // no assignments for this table - skip it + return voidFuture(); + } + return executeMutation( tableUpdater.jdbcUpdate(), jdbcParameterBindings, executionContext ) + .thenCompose( updateCount -> { + // We are done when the update count matches + if ( updateCount == expectedUpdateCount ) { + return voidFuture(); + } + + // If the table is optional, execute an insert + if ( tableUpdater.jdbcInsert() != null ) { + return executeMutation( tableUpdater.jdbcInsert(), jdbcParameterBindings, executionContext ) + .thenAccept( insertCount -> { + if(insertCount + updateCount != expectedUpdateCount){ + throw new AssertionFailure( "insertCount + updateCount != expectedUpdateCount"); + } + } ); + } + return voidFuture(); + } ); } - @Override - protected ReactiveUpdateExecutionDelegate buildExecutionDelegate( - MultiTableSqmMutationConverter sqmConverter, - TemporaryTable idTable, - AfterUseAction afterUseAction, - Function sessionUidAccess, - DomainParameterXref domainParameterXref, - TableGroup updatingTableGroup, - Map tableReferenceByAlias, - List assignments, - Predicate suppliedPredicate, - DomainQueryExecutionContext executionContext) { - return new ReactiveUpdateExecutionDelegate( - sqmConverter, - idTable, - afterUseAction, - sessionUidAccess, - domainParameterXref, - updatingTableGroup, - tableReferenceByAlias, - assignments, - suppliedPredicate, + private CompletionStage executeMutation(JdbcOperationQueryMutation jdbcUpdate, JdbcParameterBindings jdbcParameterBindings, ExecutionContext executionContext) { + return StandardReactiveJdbcMutationExecutor.INSTANCE.executeReactive( + jdbcUpdate, + jdbcParameterBindings, + sql -> executionContext.getSession() + .getJdbcCoordinator() + .getStatementPreparer() + .prepareStatement( sql ), + (integer, preparedStatement) -> { + }, executionContext ); } + } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveTemporaryTableHelper.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveTemporaryTableHelper.java index 126c168f8..9dde81ab7 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveTemporaryTableHelper.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveTemporaryTableHelper.java @@ -23,7 +23,7 @@ import org.hibernate.reactive.session.ReactiveConnectionSupplier; import org.hibernate.reactive.util.impl.CompletionStages; -import static org.hibernate.reactive.util.impl.CompletionStages.voidFuture; +import static org.hibernate.reactive.util.impl.CompletionStages.falseFuture; /** * @see org.hibernate.dialect.temptable.TemporaryTableHelper @@ -38,13 +38,12 @@ public class ReactiveTemporaryTableHelper { * @see org.hibernate.jdbc.Work */ public interface ReactiveWork { - CompletionStage reactiveExecute(ReactiveConnection connection); + CompletionStage reactiveExecute(ReactiveConnection connection); } public static class TemporaryTableCreationWork implements ReactiveWork { private final TemporaryTable temporaryTable; private final TemporaryTableExporter exporter; - private final SessionFactoryImplementor sessionFactory; public TemporaryTableCreationWork( TemporaryTable temporaryTable, @@ -62,23 +61,25 @@ public TemporaryTableCreationWork( SessionFactoryImplementor sessionFactory) { this.temporaryTable = temporaryTable; this.exporter = exporter; - this.sessionFactory = sessionFactory; } @Override - public CompletionStage reactiveExecute(ReactiveConnection connection) { + public CompletionStage reactiveExecute(ReactiveConnection connection) { try { final String creationCommand = exporter.getSqlCreateCommand( temporaryTable ); return connection.executeUnprepared( creationCommand ) .handle( (integer, throwable) -> { + if ( throwable == null ) { + return true; + } logException( "create", creationCommand, temporaryTable, throwable ); - return null; + return false; } ); } catch (Exception e) { logException( "create", null, temporaryTable, e ); - return voidFuture(); + return falseFuture(); } } } @@ -111,19 +112,22 @@ public TemporaryTableDropWork( } @Override - public CompletionStage reactiveExecute(ReactiveConnection connection) { + public CompletionStage reactiveExecute(ReactiveConnection connection) { try { final String dropCommand = exporter.getSqlDropCommand( temporaryTable ); return connection.update( dropCommand ) .handle( (integer, throwable) -> { + if ( throwable == null ) { + return true; + } logException( "drop", dropCommand, temporaryTable, throwable ); - return null; + return false; } ); } catch (Exception e) { logException( "drop", null, temporaryTable, e ); - return voidFuture(); + return falseFuture(); } } } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveUpdateExecutionDelegate.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveUpdateExecutionDelegate.java deleted file mode 100644 index 927105dfa..000000000 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/internal/temptable/ReactiveUpdateExecutionDelegate.java +++ /dev/null @@ -1,292 +0,0 @@ -/* Hibernate, Relational Persistence for Idiomatic Java - * - * SPDX-License-Identifier: Apache-2.0 - * Copyright: Red Hat Inc. and Hibernate Authors - */ -package org.hibernate.reactive.query.sqm.mutation.internal.temptable; - -import java.lang.invoke.MethodHandles; -import java.sql.PreparedStatement; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Map; -import java.util.concurrent.CompletionStage; -import java.util.function.Consumer; -import java.util.function.Function; -import java.util.function.Supplier; - -import org.hibernate.dialect.temptable.TemporaryTable; -import org.hibernate.engine.jdbc.spi.JdbcServices; -import org.hibernate.engine.spi.SharedSessionContractImplementor; -import org.hibernate.metamodel.mapping.SelectableConsumer; -import org.hibernate.query.spi.DomainQueryExecutionContext; -import org.hibernate.query.sqm.internal.DomainParameterXref; -import org.hibernate.query.sqm.mutation.internal.MultiTableSqmMutationConverter; -import org.hibernate.query.sqm.mutation.internal.temptable.UpdateExecutionDelegate; -import org.hibernate.query.sqm.mutation.spi.AfterUseAction; -import org.hibernate.reactive.logging.impl.Log; -import org.hibernate.reactive.logging.impl.LoggerFactory; -import org.hibernate.reactive.sql.exec.internal.StandardReactiveJdbcMutationExecutor; -import org.hibernate.reactive.util.impl.CompletionStages; -import org.hibernate.sql.ast.SqlAstTranslatorFactory; -import org.hibernate.sql.ast.tree.expression.ColumnReference; -import org.hibernate.sql.ast.tree.expression.Expression; -import org.hibernate.sql.ast.tree.expression.SqlTuple; -import org.hibernate.sql.ast.tree.from.NamedTableReference; -import org.hibernate.sql.ast.tree.from.TableGroup; -import org.hibernate.sql.ast.tree.from.TableReference; -import org.hibernate.sql.ast.tree.insert.InsertSelectStatement; -import org.hibernate.sql.ast.tree.predicate.ExistsPredicate; -import org.hibernate.sql.ast.tree.predicate.InSubQueryPredicate; -import org.hibernate.sql.ast.tree.predicate.Predicate; -import org.hibernate.sql.ast.tree.select.QuerySpec; -import org.hibernate.sql.ast.tree.update.Assignment; -import org.hibernate.sql.ast.tree.update.UpdateStatement; -import org.hibernate.sql.exec.spi.ExecutionContext; -import org.hibernate.sql.exec.spi.JdbcOperationQueryMutation; -import org.hibernate.sql.results.internal.SqlSelectionImpl; - -import static org.hibernate.reactive.query.sqm.mutation.internal.temptable.ReactiveExecuteWithTemporaryTableHelper.createIdTableSelectQuerySpec; -import static org.hibernate.reactive.query.sqm.mutation.internal.temptable.ReactiveExecuteWithTemporaryTableHelper.performAfterTemporaryTableUseActions; -import static org.hibernate.reactive.query.sqm.mutation.internal.temptable.ReactiveExecuteWithTemporaryTableHelper.performBeforeTemporaryTableUseActions; -import static org.hibernate.reactive.query.sqm.mutation.internal.temptable.ReactiveExecuteWithTemporaryTableHelper.saveMatchingIdsIntoIdTable; -import static org.hibernate.reactive.util.impl.CompletionStages.voidFuture; - -public class ReactiveUpdateExecutionDelegate extends UpdateExecutionDelegate implements ReactiveTableBasedUpdateHandler.ReactiveExecutionDelegate { - - private static final Log LOG = LoggerFactory.make( Log.class, MethodHandles.lookup() ); - - public ReactiveUpdateExecutionDelegate( - MultiTableSqmMutationConverter sqmConverter, - TemporaryTable idTable, - AfterUseAction afterUseAction, - Function sessionUidAccess, - DomainParameterXref domainParameterXref, - TableGroup updatingTableGroup, - Map tableReferenceByAlias, - List assignments, - Predicate suppliedPredicate, - DomainQueryExecutionContext executionContext) { - super( - sqmConverter, - idTable, - afterUseAction, - sessionUidAccess, - domainParameterXref, - updatingTableGroup, - tableReferenceByAlias, - assignments, - suppliedPredicate, - executionContext - ); - } - - private static void doNothing(Integer integer, PreparedStatement preparedStatement) { - } - - @Override - public int execute(ExecutionContext executionContext) { - throw LOG.nonReactiveMethodCall( "reactiveExecute" ); - } - - @Override - public CompletionStage reactiveExecute(ExecutionContext executionContext) { - return performBeforeTemporaryTableUseActions( - getIdTable(), - executionContext - ) - .thenCompose( v -> saveMatchingIdsIntoIdTable( - getSqmConverter(), - getSuppliedPredicate(), - getIdTable(), - getSessionUidAccess(), - getJdbcParameterBindings(), - executionContext - ) ) - .thenCompose( rows -> { - final QuerySpec idTableSubQuery = createIdTableSelectQuerySpec( - getIdTable(), - getSessionUidAccess(), - getEntityDescriptor(), - executionContext - ); - - final CompletionStage[] resultStage = new CompletionStage[] { voidFuture() }; - getEntityDescriptor().visitConstraintOrderedTables( - (tableExpression, tableKeyColumnVisitationSupplier) -> resultStage[0] = resultStage[0].thenCompose( - v -> reactiveUpdateTable( - tableExpression, - tableKeyColumnVisitationSupplier, - rows, - idTableSubQuery, - executionContext - ) ) - ); - return resultStage[0].thenApply( v -> rows ); - }) - .handle( CompletionStages::handle ) - .thenCompose( handler -> performAfterTemporaryTableUseActions( - getIdTable(), - getSessionUidAccess(), - getAfterUseAction(), - executionContext - ) - .thenCompose( handler::getResultAsCompletionStage ) - ); - } - - private CompletionStage reactiveUpdateTable( - String tableExpression, - Supplier> tableKeyColumnVisitationSupplier, - int expectedUpdateCount, - QuerySpec idTableSubQuery, - ExecutionContext executionContext) { - - // update `updatingTableReference` - // set ... - // where `keyExpression` in ( `idTableSubQuery` ) - - final TableReference updatingTableReference = getUpdatingTableGroup().getTableReference( - getUpdatingTableGroup().getNavigablePath(), - tableExpression, - true - ); - - final List assignments = getAssignmentsByTable().get( updatingTableReference ); - if ( assignments == null || assignments.isEmpty() ) { - // no assignments for this table - skip it - return voidFuture(); - } - - final NamedTableReference dmlTableReference = resolveUnionTableReference( updatingTableReference, tableExpression ); - final JdbcServices jdbcServices = getSessionFactory().getJdbcServices(); - final SqlAstTranslatorFactory sqlAstTranslatorFactory = jdbcServices.getJdbcEnvironment().getSqlAstTranslatorFactory(); - - final Expression keyExpression = resolveMutatingTableKeyExpression( tableExpression, tableKeyColumnVisitationSupplier ); - - return executeUpdate( idTableSubQuery, executionContext, assignments, dmlTableReference, sqlAstTranslatorFactory, keyExpression ) - .thenCompose( updateCount -> { - // We are done when the update count matches - if ( updateCount == expectedUpdateCount ) { - return voidFuture(); - } - // If the table is optional, execute an insert - if ( isTableOptional( tableExpression ) ) { - return executeInsert( - tableExpression, - dmlTableReference, - keyExpression, - tableKeyColumnVisitationSupplier, - idTableSubQuery, - assignments, - sqlAstTranslatorFactory, - executionContext - ) - .thenAccept( insertCount -> { - assert insertCount + updateCount == expectedUpdateCount; - } ); - } - return voidFuture(); - } ); - } - - - private CompletionStage executeUpdate(QuerySpec idTableSubQuery, ExecutionContext executionContext, List assignments, NamedTableReference dmlTableReference, SqlAstTranslatorFactory sqlAstTranslatorFactory, Expression keyExpression) { - final UpdateStatement sqlAst = new UpdateStatement( - dmlTableReference, - assignments, - new InSubQueryPredicate( keyExpression, idTableSubQuery, false ) - ); - - final JdbcOperationQueryMutation jdbcUpdate = sqlAstTranslatorFactory - .buildMutationTranslator( getSessionFactory(), sqlAst ) - .translate( getJdbcParameterBindings(), executionContext.getQueryOptions() ); - - return StandardReactiveJdbcMutationExecutor.INSTANCE - .executeReactive( - jdbcUpdate, - getJdbcParameterBindings(), - executionContext.getSession() - .getJdbcCoordinator() - .getStatementPreparer() - ::prepareStatement, - ReactiveUpdateExecutionDelegate::doNothing, - executionContext - ); - } - - private CompletionStage executeInsert( - String targetTableExpression, - NamedTableReference targetTableReference, - Expression targetTableKeyExpression, - Supplier> tableKeyColumnVisitationSupplier, - QuerySpec idTableSubQuery, - List assignments, - SqlAstTranslatorFactory sqlAstTranslatorFactory, - ExecutionContext executionContext) { - - // Execute a query in the form - - // - // insert into (...) - // select ... - // from temptable_ - // where not exists ( - // select 1 - // from dml_ - // where dml_. = temptable_. - // ) - - // Create a new QuerySpec for the "insert source" select query. This - // is mostly a copy of the incoming `idTableSubQuery` along with the - // NOT-EXISTS predicate - final QuerySpec insertSourceSelectQuerySpec = makeInsertSourceSelectQuerySpec( idTableSubQuery ); - - // create the `select 1 ...` sub-query and apply the not-exists predicate - final QuerySpec existsSubQuerySpec = createExistsSubQuerySpec( targetTableExpression, tableKeyColumnVisitationSupplier, idTableSubQuery ); - insertSourceSelectQuerySpec.applyPredicate( - new ExistsPredicate( - existsSubQuerySpec, - true, - getSessionFactory().getTypeConfiguration().getBasicTypeForJavaType( Boolean.class ) - ) - ); - - // Collect the target column references from the key expressions - final List targetColumnReferences = new ArrayList<>(); - if ( targetTableKeyExpression instanceof SqlTuple ) { - //noinspection unchecked - targetColumnReferences.addAll( (Collection) ( (SqlTuple) targetTableKeyExpression ).getExpressions() ); - } - else { - targetColumnReferences.add( (ColumnReference) targetTableKeyExpression ); - } - - // And transform assignments to target column references and selections - for ( Assignment assignment : assignments ) { - targetColumnReferences.addAll( assignment.getAssignable().getColumnReferences() ); - insertSourceSelectQuerySpec.getSelectClause() - .addSqlSelection( new SqlSelectionImpl( assignment.getAssignedValue() ) ); - } - - final InsertSelectStatement insertSqlAst = new InsertSelectStatement( targetTableReference ); - insertSqlAst.addTargetColumnReferences( targetColumnReferences.toArray( new ColumnReference[0] ) ); - insertSqlAst.setSourceSelectStatement( insertSourceSelectQuerySpec ); - - final JdbcOperationQueryMutation jdbcInsert = sqlAstTranslatorFactory - .buildMutationTranslator( getSessionFactory(), insertSqlAst ) - .translate( getJdbcParameterBindings(), executionContext.getQueryOptions() ); - - return StandardReactiveJdbcMutationExecutor.INSTANCE - .executeReactive( - jdbcInsert, - getJdbcParameterBindings(), - executionContext.getSession() - .getJdbcCoordinator() - .getStatementPreparer() - ::prepareStatement, - ReactiveUpdateExecutionDelegate::doNothing, - executionContext - ); - } -} diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/spi/ReactiveAbstractMutationHandler.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/spi/ReactiveAbstractMutationHandler.java index a4a622537..f3a3b9a42 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/spi/ReactiveAbstractMutationHandler.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/spi/ReactiveAbstractMutationHandler.java @@ -8,7 +8,6 @@ import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.metamodel.mapping.EntityMappingType; -import org.hibernate.query.sqm.tree.SqmDeleteOrUpdateStatement; import org.hibernate.reactive.query.sqm.mutation.internal.ReactiveHandler; /** @@ -16,8 +15,6 @@ */ public interface ReactiveAbstractMutationHandler extends ReactiveHandler { - SqmDeleteOrUpdateStatement getSqmDeleteOrUpdateStatement(); - EntityMappingType getEntityDescriptor(); SessionFactoryImplementor getSessionFactory(); diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/spi/ReactiveSqmMultiTableInsertStrategy.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/spi/ReactiveSqmMultiTableInsertStrategy.java index dc52d893e..93399180d 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/spi/ReactiveSqmMultiTableInsertStrategy.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/spi/ReactiveSqmMultiTableInsertStrategy.java @@ -10,10 +10,12 @@ import org.hibernate.query.spi.DomainQueryExecutionContext; import org.hibernate.query.sqm.internal.DomainParameterXref; +import org.hibernate.query.sqm.mutation.spi.MultiTableHandlerBuildResult; import org.hibernate.query.sqm.mutation.spi.SqmMultiTableInsertStrategy; import org.hibernate.query.sqm.tree.insert.SqmInsertStatement; import org.hibernate.reactive.logging.impl.Log; import org.hibernate.reactive.logging.impl.LoggerFactory; +import org.hibernate.reactive.query.sqm.mutation.internal.ReactiveHandler; public interface ReactiveSqmMultiTableInsertStrategy extends SqmMultiTableInsertStrategy { @@ -27,8 +29,18 @@ default int executeInsert( throw LOG.nonReactiveMethodCall( "reactiveExecuteInsert" ); } - CompletionStage reactiveExecuteInsert( + /** + * Execute the multi-table insert indicated by the passed SqmInsertStatement + * + * @return The number of rows affected + * @deprecated Uses {@link #buildHandler(SqmInsertStatement, DomainParameterXref, DomainQueryExecutionContext)} instead + */ + @Deprecated(forRemoval = true, since = "3.1") + default CompletionStage reactiveExecuteInsert( SqmInsertStatement sqmInsertStatement, DomainParameterXref domainParameterXref, - DomainQueryExecutionContext context); + DomainQueryExecutionContext context){ + final MultiTableHandlerBuildResult buildResult = buildHandler( sqmInsertStatement, domainParameterXref, context ); + return ((ReactiveHandler)buildResult.multiTableHandler()).reactiveExecute( buildResult.firstJdbcParameterBindings(), context ); + } } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/spi/ReactiveSqmMultiTableMutationStrategy.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/spi/ReactiveSqmMultiTableMutationStrategy.java index 41b1c6224..b4a4bd077 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/spi/ReactiveSqmMultiTableMutationStrategy.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/query/sqm/mutation/spi/ReactiveSqmMultiTableMutationStrategy.java @@ -10,11 +10,14 @@ import org.hibernate.query.spi.DomainQueryExecutionContext; import org.hibernate.query.sqm.internal.DomainParameterXref; +import org.hibernate.query.sqm.mutation.spi.MultiTableHandlerBuildResult; import org.hibernate.query.sqm.mutation.spi.SqmMultiTableMutationStrategy; +import org.hibernate.query.sqm.tree.SqmDeleteOrUpdateStatement; import org.hibernate.query.sqm.tree.delete.SqmDeleteStatement; import org.hibernate.query.sqm.tree.update.SqmUpdateStatement; import org.hibernate.reactive.logging.impl.Log; import org.hibernate.reactive.logging.impl.LoggerFactory; +import org.hibernate.reactive.query.sqm.mutation.internal.ReactiveHandler; public interface ReactiveSqmMultiTableMutationStrategy extends SqmMultiTableMutationStrategy { @@ -28,10 +31,21 @@ default int executeUpdate( throw LOG.nonReactiveMethodCall( "reactiveExecuteUpdate" ); } - CompletionStage reactiveExecuteUpdate( + /** + * Execute the multi-table update indicated by the passed SqmUpdateStatement + * + * @return The number of rows affected + * @deprecated Use {@link #buildHandler(SqmDeleteOrUpdateStatement, DomainParameterXref, DomainQueryExecutionContext)} instead + */ + @Deprecated(forRemoval = true, since = "7.1") + default CompletionStage reactiveExecuteUpdate( SqmUpdateStatement sqmUpdateStatement, DomainParameterXref domainParameterXref, - DomainQueryExecutionContext context); + DomainQueryExecutionContext context){ + final MultiTableHandlerBuildResult buildResult = buildHandler( sqmUpdateStatement, domainParameterXref, context ); + return ((ReactiveHandler)buildResult.multiTableHandler()).reactiveExecute( buildResult.firstJdbcParameterBindings(), context ); + + } @Override default int executeDelete( @@ -41,8 +55,18 @@ default int executeDelete( throw LOG.nonReactiveMethodCall( "reactiveExecuteDelete" ); } - CompletionStage reactiveExecuteDelete( + /** + * Execute the multi-table update indicated by the passed SqmUpdateStatement + * + * @return The number of rows affected + * @deprecated Use {@link #buildHandler(SqmDeleteOrUpdateStatement, DomainParameterXref, DomainQueryExecutionContext)} instead + */ + @Deprecated(forRemoval = true, since = "3.1") + default CompletionStage reactiveExecuteDelete( SqmDeleteStatement sqmDeleteStatement, DomainParameterXref domainParameterXref, - DomainQueryExecutionContext context); + DomainQueryExecutionContext context){ + final MultiTableHandlerBuildResult buildResult = buildHandler( sqmDeleteStatement, domainParameterXref, context ); + return ((ReactiveHandler)buildResult.multiTableHandler()).reactiveExecute( buildResult.firstJdbcParameterBindings(), context ); + } } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/session/ReactiveSession.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/session/ReactiveSession.java index ab2089c0d..db141aadd 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/session/ReactiveSession.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/session/ReactiveSession.java @@ -75,16 +75,33 @@ public interface ReactiveSession extends ReactiveQueryProducer, ReactiveSharedSe CompletionStage reactiveRefresh(Object entity, LockOptions lockMode); + default CompletionStage reactiveRefresh(Object entity, LockMode lockMode) { + return reactiveRefresh( entity, new LockOptions( lockMode ) ); + } + CompletionStage reactiveRefresh(Object child, RefreshContext refreshedAlready); CompletionStage reactiveLock(Object entity, LockOptions lockMode); + default CompletionStage reactiveLock(Object entity, LockMode lockMode){ + return reactiveLock( entity, new LockOptions( lockMode ) ); + } + CompletionStage reactiveLock(String entityName, Object entity, LockOptions lockMode); CompletionStage reactiveGet(Class entityClass, Object id); CompletionStage reactiveFind(Class entityClass, Object id, LockOptions lockOptions, EntityGraph fetchGraph); + default CompletionStage reactiveFind(Class entityClass, Object id){ + return reactiveFind( entityClass, id, (LockOptions) null, null ); + } + + default CompletionStage reactiveFind(Class entityClass, Object id, LockMode lockMode, EntityGraph fetchGraph ){ + return reactiveFind( entityClass, id, new LockOptions( lockMode ), fetchGraph ); + } + + CompletionStage> reactiveFind(Class entityClass, Object... ids); CompletionStage reactiveFind(Class entityClass, Map naturalIds); diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/session/impl/ReactiveSessionImpl.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/session/impl/ReactiveSessionImpl.java index 795b565ff..93fc8c076 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/session/impl/ReactiveSessionImpl.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/session/impl/ReactiveSessionImpl.java @@ -5,6 +5,14 @@ */ package org.hibernate.reactive.session.impl; +import java.lang.invoke.MethodHandles; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.CompletionException; +import java.util.concurrent.CompletionStage; +import java.util.function.Supplier; + import org.hibernate.CacheMode; import org.hibernate.FlushMode; import org.hibernate.HibernateException; @@ -14,12 +22,17 @@ import org.hibernate.MappingException; import org.hibernate.ObjectDeletedException; import org.hibernate.ObjectNotFoundException; +import org.hibernate.OrderingMode; +import org.hibernate.RemovalsMode; +import org.hibernate.SessionCheckMode; import org.hibernate.TypeMismatchException; import org.hibernate.UnknownEntityTypeException; import org.hibernate.UnresolvableObjectException; import org.hibernate.bytecode.enhance.spi.interceptor.EnhancementAsProxyLazinessInterceptor; +import org.hibernate.bytecode.enhance.spi.interceptor.LazyAttributeLoadingInterceptor; import org.hibernate.collection.spi.PersistentCollection; import org.hibernate.dialect.Dialect; +import org.hibernate.engine.creation.internal.SessionCreationOptions; import org.hibernate.engine.internal.ReactivePersistenceContextAdapter; import org.hibernate.engine.spi.EffectiveEntityGraph; import org.hibernate.engine.spi.EntityEntry; @@ -48,7 +61,6 @@ import org.hibernate.graph.GraphSemantic; import org.hibernate.graph.RootGraph; import org.hibernate.graph.spi.RootGraphImplementor; -import org.hibernate.internal.SessionCreationOptions; import org.hibernate.internal.SessionFactoryImpl; import org.hibernate.internal.SessionImpl; import org.hibernate.jpa.spi.NativeQueryTupleTransformer; @@ -109,7 +121,7 @@ import org.hibernate.reactive.query.ReactiveSelectionQuery; import org.hibernate.reactive.query.sql.internal.ReactiveNativeQueryImpl; import org.hibernate.reactive.query.sql.spi.ReactiveNativeQueryImplementor; -import org.hibernate.reactive.query.sqm.internal.ReactiveQuerySqmImpl; +import org.hibernate.reactive.query.sqm.internal.ReactiveSqmQueryImpl; import org.hibernate.reactive.query.sqm.internal.ReactiveSqmSelectionQueryImpl; import org.hibernate.reactive.session.ReactiveSession; import org.hibernate.reactive.util.impl.CompletionStages; @@ -123,13 +135,6 @@ import jakarta.persistence.criteria.CriteriaQuery; import jakarta.persistence.criteria.CriteriaUpdate; import jakarta.persistence.metamodel.Attribute; -import java.lang.invoke.MethodHandles; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.CompletionException; -import java.util.concurrent.CompletionStage; -import java.util.function.Supplier; import static java.lang.Boolean.TRUE; import static org.hibernate.engine.internal.ManagedTypeHelper.asPersistentAttributeInterceptable; @@ -146,7 +151,6 @@ import static org.hibernate.reactive.util.impl.CompletionStages.nullFuture; import static org.hibernate.reactive.util.impl.CompletionStages.rethrow; import static org.hibernate.reactive.util.impl.CompletionStages.returnNullorRethrow; -import static org.hibernate.reactive.util.impl.CompletionStages.returnOrRethrow; import static org.hibernate.reactive.util.impl.CompletionStages.supplyStage; import static org.hibernate.reactive.util.impl.CompletionStages.voidFuture; @@ -166,9 +170,7 @@ public class ReactiveSessionImpl extends SessionImpl implements ReactiveSession, //Lazily initialized private transient ExceptionConverter exceptionConverter; - public ReactiveSessionImpl( - SessionFactoryImpl delegate, SessionCreationOptions options, - ReactiveConnection connection) { + public ReactiveSessionImpl(SessionFactoryImpl delegate, SessionCreationOptions options, ReactiveConnection connection) { super( delegate, options ); InternalStateAssertions.assertUseOnEventLoop(); this.associatedWorkThread = Thread.currentThread(); @@ -184,6 +186,12 @@ public SessionImplementor getSharedContract() { return this; } + @Override + public boolean isTransactionInProgress() { + return isOpenOrWaitingForAutoClose() + && reactiveConnection.isTransactionInProgress(); + } + @Override public Dialect getDialect() { threadCheck(); @@ -195,8 +203,8 @@ private void threadCheck() { } @Override - protected PersistenceContext createPersistenceContext() { - return new ReactivePersistenceContextAdapter( super.createPersistenceContext() ); + protected PersistenceContext createPersistenceContext(SessionCreationOptions options) { + return new ReactivePersistenceContextAdapter( super.createPersistenceContext( options ) ); } @Override @@ -359,8 +367,20 @@ else if ( isPersistentAttributeInterceptable( association ) ) { @Override public CompletionStage reactiveFetch(E entity, Attribute field) { - return ( (ReactiveEntityPersister) getEntityPersister( null, entity ) ) - .reactiveInitializeLazyProperty( field, entity, this ); + final ReactiveEntityPersister entityPersister = (ReactiveEntityPersister) getEntityPersister( null, entity ); + LazyAttributeLoadingInterceptor lazyAttributeLoadingInterceptor = entityPersister.getBytecodeEnhancementMetadata() + .extractInterceptor( entity ); + final String attributeName = field.getName(); + if ( !lazyAttributeLoadingInterceptor.isAttributeLoaded( attributeName ) ) { + return ( (CompletionStage) lazyAttributeLoadingInterceptor.fetchAttribute( entity, field.getName() ) ) + .thenApply( value -> { + lazyAttributeLoadingInterceptor.attributeInitialized( attributeName ); + return value; + } ); + } + else { + return completedFuture( (T) entityPersister.getPropertyValue( entity, attributeName ) ); + } } @Override @@ -390,7 +410,7 @@ public ReactiveQuery createReactiveQuery(CriteriaQuery criteriaQuery) } protected ReactiveQueryImplementor createReactiveCriteriaQuery(SqmStatement criteria, Class resultType) { - final ReactiveQuerySqmImpl query = new ReactiveQuerySqmImpl<>( criteria, resultType, this ); + final ReactiveSqmQueryImpl query = new ReactiveSqmQueryImpl<>( criteria, resultType, this ); applyQuerySettingsAndHints( query ); return query; } @@ -400,11 +420,11 @@ public ReactiveQuery createReactiveQuery(TypedQueryReference typedQuer checksBeforeQueryCreation(); if ( typedQueryReference instanceof SelectionSpecificationImpl specification ) { final CriteriaQuery query = specification.buildCriteria( getCriteriaBuilder() ); - return new ReactiveQuerySqmImpl<>( (SqmStatement) query, specification.getResultType(), this ); + return new ReactiveSqmQueryImpl<>( (SqmStatement) query, specification.getResultType(), this ); } else if ( typedQueryReference instanceof MutationSpecificationImpl specification ) { final CommonAbstractCriteria query = specification.buildCriteria( getCriteriaBuilder() ); - return new ReactiveQuerySqmImpl<>( (SqmStatement) query, (Class) specification.getResultType(), this ); + return new ReactiveSqmQueryImpl<>( (SqmStatement) query, (Class) specification.getResultType(), this ); } else { @SuppressWarnings("unchecked") @@ -433,8 +453,8 @@ public ReactiveQuery createReactiveQuery(String queryString, Class exp try { final HqlInterpretation interpretation = interpretHql( queryString, expectedResultType ); - final ReactiveQuerySqmImpl query = - new ReactiveQuerySqmImpl<>( queryString, interpretation, expectedResultType, this ); + final ReactiveSqmQueryImpl query = + new ReactiveSqmQueryImpl<>( queryString, interpretation, expectedResultType, this ); applyQuerySettingsAndHints( query ); query.setComment( queryString ); return query; @@ -611,7 +631,7 @@ protected ReactiveNativeQueryImpl createReactiveNativeQueryImplementor(Cl return (ReactiveNativeQueryImpl) query; } - protected ReactiveQuerySqmImpl createReactiveSqmQueryImplementor(Class resultType, NamedSqmQueryMemento memento) { + protected ReactiveSqmQueryImpl createReactiveSqmQueryImplementor(Class resultType, NamedSqmQueryMemento memento) { final SqmQueryImplementor query = memento.toQuery( this, resultType ); if ( isEmpty( query.getComment() ) ) { query.setComment( "dynamic query" ); @@ -620,7 +640,7 @@ protected ReactiveQuerySqmImpl createReactiveSqmQueryImplementor(Class if ( memento.getLockOptions() != null ) { query.setLockOptions( memento.getLockOptions() ); } - return (ReactiveQuerySqmImpl) query; + return (ReactiveSqmQueryImpl) query; } private RuntimeException convertNamedQueryException(RuntimeException e) { @@ -649,7 +669,7 @@ public ReactiveMutationQuery createReactiveMutationQuery(String hqlString final QueryImplementor query = createQuery( hqlString ); final SqmStatement sqmStatement = ( (SqmQueryImplementor) query ).getSqmStatement(); checkMutationQuery( hqlString, sqmStatement ); - return new ReactiveQuerySqmImpl<>( sqmStatement, null, this ); + return new ReactiveSqmQueryImpl<>( sqmStatement, null, this ); } @Override @@ -953,20 +973,13 @@ private CompletionStage fireRemove(DeleteEvent event) { return getFactory().getEventListenerGroups().eventListenerGroup_DELETE .fireEventOnEachListener( event, (ReactiveDeleteEventListener l) -> l::reactiveOnDelete ) - .handle( (v, e) -> { + .handle( CompletionStages::handle ) + .thenCompose( handler -> { delayedAfterCompletion(); - - if ( e instanceof ObjectDeletedException ) { - throw getExceptionConverter().convert( new IllegalArgumentException( e ) ); - } - else if ( e instanceof MappingException ) { - throw getExceptionConverter().convert( new IllegalArgumentException( e.getMessage(), e ) ); - } - else if ( e instanceof RuntimeException ) { - //including HibernateException - throw getExceptionConverter().convert( (RuntimeException) e ); - } - return returnNullorRethrow( e ); + final Throwable e = handler.getThrowable(); + return e != null + ? failedFuture( convertException( e ) ) + : voidFuture(); } ); } @@ -975,20 +988,13 @@ private CompletionStage fireRemove(DeleteEvent event, DeleteContext transi return getFactory().getEventListenerGroups().eventListenerGroup_DELETE .fireEventOnEachListener( event, transientEntities, (ReactiveDeleteEventListener l) -> l::reactiveOnDelete ) - .handle( (v, e) -> { + .handle( CompletionStages::handle ) + .thenCompose( handler -> { delayedAfterCompletion(); - - if ( e instanceof ObjectDeletedException ) { - throw getExceptionConverter().convert( new IllegalArgumentException( e ) ); - } - else if ( e instanceof MappingException ) { - throw getExceptionConverter().convert( new IllegalArgumentException( e.getMessage(), e ) ); - } - else if ( e instanceof RuntimeException ) { - //including HibernateException - throw getExceptionConverter().convert( (RuntimeException) e ); - } - return returnNullorRethrow( e ); + final Throwable e = handler.getThrowable(); + return e != null + ? failedFuture( convertException( e ) ) + : voidFuture(); } ); } @@ -1012,42 +1018,45 @@ private CompletionStage fireMerge(MergeEvent event) { return getFactory().getEventListenerGroups().eventListenerGroup_MERGE .fireEventOnEachListener( event, (ReactiveMergeEventListener l) -> l::reactiveOnMerge ) - .handle( (v, e) -> { + .handle( CompletionStages::handle ) + .thenCompose( handler -> { checkNoUnresolvedActionsAfterOperation(); - - if ( e instanceof ObjectDeletedException ) { - throw getExceptionConverter().convert( new IllegalArgumentException( e ) ); - } - else if ( e instanceof MappingException ) { - throw getExceptionConverter().convert( new IllegalArgumentException( e.getMessage(), e ) ); - } - else if ( e instanceof RuntimeException ) { - //including HibernateException - throw getExceptionConverter().convert( (RuntimeException) e ); - } - return returnOrRethrow( e, (T) event.getResult() ); + final Throwable e = handler.getThrowable(); + return e != null + ? failedFuture( convertException( e ) ) + : completedFuture( (T) event.getResult() ); } ); } + private Throwable convertException(Throwable e) { + if ( e instanceof CompletionException) { + return convertException( e.getCause() ); + } + if ( e instanceof ObjectDeletedException ) { + return getExceptionConverter().convert( new IllegalArgumentException( e ) ); + } + if ( e instanceof MappingException ) { + return getExceptionConverter().convert( new IllegalArgumentException( e.getMessage(), e ) ); + } + if ( e instanceof RuntimeException ) { + //including HibernateException + return getExceptionConverter().convert( (RuntimeException) e ); + } + return e; + } + private CompletionStage fireMerge(MergeContext copiedAlready, MergeEvent event) { pulseTransactionCoordinator(); return getFactory().getEventListenerGroups().eventListenerGroup_MERGE .fireEventOnEachListener( event, copiedAlready,(ReactiveMergeEventListener l) -> l::reactiveOnMerge ) - .handle( (v, e) -> { + .handle( CompletionStages::handle ) + .thenCompose( handler -> { delayedAfterCompletion(); - - if ( e instanceof ObjectDeletedException ) { - throw getExceptionConverter().convert( new IllegalArgumentException( e ) ); - } - else if ( e instanceof MappingException ) { - throw getExceptionConverter().convert( new IllegalArgumentException( e.getMessage(), e ) ); - } - else if ( e instanceof RuntimeException ) { - //including HibernateException - throw getExceptionConverter().convert( (RuntimeException) e ); - } - return returnNullorRethrow( e ); + final Throwable e = handler.getThrowable(); + return e != null + ? failedFuture( convertException( e ) ) + : voidFuture(); } ); } @@ -1133,6 +1142,11 @@ public CompletionStage reactiveRefresh(Object entity, LockOptions lockOpti return fireRefresh( new RefreshEvent( entity, lockOptions, this ) ); } + @Override + public CompletionStage reactiveRefresh(Object entity, LockMode lockMode) { + return reactiveRefresh( entity, toLockOptions( lockMode ) ); + } + @Override public CompletionStage reactiveRefresh(Object object, RefreshContext refreshedAlready) { checkOpenOrWaitingForAutoClose(); @@ -1193,6 +1207,11 @@ public CompletionStage reactiveLock(Object object, LockOptions lockOptions return fireLock( new LockEvent( object, lockOptions, this ) ); } + @Override + public CompletionStage reactiveLock(Object entity, LockMode lockMode) { + return reactiveLock( entity, toLockOptions( lockMode ) ); + } + @Override public CompletionStage reactiveLock(String entityName, Object object, LockOptions lockOptions) { checkOpen(); @@ -1254,6 +1273,11 @@ public CompletionStage reactiveFind( } ); } + @Override + public CompletionStage reactiveFind(Class entityClass, Object id, LockMode lockMode, EntityGraph fetchGraph){ + return reactiveFind( entityClass, id, toLockOptions( lockMode ), fetchGraph ); + } + private CompletionStage handleReactiveFindException( Class entityClass, Object primaryKey, @@ -1318,7 +1342,7 @@ public CompletionStage reactiveFind(Class entityClass, Map( this, persister, requireEntityPersister( entityClass ) ) .resolveNaturalId( normalizedIdValues ) - .thenCompose( id -> reactiveFind( entityClass, id, null, null ) ); + .thenCompose( id -> reactiveFind( entityClass, id ) ); } private ReactiveEntityPersister entityPersister(Class entityClass) { @@ -1446,10 +1470,9 @@ private class ReactiveMultiIdentifierLoadAccessImpl implements MultiIdLoadOpt private GraphSemantic graphSemantic; private Integer batchSize; - private boolean sessionCheckingEnabled; - private boolean returnOfDeletedEntitiesEnabled; - private boolean orderedReturnEnabled = true; - private boolean readOnly; + private SessionCheckMode sessionCheckMode = SessionCheckMode.DISABLED; + private RemovalsMode removalsMode = RemovalsMode.REPLACE; + protected OrderingMode orderingMode = OrderingMode.ORDERED; public ReactiveMultiIdentifierLoadAccessImpl(EntityPersister entityPersister) { this.entityPersister = (ReactiveEntityPersister) entityPersister; @@ -1496,8 +1519,8 @@ public ReactiveMultiIdentifierLoadAccessImpl withBatchSize(int batchSize) { } @Override - public boolean isSessionCheckingEnabled() { - return sessionCheckingEnabled; + public SessionCheckMode getSessionCheckMode() { + return sessionCheckMode; } @Override @@ -1506,27 +1529,27 @@ public boolean isSecondLevelCacheCheckingEnabled() { } public ReactiveMultiIdentifierLoadAccessImpl enableSessionCheck(boolean enabled) { - this.sessionCheckingEnabled = enabled; + this.sessionCheckMode = enabled ? SessionCheckMode.ENABLED : SessionCheckMode.DISABLED; return this; } @Override - public boolean isReturnOfDeletedEntitiesEnabled() { - return returnOfDeletedEntitiesEnabled; + public RemovalsMode getRemovalsMode() { + return removalsMode; } - public ReactiveMultiIdentifierLoadAccessImpl enableReturnOfDeletedEntities(boolean enabled) { - this.returnOfDeletedEntitiesEnabled = enabled; - return this; + @Override + public OrderingMode getOrderingMode() { + return orderingMode; } - @Override - public boolean isOrderReturnEnabled() { - return orderedReturnEnabled; + public ReactiveMultiIdentifierLoadAccessImpl enableReturnOfDeletedEntities(boolean enabled) { + this.removalsMode = enabled ? RemovalsMode.INCLUDE : RemovalsMode.REPLACE; + return this; } public ReactiveMultiIdentifierLoadAccessImpl enableOrderedReturn(boolean enabled) { - this.orderedReturnEnabled = enabled; + this.orderingMode = enabled ? OrderingMode.ORDERED : OrderingMode.UNORDERED; return this; } @@ -1700,7 +1723,23 @@ public void close() throws HibernateException { @Override public CompletionStage reactiveClose() { - super.close(); + try { + super.close(); + return closeConnection(); + } + catch (RuntimeException e) { + return closeConnection() + .handle( CompletionStages::handle ) + .thenCompose( closeConnectionHandler -> { + if ( closeConnectionHandler.hasFailed() ) { + LOG.errorClosingConnection( closeConnectionHandler.getThrowable() ); + } + return failedFuture( e ); + } ); + } + } + + private CompletionStage closeConnection() { return reactiveConnection != null ? reactiveConnection.close() : voidFuture(); @@ -1811,4 +1850,21 @@ public RootGraphImplementor getEntityGraph(Class entity, String name) } return (RootGraphImplementor) entityGraph; } + + /** + * Convert a {@link LockMode} into a {@link LockOptions} object. + *

+ * We need to make sure that we use the method {@link LockOptions#setLockMode(LockMode)} for the conversion + * because it also set a {@link LockOptions#timeout} that will affect the way SQL queries are generated. + * There's also the constructor {@link LockOptions#LockOptions(LockMode)}, but it doesn't set a time-out + * causing some generated SQL queries to not have the expected syntax (for example, it won't apply + * the "nowait" clause in PostgreSQL, even if set to {@link LockMode#UPGRADE_NOWAIT} ). + *

+ * @see Hibernate Reactive issue 2534 + */ + private static LockOptions toLockOptions(LockMode lockMode) { + return lockMode == null + ? null + : new LockOptions().setLockMode( lockMode ); + } } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/session/impl/ReactiveStatelessSessionImpl.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/session/impl/ReactiveStatelessSessionImpl.java index d67cc2018..340fcfeb6 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/session/impl/ReactiveStatelessSessionImpl.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/session/impl/ReactiveStatelessSessionImpl.java @@ -5,6 +5,12 @@ */ package org.hibernate.reactive.session.impl; +import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; +import java.util.function.BiConsumer; +import java.util.function.Supplier; + import org.hibernate.HibernateException; import org.hibernate.LockMode; import org.hibernate.SessionException; @@ -14,6 +20,7 @@ import org.hibernate.cache.spi.access.EntityDataAccess; import org.hibernate.collection.spi.PersistentCollection; import org.hibernate.dialect.Dialect; +import org.hibernate.engine.creation.internal.SessionCreationOptions; import org.hibernate.engine.internal.ReactivePersistenceContextAdapter; import org.hibernate.engine.spi.CollectionEntry; import org.hibernate.engine.spi.LoadQueryInfluencers; @@ -27,7 +34,6 @@ import org.hibernate.graph.internal.RootGraphImpl; import org.hibernate.graph.spi.RootGraphImplementor; import org.hibernate.id.IdentifierGenerationException; -import org.hibernate.internal.SessionCreationOptions; import org.hibernate.internal.SessionFactoryImpl; import org.hibernate.internal.StatelessSessionImpl; import org.hibernate.jpa.spi.NativeQueryTupleTransformer; @@ -68,7 +74,7 @@ import org.hibernate.reactive.query.ReactiveSelectionQuery; import org.hibernate.reactive.query.sql.internal.ReactiveNativeQueryImpl; import org.hibernate.reactive.query.sql.spi.ReactiveNativeQueryImplementor; -import org.hibernate.reactive.query.sqm.internal.ReactiveQuerySqmImpl; +import org.hibernate.reactive.query.sqm.internal.ReactiveSqmQueryImpl; import org.hibernate.reactive.query.sqm.internal.ReactiveSqmSelectionQueryImpl; import org.hibernate.reactive.session.ReactiveSqmQueryImplementor; import org.hibernate.reactive.session.ReactiveStatelessSession; @@ -82,11 +88,6 @@ import jakarta.persistence.criteria.CriteriaDelete; import jakarta.persistence.criteria.CriteriaQuery; import jakarta.persistence.criteria.CriteriaUpdate; -import java.util.List; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.CompletionStage; -import java.util.function.BiConsumer; -import java.util.function.Supplier; import static java.lang.Boolean.TRUE; import static java.lang.invoke.MethodHandles.lookup; @@ -319,11 +320,12 @@ public void whenComplete(BiConsumer consumer) { private CompletionStage recreateCollections(Object entity, Object id, EntityPersister persister) { final Completable stage = new Completable<>(); + final String entityName = persister.getEntityName(); + final EventMonitor eventMonitor = getEventMonitor(); final Loop loop = new Loop(); forEachOwnedCollection( entity, id, persister, (descriptor, collection) -> { - firePreRecreate( collection, descriptor ); - final EventMonitor eventMonitor = getEventMonitor(); + firePreRecreate( collection, descriptor, entityName, entity ); final DiagnosticEvent event = eventMonitor.beginCollectionRecreateEvent(); loop.then( () -> supplyStage( () -> ( (ReactiveCollectionPersister) descriptor ) .reactiveRecreate( collection, id, this ) ) @@ -335,7 +337,7 @@ private CompletionStage recreateCollections(Object entity, Object id, Enti if ( statistics.isStatisticsEnabled() ) { statistics.recreateCollection( descriptor.getRole() ); } - firePostRecreate( collection, descriptor ); + firePostRecreate( collection, id, entityName, descriptor ); } ) ); } @@ -438,7 +440,7 @@ private CompletionStage generatedIdBeforeInsert( private CompletionStage generateIdForInsert(Object entity, Generator generator, ReactiveEntityPersister persister) { if ( generator instanceof ReactiveIdentifierGenerator reactiveGenerator ) { - return reactiveGenerator.generate( this, this ) + return reactiveGenerator.generate( this, entity ) .thenApply( id -> castToIdentifierType( id, persister ) ); } @@ -480,29 +482,35 @@ public CompletionStage reactiveDelete(Object entity) { } private CompletionStage removeCollections(Object entity, Object id, EntityPersister persister) { - final Completable stage = new Completable<>(); - final Loop loop = new Loop(); - forEachOwnedCollection( entity, id, persister, - (descriptor, collection) -> { - firePreRemove( collection, entity, descriptor ); - final EventMonitor eventMonitor = getEventMonitor(); - final DiagnosticEvent event = eventMonitor.beginCollectionRemoveEvent(); - loop.then( () -> supplyStage( () -> ( (ReactiveCollectionPersister) descriptor ) - .reactiveRemove( id, this ) ) - .whenComplete( (unused, throwable) -> eventMonitor - .completeCollectionRemoveEvent( event, id, descriptor.getRole(), throwable != null, this ) - ) - .thenAccept( v -> { - firePostRemove( collection, entity, descriptor ); - final StatisticsImplementor statistics = getFactory().getStatistics(); - if ( statistics.isStatisticsEnabled() ) { - statistics.removeCollection( descriptor.getRole() ); - } - } ) - ); - } ); - loop.whenComplete( stage::complete ); - return stage.getStage(); + if ( persister.hasOwnedCollections() ) { + final Loop loop = new Loop(); + final Completable stage = new Completable<>(); + final String entityName = persister.getEntityName(); + forEachOwnedCollection( + entity, id, persister, + (descriptor, collection) -> { + firePreRemove( collection, id, entityName, entity ); + final EventMonitor eventMonitor = getEventMonitor(); + final DiagnosticEvent event = eventMonitor.beginCollectionRemoveEvent(); + loop.then( () -> supplyStage( () -> ( (ReactiveCollectionPersister) descriptor ) + .reactiveRemove( id, this ) ) + .whenComplete( (unused, throwable) -> eventMonitor + .completeCollectionRemoveEvent( event, id, descriptor.getRole(), throwable != null, this ) + ) + .thenAccept( v -> { + firePostRemove( collection, id, entityName, entity ); + final StatisticsImplementor statistics = getFactory().getStatistics(); + if ( statistics.isStatisticsEnabled() ) { + statistics.removeCollection( descriptor.getRole() ); + } + } ) + ); + } + ); + loop.whenComplete( stage::complete ); + return stage.getStage(); + } + return voidFuture(); } @Override @@ -561,31 +569,37 @@ private CompletionStage executeReactiveUpdate(Object entity) { } private CompletionStage removeAndRecreateCollections(Object entity, Object id, EntityPersister persister) { - final Completable stage = new Completable<>(); - final Loop loop = new Loop(); - forEachOwnedCollection( entity, id, persister, - (descriptor, collection) -> { - firePreUpdate( collection, descriptor ); - final EventMonitor eventMonitor = getEventMonitor(); - final DiagnosticEvent event = eventMonitor.beginCollectionRemoveEvent(); - ReactiveCollectionPersister reactivePersister = (ReactiveCollectionPersister) persister; - loop.then( () -> supplyStage( () -> reactivePersister - .reactiveRemove( id, this ) - .thenCompose( v -> reactivePersister.reactiveRecreate( collection, id, this ) ) ) - .whenComplete( (unused, throwable) -> eventMonitor - .completeCollectionRemoveEvent( event, id, descriptor.getRole(), throwable != null, this ) - ) - .thenAccept( v -> { - firePostUpdate( collection, descriptor ); - final StatisticsImplementor statistics = getFactory().getStatistics(); - if ( statistics.isStatisticsEnabled() ) { - statistics.updateCollection( descriptor.getRole() ); - } - } ) - ); - } ); - loop.whenComplete( stage::complete ); - return stage.getStage(); + if ( persister.hasOwnedCollections() ) { + final String entityName = persister.getEntityName(); + final Completable stage = new Completable<>(); + final Loop loop = new Loop(); + forEachOwnedCollection( + entity, id, persister, + (descriptor, collection) -> { + firePreUpdate( collection, id, entityName, entity ); + final EventMonitor eventMonitor = getEventMonitor(); + final DiagnosticEvent event = eventMonitor.beginCollectionRemoveEvent(); + ReactiveCollectionPersister reactivePersister = (ReactiveCollectionPersister) persister; + loop.then( () -> supplyStage( () -> reactivePersister + .reactiveRemove( id, this ) + .thenCompose( v -> reactivePersister.reactiveRecreate( collection, id, this ) ) ) + .whenComplete( (unused, throwable) -> eventMonitor + .completeCollectionRemoveEvent( event, id, descriptor.getRole(), throwable != null, this ) + ) + .thenAccept( v -> { + firePostUpdate( collection, id, entityName, entity); + final StatisticsImplementor statistics = getFactory().getStatistics(); + if ( statistics.isStatisticsEnabled() ) { + statistics.updateCollection( descriptor.getRole() ); + } + } ) + ); + } + ); + loop.whenComplete( stage::complete ); + return stage.getStage(); + } + return voidFuture(); } @Override @@ -938,11 +952,11 @@ public ReactiveQuery createReactiveQuery(TypedQueryReference typedQuer checksBeforeQueryCreation(); if ( typedQueryReference instanceof SelectionSpecificationImpl specification ) { final CriteriaQuery query = specification.buildCriteria( getCriteriaBuilder() ); - return new ReactiveQuerySqmImpl<>( (SqmStatement) query, specification.getResultType(), this ); + return new ReactiveSqmQueryImpl<>( (SqmStatement) query, specification.getResultType(), this ); } if ( typedQueryReference instanceof MutationSpecificationImpl specification ) { final CommonAbstractCriteria query = specification.buildCriteria( getCriteriaBuilder() ); - return new ReactiveQuerySqmImpl<>( (SqmStatement) query, (Class) specification.getResultType(), this ); + return new ReactiveSqmQueryImpl<>( (SqmStatement) query, (Class) specification.getResultType(), this ); } @SuppressWarnings("unchecked") // this cast is fine because of all our impls of TypedQueryReference return Class @@ -987,7 +1001,7 @@ public ReactiveQuery createReactiveQuery(CriteriaQuery criteriaQuery) } private ReactiveQuery createReactiveCriteriaQuery(SqmStatement criteria, Class resultType) { - final ReactiveQuerySqmImpl query = new ReactiveQuerySqmImpl<>( criteria, resultType, this ); + final ReactiveSqmQueryImpl query = new ReactiveSqmQueryImpl<>( criteria, resultType, this ); applyQuerySettingsAndHints( query ); return query; } @@ -1013,8 +1027,8 @@ public ReactiveSqmQueryImplementor createReactiveQuery(String queryString try { final HqlInterpretation interpretation = interpretHql( queryString, expectedResultType ); - final ReactiveQuerySqmImpl query = - new ReactiveQuerySqmImpl<>( queryString, interpretation, expectedResultType, this ); + final ReactiveSqmQueryImpl query = + new ReactiveSqmQueryImpl<>( queryString, interpretation, expectedResultType, this ); applyQuerySettingsAndHints( query ); query.setComment( queryString ); @@ -1157,7 +1171,7 @@ public ReactiveMutationQuery createReactiveMutationQuery(String hqlString final QueryImplementor query = createQuery( hqlString ); final SqmStatement sqmStatement = ( (SqmQueryImplementor) query ).getSqmStatement(); checkMutationQuery( hqlString, sqmStatement ); - return new ReactiveQuerySqmImpl<>( sqmStatement, null, this ); + return new ReactiveSqmQueryImpl<>( sqmStatement, null, this ); } @Override diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/exec/internal/StandardReactiveSelectExecutor.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/exec/internal/StandardReactiveSelectExecutor.java index 5993d1893..bd50726b1 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/exec/internal/StandardReactiveSelectExecutor.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/exec/internal/StandardReactiveSelectExecutor.java @@ -35,8 +35,8 @@ import org.hibernate.sql.exec.SqlExecLogger; import org.hibernate.sql.exec.internal.StandardStatementCreator; import org.hibernate.sql.exec.spi.ExecutionContext; -import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; import org.hibernate.sql.exec.spi.JdbcParameterBindings; +import org.hibernate.sql.exec.spi.JdbcSelect; import org.hibernate.sql.exec.spi.JdbcSelectExecutor; import org.hibernate.sql.results.graph.DomainResult; import org.hibernate.sql.results.internal.RowTransformerStandardImpl; @@ -66,7 +66,7 @@ private StandardReactiveSelectExecutor() { } public CompletionStage> list( - JdbcOperationQuerySelect jdbcSelect, + JdbcSelect jdbcSelect, JdbcParameterBindings jdbcParameterBindings, ExecutionContext executionContext, RowTransformer rowTransformer, @@ -75,7 +75,7 @@ public CompletionStage> list( } public CompletionStage> list( - JdbcOperationQuerySelect jdbcSelect, + JdbcSelect jdbcSelect, JdbcParameterBindings jdbcParameterBindings, ExecutionContext executionContext, RowTransformer rowTransformer, @@ -96,7 +96,7 @@ public CompletionStage> list( * @since 2.4 (and ORM 6.6) */ public CompletionStage> list( - JdbcOperationQuerySelect jdbcSelect, + JdbcSelect jdbcSelect, JdbcParameterBindings jdbcParameterBindings, ExecutionContext executionContext, RowTransformer rowTransformer, @@ -119,7 +119,7 @@ public CompletionStage> list( * @since 2.4 (and Hibernate ORM 6.6) */ public CompletionStage executeQuery( - JdbcOperationQuerySelect jdbcSelect, + JdbcSelect jdbcSelect, JdbcParameterBindings jdbcParameterBindings, ExecutionContext executionContext, RowTransformer rowTransformer, @@ -140,7 +140,7 @@ public CompletionStage executeQuery( @Override public CompletionStage executeQuery( - JdbcOperationQuerySelect jdbcSelect, + JdbcSelect jdbcSelect, JdbcParameterBindings jdbcParameterBindings, ExecutionContext executionContext, RowTransformer rowTransformer, @@ -172,7 +172,7 @@ public CompletionStage executeQuery( } private CompletionStage doExecuteQuery( - JdbcOperationQuerySelect jdbcSelect, + JdbcSelect jdbcSelect, JdbcParameterBindings jdbcParameterBindings, ExecutionContext executionContext, RowTransformer transformer, @@ -281,7 +281,7 @@ private static RowTransformer rowTransformer( public CompletionStage resolveJdbcValuesSource( String queryIdentifier, - JdbcOperationQuerySelect jdbcSelect, + JdbcSelect jdbcSelect, boolean canBeCached, ExecutionContext executionContext, ReactiveDeferredResultSetAccess resultSetAccess) { @@ -368,7 +368,7 @@ public CompletionStage resolveJdbcValuesSource( null, queryIdentifier, queryOptions, - resultSetAccess.usesFollowOnLocking(), + false, jdbcValuesMapping, null, executionContext @@ -383,7 +383,7 @@ public CompletionStage resolveJdbcValuesSource( queryResultsCacheKey, queryIdentifier, queryOptions, - resultSetAccess.usesFollowOnLocking(), + false, jdbcValuesMapping, capturingMetadata.resolveMetadataForCache(), executionContext @@ -404,7 +404,7 @@ public CompletionStage resolveJdbcValuesSource( queryResultsCacheKey, queryIdentifier, queryOptions, - resultSetAccess.usesFollowOnLocking(), + false, jdbcValuesMapping, capturingMetadata.resolveMetadataForCache(), executionContext @@ -535,7 +535,7 @@ public Statistics(ExecutionContext executionContext, ReactiveValuesResultSet jdb } } - public void end(JdbcOperationQuerySelect jdbcSelect, T result) { + public void end(JdbcSelect jdbcSelect, T result) { if ( enabled ) { final long endTime = System.nanoTime(); final long milliseconds = TimeUnit.MILLISECONDS diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/exec/spi/ReactiveSelectExecutor.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/exec/spi/ReactiveSelectExecutor.java index ab68e34be..80e83a5f9 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/exec/spi/ReactiveSelectExecutor.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/exec/spi/ReactiveSelectExecutor.java @@ -10,10 +10,11 @@ import org.hibernate.reactive.sql.results.spi.ReactiveListResultsConsumer; import org.hibernate.reactive.sql.results.spi.ReactiveResultsConsumer; +import org.hibernate.sql.exec.internal.JdbcOperationQuerySelect; import org.hibernate.sql.exec.internal.StandardStatementCreator; import org.hibernate.sql.exec.spi.ExecutionContext; -import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; import org.hibernate.sql.exec.spi.JdbcParameterBindings; +import org.hibernate.sql.exec.spi.JdbcSelect; import org.hibernate.sql.exec.spi.JdbcSelectExecutor; import org.hibernate.sql.results.spi.RowTransformer; @@ -46,7 +47,7 @@ default CompletionStage executeQuery( } CompletionStage executeQuery( - JdbcOperationQuerySelect jdbcSelect, + JdbcSelect jdbcSelect, JdbcParameterBindings jdbcParameterBindings, ExecutionContext executionContext, RowTransformer rowTransformer, @@ -56,7 +57,7 @@ CompletionStage executeQuery( ReactiveResultsConsumer resultsConsumer); CompletionStage> list( - JdbcOperationQuerySelect jdbcSelect, + JdbcSelect jdbcSelect, JdbcParameterBindings jdbcParameterBindings, ExecutionContext executionContext, RowTransformer rowTransformer, diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/ReactiveResultSetMapping.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/ReactiveResultSetMapping.java index a4b45101f..8b66a3bfb 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/ReactiveResultSetMapping.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/ReactiveResultSetMapping.java @@ -27,7 +27,7 @@ import org.hibernate.sql.results.jdbc.spi.JdbcValuesMetadata; /** - * @see org.hibernate.query.results.ResultSetMappingImpl + * @see org.hibernate.query.results.internal.ResultSetMappingImpl */ public class ReactiveResultSetMapping implements ResultSetMapping, ReactiveValuesMappingProducer { @@ -62,6 +62,11 @@ public CompletionStage reactiveResolve( .thenApply( columnCount -> delegate.resolve( jdbcResultsMetadata, loadQueryInfluencers, sessionFactory ) ); } + @Override + public ResultSetMapping cacheKeyInstance() { + return new ReactiveResultSetMapping( delegate.cacheKeyInstance() ); + } + @Override public String getMappingIdentifier() { return delegate.getMappingIdentifier(); diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/graph/embeddable/internal/ReactiveEmbeddableAssembler.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/graph/embeddable/internal/ReactiveEmbeddableAssembler.java new file mode 100644 index 000000000..db8222838 --- /dev/null +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/graph/embeddable/internal/ReactiveEmbeddableAssembler.java @@ -0,0 +1,42 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.sql.results.graph.embeddable.internal; + +import org.hibernate.reactive.sql.exec.spi.ReactiveRowProcessingState; +import org.hibernate.reactive.sql.results.graph.ReactiveDomainResultsAssembler; +import org.hibernate.reactive.sql.results.graph.ReactiveInitializer; +import org.hibernate.sql.results.graph.Initializer; +import org.hibernate.sql.results.graph.InitializerData; +import org.hibernate.sql.results.graph.embeddable.EmbeddableInitializer; +import org.hibernate.sql.results.graph.embeddable.internal.EmbeddableAssembler; +import org.hibernate.sql.results.jdbc.spi.JdbcValuesSourceProcessingOptions; + +import java.util.concurrent.CompletionStage; + +import static org.hibernate.reactive.util.impl.CompletionStages.completedFuture; + +/** + * @see org.hibernate.sql.results.graph.embeddable.internal.EmbeddableAssembler + */ +public class ReactiveEmbeddableAssembler extends EmbeddableAssembler implements ReactiveDomainResultsAssembler { + + public ReactiveEmbeddableAssembler(EmbeddableInitializer initializer) { + super( initializer ); + } + + @Override + public CompletionStage reactiveAssemble(ReactiveRowProcessingState rowProcessingState, JdbcValuesSourceProcessingOptions options) { + final ReactiveInitializer reactiveInitializer = (ReactiveInitializer) getInitializer(); + final InitializerData data = reactiveInitializer.getData( rowProcessingState ); + final Initializer.State state = data.getState(); + if ( state == Initializer.State.KEY_RESOLVED ) { + return reactiveInitializer + .reactiveResolveInstance( data ) + .thenApply( v -> reactiveInitializer.getResolvedInstance( data ) ); + } + return completedFuture( reactiveInitializer.getResolvedInstance( data ) ); + } +} diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/graph/embeddable/internal/ReactiveEmbeddableFetchImpl.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/graph/embeddable/internal/ReactiveEmbeddableFetchImpl.java index 50034533b..685717f35 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/graph/embeddable/internal/ReactiveEmbeddableFetchImpl.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/graph/embeddable/internal/ReactiveEmbeddableFetchImpl.java @@ -6,14 +6,20 @@ package org.hibernate.reactive.sql.results.graph.embeddable.internal; import org.hibernate.engine.FetchTiming; +import org.hibernate.reactive.sql.results.graph.entity.internal.ReactiveEntityFetchSelectImpl; import org.hibernate.spi.NavigablePath; import org.hibernate.sql.results.graph.AssemblerCreationState; +import org.hibernate.sql.results.graph.DomainResultAssembler; import org.hibernate.sql.results.graph.DomainResultCreationState; +import org.hibernate.sql.results.graph.Fetch; import org.hibernate.sql.results.graph.FetchParent; +import org.hibernate.sql.results.graph.Fetchable; +import org.hibernate.sql.results.graph.Initializer; import org.hibernate.sql.results.graph.InitializerParent; import org.hibernate.sql.results.graph.embeddable.EmbeddableInitializer; import org.hibernate.sql.results.graph.embeddable.EmbeddableValuedFetchable; import org.hibernate.sql.results.graph.embeddable.internal.EmbeddableFetchImpl; +import org.hibernate.sql.results.graph.entity.internal.EntityFetchSelectImpl; public class ReactiveEmbeddableFetchImpl extends EmbeddableFetchImpl { @@ -32,9 +38,26 @@ public ReactiveEmbeddableFetchImpl(EmbeddableFetchImpl original) { } @Override - public EmbeddableInitializer createInitializer( - InitializerParent parent, - AssemblerCreationState creationState) { - return new ReactiveEmbeddableInitializerImpl( this, getDiscriminatorFetch(), parent, creationState, true ); + public EmbeddableInitializer createInitializer(InitializerParent parent, AssemblerCreationState creationState) { + return new ReactiveEmbeddableInitializerImpl( this, getDiscriminatorFetch(), getNullIndicatorResult(), parent, creationState, true ); + } + + @Override + public DomainResultAssembler createAssembler(InitializerParent parent, AssemblerCreationState creationState) { + Initializer initializer = creationState.resolveInitializer( this, parent, this ); + EmbeddableInitializer embeddableInitializer = initializer.asEmbeddableInitializer(); + return new ReactiveEmbeddableAssembler( embeddableInitializer ); + } + + @Override + public Fetch findFetch(Fetchable fetchable) { + Fetch fetch = super.findFetch( fetchable ); + if ( fetch instanceof EntityFetchSelectImpl entityFetchSelect ) { + return new ReactiveEntityFetchSelectImpl( entityFetchSelect ); + } + else if ( fetch instanceof EmbeddableFetchImpl embeddableFetch ) { + return new ReactiveEmbeddableFetchImpl( embeddableFetch ); + } + return fetch; } } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/graph/embeddable/internal/ReactiveEmbeddableForeignKeyResultImpl.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/graph/embeddable/internal/ReactiveEmbeddableForeignKeyResultImpl.java index 1e3433320..426345ab5 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/graph/embeddable/internal/ReactiveEmbeddableForeignKeyResultImpl.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/graph/embeddable/internal/ReactiveEmbeddableForeignKeyResultImpl.java @@ -20,7 +20,7 @@ public ReactiveEmbeddableForeignKeyResultImpl(EmbeddableForeignKeyResultImpl @Override public EmbeddableInitializer createInitializer(InitializerParent parent, AssemblerCreationState creationState) { return getReferencedModePart() instanceof NonAggregatedIdentifierMapping - ? new ReactiveNonAggregatedIdentifierMappingInitializer( this, null, creationState, true ) - : new ReactiveEmbeddableInitializerImpl( this, null, null, creationState, true ); + ? new ReactiveNonAggregatedIdentifierMappingInitializer( this, null, creationState, true ) + : new ReactiveEmbeddableInitializerImpl( this, null, null, null, creationState, true ); } } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/graph/embeddable/internal/ReactiveEmbeddableInitializerImpl.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/graph/embeddable/internal/ReactiveEmbeddableInitializerImpl.java index a1d4a890e..6d5fbcd5e 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/graph/embeddable/internal/ReactiveEmbeddableInitializerImpl.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/graph/embeddable/internal/ReactiveEmbeddableInitializerImpl.java @@ -5,12 +5,19 @@ */ package org.hibernate.reactive.sql.results.graph.embeddable.internal; + import java.util.concurrent.CompletionStage; import java.util.function.BiFunction; import org.hibernate.metamodel.mapping.EmbeddableMappingType; +import org.hibernate.metamodel.mapping.VirtualModelPart; +import org.hibernate.metamodel.spi.EmbeddableInstantiator; +import org.hibernate.reactive.sql.exec.spi.ReactiveRowProcessingState; +import org.hibernate.reactive.sql.results.graph.ReactiveDomainResultsAssembler; import org.hibernate.reactive.sql.results.graph.ReactiveInitializer; 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.Initializer; import org.hibernate.sql.results.graph.InitializerData; import org.hibernate.sql.results.graph.InitializerParent; @@ -19,8 +26,13 @@ import org.hibernate.sql.results.graph.embeddable.internal.EmbeddableInitializerImpl; import org.hibernate.sql.results.jdbc.spi.RowProcessingState; +import static org.hibernate.reactive.util.impl.CompletionStages.completedFuture; import static org.hibernate.reactive.util.impl.CompletionStages.loop; +import static org.hibernate.reactive.util.impl.CompletionStages.nullFuture; import static org.hibernate.reactive.util.impl.CompletionStages.voidFuture; +import static org.hibernate.reactive.util.impl.CompletionStages.whileLoop; +import static org.hibernate.sql.results.graph.embeddable.EmbeddableLoadingLogger.EMBEDDED_LOAD_LOGGER; +import static org.hibernate.sql.results.graph.entity.internal.BatchEntityInsideEmbeddableSelectFetchInitializer.BATCH_PROPERTY; public class ReactiveEmbeddableInitializerImpl extends EmbeddableInitializerImpl implements ReactiveInitializer { @@ -33,6 +45,10 @@ public ReactiveEmbeddableInitializerData( super( initializer, rowProcessingState ); } + public Object[] getRowState(){ + return rowState; + } + @Override public void setState(State state) { super.setState( state ); @@ -51,10 +67,11 @@ public EmbeddableMappingType.ConcreteEmbeddableType getConcreteEmbeddableType() public ReactiveEmbeddableInitializerImpl( EmbeddableResultGraphNode resultDescriptor, BasicFetch discriminatorFetch, + DomainResult nullIndicatorResult, InitializerParent parent, AssemblerCreationState creationState, boolean isResultInitializer) { - super( resultDescriptor, discriminatorFetch, parent, creationState, isResultInitializer ); + super( resultDescriptor, discriminatorFetch, nullIndicatorResult, parent, creationState, isResultInitializer ); } @Override @@ -64,10 +81,128 @@ protected InitializerData createInitializerData(RowProcessingState rowProcessing @Override public CompletionStage reactiveResolveInstance(EmbeddableInitializerData data) { - super.resolveInstance( data ); + if ( data.getState() != State.KEY_RESOLVED ) { + return voidFuture(); + } + + data.setState( State.RESOLVED ); + return extractRowState( (ReactiveEmbeddableInitializerData) data ) + .thenCompose( unused -> prepareCompositeInstance( (ReactiveEmbeddableInitializerData) data ) ); + } + + private CompletionStage extractRowState(ReactiveEmbeddableInitializerData data) { + final DomainResultAssembler[] subAssemblers = assemblers[data.getSubclassId()]; + final RowProcessingState rowProcessingState = data.getRowProcessingState(); + final Object[] rowState = data.getRowState(); + final boolean[] stateAllNull = {true}; + final int[] index = {0}; + final boolean[] forceExit = { false }; + return whileLoop( + () -> index[0] < subAssemblers.length && !forceExit[0], + () -> { + final int i = index[0]++; + final DomainResultAssembler assembler = subAssemblers[i]; + if ( assembler instanceof ReactiveDomainResultsAssembler reactiveAssembler ) { + return reactiveAssembler.reactiveAssemble( (ReactiveRowProcessingState) rowProcessingState ) + .thenAccept( contributorValue -> setContributorValue( + contributorValue, + i, + rowState, + stateAllNull, + forceExit + ) ); + } + else { + setContributorValue( + assembler == null ? null : assembler.assemble( rowProcessingState ), + i, + rowState, + stateAllNull, + forceExit + ); + return voidFuture(); + } + }) + .whenComplete( + (unused, throwable) -> { + if ( stateAllNull[0] ) { + data.setState( State.MISSING ); + } + } + ); + } + + private void setContributorValue( + Object contributorValue, + int index, + Object[] rowState, + boolean[] stateAllNull, + boolean[] forceExit) { + if ( contributorValue == BATCH_PROPERTY ) { + rowState[index] = null; + } + else { + rowState[index] = contributorValue; + } + if ( contributorValue != null ) { + stateAllNull[0] = false; + } + else if ( isPartOfKey() ) { + // If this is a foreign key and there is a null part, the whole thing has to be turned into null + stateAllNull[0] = true; + forceExit[0] = true; + } + } + + private CompletionStage prepareCompositeInstance(ReactiveEmbeddableInitializerData data) { + // Virtual model parts use the owning entity as container which the fetch parent access provides. + // For an identifier or foreign key this is called during the resolveKey phase of the fetch parent, + // so we can't use the fetch parent access in that case. + final ReactiveInitializer parent = (ReactiveInitializer) getParent(); + if ( parent != null && getInitializedPart() instanceof VirtualModelPart && !isPartOfKey() && data.getState() != State.MISSING ) { + final ReactiveEmbeddableInitializerData subData = parent.getData( data.getRowProcessingState() ); + return parent + .reactiveResolveInstance( subData ) + .thenCompose( + unused -> { + data.setInstance( parent.getResolvedInstance( subData ) ); + if ( data.getState() == State.INITIALIZED ) { + return voidFuture(); + } + return doCreateCompositeInstance( data ) + .thenAccept( v -> EMBEDDED_LOAD_LOGGER.debugf( + "Created composite instance [%s]", + getNavigablePath() + ) ); + } ); + } + + return doCreateCompositeInstance( data ) + .thenAccept( v -> EMBEDDED_LOAD_LOGGER.debugf( "Created composite instance [%s]", getNavigablePath() ) ); + + } + + private CompletionStage doCreateCompositeInstance(ReactiveEmbeddableInitializerData data) { + if ( data.getInstance() == null ) { + return createCompositeInstance( data ) + .thenAccept( data::setInstance ); + } return voidFuture(); } + private CompletionStage createCompositeInstance(ReactiveEmbeddableInitializerData data) { + if ( data.getState() == State.MISSING ) { + return nullFuture(); + } + + final EmbeddableInstantiator instantiator = data.getConcreteEmbeddableType() == null + ? getInitializedPart().getEmbeddableTypeDescriptor().getRepresentationStrategy().getInstantiator() + : data.getConcreteEmbeddableType().getInstantiator(); + final Object instance = instantiator.instantiate( data ); + data.setState( State.RESOLVED ); + return completedFuture( instance ); + } + @Override public CompletionStage reactiveInitializeInstance(EmbeddableInitializerData data) { super.initializeInstance( data ); @@ -81,16 +216,21 @@ public CompletionStage forEachReactiveSubInitializer( final ReactiveEmbeddableInitializerData embeddableInitializerData = (ReactiveEmbeddableInitializerData) data; final RowProcessingState rowProcessingState = embeddableInitializerData.getRowProcessingState(); if ( embeddableInitializerData.getConcreteEmbeddableType() == null ) { - return loop( subInitializers, subInitializer -> loop( subInitializer, initializer -> consumer - .apply( (ReactiveInitializer) initializer, rowProcessingState ) - ) ); + return loop( subInitializers, subInitializer -> + loop( subInitializer, initializer -> + initializer != null + ? consumer.apply( (ReactiveInitializer) initializer, rowProcessingState ) + : voidFuture() + ) + ); } else { Initializer[] initializers = subInitializers[embeddableInitializerData.getSubclassId()]; - return loop( 0, initializers.length, i -> { - ReactiveInitializer reactiveInitializer = (ReactiveInitializer) initializers[i]; - return consumer.apply( reactiveInitializer, rowProcessingState ); - } ); + return loop(0, initializers.length, i -> + initializers[i] != null + ? consumer.apply( (ReactiveInitializer) initializers[i], rowProcessingState ) + : voidFuture() + ); } } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/graph/entity/internal/ReactiveEntityInitializerImpl.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/graph/entity/internal/ReactiveEntityInitializerImpl.java index a7342275d..0ae83243f 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/graph/entity/internal/ReactiveEntityInitializerImpl.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/graph/entity/internal/ReactiveEntityInitializerImpl.java @@ -26,6 +26,7 @@ import org.hibernate.proxy.map.MapProxy; import org.hibernate.reactive.session.ReactiveQueryProducer; import org.hibernate.reactive.sql.exec.spi.ReactiveRowProcessingState; +import org.hibernate.reactive.sql.results.graph.ReactiveDomainResultsAssembler; import org.hibernate.reactive.sql.results.graph.ReactiveInitializer; import org.hibernate.sql.results.graph.AssemblerCreationState; import org.hibernate.sql.results.graph.DomainResult; @@ -215,7 +216,7 @@ else if ( lazyInitializer.isUninitialized() ) { if ( data.getState() == State.INITIALIZED ) { registerReloadedEntity( data ); resolveInstanceSubInitializers( data ); - if ( rowProcessingState.needsResolveState() ) { + if ( data.getState() == State.INITIALIZED && rowProcessingState.needsResolveState() ) { // We need to read result set values to correctly populate the query cache resolveState( data ); } @@ -254,16 +255,21 @@ public CompletionStage reactiveResolveInstance(EntityInitializerData origi final RowProcessingState rowProcessingState = data.getRowProcessingState(); data.setState( State.RESOLVED ); if ( data.getEntityKey() == null ) { - assert getIdentifierAssembler() != null; - final Object id = getIdentifierAssembler().assemble( rowProcessingState ); - if ( id == null ) { - setMissing( data ); - return voidFuture(); - } - resolveEntityKey( data, id ); + return assembleId( rowProcessingState ) + .thenCompose( id -> { + if ( id == null ) { + setMissing( data ); + return voidFuture(); + } + resolveEntityKey( data, id ); + return postAssembleId( rowProcessingState, data ); + } ); } - final PersistenceContext persistenceContext = rowProcessingState.getSession() - .getPersistenceContextInternal(); + return postAssembleId( rowProcessingState, data ); + } + + private CompletionStage postAssembleId(RowProcessingState rowProcessingState, ReactiveEntityInitializerData data) { + final PersistenceContext persistenceContext = rowProcessingState.getSession().getPersistenceContextInternal(); data.setEntityHolder( persistenceContext.claimEntityHolderIfPossible( data.getEntityKey(), null, @@ -274,29 +280,37 @@ public CompletionStage reactiveResolveInstance(EntityInitializerData origi if ( useEmbeddedIdentifierInstanceAsEntity( data ) ) { data.setEntityInstanceForNotify( rowProcessingState.getEntityId() ); data.setInstance( data.getEntityInstanceForNotify() ); + postResolveInstance( data ); + return voidFuture(); } - else { - return reactiveResolveEntityInstance1( data ) - .thenAccept( v -> { - if ( data.getUniqueKeyAttributePath() != null ) { - final SharedSessionContractImplementor session = rowProcessingState.getSession(); - final EntityPersister concreteDescriptor = getConcreteDescriptor( data ); - final EntityUniqueKey euk = new EntityUniqueKey( - concreteDescriptor.getEntityName(), - data.getUniqueKeyAttributePath(), - rowProcessingState.getEntityUniqueKey(), - data.getUniqueKeyPropertyTypes()[concreteDescriptor.getSubclassId()], - session.getFactory() - ); - session.getPersistenceContextInternal().addEntity( euk, data.getInstance() ); - } - postResolveInstance( data ); - } ); - } - postResolveInstance( data ); - return voidFuture(); + + return reactiveResolveEntityInstance1( data ) + .thenAccept( v -> { + if ( data.getUniqueKeyAttributePath() != null ) { + final SharedSessionContractImplementor session = rowProcessingState.getSession(); + final EntityPersister concreteDescriptor = getConcreteDescriptor( data ); + final EntityUniqueKey euk = new EntityUniqueKey( + concreteDescriptor.getEntityName(), + data.getUniqueKeyAttributePath(), + rowProcessingState.getEntityUniqueKey(), + data.getUniqueKeyPropertyTypes()[concreteDescriptor.getSubclassId()], + session.getFactory() + ); + session.getPersistenceContextInternal().addEntity( euk, data.getInstance() ); + } + postResolveInstance( data ); + } ); } + private CompletionStage assembleId(RowProcessingState rowProcessingState) { + final DomainResultAssembler identifierAssembler = getIdentifierAssembler(); + assert identifierAssembler != null; + return identifierAssembler instanceof ReactiveDomainResultsAssembler reactiveAssembler + ? reactiveAssembler.reactiveAssemble( (ReactiveRowProcessingState) rowProcessingState ) + : completedFuture( identifierAssembler.assemble( rowProcessingState ) ); + } + + // We could move this method in ORM private void postResolveInstance(ReactiveEntityInitializerData data) { if ( data.getInstance() != null ) { upgradeLockMode( data ); @@ -322,6 +336,19 @@ public CompletionStage reactiveInitializeInstance(EntityInitializerData da assert consistentInstance( data ); return reactiveInitializeEntityInstance( (ReactiveEntityInitializerData) data ); } + else { + if ( data.getRowProcessingState().needsResolveState() ) { + // A sub-initializer might have taken responsibility for this entity, + // but we still need to resolve the state to correctly populate a query cache + resolveState( data ); + } + final ReactiveEntityInitializerData reactiveData = (ReactiveEntityInitializerData) data; + if ( getEntityDescriptor().getBytecodeEnhancementMetadata().isEnhancedForLazyLoading() + && reactiveData.getEntityHolder().getEntityInitializer() != this + && reactiveData.getEntityHolder().isInitialized() ) { + updateInitializedEntityInstance( data ); + } + } data.setState( State.INITIALIZED ); return voidFuture(); } @@ -334,7 +361,7 @@ protected CompletionStage reactiveInitializeEntityInstance(ReactiveEntityI return reactiveExtractConcreteTypeStateValues( data ) .thenAccept( resolvedEntityState -> { - + rowProcessingState.getJdbcValuesSourceProcessingState().registerLoadingEntityHolder( data.getEntityHolder() ); preLoad( data, resolvedEntityState ); if ( isPersistentAttributeInterceptable( data.getEntityInstanceForNotify() ) ) { @@ -358,10 +385,6 @@ protected CompletionStage reactiveInitializeEntityInstance(ReactiveEntityI final Object version = getVersionAssembler() != null ? getVersionAssembler().assemble( rowProcessingState ) : null; final Object rowId = getRowIdAssembler() != null ? getRowIdAssembler().assemble( rowProcessingState ) : null; - // from the perspective of Hibernate, an entity is read locked as soon as it is read - // so regardless of the requested lock mode, we upgrade to at least the read level - final LockMode lockModeToAcquire = data.getLockMode() == LockMode.NONE ? LockMode.READ : data.getLockMode(); - final EntityEntry entityEntry = persistenceContext.addEntry( data.getEntityInstanceForNotify(), Status.LOADING, @@ -369,7 +392,7 @@ protected CompletionStage reactiveInitializeEntityInstance(ReactiveEntityI rowId, data.getEntityKey().getIdentifier(), version, - lockModeToAcquire, + lockModeToAcquire( data ), true, data.getConcreteDescriptor(), false @@ -390,16 +413,15 @@ protected CompletionStage reactiveInitializeEntityInstance(ReactiveEntityI statistics.loadEntity( data.getConcreteDescriptor().getEntityName() ); } } - updateCaches( - data, - session, - session.getPersistenceContextInternal(), - resolvedEntityState, - version - ); + updateCaches( data, session, session.getPersistenceContextInternal(), resolvedEntityState, version ); } ); } + // Hibernate ORM has a similar method, but it checks if we are in a transaction first + private static LockMode lockModeToAcquire(ReactiveEntityInitializerData data) { + return data.getLockMode() == LockMode.NONE ? LockMode.READ : data.getLockMode(); + } + protected CompletionStage reactiveExtractConcreteTypeStateValues(ReactiveEntityInitializerData data) { final RowProcessingState rowProcessingState = data.getRowProcessingState(); final Object[] values = new Object[data.getConcreteDescriptor().getNumberOfAttributeMappings()]; @@ -433,7 +455,9 @@ protected CompletionStage reactiveResolveEntityInstance1(ReactiveEntityIni else { data.setInstance( proxy ); if ( Hibernate.isInitialized( data.getInstance() ) ) { - data.setState( State.INITIALIZED ); + if ( data.getEntityHolder().isInitialized() ) { + data.setState( State.INITIALIZED ); + } data.setEntityInstanceForNotify( Hibernate.unproxy( data.getInstance() ) ); } else { diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/internal/ReactiveDeferredResultSetAccess.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/internal/ReactiveDeferredResultSetAccess.java index 3f88bcffb..d7595a902 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/internal/ReactiveDeferredResultSetAccess.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/internal/ReactiveDeferredResultSetAccess.java @@ -7,13 +7,14 @@ import java.lang.invoke.MethodHandles; import java.sql.ResultSet; -import java.sql.ResultSetMetaData; import java.sql.SQLException; import java.util.concurrent.CompletionException; import java.util.concurrent.CompletionStage; import org.hibernate.HibernateException; +import org.hibernate.JDBCException; import org.hibernate.LockOptions; +import org.hibernate.engine.jdbc.spi.JdbcServices; import org.hibernate.engine.jdbc.spi.SqlStatementLogger; import org.hibernate.engine.spi.SessionEventListenerManager; import org.hibernate.engine.spi.SessionFactoryImplementor; @@ -28,8 +29,8 @@ import org.hibernate.resource.jdbc.spi.JdbcSessionContext; import org.hibernate.resource.jdbc.spi.LogicalConnectionImplementor; import org.hibernate.sql.exec.spi.ExecutionContext; -import org.hibernate.sql.exec.spi.JdbcOperationQuerySelect; import org.hibernate.sql.exec.spi.JdbcParameterBindings; +import org.hibernate.sql.exec.spi.JdbcSelect; import org.hibernate.sql.exec.spi.JdbcSelectExecutor; import org.hibernate.sql.results.jdbc.internal.DeferredResultSetAccess; import org.hibernate.sql.results.jdbc.spi.JdbcValuesMetadata; @@ -53,7 +54,7 @@ public class ReactiveDeferredResultSetAccess extends DeferredResultSetAccess imp private ResultSet resultSet; public ReactiveDeferredResultSetAccess( - JdbcOperationQuerySelect jdbcSelect, + JdbcSelect jdbcSelect, JdbcParameterBindings jdbcParameterBindings, ExecutionContext executionContext, JdbcSelectExecutor.StatementCreator statementCreator, @@ -63,6 +64,17 @@ public ReactiveDeferredResultSetAccess( this.sqlStatementLogger = executionContext.getSession().getJdbcServices().getSqlStatementLogger(); } + @Override + public JdbcServices getJdbcServices() { + return getFactory().getJdbcServices(); + } + + @Override + public JDBCException convertSqlException(SQLException e, String message) { + return getJdbcServices().getJdbcEnvironment().getSqlExceptionHelper() + .convert( e, message ); + } + /** * Reactive version of {@link org.hibernate.sql.results.jdbc.internal.DeferredResultSetAccess#registerAfterLoadAction(ExecutionContext, LockOptions)} * calling {@link ReactiveSession#reactiveLock(String, Object, LockOptions)} @@ -139,20 +151,6 @@ private JdbcValuesMetadata convertToMetadata(ResultSet resultSet) { return (JdbcValuesMetadata) resultSet; } - @Override - public CompletionStage getReactiveMetadata() { - return getReactiveResultSet().thenApply( this::reactiveMetadata ); - } - - private ResultSetMetaData reactiveMetadata(ResultSet resultSet) { - try { - return resultSet.getMetaData(); - } - catch (SQLException e) { - throw new RuntimeException( e ); - } - } - private static int columnCount(ResultSet resultSet) { try { return resultSet.getMetaData().getColumnCount(); diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/internal/ReactiveDirectResultSetAccess.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/internal/ReactiveDirectResultSetAccess.java index 2412f3941..80d3217aa 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/internal/ReactiveDirectResultSetAccess.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/internal/ReactiveDirectResultSetAccess.java @@ -8,9 +8,11 @@ import java.lang.invoke.MethodHandles; import java.sql.PreparedStatement; import java.sql.ResultSet; -import java.sql.ResultSetMetaData; +import java.sql.SQLException; import java.util.concurrent.CompletionStage; +import org.hibernate.JDBCException; +import org.hibernate.engine.jdbc.spi.JdbcServices; import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.engine.spi.SharedSessionContractImplementor; import org.hibernate.reactive.logging.impl.Log; @@ -40,8 +42,13 @@ public ReactiveDirectResultSetAccess( } @Override - public SessionFactoryImplementor getFactory() { - return getPersistenceContext().getFactory(); + public JdbcServices getJdbcServices() { + return getFactory().getJdbcServices(); + } + + @Override + public JDBCException convertSqlException(SQLException e, String message) { + return getJdbcServices().getJdbcEnvironment().getSqlExceptionHelper().convert( e, message ); } @Override @@ -73,11 +80,6 @@ public CompletionStage getReactiveResultSet() { return completedFuture( resultSet ); } - @Override - public CompletionStage getReactiveMetadata() { - return completedFuture( getMetaData() ); - } - @Override public CompletionStage getReactiveColumnCount() { return completedFuture( getColumnCount() ); diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/internal/ReactiveResultSetAccess.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/internal/ReactiveResultSetAccess.java index 75a265ed5..ad9be5a6a 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/internal/ReactiveResultSetAccess.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/internal/ReactiveResultSetAccess.java @@ -10,9 +10,9 @@ import java.sql.SQLException; import java.util.concurrent.CompletionStage; +import org.hibernate.JDBCException; import org.hibernate.dialect.Dialect; import org.hibernate.engine.jdbc.spi.JdbcServices; -import org.hibernate.engine.spi.SessionFactoryImplementor; import org.hibernate.sql.results.jdbc.spi.JdbcValuesMetadata; import org.hibernate.type.BasicType; import org.hibernate.type.descriptor.java.JavaType; @@ -27,14 +27,14 @@ */ public interface ReactiveResultSetAccess extends JdbcValuesMetadata { CompletionStage getReactiveResultSet(); - CompletionStage getReactiveMetadata(); CompletionStage getReactiveColumnCount(); CompletionStage resolveJdbcValueMetadata(); ResultSet getResultSet(); - SessionFactoryImplementor getFactory(); + JdbcServices getJdbcServices(); + void release(); /** @@ -51,43 +51,35 @@ default int getColumnCount() { return getResultSet().getMetaData().getColumnCount(); } catch (SQLException e) { - throw getFactory().getJdbcServices().getJdbcEnvironment().getSqlExceptionHelper().convert( - e, - "Unable to access ResultSet column count" - ); + throw convertSqlException( e, "Unable to access ResultSet column count" ); } } + JDBCException convertSqlException(SQLException e, String message); + default int resolveColumnPosition(String columnName) { try { return getResultSet().findColumn( columnName ); } catch (SQLException e) { - throw getFactory().getJdbcServices().getJdbcEnvironment().getSqlExceptionHelper().convert( - e, - "Unable to find column position by name" - ); + throw convertSqlException( e, "Unable to find column position by name" ); } } default String resolveColumnName(int position) { try { - return getFactory().getJdbcServices().getJdbcEnvironment() + return getJdbcServices().getJdbcEnvironment() .getDialect() .getColumnAliasExtractor() .extractColumnAlias( getResultSet().getMetaData(), position ); } catch (SQLException e) { - throw getFactory().getJdbcServices().getJdbcEnvironment().getSqlExceptionHelper().convert( - e, - "Unable to find column name by position" - ); + throw convertSqlException( e, "Unable to find column name by position" ); } } @Override default BasicType resolveType(int position, JavaType explicitJavaType, TypeConfiguration typeConfiguration) { - final JdbcServices jdbcServices = getFactory().getJdbcServices(); try { final ResultSetMetaData metaData = getResultSet().getMetaData(); final String columnTypeName = metaData.getColumnTypeName( position ); @@ -95,7 +87,7 @@ default BasicType resolveType(int position, JavaType explicitJavaType, final int scale = metaData.getScale( position ); final int precision = metaData.getPrecision( position ); final int displaySize = metaData.getColumnDisplaySize( position ); - final Dialect dialect = jdbcServices.getDialect(); + final Dialect dialect = getJdbcServices().getDialect(); final int length = dialect.resolveSqlTypeLength( columnTypeName, columnType, @@ -104,13 +96,7 @@ default BasicType resolveType(int position, JavaType explicitJavaType, displaySize ); final JdbcType resolvedJdbcType = dialect - .resolveSqlTypeDescriptor( - columnTypeName, - columnType, - length, - scale, - typeConfiguration.getJdbcTypeRegistry() - ); + .resolveSqlTypeDescriptor( columnTypeName, columnType, length, scale, typeConfiguration.getJdbcTypeRegistry() ); final JavaType javaType; final JdbcType jdbcType; // If there is an explicit JavaType, then prefer its recommended JDBC type @@ -145,26 +131,19 @@ public EnumType getEnumeratedType() { @Override public Dialect getDialect() { - return getFactory().getJdbcServices().getDialect(); + return getJdbcServices().getDialect(); } } ); } else { jdbcType = resolvedJdbcType; - javaType = jdbcType.getJdbcRecommendedJavaTypeMapping( - length, - scale, - typeConfiguration - ); + javaType = jdbcType.getJdbcRecommendedJavaTypeMapping( length, scale, typeConfiguration ); } return typeConfiguration.getBasicTypeRegistry().resolve( javaType, jdbcType ); } catch (SQLException e) { - throw jdbcServices.getSqlExceptionHelper().convert( - e, - "Unable to determine JDBC type code for ResultSet position " + position - ); + throw convertSqlException( e, "Unable to determine JDBC type code for ResultSet position " + position ); } } } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/internal/ReactiveStandardRowReader.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/internal/ReactiveStandardRowReader.java index 87aa57103..f19031a55 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/internal/ReactiveStandardRowReader.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/internal/ReactiveStandardRowReader.java @@ -29,7 +29,7 @@ import static org.hibernate.reactive.logging.impl.LoggerFactory.make; import static org.hibernate.reactive.util.impl.CompletionStages.loop; import static org.hibernate.reactive.util.impl.CompletionStages.voidFuture; -import static org.hibernate.sql.results.LoadingLogger.LOGGER; +import static org.hibernate.sql.results.LoadingLogger.LOADING_LOGGER; /** @@ -121,7 +121,7 @@ public R readRow(RowProcessingState processingState) { @Override public CompletionStage reactiveReadRow(ReactiveRowProcessingState rowProcessingState, JdbcValuesSourceProcessingOptions options) { - LOGGER.trace( "ReactiveStandardRowReader#readRow" ); + LOADING_LOGGER.trace( "ReactiveStandardRowReader#readRow" ); return coordinateInitializers( rowProcessingState ) .thenCompose( v -> { @@ -160,7 +160,7 @@ private CompletionStage booleanComponent( final boolean[] resultRow = new boolean[resultAssemblers.length]; return loop( 0, assemblerCount, i -> { final DomainResultAssembler assembler = resultAssemblers[i]; - LOGGER.debugf( "Calling top-level assembler (%s / %s) : %s", i, assemblerCount, assembler ); + LOADING_LOGGER.debugf( "Calling top-level assembler (%s / %s) : %s", i, assemblerCount, assembler ); if ( assembler instanceof ReactiveDomainResultsAssembler ) { return ( (ReactiveDomainResultsAssembler) assembler ) .reactiveAssemble( rowProcessingState, options ) @@ -181,7 +181,7 @@ private CompletionStage byteComponent( final byte[] resultRow = new byte[resultAssemblers.length]; return loop( 0, assemblerCount, i -> { final DomainResultAssembler assembler = resultAssemblers[i]; - LOGGER.debugf( "Calling top-level assembler (%s / %s) : %s", i, assemblerCount, assembler ); + LOADING_LOGGER.debugf( "Calling top-level assembler (%s / %s) : %s", i, assemblerCount, assembler ); if ( assembler instanceof ReactiveDomainResultsAssembler ) { return ( (ReactiveDomainResultsAssembler) assembler ) .reactiveAssemble( rowProcessingState, options ) @@ -202,7 +202,7 @@ private CompletionStage charComponent( final char[] resultRow = new char[resultAssemblers.length]; return loop( 0, assemblerCount, i -> { final DomainResultAssembler assembler = resultAssemblers[i]; - LOGGER.debugf( "Calling top-level assembler (%s / %s) : %s", i, assemblerCount, assembler ); + LOADING_LOGGER.debugf( "Calling top-level assembler (%s / %s) : %s", i, assemblerCount, assembler ); if ( assembler instanceof ReactiveDomainResultsAssembler ) { return ( (ReactiveDomainResultsAssembler) assembler ) .reactiveAssemble( rowProcessingState, options ) @@ -223,7 +223,7 @@ private CompletionStage shortComponent( final short[] resultRow = new short[resultAssemblers.length]; return loop( 0, assemblerCount, i -> { final DomainResultAssembler assembler = resultAssemblers[i]; - LOGGER.debugf( "Calling top-level assembler (%s / %s) : %s", i, assemblerCount, assembler ); + LOADING_LOGGER.debugf( "Calling top-level assembler (%s / %s) : %s", i, assemblerCount, assembler ); if ( assembler instanceof ReactiveDomainResultsAssembler ) { return ( (ReactiveDomainResultsAssembler) assembler ) .reactiveAssemble( rowProcessingState, options ) @@ -244,7 +244,7 @@ private CompletionStage intComponent( final int[] resultRow = new int[resultAssemblers.length]; return loop( 0, assemblerCount, i -> { final DomainResultAssembler assembler = resultAssemblers[i]; - LOGGER.debugf( "Calling top-level assembler (%s / %s) : %s", i, assemblerCount, assembler ); + LOADING_LOGGER.debugf( "Calling top-level assembler (%s / %s) : %s", i, assemblerCount, assembler ); if ( assembler instanceof ReactiveDomainResultsAssembler ) { return ( (ReactiveDomainResultsAssembler) assembler ) .reactiveAssemble( rowProcessingState, options ) @@ -265,7 +265,7 @@ private CompletionStage longComponent( final long[] resultRow = new long[resultAssemblers.length]; return loop( 0, assemblerCount, i -> { final DomainResultAssembler assembler = resultAssemblers[i]; - LOGGER.debugf( "Calling top-level assembler (%s / %s) : %s", i, assemblerCount, assembler ); + LOADING_LOGGER.debugf( "Calling top-level assembler (%s / %s) : %s", i, assemblerCount, assembler ); if ( assembler instanceof ReactiveDomainResultsAssembler ) { return ( (ReactiveDomainResultsAssembler) assembler ) .reactiveAssemble( rowProcessingState, options ) @@ -286,7 +286,7 @@ private CompletionStage floatComponent( final float[] resultRow = new float[resultAssemblers.length]; return loop( 0, assemblerCount, i -> { final DomainResultAssembler assembler = resultAssemblers[i]; - LOGGER.debugf( "Calling top-level assembler (%s / %s) : %s", i, assemblerCount, assembler ); + LOADING_LOGGER.debugf( "Calling top-level assembler (%s / %s) : %s", i, assemblerCount, assembler ); if ( assembler instanceof ReactiveDomainResultsAssembler ) { return ( (ReactiveDomainResultsAssembler) assembler ) .reactiveAssemble( rowProcessingState, options ) @@ -307,7 +307,7 @@ private CompletionStage doubleComponent( final double[] resultRow = new double[resultAssemblers.length]; return loop( 0, assemblerCount, i -> { final DomainResultAssembler assembler = resultAssemblers[i]; - LOGGER.debugf( "Calling top-level assembler (%s / %s) : %s", i, assemblerCount, assembler ); + LOADING_LOGGER.debugf( "Calling top-level assembler (%s / %s) : %s", i, assemblerCount, assembler ); if ( assembler instanceof ReactiveDomainResultsAssembler ) { return ( (ReactiveDomainResultsAssembler) assembler ) .reactiveAssemble( rowProcessingState, options ) @@ -328,7 +328,7 @@ private CompletionStage objectComponent( final Object[] resultRow = (Object[]) Array.newInstance( resultElementClass, resultAssemblers.length ); return loop( 0, assemblerCount, i -> { final DomainResultAssembler assembler = resultAssemblers[i]; - LOGGER.debugf( "Calling top-level assembler (%s / %s) : %s", i, assemblerCount, assembler ); + LOADING_LOGGER.debugf( "Calling top-level assembler (%s / %s) : %s", i, assemblerCount, assembler ); if ( assembler instanceof ReactiveDomainResultsAssembler ) { return ( (ReactiveDomainResultsAssembler) assembler ) .reactiveAssemble( rowProcessingState, options ) @@ -362,7 +362,7 @@ public List> getResultJavaTypes() { } private void afterRow(RowProcessingState rowProcessingState) { - LOGGER.trace( "ReactiveStandardRowReader#afterRow" ); + LOADING_LOGGER.trace( "ReactiveStandardRowReader#afterRow" ); finishUpRow(); } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/spi/ReactiveListResultsConsumer.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/spi/ReactiveListResultsConsumer.java index e7283647f..108920f1a 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/spi/ReactiveListResultsConsumer.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/sql/results/spi/ReactiveListResultsConsumer.java @@ -271,7 +271,7 @@ private JavaType resolveDomainResultJavaType( return (JavaType) resultJavaTypes.get( 0 ); } - return javaTypeRegistry.resolveDescriptor( Object[].class ); + return javaTypeRegistry.getDescriptor( Object[].class ); } @Override diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/stage/Stage.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/stage/Stage.java index dccb3a072..9941bd257 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/stage/Stage.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/stage/Stage.java @@ -1979,6 +1979,55 @@ interface Transaction { */ interface SessionFactory extends AutoCloseable { + /** + * Obtain a new {@link Session reactive session}. + *

+ * The underlying database connection is obtained lazily + * when the returned {@link Session} needs to access the + * database. + *

+ * The client must close the session using {@link Session#close()}. + */ + @Incubating + Session createSession(); + + /** + * Obtain a new {@link Session reactive session}. + *

+ * The underlying database connection is obtained lazily + * when the returned {@link Session} needs to access the + * database. + *

+ * The client must close the session using {@link Session#close()}. + */ + @Incubating + Session createSession(String tenantId); + + /** + * Obtain a new {@link Session reactive session}. + *

+ * The underlying database connection is obtained lazily + * when the returned {@link Session} needs to access the + * database. + *

+ * The client must close the session using {@link Session#close()}. + */ + @Incubating + StatelessSession createStatelessSession(); + + /** + * Obtain a new {@link StatelessSession reactive stateless session}. + *

+ * The underlying database connection is obtained lazily + * when the returned {@link StatelessSession} needs to access the + * database. + *

+ * The client must close the session using {@link Session#close()}. + * @param tenantId the id of the tenant + */ + @Incubating + StatelessSession createStatelessSession(String tenantId); + /** * Obtain a new {@linkplain Session reactive session} {@link CompletionStage}, the main * interaction point between the user's program and Hibernate diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/stage/impl/StageSessionFactoryImpl.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/stage/impl/StageSessionFactoryImpl.java index 56c574ce4..6a04e25ae 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/stage/impl/StageSessionFactoryImpl.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/stage/impl/StageSessionFactoryImpl.java @@ -5,10 +5,19 @@ */ package org.hibernate.reactive.stage.impl; -import jakarta.persistence.metamodel.Metamodel; +import java.lang.invoke.MethodHandles; +import java.util.Objects; +import java.util.concurrent.CompletionStage; +import java.util.function.BiFunction; +import java.util.function.Function; +import java.util.function.Supplier; + import org.hibernate.Cache; -import org.hibernate.internal.SessionCreationOptions; +import org.hibernate.engine.creation.internal.SessionBuilderImpl; +import org.hibernate.engine.creation.internal.SessionCreationOptions; +import org.hibernate.engine.spi.SessionImplementor; import org.hibernate.internal.SessionFactoryImpl; +import org.hibernate.internal.SessionImpl; import org.hibernate.query.criteria.HibernateCriteriaBuilder; import org.hibernate.reactive.common.spi.Implementor; import org.hibernate.reactive.context.Context; @@ -18,18 +27,14 @@ import org.hibernate.reactive.logging.impl.LoggerFactory; import org.hibernate.reactive.pool.ReactiveConnection; import org.hibernate.reactive.pool.ReactiveConnectionPool; +import org.hibernate.reactive.session.ReactiveStatelessSession; import org.hibernate.reactive.session.impl.ReactiveSessionImpl; import org.hibernate.reactive.session.impl.ReactiveStatelessSessionImpl; import org.hibernate.reactive.stage.Stage; import org.hibernate.service.ServiceRegistry; import org.hibernate.stat.Statistics; -import java.lang.invoke.MethodHandles; -import java.util.Objects; -import java.util.concurrent.CompletionStage; -import java.util.function.BiFunction; -import java.util.function.Function; -import java.util.function.Supplier; +import jakarta.persistence.metamodel.Metamodel; import static org.hibernate.reactive.util.impl.CompletionStages.completedFuture; import static org.hibernate.reactive.util.impl.CompletionStages.rethrow; @@ -73,10 +78,36 @@ public Context getContext() { return context; } + @Override + public Stage.Session createSession() { + return createSession( getTenantIdentifier( options() ) ); + } + + @Override + public Stage.Session createSession(String tenantId) { + final SessionCreationOptions options = options(); + ReactiveConnectionPool pool = delegate.getServiceRegistry().getService( ReactiveConnectionPool.class ); + ReactiveSessionImpl sessionImpl = new ReactiveSessionImpl( delegate, options, pool.getProxyConnection( tenantId ) ); + return new StageSessionImpl( sessionImpl ); + } + + @Override + public Stage.StatelessSession createStatelessSession() { + return createStatelessSession( getTenantIdentifier( options() ) ); + } + + @Override + public Stage.StatelessSession createStatelessSession(String tenantId) { + final SessionCreationOptions options = options(); + ReactiveConnectionPool pool = delegate.getServiceRegistry().getService( ReactiveConnectionPool.class ); + ReactiveStatelessSession sessionImpl = new ReactiveStatelessSessionImpl( delegate, options, pool.getProxyConnection( tenantId ) ); + return new StageStatelessSessionImpl( sessionImpl ); + } + @Override public CompletionStage openSession() { SessionCreationOptions options = options(); - return connection( options.getTenantIdentifier() ) + return connection( getTenantIdentifier( options ) ) .thenCompose( connection -> create( connection, () -> new ReactiveSessionImpl( delegate, options, connection ) ) ) .thenApply( StageSessionImpl::new ); @@ -93,7 +124,7 @@ public CompletionStage openSession(String tenantId) { @Override public CompletionStage openStatelessSession() { SessionCreationOptions options = options(); - return connection( options.getTenantIdentifier() ) + return connection( getTenantIdentifier( options ) ) .thenCompose( connection -> create( connection, () -> new ReactiveStatelessSessionImpl( delegate, options, connection ) ) ) .thenApply( StageStatelessSessionImpl::new ); @@ -124,12 +155,26 @@ private CompletionStage create(ReactiveConnection connection, Supplier } private SessionCreationOptions options() { - return new SessionFactoryImpl.SessionBuilderImpl( delegate ); + return new SessionBuilderImpl( delegate ) { + @Override + protected SessionImplementor createSession() { + return new SessionImpl( delegate, this ); + } + }; } - private SessionCreationOptions options(String tenantIdentifier) { - return new SessionFactoryImpl.SessionBuilderImpl( delegate ) - .tenantIdentifier( tenantIdentifier ); + private SessionCreationOptions options(String tenantId) { + return new SessionBuilderImpl( delegate ) { + @Override + protected SessionImplementor createSession() { + return new SessionImpl( delegate, this ); + } + + @Override + public Object getTenantIdentifierValue() { + return tenantId; + } + }; } private CompletionStage connection(String tenantId) { @@ -288,4 +333,9 @@ public HibernateCriteriaBuilder getCriteriaBuilder() { return delegate.getCriteriaBuilder(); } + private String getTenantIdentifier(SessionCreationOptions options) { + return options.getTenantIdentifierValue() == null + ? null + : delegate.getTenantIdentifierJavaType().toString( options.getTenantIdentifierValue() ); + } } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/stage/impl/StageSessionImpl.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/stage/impl/StageSessionImpl.java index da09a284b..882393ed9 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/stage/impl/StageSessionImpl.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/stage/impl/StageSessionImpl.java @@ -135,7 +135,7 @@ public MutationQuery createMutationQuery(JpaCriteriaInsert insert) { @Override public CompletionStage find(Class entityClass, Object primaryKey) { - return delegate.reactiveFind( entityClass, primaryKey, null, null ); + return delegate.reactiveFind( entityClass, primaryKey ); } @Override @@ -150,7 +150,7 @@ public CompletionStage find(Class entityClass, Identifier id) { @Override public CompletionStage find(Class entityClass, Object primaryKey, LockMode lockMode) { - return delegate.reactiveFind( entityClass, primaryKey, new LockOptions( lockMode ), null ); + return delegate.reactiveFind( entityClass, primaryKey, lockMode, null ); } @Override @@ -167,7 +167,7 @@ public CompletionStage find(Class entityClass, Object primaryKey, Lock @Override public CompletionStage find(EntityGraph entityGraph, Object id) { Class entityClass = ( (RootGraph) entityGraph ).getGraphedType().getJavaType(); - return delegate.reactiveFind( entityClass, id, null, entityGraph ); + return delegate.reactiveFind( entityClass, id, (LockOptions) null, entityGraph ); } @Override @@ -212,7 +212,7 @@ public CompletionStage refresh(Object entity) { @Override public CompletionStage refresh(Object entity, LockMode lockMode) { - return delegate.reactiveRefresh( entity, new LockOptions(lockMode) ); + return delegate.reactiveRefresh( entity, lockMode ); } @Override @@ -232,7 +232,7 @@ public CompletionStage refresh(Object... entity) { @Override public CompletionStage lock(Object entity, LockMode lockMode) { - return delegate.reactiveLock( entity, new LockOptions(lockMode) ); + return delegate.reactiveLock( entity, lockMode ); } @Override @@ -391,7 +391,7 @@ public Stage.Session setSubselectFetchingEnabled(boolean enabled) { @Override public CompletionStage withTransaction(Function> work) { - return currentTransaction==null ? new Transaction().execute(work) : work.apply(currentTransaction); + return currentTransaction == null ? new Transaction().execute( work ) : work.apply( currentTransaction ); } private Transaction currentTransaction; diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/tuple/entity/ReactiveEntityMetamodel.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/tuple/entity/ReactiveEntityMetamodel.java index 2e20e7781..44b2badaf 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/tuple/entity/ReactiveEntityMetamodel.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/tuple/entity/ReactiveEntityMetamodel.java @@ -5,11 +5,15 @@ */ package org.hibernate.reactive.tuple.entity; +import java.util.Set; import java.util.function.Function; - +import org.hibernate.boot.model.relational.Database; +import org.hibernate.boot.model.relational.SqlStringGenerationContext; +import org.hibernate.bytecode.spi.BytecodeEnhancementMetadata; import org.hibernate.generator.Generator; import org.hibernate.generator.GeneratorCreationContext; +import org.hibernate.id.CompositeNestedGeneratedValueGenerator; import org.hibernate.id.Configurable; import org.hibernate.id.IdentifierGenerator; import org.hibernate.id.SelectGenerator; @@ -18,33 +22,42 @@ import org.hibernate.id.enhanced.SequenceStyleGenerator; import org.hibernate.id.enhanced.TableGenerator; import org.hibernate.id.enhanced.TableStructure; -import org.hibernate.mapping.GeneratorCreator; +import org.hibernate.mapping.GeneratorSettings; import org.hibernate.mapping.PersistentClass; +import org.hibernate.mapping.Property; +import org.hibernate.mapping.RootClass; import org.hibernate.mapping.SimpleValue; +import org.hibernate.mapping.Value; import org.hibernate.metamodel.spi.RuntimeModelCreationContext; -import org.hibernate.persister.entity.EntityPersister; +import org.hibernate.reactive.bythecode.spi.ReactiveBytecodeEnhancementMetadataPojoImplAdapter; import org.hibernate.reactive.id.ReactiveIdentifierGenerator; import org.hibernate.reactive.id.impl.EmulatedSequenceReactiveIdentifierGenerator; +import org.hibernate.reactive.id.impl.ReactiveCompositeNestedGeneratedValueGenerator; import org.hibernate.reactive.id.impl.ReactiveGeneratorWrapper; import org.hibernate.reactive.id.impl.ReactiveSequenceIdentifierGenerator; import org.hibernate.reactive.id.impl.TableReactiveIdentifierGenerator; import org.hibernate.reactive.logging.impl.Log; +import org.hibernate.service.ServiceRegistry; import org.hibernate.tuple.entity.EntityMetamodel; +import org.hibernate.type.CompositeType; +import org.hibernate.type.Type; import static java.lang.invoke.MethodHandles.lookup; import static org.hibernate.reactive.logging.impl.LoggerFactory.make; +/** + * @deprecated No Longer used + */ +@Deprecated(since = "4.2", forRemoval = true) public class ReactiveEntityMetamodel extends EntityMetamodel { private static final Log LOG = make( Log.class, lookup() ); public ReactiveEntityMetamodel( PersistentClass persistentClass, - EntityPersister persister, RuntimeModelCreationContext creationContext) { this( persistentClass, - persister, creationContext, s -> buildIdGenerator( s, persistentClass, creationContext ) ); @@ -52,10 +65,9 @@ public ReactiveEntityMetamodel( public ReactiveEntityMetamodel( PersistentClass persistentClass, - EntityPersister persister, RuntimeModelCreationContext creationContext, Function generatorSupplier) { - super( persistentClass, persister, creationContext, generatorSupplier ); + super( persistentClass, creationContext, generatorSupplier ); } private static Generator buildIdGenerator( @@ -67,20 +79,22 @@ private static Generator buildIdGenerator( return existing; } else { - SimpleValue identifier = (SimpleValue) persistentClass.getIdentifier(); - GeneratorCreator customIdGeneratorCreator = identifier.getCustomIdGeneratorCreator(); - identifier.setCustomIdGeneratorCreator( context -> { - Generator generator = customIdGeneratorCreator.createGenerator( context ); - return augmentWithReactiveGenerator( generator, context, creationContext ); - } ); - final Generator idgenerator = identifier - // returns the cached Generator if it was already created - .createGenerator( + final SimpleValue identifier = (SimpleValue) persistentClass.getIdentifier(); + final Generator idgenerator = augmentWithReactiveGenerator( + identifier.createGenerator( creationContext.getDialect(), persistentClass.getRootClass(), persistentClass.getIdentifierProperty(), creationContext.getGeneratorSettings() - ); + ), + new IdGeneratorCreationContext( + persistentClass.getRootClass(), + persistentClass.getIdentifierProperty(), + creationContext.getGeneratorSettings(), + identifier, + creationContext + ), + creationContext ); creationContext.getGenerators().put( rootName, idgenerator ); return idgenerator; } @@ -90,8 +104,8 @@ public static Generator augmentWithReactiveGenerator( Generator generator, GeneratorCreationContext creationContext, RuntimeModelCreationContext runtimeModelCreationContext) { - if ( generator instanceof SequenceStyleGenerator ) { - final DatabaseStructure structure = ( (SequenceStyleGenerator) generator ).getDatabaseStructure(); + if ( generator instanceof SequenceStyleGenerator sequenceStyleGenerator) { + final DatabaseStructure structure = sequenceStyleGenerator.getDatabaseStructure(); if ( structure instanceof TableStructure ) { return initialize( (IdentifierGenerator) generator, new EmulatedSequenceReactiveIdentifierGenerator( (TableStructure) structure, runtimeModelCreationContext ), creationContext ); } @@ -100,16 +114,28 @@ public static Generator augmentWithReactiveGenerator( } throw LOG.unknownStructureType(); } - if ( generator instanceof TableGenerator ) { + if ( generator instanceof TableGenerator tableGenerator ) { return initialize( (IdentifierGenerator) generator, - new TableReactiveIdentifierGenerator( (TableGenerator) generator, runtimeModelCreationContext ), + new TableReactiveIdentifierGenerator( tableGenerator, runtimeModelCreationContext ), creationContext ); } if ( generator instanceof SelectGenerator ) { throw LOG.selectGeneratorIsNotSupportedInHibernateReactive(); } + if ( generator instanceof CompositeNestedGeneratedValueGenerator compositeNestedGeneratedValueGenerator ) { + final ReactiveCompositeNestedGeneratedValueGenerator reactiveCompositeNestedGeneratedValueGenerator = new ReactiveCompositeNestedGeneratedValueGenerator( + compositeNestedGeneratedValueGenerator, + creationContext, + runtimeModelCreationContext + ); + return initialize( + (IdentifierGenerator) generator, + reactiveCompositeNestedGeneratedValueGenerator, + creationContext + ); + } //nothing to do return generator; } @@ -121,4 +147,74 @@ private static Generator initialize( ( (Configurable) reactiveIdGenerator ).initialize( creationContext.getSqlStringGenerationContext() ); return new ReactiveGeneratorWrapper( reactiveIdGenerator, idGenerator ); } + + private record IdGeneratorCreationContext( + RootClass rootClass, + Property property, + GeneratorSettings defaults, + SimpleValue identifier, + RuntimeModelCreationContext buildingContext) implements GeneratorCreationContext { + + @Override + public Database getDatabase() { + return buildingContext.getBootModel().getDatabase(); + } + + @Override + public ServiceRegistry getServiceRegistry() { + return buildingContext.getBootstrapContext().getServiceRegistry(); + } + + @Override + public SqlStringGenerationContext getSqlStringGenerationContext() { + return defaults.getSqlStringGenerationContext(); + } + + @Override + public String getDefaultCatalog() { + return defaults.getDefaultCatalog(); + } + + @Override + public String getDefaultSchema() { + return defaults.getDefaultSchema(); + } + + @Override + public RootClass getRootClass() { + return rootClass; + } + + @Override + public PersistentClass getPersistentClass() { + return rootClass; + } + + @Override + public Property getProperty() { + return property; + } + + @Override + public Value getValue() { + return identifier; + } + + @Override + public Type getType() { + return identifier.getType(); + } + } + + @Override + protected BytecodeEnhancementMetadata getBytecodeEnhancementMetadataPojo(PersistentClass persistentClass, RuntimeModelCreationContext creationContext, Set idAttributeNames, CompositeType nonAggregatedCidMapper, boolean collectionsInDefaultFetchGroupEnabled) { + return ReactiveBytecodeEnhancementMetadataPojoImplAdapter.from( + persistentClass, + idAttributeNames, + nonAggregatedCidMapper, + collectionsInDefaultFetchGroupEnabled, + creationContext.getMetadata() + ); + } + } diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/type/descriptor/jdbc/ReactiveArrayJdbcType.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/type/descriptor/jdbc/ReactiveArrayJdbcType.java index 99d7efaf7..5aee97bc7 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/type/descriptor/jdbc/ReactiveArrayJdbcType.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/type/descriptor/jdbc/ReactiveArrayJdbcType.java @@ -6,6 +6,7 @@ package org.hibernate.reactive.type.descriptor.jdbc; import java.lang.reflect.Array; +import java.lang.reflect.Type; import java.sql.CallableStatement; import java.sql.Date; import java.sql.PreparedStatement; @@ -20,6 +21,7 @@ import org.hibernate.reactive.adaptor.impl.ArrayAdaptor; import org.hibernate.reactive.adaptor.impl.ResultSetAdaptor; +import org.hibernate.type.BasicPluralType; import org.hibernate.type.descriptor.ValueBinder; import org.hibernate.type.descriptor.ValueExtractor; import org.hibernate.type.descriptor.WrapperOptions; @@ -32,8 +34,10 @@ import org.hibernate.type.descriptor.jdbc.JdbcLiteralFormatter; import org.hibernate.type.descriptor.jdbc.JdbcType; import org.hibernate.type.descriptor.jdbc.internal.JdbcLiteralFormatterArray; +import org.hibernate.type.internal.ParameterizedTypeImpl; import org.hibernate.type.spi.TypeConfiguration; +import static java.lang.reflect.Array.newInstance; /** @@ -63,10 +67,22 @@ public JavaType getJdbcRecommendedJavaTypeMapping( Integer precision, Integer scale, TypeConfiguration typeConfiguration) { - final JavaType elementJavaType = elementJdbcType - .getJdbcRecommendedJavaTypeMapping( precision, scale, typeConfiguration ); - return typeConfiguration.getJavaTypeRegistry() - .resolveDescriptor( Array.newInstance( elementJavaType.getJavaTypeClass(), 0 ).getClass() ); + final JavaType elementJavaType = + elementJdbcType.getJdbcRecommendedJavaTypeMapping( precision, scale, typeConfiguration ); + final var javaType = + typeConfiguration.getJavaTypeRegistry() + .resolveDescriptor( newInstance( elementJavaType.getJavaTypeClass(), 0 ).getClass() ); + if ( javaType instanceof BasicPluralType ) { + //noinspection unchecked + return (JavaType) javaType; + } + else { + //noinspection unchecked + return (JavaType) javaType.createJavaType( + new ParameterizedTypeImpl( javaType.getJavaTypeClass(), new Type[0], null ), + typeConfiguration + ); + } } @Override diff --git a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/type/descriptor/jdbc/ReactiveJsonArrayJdbcType.java b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/type/descriptor/jdbc/ReactiveJsonArrayJdbcType.java index 81af936ba..ab84952ef 100644 --- a/hibernate-reactive-core/src/main/java/org/hibernate/reactive/type/descriptor/jdbc/ReactiveJsonArrayJdbcType.java +++ b/hibernate-reactive-core/src/main/java/org/hibernate/reactive/type/descriptor/jdbc/ReactiveJsonArrayJdbcType.java @@ -5,26 +5,27 @@ */ package org.hibernate.reactive.type.descriptor.jdbc; -import org.hibernate.metamodel.mapping.EmbeddableMappingType; +import java.sql.CallableStatement; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; + import org.hibernate.type.SqlTypes; import org.hibernate.type.descriptor.ValueBinder; import org.hibernate.type.descriptor.ValueExtractor; import org.hibernate.type.descriptor.WrapperOptions; import org.hibernate.type.descriptor.java.BasicPluralJavaType; import org.hibernate.type.descriptor.java.JavaType; -import org.hibernate.type.descriptor.jdbc.AggregateJdbcType; +import org.hibernate.type.descriptor.java.spi.UnknownBasicJavaType; import org.hibernate.type.descriptor.jdbc.BasicBinder; import org.hibernate.type.descriptor.jdbc.BasicExtractor; import org.hibernate.type.descriptor.jdbc.JdbcLiteralFormatter; import org.hibernate.type.descriptor.jdbc.JdbcType; import org.hibernate.type.descriptor.jdbc.JsonHelper; -import org.hibernate.type.descriptor.jdbc.JsonJdbcType; +import org.hibernate.type.descriptor.jdbc.spi.JsonGeneratingVisitor; +import org.hibernate.type.format.StringJsonDocumentWriter; import io.vertx.core.json.JsonArray; -import java.sql.CallableStatement; -import java.sql.PreparedStatement; -import java.sql.ResultSet; -import java.sql.SQLException; /** * @see org.hibernate.type.descriptor.jdbc.JsonArrayJdbcType @@ -65,21 +66,24 @@ protected X fromString(String string, JavaType javaType, WrapperOptions o if ( string == null ) { return null; } - - return JsonHelper.arrayFromString( javaType, getElementJdbcType(), string, options ); + if ( ((BasicPluralJavaType) javaType).getElementJavaType() instanceof UnknownBasicJavaType ) { + return options.getJsonFormatMapper().fromString( string, javaType, options ); + } + else { + return JsonHelper.arrayFromString( javaType, this.getElementJdbcType(), string, options ); + } } protected String toString(X value, JavaType javaType, WrapperOptions options) { - final JdbcType elementJdbcType = getElementJdbcType(); - final Object[] domainObjects = javaType.unwrap( value, Object[].class, options ); - if ( elementJdbcType instanceof JsonJdbcType jsonElementJdbcType ) { - final EmbeddableMappingType embeddableMappingType = jsonElementJdbcType.getEmbeddableMappingType(); - return JsonHelper.arrayToString( embeddableMappingType, domainObjects, options ); + final JavaType elementJavaType = ( (BasicPluralJavaType) javaType ).getElementJavaType(); + if ( elementJavaType instanceof UnknownBasicJavaType ) { + return options.getJsonFormatMapper().toString( value, javaType, options); } else { - assert !( elementJdbcType instanceof AggregateJdbcType ); - final JavaType elementJavaType = ( (BasicPluralJavaType) javaType ).getElementJavaType(); - return JsonHelper.arrayToString( elementJavaType, elementJdbcType, domainObjects, options ); + final Object[] domainObjects = javaType.unwrap( value, Object[].class, options ); + final StringJsonDocumentWriter writer = new StringJsonDocumentWriter(); + JsonGeneratingVisitor.INSTANCE.visitArray( elementJavaType, getElementJdbcType(), domainObjects, options, writer ); + return writer.getJson(); } } @@ -121,12 +125,8 @@ protected X doExtract(CallableStatement statement, String name, WrapperOptions o } private X getObject(Object array, WrapperOptions options) throws SQLException { - if ( array == null ) { - return null; - } - - return ( (ReactiveJsonArrayJdbcType) getJdbcType() ) - .fromString( ( (JsonArray) array ).encode(), getJavaType(), options ); + final String json = array == null ? null : ( (JsonArray) array ).encode(); + return ( (ReactiveJsonArrayJdbcType) getJdbcType() ).fromString( json, getJavaType(), options ); } }; } diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/BaseReactiveTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/BaseReactiveTest.java index 07a79379b..fc8a7b8f4 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/BaseReactiveTest.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/BaseReactiveTest.java @@ -308,38 +308,32 @@ protected CompletionStage cleanDb() { } protected static CompletionStage closeSession(Object closable) { - if ( closable instanceof CompletionStage ) { - CompletionStage closableStage = (CompletionStage) closable; + if ( closable instanceof CompletionStage closableStage ) { return closableStage.thenCompose( BaseReactiveTest::closeSession ); } - if ( closable instanceof Uni ) { - Uni closableUni = (Uni) closable; + if ( closable instanceof Uni closableUni ) { return closableUni.subscribeAsCompletionStage() .thenCompose( BaseReactiveTest::closeSession ); } - if ( closable instanceof ReactiveConnection ) { - return ( (ReactiveConnection) closable ).close(); + if ( closable instanceof ReactiveConnection reactiveConnection) { + return reactiveConnection.close(); } - if ( closable instanceof Mutiny.Session ) { - Mutiny.Session mutiny = (Mutiny.Session) closable; + if ( closable instanceof Mutiny.Session mutiny ) { if ( mutiny.isOpen() ) { return mutiny.close().subscribeAsCompletionStage(); } } - if ( closable instanceof Stage.Session ) { - Stage.Session stage = (Stage.Session) closable; + if ( closable instanceof Stage.Session stage ) { if ( stage.isOpen() ) { return stage.close(); } } - if ( closable instanceof Mutiny.StatelessSession ) { - Mutiny.StatelessSession mutiny = (Mutiny.StatelessSession) closable; + if ( closable instanceof Mutiny.StatelessSession mutiny ) { if ( mutiny.isOpen() ) { return mutiny.close().subscribeAsCompletionStage(); } } - if ( closable instanceof Stage.StatelessSession ) { - Stage.StatelessSession stage = (Stage.StatelessSession) closable; + if ( closable instanceof Stage.StatelessSession stage ) { if ( stage.isOpen() ) { return stage.close(); } diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/BatchFetchTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/BatchFetchTest.java index 48032f684..95394f63a 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/BatchFetchTest.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/BatchFetchTest.java @@ -38,12 +38,9 @@ import jakarta.persistence.Version; import static java.util.concurrent.TimeUnit.MINUTES; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.assertj.core.api.Assertions.assertThat; @Timeout(value = 10, timeUnit = MINUTES) - public class BatchFetchTest extends BaseReactiveTest { @Override @@ -70,43 +67,40 @@ public void testQuery(VertxTestContext context) { test( context, getSessionFactory() .withTransaction( s -> s.persist( basik ) ) - .thenCompose( v -> openSession() ) - .thenCompose( s -> s.createSelectionQuery( "from Node n order by id", Node.class ) + .thenCompose( v -> getSessionFactory().withSession( s -> s + .createSelectionQuery( "from Node n order by id", Node.class ) .getResultList() .thenCompose( list -> { - assertEquals( list.size(), 2 ); + assertThat( list ).hasSize( 2 ); Node n1 = list.get( 0 ); Node n2 = list.get( 1 ); - assertFalse( Hibernate.isInitialized( n1.getElements() ), "'n1.elements' should not be initialize" ); - assertFalse( Hibernate.isInitialized( n2.getElements() ), "'n2.elements' should not be initialize" ); + assertThat( Hibernate.isInitialized( n1.getElements() ) ).as( "'n1.elements' should not be initialized" ).isFalse(); + assertThat( Hibernate.isInitialized( n2.getElements() ) ).as( "'n2.elements' should not be initialized" ).isFalse(); return s.fetch( n1.getElements() ) .thenAccept( elements -> { - assertTrue( Hibernate.isInitialized( elements ), "'elements' after fetch should not be initialize" ); - assertTrue( Hibernate.isInitialized( n1.getElements() ), "'n1.elements' after fetch should be initialize" ); - assertTrue( Hibernate.isInitialized( n2.getElements() ), "'n2.elements' after fetch should be initialize" ); + assertThat( Hibernate.isInitialized( elements ) ).as( "'elements' after fetch should not be initialize" ).isTrue(); + assertThat( Hibernate.isInitialized( n1.getElements() ) ).as( "'n1.elements' after fetch should be initialize" ).isTrue(); + assertThat( Hibernate.isInitialized( n2.getElements() ) ).as( "'n2.elements' after fetch should be initialize" ).isTrue(); } ); } ) - ) - .thenCompose( v -> openSession() ) - .thenCompose( s -> s.createSelectionQuery( "from Element e order by id", Element.class ) + ) ) + .thenCompose( v -> getSessionFactory().withTransaction( s -> s + .createSelectionQuery( "from Element e order by id", Element.class ) .getResultList() .thenCompose( list -> { - assertEquals( list.size(), 5 ); - list.forEach( element -> assertFalse( Hibernate.isInitialized( element.node ) ) ); - list.forEach( element -> assertEquals( s.getLockMode( element.node ), LockMode.NONE ) ); + assertThat( list ).hasSize( 5 ); + list.forEach( element -> assertThat( Hibernate.isInitialized( element.node ) ).isFalse() ); + list.forEach( element -> assertThat( s.getLockMode( element.node ) ).isEqualTo( LockMode.NONE ) ); return s.fetch( list.get( 0 ).node ) .thenAccept( node -> { - assertTrue( Hibernate.isInitialized( node ) ); + assertThat( Hibernate.isInitialized( node ) ).isTrue(); //TODO: I would like to assert that they're all initialized // but apparently it doesn't set the proxies to init'd // so check the LockMode as a workaround - list.forEach( element -> assertEquals( - s.getLockMode( element.node ), - LockMode.READ - ) ); + list.forEach( element -> assertThat( s.getLockMode( element.node ) ).isEqualTo( LockMode.READ ) ); } ); } ) - ) + ) ) ); } @@ -125,11 +119,11 @@ public void testBatchLoad(VertxTestContext context) { .thenCompose( v -> openSession() ) .thenCompose( s -> s.find( Element.class, basik.elements.get( 1 ).id, basik.elements.get( 2 ).id, basik.elements.get( 0 ).id ) ) .thenAccept( elements -> { - assertFalse( elements.isEmpty() ); - assertEquals( 3, elements.size() ); - assertEquals( basik.elements.get( 1 ).id, elements.get( 0 ).id ); - assertEquals( basik.elements.get( 2 ).id, elements.get( 1 ).id ); - assertEquals( basik.elements.get( 0 ).id, elements.get( 2 ).id ); + assertThat( elements ).isNotEmpty(); + assertThat( elements ).hasSize( 3 ); + assertThat( elements.get( 0 ).id ).isEqualTo( basik.elements.get( 1 ).id ); + assertThat( elements.get( 1 ).id ).isEqualTo( basik.elements.get( 2 ).id ); + assertThat( elements.get( 2 ).id ).isEqualTo( basik.elements.get( 0 ).id ); } ) ); } @@ -150,12 +144,12 @@ public void testWithCollection(VertxTestContext context) { .createSelectionQuery( "from Node n order by id", Node.class ) .getResultList() .thenCompose( list -> { - assertEquals( list.size(), 1 ); + assertThat( list ).hasSize( 1 ); Node n1 = list.get( 0 ); - assertFalse( Hibernate.isInitialized( n1.elements ) ); + assertThat( Hibernate.isInitialized( n1.elements ) ).isFalse(); return s.fetch( n1.elements ).thenAccept( elements -> { - assertTrue( Hibernate.isInitialized( elements ) ); - assertTrue( Hibernate.isInitialized( n1.elements ) ); + assertThat( Hibernate.isInitialized( elements ) ).isTrue(); + assertThat( Hibernate.isInitialized( n1.elements ) ).isTrue(); } ); } ) ) @@ -177,7 +171,7 @@ public Element(Node node) { this.node = node; } - Element() { + public Element() { } public Node getNode() { @@ -227,7 +221,7 @@ public Node(String string) { this.string = string; } - Node() { + public Node() { } @PrePersist diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/BatchingConnectionTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/BatchingConnectionTest.java index 75d90375e..869d2ab9b 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/BatchingConnectionTest.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/BatchingConnectionTest.java @@ -5,13 +5,13 @@ */ package org.hibernate.reactive; - import org.hibernate.boot.registry.StandardServiceRegistryBuilder; import org.hibernate.cfg.AvailableSettings; import org.hibernate.cfg.Configuration; import org.hibernate.reactive.mutiny.impl.MutinySessionImpl; import org.hibernate.reactive.mutiny.impl.MutinyStatelessSessionImpl; import org.hibernate.reactive.pool.BatchingConnection; +import org.hibernate.reactive.pool.ReactiveConnection; import org.hibernate.reactive.pool.impl.SqlClientConnection; import org.hibernate.reactive.stage.impl.StageSessionImpl; import org.hibernate.reactive.stage.impl.StageStatelessSessionImpl; @@ -30,7 +30,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; @Timeout(value = 10, timeUnit = MINUTES) - public class BatchingConnectionTest extends ReactiveSessionTest { private static SqlStatementTracker sqlTracker; @@ -65,6 +64,27 @@ private static boolean filter(String s) { return false; } + @Override + protected void assertConnectionIsLazy(ReactiveConnection connection) { + assertConnectionIsLazy( connection, false ); + } + + @Override + protected void assertConnectionIsLazy(ReactiveConnection connection, boolean stateless) { + final ReactiveConnection actualConnection; + if ( !stateless ) { + // Only the stateful session creates a batching connection + assertThat( connection ).isInstanceOf( BatchingConnection.class ); + // A little hack, withBatchSize returns the underlying connection when the parameter is less than 1 + actualConnection = connection.withBatchSize( -1 ); + } + else { + actualConnection = connection; + } + assertThat( actualConnection.getClass().getName() ) + .isEqualTo( org.hibernate.reactive.pool.impl.SqlClientPool.class.getName() + "$ProxyConnection" ); + } + @Test public void testBatchingWithPersistAll(VertxTestContext context) { test( context, openSession().thenCompose( s -> s diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/CancelSignalTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/CancelSignalTest.java index d1ae51355..933bc51bb 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/CancelSignalTest.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/CancelSignalTest.java @@ -5,16 +5,19 @@ */ package org.hibernate.reactive; +import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Objects; import java.util.Queue; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionStage; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; -import java.util.stream.IntStream; + + import org.junit.jupiter.api.Test; @@ -27,73 +30,76 @@ import jakarta.persistence.Id; import jakarta.persistence.Table; -import static java.util.Arrays.stream; import static java.util.concurrent.CompletableFuture.allOf; import static java.util.concurrent.CompletableFuture.runAsync; -import static java.util.stream.Stream.concat; import static org.assertj.core.api.Assertions.assertThat; +import static org.hibernate.reactive.util.impl.CompletionStages.voidFuture; public class CancelSignalTest extends BaseReactiveTest { private static final Logger LOG = Logger.getLogger( CancelSignalTest.class ); + private static final int EXECUTION_SIZE = 10; + @Override protected Collection> annotatedEntities() { return List.of( GuineaPig.class ); } + @Override + public CompletionStage deleteEntities(Class... entities) { + // We don't need to delete anything + return voidFuture(); + } + @Test public void cleanupConnectionWhenCancelSignal(VertxTestContext context) { // larger than 'sql pool size' to check entering the 'pool waiting queue' - int executeSize = 10; CountDownLatch firstSessionWaiter = new CountDownLatch( 1 ); Queue cancellableQueue = new ConcurrentLinkedQueue<>(); - ExecutorService withSessionExecutor = Executors.newFixedThreadPool( executeSize ); - // Create some jobs that are going to be cancelled asynchronously - CompletableFuture[] withSessionFutures = IntStream - .range( 0, executeSize ) - .mapToObj( i -> runAsync( - () -> { - CountDownLatch countDownLatch = new CountDownLatch( 1 ); - Cancellable cancellable = getMutinySessionFactory() - .withSession( s -> { - LOG.debug( "start withSession: " + i ); - sleep( 100 ); - firstSessionWaiter.countDown(); - return s.find( GuineaPig.class, 1 ); - } ) - .onTermination().invoke( () -> { - countDownLatch.countDown(); - LOG.debug( "future " + i + " terminated" ); - } ) - .subscribe().with( item -> LOG.debug( "end withSession: " + i ) ); - cancellableQueue.add( cancellable ); - await( countDownLatch ); - }, - withSessionExecutor - ) ) - .toArray( CompletableFuture[]::new ); - - // Create jobs that are going to cancel the previous ones - ExecutorService cancelExecutor = Executors.newFixedThreadPool( executeSize ); - CompletableFuture[] cancelFutures = IntStream - .range( 0, executeSize ) - .mapToObj( i -> runAsync( - () -> { - await( firstSessionWaiter ); - cancellableQueue.poll().cancel(); - sleep( 500 ); - }, - cancelExecutor - ) ) - .toArray( CompletableFuture[]::new ); - - CompletableFuture allFutures = allOf( concat( stream( withSessionFutures ), stream( cancelFutures ) ) - .toArray( CompletableFuture[]::new ) - ); + final List> allFutures = new ArrayList<>(); + + ExecutorService withSessionExecutor = Executors.newFixedThreadPool( EXECUTION_SIZE ); + for ( int j = 0; j < EXECUTION_SIZE; j++ ) { + final int i = j; + allFutures.add( runAsync( () -> { + CountDownLatch countDownLatch = new CountDownLatch( 1 ); + Cancellable cancellable = getMutinySessionFactory() + .withSession( s -> { + LOG.info( "start withSession: " + i ); + sleep( 100 ); + firstSessionWaiter.countDown(); + return s.find( GuineaPig.class, 1 ); + } ) + .onCancellation().invoke( () -> { + LOG.info( "future " + i + " cancelled" ); + countDownLatch.countDown(); + } ) + .subscribe() + // We cancelled the job, it shouldn't really finish + .with( item -> LOG.info( "end withSession: " + i ) ); + cancellableQueue.add( cancellable ); + await( countDownLatch ); + }, + withSessionExecutor + ) ); + } - // Test that there shouldn't be any pending process - test( context, allFutures.thenAccept( x -> assertThat( sqlPendingMetric() ).isEqualTo( 0.0 ) ) ); + ExecutorService cancelExecutor = Executors.newFixedThreadPool( EXECUTION_SIZE ); + for ( int i = 0; i < EXECUTION_SIZE; i++ ) { + allFutures.add( runAsync( () -> { + await( firstSessionWaiter ); + cancellableQueue.poll().cancel(); + sleep( 500 ); + }, + cancelExecutor + ) ); + } + + test( + context, allOf( allFutures.toArray( new CompletableFuture[0] ) ) + .thenAccept( x -> assertThat( sqlPendingMetric() ).isEqualTo( 0.0 ) ) + ); } private static double sqlPendingMetric() { diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/CascadeTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/CascadeTest.java index ec94d6c8a..9845a6c96 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/CascadeTest.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/CascadeTest.java @@ -5,6 +5,7 @@ */ package org.hibernate.reactive; +import jakarta.persistence.Tuple; import java.util.ArrayList; import java.util.List; import java.util.Objects; @@ -12,7 +13,6 @@ import org.hibernate.Hibernate; import org.hibernate.cfg.Configuration; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import io.vertx.junit5.Timeout; @@ -36,11 +36,7 @@ import jakarta.persistence.Version; import static java.util.concurrent.TimeUnit.MINUTES; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; - +import static org.assertj.core.api.Assertions.assertThat; @Timeout(value = 10, timeUnit = MINUTES) public class CascadeTest extends BaseReactiveTest { @@ -55,119 +51,129 @@ protected Configuration constructConfiguration() { @Test public void testQuery(VertxTestContext context) { - - Node basik = new Node("Child"); - basik.parent = new Node("Parent"); - basik.elements.add(new Element(basik)); - basik.elements.add(new Element(basik)); - basik.elements.add(new Element(basik)); - - test( context, + Node basik = new Node( "Child" ); + basik.parent = new Node( "Parent" ); + basik.elements.add( new Element( basik ) ); + basik.elements.add( new Element( basik ) ); + basik.elements.add( new Element( basik ) ); + + test( + context, openSession() - .thenCompose(s -> s.persist(basik).thenCompose(v -> s.flush())) + .thenCompose( s -> s.persist( basik ).thenCompose( v -> s.flush() ) ) .thenCompose( v -> openSession() ) - .thenCompose( s -> s.createQuery("select distinct n from Node n left join fetch n.elements").getResultList()) - .thenAccept( list -> assertEquals( list.size(), 2 ) ) + .thenCompose( s -> s.createQuery( "select distinct n from Node n left join fetch n.elements", Node.class ).getResultList() ) + .thenAccept( list -> assertThat( list ).hasSize( 2 ) ) .thenCompose( v -> openSession() ) - .thenCompose( s -> s.createQuery("select distinct n, e from Node n join n.elements e").getResultList()) - .thenAccept( list -> assertEquals( list.size(), 3 ) ) + .thenCompose( s -> s.createQuery( "select distinct n, e from Node n join n.elements e", Tuple.class ).getResultList() ) + .thenAccept( list -> assertThat( list ).hasSize( 3 ) ) .thenCompose( v -> openSession() ) - .thenCompose( s -> s.createQuery("select distinct n.id, e.id from Node n join n.elements e").getResultList()) - .thenAccept( list -> assertEquals( list.size(), 3 ) ) + .thenCompose( s -> s.createQuery( "select distinct n.id, e.id from Node n join n.elements e", Tuple.class ).getResultList() ) + .thenAccept( list -> assertThat( list ).hasSize( 3 ) ) .thenCompose( v -> openSession() ) - .thenCompose( s -> s.createQuery("select max(e.id), min(e.id), sum(e.id) from Node n join n.elements e group by n.id order by n.id").getResultList()) - .thenAccept( list -> assertEquals( list.size(), 1 ) ) + .thenCompose( s -> s.createQuery( "select max(e.id), min(e.id), sum(e.id) from Node n join n.elements e group by n.id order by n.id", Tuple.class ).getResultList() ) + .thenAccept( list -> assertThat( list ).hasSize( 1 ) ) ); } @Test public void testCascade(VertxTestContext context) { - - Node basik = new Node("Child"); - basik.parent = new Node("Parent"); - basik.elements.add( new Element(basik) ); - basik.elements.add( new Element(basik) ); - basik.elements.add( new Element(basik) ); - - test( context, + Node basik = new Node( "Child" ); + basik.parent = new Node( "Parent" ); + basik.elements.add( new Element( basik ) ); + basik.elements.add( new Element( basik ) ); + basik.elements.add( new Element( basik ) ); + + test( + context, openSession() - .thenCompose(s -> s.persist(basik) - .thenApply(v -> { assertTrue(basik.prePersisted && !basik.postPersisted); return s; } ) - .thenApply(v -> { assertTrue(basik.parent.prePersisted && !basik.parent.postPersisted); return s; } ) - .thenCompose(v -> s.flush()) - .thenApply(v -> { assertTrue(basik.prePersisted && basik.postPersisted); return s; } ) - .thenApply(v -> { assertTrue(basik.parent.prePersisted && basik.parent.postPersisted); return s; } ) + .thenCompose( s -> s + .persist( basik ) + .thenAccept( v -> { + assertThat( basik.prePersisted && !basik.postPersisted ).isTrue(); + assertThat( basik.parent.prePersisted && !basik.parent.postPersisted ).isTrue(); + } ) + .thenCompose( v -> s.flush() ) + .thenAccept( v -> { + assertThat( basik.prePersisted && basik.postPersisted ).isTrue(); + assertThat( basik.parent.prePersisted && basik.parent.postPersisted ).isTrue(); + } ) ) .thenCompose( v -> openSession() ) - .thenCompose(s2 -> s2.find( Node.class, basik.getId() ) + .thenCompose( s2 -> s2 + .find( Node.class, basik.getId() ) .thenCompose( node -> { - assertNotNull( node ); - assertTrue( node.loaded ); - assertEquals( node.string, basik.string); - assertEquals( node.version, 0 ); - assertEquals( node.elements.size(), basik.elements.size() ); + assertThat( node ).isNotNull(); + assertThat( node.loaded ).isTrue(); + assertThat( node.string ).isEqualTo( basik.string ); + assertThat( node.version ).isEqualTo( 0 ); + assertThat( node.elements.size() ).isEqualTo( basik.elements.size() ); node.string = "Adopted"; - node.parent = new Node("New Parent"); + node.parent = new Node( "New Parent" ); return s2.flush() - .thenAccept(v -> { - assertNotNull( node ); - assertTrue( node.postUpdated && node.preUpdated ); - assertFalse( node.postPersisted && node.prePersisted ); - assertTrue( node.parent.postPersisted && node.parent.prePersisted ); - assertEquals( node.version, 1 ); - }); - })) + .thenAccept( v -> { + assertThat( node ).isNotNull(); + assertThat( node.postUpdated && node.preUpdated ).isTrue(); + assertThat( node.postPersisted && node.prePersisted ).isFalse(); + assertThat( node.parent.postPersisted && node.parent.prePersisted ).isTrue(); + assertThat( node.version ).isEqualTo( 1 ); + } ); + } ) ) .thenCompose( v -> openSession() ) - .thenCompose(s2 -> s2.find( Node.class, basik.getId() ) + .thenCompose( s2 -> s2.find( Node.class, basik.getId() ) .thenCompose( node -> { - assertNotNull( node ); - assertEquals( node.version, 1 ); - assertEquals( node.string, "Adopted"); - assertTrue(Hibernate.isInitialized(node.elements)); - assertFalse(Hibernate.isInitialized(node.parent)); + assertThat( node ).isNotNull(); + assertThat( node.version ).isEqualTo( 1 ); + assertThat( node.string ).isEqualTo( "Adopted" ); + assertThat( Hibernate.isInitialized( node.elements ) ).isTrue(); + assertThat( Hibernate.isInitialized( node.parent ) ).isFalse(); return s2.fetch( node.parent ) .thenCompose( parent -> { - assertNotNull( parent ); - return s2.createQuery("update Node set string = upper(string)").executeUpdate() - .thenCompose(v -> s2.refresh(node)) - .thenAccept(v -> { - assertEquals( node.getString(), "ADOPTED" ); - assertEquals( parent.getString(), "NEW PARENT" ); - assertTrue( Hibernate.isInitialized( node.elements ) ); - assertTrue( Hibernate.isInitialized( parent.elements ) ); - }); - }); - })) + assertThat( parent ).isNotNull(); + return s2.createMutationQuery( "update Node set string = upper(string)" ) + .executeUpdate() + .thenCompose( v -> s2.refresh( node ) ) + .thenAccept( v -> { + assertThat( node.getString() ).isEqualTo( "ADOPTED" ); + assertThat( parent.getString() ).isEqualTo( "NEW PARENT" ); + assertThat( Hibernate.isInitialized( node.elements ) ).isTrue(); + assertThat( Hibernate.isInitialized( parent.elements ) ).isTrue(); + } ); + } ); + } ) ) .thenCompose( v -> openSession() ) - .thenCompose(s3 -> s3.find( Node.class, basik.getId() ) + .thenCompose( s3 -> s3.find( Node.class, basik.getId() ) .thenCompose( node -> { - assertFalse( node.postUpdated && node.preUpdated ); - assertFalse( node.postPersisted && node.prePersisted ); - assertEquals( node.version, 1 ); - assertEquals( node.string, "ADOPTED"); + assertThat( node.postUpdated && node.preUpdated ).isFalse(); + assertThat( node.postPersisted && node.prePersisted ).isFalse(); + assertThat( node.version ).isEqualTo( 1 ); + assertThat( node.string ).isEqualTo( "ADOPTED" ); basik.version = node.version; basik.string = "Hello World!"; basik.parent.string = "Goodbye World!"; - return s3.merge(basik) + return s3.merge( basik ) .thenAccept( b -> { - assertEquals( b.string, "Hello World!"); - assertEquals( b.parent.string, "Goodbye World!"); - }) - .thenCompose(v -> s3.remove(node)) - .thenAccept(v -> assertTrue( !node.postRemoved && node.preRemoved ) ) - .thenCompose(v -> s3.flush()) - .thenAccept(v -> assertTrue( node.postRemoved && node.preRemoved ) ); - })) + assertThat( b.string ).isEqualTo( "Hello World!" ); + assertThat( b.parent.string ).isEqualTo( "Goodbye World!" ); + } ) + .thenCompose( v -> s3.remove( node ) ) + .thenAccept( v -> assertThat( !node.postRemoved && node.preRemoved ).isTrue() ) + .thenCompose( v -> s3.flush() ) + .thenAccept( v -> assertThat( node.postRemoved && node.preRemoved ).isTrue() ); + } ) ) .thenCompose( v -> openSession() ) - .thenCompose(s4 -> s4.find( Node.class, basik.getId() ) - .thenAccept( Assertions::assertNull)) + .thenCompose( s4 -> s4.find( Node.class, basik.getId() ) + .thenAccept( result -> assertThat( result ).isNull() ) ) ); } - @Entity(name = "Element") @Table(name="Element") + @Entity(name = "Element") + @Table(name = "Element") public static class Element { - @Id @GeneratedValue Integer id; + @Id + @GeneratedValue + Integer id; @ManyToOne Node node; @@ -176,42 +182,59 @@ public Element(Node node) { this.node = node; } - Element() {} + public Element() { + } } - @Entity(name = "Node") @Table(name="Node") + @Entity(name = "Node") + @Table(name = "Node") public static class Node { - @Id @GeneratedValue Integer id; - @Version Integer version; + @Id + @GeneratedValue + Integer id; + @Version + Integer version; String string; @ManyToOne(fetch = FetchType.LAZY, - cascade = {CascadeType.PERSIST, + cascade = { + CascadeType.PERSIST, CascadeType.REFRESH, CascadeType.MERGE, - CascadeType.REMOVE}) + CascadeType.REMOVE + }) Node parent; @OneToMany(fetch = FetchType.EAGER, - cascade = {CascadeType.PERSIST, - CascadeType.REMOVE}, + cascade = { + CascadeType.PERSIST, + CascadeType.REMOVE + }, mappedBy = "node") List elements = new ArrayList<>(); - @Transient boolean prePersisted; - @Transient boolean postPersisted; - @Transient boolean preUpdated; - @Transient boolean postUpdated; - @Transient boolean postRemoved; - @Transient boolean preRemoved; - @Transient boolean loaded; + @Transient + boolean prePersisted; + @Transient + boolean postPersisted; + @Transient + boolean preUpdated; + @Transient + boolean postUpdated; + @Transient + boolean postRemoved; + @Transient + boolean preRemoved; + @Transient + boolean loaded; public Node(String string) { this.string = string; } - Node() {} + public Node() { + } @PrePersist void prePersist() { @@ -278,12 +301,12 @@ public boolean equals(Object o) { return false; } Node node = (Node) o; - return Objects.equals(string, node.string); + return Objects.equals( string, node.string ); } @Override public int hashCode() { - return Objects.hash(string); + return Objects.hash( string ); } } } diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/CollectionStatelessSessionListenerTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/CollectionStatelessSessionListenerTest.java new file mode 100644 index 000000000..0d88a6700 --- /dev/null +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/CollectionStatelessSessionListenerTest.java @@ -0,0 +1,245 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.Set; + +import org.hibernate.engine.spi.SessionFactoryImplementor; +import org.hibernate.event.service.spi.EventListenerRegistry; +import org.hibernate.event.spi.AbstractCollectionEvent; +import org.hibernate.event.spi.EventType; +import org.hibernate.event.spi.PostCollectionRecreateEvent; +import org.hibernate.event.spi.PostCollectionRecreateEventListener; +import org.hibernate.event.spi.PostCollectionRemoveEvent; +import org.hibernate.event.spi.PostCollectionRemoveEventListener; +import org.hibernate.event.spi.PreCollectionRecreateEvent; +import org.hibernate.event.spi.PreCollectionRecreateEventListener; +import org.hibernate.event.spi.PreCollectionRemoveEvent; +import org.hibernate.event.spi.PreCollectionRemoveEventListener; + +import org.junit.jupiter.api.Test; + +import io.vertx.junit5.VertxTestContext; +import jakarta.persistence.Column; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToMany; +import jakarta.persistence.Table; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Adapt test in ORM: CollectionStatelessSessionListenerTest + */ +public class CollectionStatelessSessionListenerTest extends BaseReactiveTest { + + @Override + protected Collection> annotatedEntities() { + return Set.of( EntityA.class, EntityB.class ); + } + + @Test + public void mutinyStatelessInsert(VertxTestContext context) { + final List events = new ArrayList<>(); + initializeListeners( events ); + + EntityA a = new EntityA(); + EntityB b = new EntityB(); + a.children.add( b ); + test( context, getMutinySessionFactory() + .withStatelessTransaction( statelessSession -> statelessSession + .insert( b ) + .chain( () -> statelessSession.insert( a ) ) + .chain( () -> statelessSession.delete( a ) ) + .chain( () -> statelessSession.delete( b ) ) + ) + .invoke( () -> assertEvents( events ) ) + ); + } + + @Test + public void mutinyStatelessInsertAll(VertxTestContext context) { + final List events = new ArrayList<>(); + initializeListeners( events ); + + EntityA a = new EntityA(); + EntityB b = new EntityB(); + a.children.add( b ); + test( context, getMutinySessionFactory() + .withStatelessTransaction( statelessSession -> statelessSession + .insertAll( b, a ) + .chain( () -> statelessSession.deleteAll( a, b ) ) + ) + .invoke( () -> assertEvents( events ) ) + ); + } + + @Test + public void stageStatelessInsert(VertxTestContext context) { + final List events = new ArrayList<>(); + initializeListeners( events ); + + EntityA a = new EntityA(); + EntityB b = new EntityB(); + a.children.add( b ); + test( context, getSessionFactory() + .withStatelessTransaction( statelessSession -> statelessSession + .insert( b ) + .thenCompose( v -> statelessSession.insert( a ) ) + .thenCompose( v -> statelessSession.delete( a ) ) + .thenCompose( v -> statelessSession.delete( b ) ) + ) + .thenAccept( v -> assertEvents( events ) ) + ); + } + + @Test + public void stageStatelessInsertAll(VertxTestContext context) { + final List events = new ArrayList<>(); + initializeListeners( events ); + + EntityA a = new EntityA(); + EntityB b = new EntityB(); + a.children.add( b ); + test( context, getSessionFactory() + .withStatelessTransaction( statelessSession -> statelessSession + .insert( b, a ) + .thenCompose( v -> statelessSession.delete( a, b ) ) + ) + .thenAccept( v -> assertEvents( events ) ) + ); + } + + private static void assertEvents(List events) { + assertThat( events ).hasSize( 4 ); + assertThat( events.get( 0 ) ) + .isInstanceOf( PreCollectionRecreateEvent.class ) + .extracting( AbstractCollectionEvent::getAffectedOwnerEntityName ).isEqualTo( EntityA.class.getName() ); + assertThat( events.get( 1 ) ) + .isInstanceOf( PostCollectionRecreateEvent.class ) + .extracting( AbstractCollectionEvent::getAffectedOwnerEntityName ).isEqualTo( EntityA.class.getName() ); + assertThat( events.get( 2 ) ) + .isInstanceOf( PreCollectionRemoveEvent.class ) + .extracting( AbstractCollectionEvent::getAffectedOwnerEntityName ).isEqualTo( EntityA.class.getName() ); + assertThat( events.get( 3 ) ) + .isInstanceOf( PostCollectionRemoveEvent.class ) + .extracting( AbstractCollectionEvent::getAffectedOwnerEntityName ).isEqualTo( EntityA.class.getName() ); + } + + private void initializeListeners(List events) { + final EventListenerRegistry registry = ( (SessionFactoryImplementor) factoryManager + .getHibernateSessionFactory() ) + .getEventListenerRegistry(); + + // Clear previous listeners + registry.getEventListenerGroup( EventType.PRE_COLLECTION_RECREATE ) + .clearListeners(); + registry.getEventListenerGroup( EventType.PRE_COLLECTION_REMOVE ) + .clearListeners(); + registry.getEventListenerGroup( EventType.POST_COLLECTION_RECREATE ) + .clearListeners(); + registry.getEventListenerGroup( EventType.POST_COLLECTION_REMOVE ) + .clearListeners(); + + // Add new listeners + registry.getEventListenerGroup( EventType.PRE_COLLECTION_RECREATE ) + .appendListener( new MyPreCollectionRecreateEventListener( events ) ); + registry.getEventListenerGroup( EventType.PRE_COLLECTION_REMOVE ) + .appendListener( new MyPreCollectionRemoveEventListener( events ) ); + registry.getEventListenerGroup( EventType.POST_COLLECTION_RECREATE ) + .appendListener( new MyPostCollectionRecreateEventListener( events ) ); + registry.getEventListenerGroup( EventType.POST_COLLECTION_REMOVE ) + .appendListener( new MyPostCollectionRemoveEventListener( events ) ); + } + + @Entity + @Table(name = "ENTITY_A") + public static class EntityA { + + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + @Column(name = "ID") + Integer id; + + @OneToMany + @JoinColumn(name = "ENTITY_A") + Collection children = new ArrayList<>(); + } + + @Entity + @Table(name = "ENTITY_B") + public static class EntityB { + @Id + @GeneratedValue(strategy = GenerationType.AUTO) + @Column(name = "ID") + Integer id; + } + + public static class MyPreCollectionRecreateEventListener implements PreCollectionRecreateEventListener { + + private final List events; + + + public MyPreCollectionRecreateEventListener(List events) { + this.events = events; + } + + @Override + public void onPreRecreateCollection(PreCollectionRecreateEvent event) { + events.add( event ); + } + + } + + public static class MyPreCollectionRemoveEventListener implements PreCollectionRemoveEventListener { + + private final List events; + + public MyPreCollectionRemoveEventListener(List events) { + this.events = events; + } + + @Override + public void onPreRemoveCollection(PreCollectionRemoveEvent event) { + events.add( event ); + } + + } + + public static class MyPostCollectionRecreateEventListener implements PostCollectionRecreateEventListener { + + private final List events; + + public MyPostCollectionRecreateEventListener(List events) { + this.events = events; + } + + @Override + public void onPostRecreateCollection(PostCollectionRecreateEvent event) { + events.add( event ); + } + } + + public static class MyPostCollectionRemoveEventListener implements PostCollectionRemoveEventListener { + + private final List events; + + public MyPostCollectionRemoveEventListener(List events) { + this.events = events; + } + + @Override + public void onPostRemoveCollection(PostCollectionRemoveEvent event) { + events.add( event ); + } + } +} diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/CompositeIdWithGeneratedValuesTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/CompositeIdWithGeneratedValuesTest.java new file mode 100644 index 000000000..0192bbbf7 --- /dev/null +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/CompositeIdWithGeneratedValuesTest.java @@ -0,0 +1,107 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive; + +import org.junit.jupiter.api.Test; + +import io.vertx.junit5.Timeout; +import io.vertx.junit5.VertxTestContext; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.IdClass; +import jakarta.persistence.SequenceGenerator; + +import java.util.Collection; +import java.util.List; + +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.assertj.core.api.Assertions.assertThat; + +@Timeout(value = 10, timeUnit = MINUTES) + +public class CompositeIdWithGeneratedValuesTest extends BaseReactiveTest { + + @Override + protected Collection> annotatedEntities() { + return List.of( Product.class ); + } + + @Test + public void testCompositeIdWithGeneratedValues(VertxTestContext context) { + final Product product = new Product( "name", 1150L ); + test( + context, + getMutinySessionFactory().withTransaction( session -> session.persist( product ) ) + .invoke( () -> assertThat( product.id ).isNotNull() ) + .chain( () -> getMutinySessionFactory().withTransaction( session -> session.find( + Product.class, + new ProductId( product.version, product.id ) + ) ) ) + .invoke( found -> { + assertThat( found ).hasFieldOrPropertyWithValue( "id", product.id ); + assertThat( found ).hasFieldOrPropertyWithValue( "version", product.version ); + assertThat( found ).hasFieldOrPropertyWithValue( "name", product.name ); + } ) + ); + } + + @Entity + @IdClass(ProductId.class) + public static class Product { + @Id + private Long version; + + @Id + @GeneratedValue + @SequenceGenerator(name = "product_seq", sequenceName = "product_seq") + private Long id; + + private String name; + + public Product() { + } + + public Product(String name, Long version) { + this.name = name; + this.version = version; + } + + public Long getVersion() { + return version; + } + + public void setVersion(Long version) { + this.version = version; + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + @Override + public String toString() { + return "(" + id + "," + version + "):" + name; + } + } + + public static class ProductId { + private Long version; + private Long id; + + private ProductId() { + } + + public ProductId(Long version, Long id) { + this.version = version; + this.id = id; + } + } +} diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/EmbeddedIdTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/EmbeddedIdTest.java index de3590fab..14ac0d56f 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/EmbeddedIdTest.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/EmbeddedIdTest.java @@ -42,6 +42,19 @@ public void populateDb(VertxTestContext context) { test( context, getMutinySessionFactory().withTransaction( s -> s.persistAll( pizza, schnitzel ) ) ); } + @Test + public void testStatelessInsert(VertxTestContext context) { + LocationId nottingham = new LocationId( "UK", "Nottingham" ); + Delivery mushyPeas = new Delivery( nottingham, "Mushy Peas with mint sauce" ); + test( context, getMutinySessionFactory() + .withStatelessTransaction( s -> s.insert( mushyPeas ) ) + .chain( () -> getMutinySessionFactory() + .withTransaction( s -> s.find( Delivery.class, nottingham ) ) + ) + .invoke( result -> assertThat( result ).isEqualTo( mushyPeas ) ) + ); + } + @Test public void testFindSingleId(VertxTestContext context) { test( context, getMutinySessionFactory().withTransaction( s -> s.find( Delivery.class, verbania ) ) diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/EmbeddedIdWithOneToOneTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/EmbeddedIdWithOneToOneTest.java new file mode 100644 index 000000000..f5cbf5fd9 --- /dev/null +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/EmbeddedIdWithOneToOneTest.java @@ -0,0 +1,131 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive; + +import org.junit.jupiter.api.Test; + +import io.vertx.junit5.VertxTestContext; +import jakarta.persistence.Embeddable; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.FetchType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.OneToOne; +import java.util.Collection; +import java.util.List; +import java.util.Objects; + +import static org.assertj.core.api.Assertions.assertThat; + +public class EmbeddedIdWithOneToOneTest extends BaseReactiveTest { + + @Override + protected Collection> annotatedEntities() { + return List.of( FooEntity.class, BarEntity.class ); + } + + @Test + public void test(VertxTestContext context) { + BarEntity barEntity = new BarEntity( "1" ); + FooId fooId = new FooId( barEntity ); + FooEntity entity = new FooEntity( fooId ); + + test( + context, getMutinySessionFactory() + .withTransaction( s -> s.persistAll( barEntity, entity ) ) + .chain( () -> getMutinySessionFactory() + .withTransaction( s -> s.find( FooEntity.class, fooId ) ) + ) + .invoke( result -> { + assertThat( result.getId() ).isEqualTo( fooId ); + assertThat( result.getId().getIdEntity() ).isEqualTo( fooId.getIdEntity() ); + assertThat( result.getId().getIdEntity().getId() ).isEqualTo( fooId.getIdEntity().getId() ); + } ) + ); + } + + @Entity(name = "bar") + public static class BarEntity { + + @Id + private String id; + + public BarEntity() { + } + + public BarEntity(String id) { + this.id = id; + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + } + + @Entity(name = "foo") + public static class FooEntity { + + @EmbeddedId + private FooId id; + + public FooEntity() { + } + + public FooEntity(FooId id) { + this.id = id; + } + + public FooId getId() { + return id; + } + + public void setId(FooId id) { + this.id = id; + } + } + + @Embeddable + public static class FooId { + + @OneToOne(fetch = FetchType.EAGER) + @JoinColumn(name = "id", nullable = false) + private BarEntity barEntity; + + public FooId() { + } + + public FooId(BarEntity barEntity) { + this.barEntity = barEntity; + } + + public BarEntity getIdEntity() { + return barEntity; + } + + public void setIdEntity(BarEntity barEntity) { + this.barEntity = barEntity; + } + + @Override + public boolean equals(Object o) { + if ( o == null || getClass() != o.getClass() ) { + return false; + } + FooId fooId = (FooId) o; + return Objects.equals( barEntity, fooId.barEntity ); + } + + @Override + public int hashCode() { + return Objects.hashCode( barEntity ); + } + } +} diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/FetchModeSubselectEagerTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/FetchModeSubselectEagerTest.java index 388baec9a..490e06d0c 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/FetchModeSubselectEagerTest.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/FetchModeSubselectEagerTest.java @@ -21,9 +21,7 @@ import io.vertx.junit5.Timeout; import io.vertx.junit5.VertxTestContext; -import jakarta.persistence.CascadeType; import jakarta.persistence.Entity; -import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; import jakarta.persistence.ManyToOne; @@ -39,13 +37,20 @@ import jakarta.persistence.Transient; import jakarta.persistence.Version; +import static jakarta.persistence.CascadeType.MERGE; +import static jakarta.persistence.CascadeType.PERSIST; +import static jakarta.persistence.CascadeType.REFRESH; +import static jakarta.persistence.CascadeType.REMOVE; +import static jakarta.persistence.FetchType.EAGER; +import static jakarta.persistence.FetchType.LAZY; import static java.util.concurrent.TimeUnit.MINUTES; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hibernate.annotations.FetchMode.SUBSELECT; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertTrue; @Timeout(value = 10, timeUnit = MINUTES) - public class FetchModeSubselectEagerTest extends BaseReactiveTest { @Override @@ -70,13 +75,11 @@ protected CompletionStage cleanDb() { @Test public void testEagerCollectionFetch(VertxTestContext context) { - Node basik = new Node( "Child" ); basik.parent = new Node( "Parent" ); basik.elements.add( new Element( basik ) ); basik.elements.add( new Element( basik ) ); basik.elements.add( new Element( basik ) ); - test( context, openSession() @@ -84,7 +87,7 @@ public void testEagerCollectionFetch(VertxTestContext context) { .thenCompose( v -> openSession() ) .thenCompose( s -> s.find( Node.class, basik.getId() ) ) .thenAccept( node -> { - assertTrue( Hibernate.isInitialized( node.elements ) ); + assertThat( Hibernate.isInitialized( node.elements ) ).isTrue(); assertEquals( 3, node.elements.size() ); for ( Element element : node.elements ) { assertSame( element.node, node ); @@ -95,7 +98,6 @@ public void testEagerCollectionFetch(VertxTestContext context) { @Test public void testEagerParentFetch(VertxTestContext context) { - Node basik = new Node( "Child" ); basik.parent = new Node( "Parent" ); basik.elements.add( new Element( basik ) ); @@ -109,16 +111,15 @@ public void testEagerParentFetch(VertxTestContext context) { .thenCompose( v -> openSession() ) .thenCompose( s -> s.find( Element.class, basik.elements.get( 0 ).id ) ) .thenAccept( element -> { - assertTrue( Hibernate.isInitialized( element.node ) ); - assertTrue( Hibernate.isInitialized( element.node.elements ) ); - assertEquals( 3, element.node.elements.size() ); + assertThat( Hibernate.isInitialized( element.node ) ).isTrue(); + assertThat( Hibernate.isInitialized( element.node.elements ) ).isTrue(); + assertThat( element.node.elements ).hasSize( 3 ); } ) ); } @Test public void testEagerFetchQuery(VertxTestContext context) { - Node basik = new Node( "Child" ); basik.parent = new Node( "Parent" ); basik.elements.add( new Element( basik ) ); @@ -130,21 +131,26 @@ public void testEagerFetchQuery(VertxTestContext context) { openSession() .thenCompose( s -> s.persist( basik ).thenCompose( v -> s.flush() ) ) .thenCompose( v -> openSession() ) - .thenCompose( s -> s.createSelectionQuery( "from Node order by id", Node.class ).getResultList() ) + .thenCompose( s -> s.createSelectionQuery( "from Node order by id", Node.class ) + .getResultList() ) .thenAccept( list -> { - assertEquals( list.size(), 2 ); - assertTrue( Hibernate.isInitialized( list.get( 0 ).elements ) ); - assertEquals( list.get( 0 ).elements.size(), 3 ); - assertEquals( list.get( 1 ).elements.size(), 0 ); + assertThat( list ).hasSize( 2 ); + assertThat( Hibernate.isInitialized( list.get( 0 ).elements ) ).isTrue(); + assertThat( list.get( 0 ).elements ).hasSize( 3 ); + assertThat( list.get( 1 ).elements ).isEmpty(); } ) .thenCompose( v -> openSession() ) - .thenCompose( s -> s.createSelectionQuery( - "select distinct n, e from Node n join n.elements e order by n.id", Object[].class ).getResultList() ) + .thenCompose( s -> s + .createSelectionQuery( + "select distinct n, e from Node n join n.elements e order by n.id", + Object[].class + ) + .getResultList() ) .thenAccept( list -> { - assertEquals( list.size(), 3 ); + assertThat( list ).hasSize( 3 ); Object[] tup = list.get( 0 ); assertTrue( Hibernate.isInitialized( ( (Node) tup[0] ).elements ) ); - assertEquals( ( (Node) tup[0] ).elements.size(), 3 ); + assertThat( ( (Node) tup[0] ).elements ).hasSize( 3 ); } ) ); } @@ -179,22 +185,11 @@ public static class Node { Integer version; String string; - @ManyToOne(fetch = FetchType.LAZY, - cascade = { - CascadeType.PERSIST, - CascadeType.REFRESH, - CascadeType.MERGE, - CascadeType.REMOVE - }) + @ManyToOne(fetch = LAZY, cascade = { PERSIST, REFRESH, MERGE, REMOVE }) Node parent; - @OneToMany(fetch = FetchType.EAGER, - cascade = { - CascadeType.PERSIST, - CascadeType.REMOVE - }, - mappedBy = "node") - @Fetch(FetchMode.SUBSELECT) + @OneToMany(fetch = EAGER, cascade = { PERSIST, REMOVE }, mappedBy = "node") + @Fetch(SUBSELECT) List elements = new ArrayList<>(); @Transient diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/FindByIdWithLockTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/FindByIdWithLockTest.java index e78c549ca..9eb8bb8f8 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/FindByIdWithLockTest.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/FindByIdWithLockTest.java @@ -5,11 +5,18 @@ */ package org.hibernate.reactive; +import org.hibernate.LockMode; +import org.hibernate.boot.registry.StandardServiceRegistryBuilder; +import org.hibernate.cfg.Configuration; +import org.hibernate.reactive.testing.SqlStatementTracker; + import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import io.vertx.junit5.Timeout; @@ -22,11 +29,38 @@ import jakarta.persistence.OneToMany; import static org.assertj.core.api.Assertions.assertThat; +import static org.hibernate.reactive.containers.DatabaseConfiguration.dbType; @Timeout(value = 10, timeUnit = TimeUnit.MINUTES) public class FindByIdWithLockTest extends BaseReactiveTest { private static final Long CHILD_ID = 1L; + private static SqlStatementTracker sqlTracker; + + @Override + protected Configuration constructConfiguration() { + Configuration configuration = super.constructConfiguration(); + + // Construct a tracker that collects query statements via the SqlStatementLogger framework. + // Pass in configuration properties to hand off any actual logging properties + sqlTracker = new SqlStatementTracker( FindByIdWithLockTest::selectQueryFilter, configuration.getProperties() ); + return configuration; + } + + @BeforeEach + public void clearTracker() { + sqlTracker.clear(); + } + + @Override + protected void addServices(StandardServiceRegistryBuilder builder) { + sqlTracker.registerService( builder ); + } + + private static boolean selectQueryFilter(String s) { + return s.toLowerCase().startsWith( "select " ); + } + @Override protected Collection> annotatedEntities() { return List.of( Parent.class, Child.class ); @@ -50,6 +84,45 @@ context, getMutinySessionFactory() ); } + @Disabled + @Test + public void testFindUpgradeNoWait(VertxTestContext context) { + Child child = new Child( CHILD_ID, "And" ); + test( + context, getMutinySessionFactory() + .withTransaction( session -> session.persistAll( child ) ) + .invoke( () -> sqlTracker.clear() ) + .chain( () -> getMutinySessionFactory().withTransaction( session -> session + .find( Child.class, CHILD_ID, LockMode.UPGRADE_NOWAIT ) + .invoke( c -> { + assertThat( c ).isNotNull(); + assertThat( c.getId() ).isEqualTo( CHILD_ID ); + String selectQuery = sqlTracker.getLoggedQueries().get( 0 ); + assertThat( sqlTracker.getLoggedQueries() ).hasSize( 1 ); + assertThat( selectQuery ) + .matches( this::noWaitLockingPredicate, "SQL query with nowait lock for " + dbType().name() ); + } + ) ) ) + ); + } + + /** + * @return true if the query contains the expected nowait keyword for the selected database + */ + private boolean noWaitLockingPredicate(String selectQuery) { + return switch ( dbType() ) { + case POSTGRESQL -> selectQuery.endsWith( "for no key update of c1_0 nowait" ); + case COCKROACHDB -> selectQuery.endsWith( "for update of c1_0 nowait" ); + case SQLSERVER -> selectQuery.contains( "with (updlock,holdlock,rowlock,nowait)" ); + case ORACLE -> selectQuery.contains( "for update of c1_0.id nowait" ); + // DB2 does not support nowait + case DB2 -> selectQuery.contains( "for read only with rs use and keep update locks" ); + case MARIA -> selectQuery.contains( "for update nowait" ); + case MYSQL -> selectQuery.contains( "for update of c1_0 nowait" ); + default -> throw new IllegalArgumentException( "Database not recognized: " + dbType().name() ); + }; + } + @Entity(name = "Parent") public static class Parent { @@ -59,7 +132,7 @@ public static class Parent { private String name; @OneToMany(fetch = FetchType.EAGER) - public List children; + public List children = new ArrayList<>(); public Parent() { } @@ -70,9 +143,6 @@ public Parent(Long id, String name) { } public void add(Child child) { - if ( children == null ) { - children = new ArrayList<>(); - } children.add( child ); } @@ -89,7 +159,6 @@ public List getChildren() { } } - @Entity(name = "Child") public static class Child { @@ -109,13 +178,6 @@ public Child(Long id, String name) { this.name = name; } - public Child(Long id, String name, Parent parent) { - this.id = id; - this.name = name; - this.parent = parent; - parent.add( this ); - } - public Long getId() { return id; } @@ -128,6 +190,4 @@ public Parent getParent() { return parent; } } - - } diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/HQLQueryTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/HQLQueryTest.java index e48d3ec69..77331fe2b 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/HQLQueryTest.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/HQLQueryTest.java @@ -5,6 +5,7 @@ */ package org.hibernate.reactive; +import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Objects; @@ -17,32 +18,41 @@ import io.vertx.junit5.Timeout; import io.vertx.junit5.VertxTestContext; import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.OneToMany; import jakarta.persistence.Table; +import static jakarta.persistence.CascadeType.PERSIST; +import static jakarta.persistence.FetchType.LAZY; import static java.util.concurrent.TimeUnit.MINUTES; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; @Timeout(value = 10, timeUnit = MINUTES) - public class HQLQueryTest extends BaseReactiveTest { Flour spelt = new Flour( 1, "Spelt", "An ancient grain, is a hexaploid species of wheat.", "Wheat flour" ); Flour rye = new Flour( 2, "Rye", "Used to bake the traditional sourdough breads of Germany.", "Wheat flour" ); Flour almond = new Flour( 3, "Almond", "made from ground almonds.", "Gluten free" ); + Author miller = new Author( "Madeline Miller"); + Author camilleri = new Author( "Andrea Camilleri"); + Book circe = new Book( "9780316556347", "Circe", miller ); + Book shapeOfWater = new Book( "0-330-49286-1 ", "The Shape of Water", camilleri ); + Book spider = new Book( "978-0-14-311203-7", "The Patience of the Spider", camilleri ); + @Override protected Collection> annotatedEntities() { - return List.of( Flour.class ); + return List.of( Flour.class, Book.class, Author.class ); } @BeforeEach public void populateDb(VertxTestContext context) { - test( context, getMutinySessionFactory() - .withTransaction( (session, transaction) -> session.persistAll( spelt, rye, almond ) ) ); + test( context, getMutinySessionFactory().withTransaction( session -> session + .persistAll( spelt, rye, almond, miller, camilleri, circe, shapeOfWater, spider ) ) + ); } @Test @@ -69,7 +79,7 @@ public void testAutoFlushOnResultList(VertxTestContext context) { public void testSelectScalarString(VertxTestContext context) { test( context, getSessionFactory().withSession( s -> { Stage.SelectionQuery qr = s.createSelectionQuery( "SELECT 'Prova' FROM Flour WHERE id = " + rye.getId(), String.class ); - assertNotNull( qr ); + assertThat( qr ).isNotNull(); return qr.getSingleResult(); } ).thenAccept( found -> assertEquals( "Prova", found ) ) ); } @@ -78,7 +88,7 @@ public void testSelectScalarString(VertxTestContext context) { public void testSelectScalarCount(VertxTestContext context) { test( context, getSessionFactory().withSession( s -> { Stage.SelectionQuery qr = s.createSelectionQuery( "SELECT count(*) FROM Flour", Long.class ); - assertNotNull( qr ); + assertThat( qr ).isNotNull(); return qr.getSingleResult(); } ).thenAccept( found -> assertEquals( 3L, found ) ) ); } @@ -86,14 +96,17 @@ public void testSelectScalarCount(VertxTestContext context) { @Test public void testSelectWithMultipleScalarValues(VertxTestContext context) { test( context, getSessionFactory().withSession( s -> { - Stage.SelectionQuery qr = s.createSelectionQuery( "SELECT 'Prova', f.id FROM Flour f WHERE f.id = " + rye.getId(), Object[].class ); - assertNotNull( qr ); - return qr.getSingleResult(); - } ).thenAccept( found -> { - assertTrue( found instanceof Object[] ); - assertEquals( "Prova", ( (Object[]) found )[0] ); - assertEquals( rye.getId(), ( (Object[]) found )[1] ); - } ) + Stage.SelectionQuery qr = s.createSelectionQuery( + "SELECT 'Prova', f.id FROM Flour f WHERE f.id = " + rye.getId(), + Object[].class + ); + assertThat( qr ).isNotNull(); + return qr.getSingleResult(); + } ).thenAccept( found -> { + assertThat( found ).isInstanceOf( Object[].class ); + assertEquals( "Prova", ( (Object[]) found )[0] ); + assertEquals( rye.getId(), ( (Object[]) found )[1] ); + } ) ); } @@ -101,7 +114,7 @@ public void testSelectWithMultipleScalarValues(VertxTestContext context) { public void testSingleResultQueryOnId(VertxTestContext context) { test( context, getSessionFactory().withSession( s -> { Stage.SelectionQuery qr = s.createSelectionQuery( "FROM Flour WHERE id = 1", Flour.class ); - assertNotNull( qr ); + assertThat( qr ).isNotNull(); return qr.getSingleResult(); } ).thenAccept( flour -> assertEquals( spelt, flour ) ) ); @@ -111,7 +124,7 @@ public void testSingleResultQueryOnId(VertxTestContext context) { public void testSingleResultQueryOnName(VertxTestContext context) { test( context, getSessionFactory().withSession( s -> { Stage.SelectionQuery qr = s.createSelectionQuery( "FROM Flour WHERE name = 'Almond'", Flour.class ); - assertNotNull( qr ); + assertThat( qr ).isNotNull(); return qr.getSingleResult(); } ).thenAccept( flour -> assertEquals( almond, flour ) ) ); @@ -122,13 +135,28 @@ public void testFromQuery(VertxTestContext context) { test( context, getSessionFactory() .withSession( s -> { Stage.SelectionQuery qr = s.createSelectionQuery( "FROM Flour ORDER BY name", Flour.class ); - assertNotNull( qr ); + assertThat( qr ).isNotNull(); return qr.getResultList(); } ) .thenAccept( results -> assertThat( results ).containsExactly( almond, rye, spelt ) ) ); } + @Test + public void testSelectNewConstructor(VertxTestContext context) { + test( context, getMutinySessionFactory() + .withTransaction( session -> session + .createQuery( "SELECT NEW Book(b.title, b.author) FROM Book b ORDER BY b.title DESC", Book.class ) + .getResultList() + ) + .invoke( books -> assertThat( books ).containsExactly( + new Book( shapeOfWater.title, camilleri ), + new Book( spider.title, camilleri ), + new Book( circe.title, miller ) + ) ) + ); + } + @Entity(name = "Flour") @Table(name = "Flour") public static class Flour { @@ -204,4 +232,122 @@ public int hashCode() { return Objects.hash( name, description, type ); } } + + @Entity(name = "Book") + @Table(name = "Book_HQL") + public static class Book { + @Id + @GeneratedValue + private Integer id; + + private String isbn; + + private String title; + + @ManyToOne(fetch = LAZY) + private Author author; + + public Book() { + } + + public Book(String title, Author author) { + this.title = title; + this.author = author; + } + + public Book(String isbn, String title, Author author) { + this.isbn = isbn; + this.title = title; + this.author = author; + author.books.add( this ); + } + + public Integer getId() { + return id; + } + + public String getIsbn() { + return isbn; + } + + public String getTitle() { + return title; + } + + public Author getAuthor() { + return author; + } + + @Override + public boolean equals(Object o) { + if ( o == null || getClass() != o.getClass() ) { + return false; + } + Book book = (Book) o; + return Objects.equals( isbn, book.isbn ) && Objects.equals( + title, + book.title + ) && Objects.equals( author, book.author ); + } + + @Override + public int hashCode() { + return Objects.hash( isbn, title, author ); + } + + @Override + public String toString() { + return id + ":" + isbn + ":" + title + ":" + author; + } + } + + @Entity(name = "Author") + @Table(name = "Author_HQL") + public static class Author { + @Id @GeneratedValue + private Integer id; + + private String name; + + @OneToMany(mappedBy = "author", cascade = PERSIST) + private List books = new ArrayList<>(); + + public Author() { + } + + public Author(String name) { + this.name = name; + } + + public Integer getId() { + return id; + } + + public String getName() { + return name; + } + + public List getBooks() { + return books; + } + + @Override + public boolean equals(Object o) { + if ( o == null || getClass() != o.getClass() ) { + return false; + } + Author author = (Author) o; + return Objects.equals( name, author.name ); + } + + @Override + public int hashCode() { + return Objects.hashCode( name ); + } + + @Override + public String toString() { + return id + ":" + name; + } + } } diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/ImplicitSoftDeleteTests.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/ImplicitSoftDeleteTests.java new file mode 100644 index 000000000..f1c08d4ce --- /dev/null +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/ImplicitSoftDeleteTests.java @@ -0,0 +1,225 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive; + +import java.util.Collection; +import java.util.List; +import java.util.Objects; +import java.util.concurrent.CompletionStage; + +import org.hibernate.ObjectNotFoundException; +import org.hibernate.annotations.NaturalId; +import org.hibernate.annotations.SoftDelete; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import io.vertx.junit5.Timeout; +import io.vertx.junit5.VertxTestContext; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; +import jakarta.persistence.Tuple; + +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hibernate.reactive.common.Identifier.id; +import static org.hibernate.reactive.testing.ReactiveAssertions.assertThrown; +import static org.hibernate.reactive.util.impl.CompletionStages.voidFuture; + +@Timeout(value = 10, timeUnit = MINUTES) +public class ImplicitSoftDeleteTests extends BaseReactiveTest { + + @Override + protected Collection> annotatedEntities() { + return List.of( ImplicitEntity.class ); + } + + @Override + protected CompletionStage cleanDb() { + return voidFuture(); + } + + @BeforeEach + void createTestData(VertxTestContext context) { + test( + context, getMutinySessionFactory() + .withTransaction( s -> s.createNativeQuery( "delete from implicit_entities" ).executeUpdate() ) + .call( () -> getMutinySessionFactory().withTransaction( s -> s + .persistAll( new ImplicitEntity( 1, "first" ), new ImplicitEntity( 2, "second" ), new ImplicitEntity( 3, "third" ) ) + ) ) + .chain( () -> getMutinySessionFactory().withTransaction( s -> { + final ImplicitEntity first = s.getReference( ImplicitEntity.class, 1 ); + return s.remove( first ).call( s::flush ); + } ) ) + .call( () -> getMutinySessionFactory() + .withTransaction( s -> s.createNativeQuery( "select * from implicit_entities e order by id", Tuple.class ).getResultList() ) + .invoke( tuples -> assertThat( tuples ).hasSize( 3 ) ) + ) + ); + } + + @Test + void testSelectionQuery(VertxTestContext context) { + test( + context, getMutinySessionFactory() + .withTransaction( s -> s.createQuery( "from ImplicitEntity", ImplicitEntity.class ).getResultList() ) + .invoke( list -> assertThat( list ).hasSize( 2 ) ) + ); + } + + @Test + void testLoading(VertxTestContext context) { + test( + context, getMutinySessionFactory() + // Load + .withTransaction( s -> s + .find( ImplicitEntity.class, 1 ).invoke( entity -> assertThat( entity ).isNull() ) + .call( () -> s.find( ImplicitEntity.class, 2 ).invoke( entity -> assertThat( entity ).isNotNull() ) ) + .call( () -> s.find( ImplicitEntity.class, 3 ).invoke( entity -> assertThat( entity ).isNotNull() ) ) + ) + // Proxy + // We deleted the entity, so we expect an ObjectNotFoundException + .chain( () -> assertThrown( ObjectNotFoundException.class, getMutinySessionFactory().withTransaction( s -> { + final ImplicitEntity reference = s.getReference( ImplicitEntity.class, 1 ); + return s.fetch( reference ).map( ImplicitEntity::getName ); + } ) ) ) + .chain( () -> getMutinySessionFactory().withTransaction( s -> { + final ImplicitEntity reference = s.getReference( ImplicitEntity.class, 2 ); + return s.fetch( reference ).map( ImplicitEntity::getName ); + } ) ) + .invoke( name -> assertThat( name ).isEqualTo( "second" ) ) + .chain( () -> getMutinySessionFactory().withTransaction( s -> { + final ImplicitEntity reference = s.getReference( ImplicitEntity.class, 3 ); + return s.fetch( reference ).map( ImplicitEntity::getName ); + } ) ) + .invoke( name -> assertThat( name ).isEqualTo( "third" ) ) + ); + } + + @Test + void testMultiLoading(VertxTestContext context) { + test( + context, getMutinySessionFactory() + .withTransaction( s -> s.find( ImplicitEntity.class, 1, 2, 3 ) ) + .invoke( list -> assertThat( list ) + .containsExactly( null, new ImplicitEntity( null, "second" ), new ImplicitEntity( null, "third" ) ) ) + ); + } + + @Test + void testNaturalIdLoading(VertxTestContext context) { + test( + context, getMutinySessionFactory() + .withTransaction( s -> s.find( ImplicitEntity.class, id( "name", "first" ) ) ) + .invoke( entity -> assertThat( entity ).isNull() ) + .chain( () -> getMutinySessionFactory().withTransaction( s -> s.find( ImplicitEntity.class, id( "name", "second" ) ) ) ) + .invoke( entity -> assertThat( entity ).extracting( ImplicitEntity::getId ).isEqualTo( 2 ) ) + .chain( () -> getMutinySessionFactory().withTransaction( s -> s.find( ImplicitEntity.class, id( "name", "third" ) ) ) ) + .invoke( entity -> assertThat( entity ).extracting( ImplicitEntity::getId ).isEqualTo( 3 ) ) + ); + } + + @Test + void testDeletion(VertxTestContext context) { + test( + context, getMutinySessionFactory().withTransaction( s -> { + final ImplicitEntity reference = s.getReference( ImplicitEntity.class, 2 ); + return s.remove( reference ).call( s::flush ) + .call( () -> s.createSelectionQuery( "from ImplicitEntity", ImplicitEntity.class ).getResultList() + // #1 was "deleted" up front and we just "deleted" #2... only #3 should be active + .invoke( list -> { + assertThat( list ).extracting( ImplicitEntity::getId ).containsExactly( 3 ); + assertThat( list ).extracting( ImplicitEntity::getName ).containsExactly( "third" ); + } ) + ); + } ) + ); + } + + @Test + void testFullUpdateMutationQuery(VertxTestContext context) { + test( + context, getMutinySessionFactory() + .withTransaction( s -> s.createMutationQuery( "update ImplicitEntity set name = null" ).executeUpdate() ) + .invoke( affected -> assertThat( affected ).isEqualTo( 2 ) ) + ); + } + + @Test + void testRestrictedUpdateMutationQuery(VertxTestContext context) { + test( + context, getMutinySessionFactory() + .withTransaction( s -> s.createMutationQuery( "update ImplicitEntity set name = null where name = 'second'" ).executeUpdate() ) + .invoke( affected -> assertThat( affected ).isEqualTo( 1 ) ) + ); + } + + @Test + void testFullDeleteMutationQuery(VertxTestContext context) { + test( + context, getMutinySessionFactory() + .withTransaction( s -> s.createMutationQuery( "delete ImplicitEntity" ).executeUpdate() ) + // only #2 and #3 + .invoke( affected -> assertThat( affected ).isEqualTo( 2 ) ) + ); + } + + @Test + void testRestrictedDeleteMutationQuery(VertxTestContext context) { + test( + context, getMutinySessionFactory() + .withTransaction( s -> s.createMutationQuery( "delete ImplicitEntity where name = 'second'" ).executeUpdate() ) + // only #2 + .invoke( affected -> assertThat( affected ).isEqualTo( 1 ) ) + ); + } + + + @Entity(name = "ImplicitEntity") + @Table(name = "implicit_entities") + @SoftDelete + public static class ImplicitEntity { + @Id + private Integer id; + @NaturalId + private String name; + + public ImplicitEntity() { + } + + public ImplicitEntity(Integer id, String name) { + this.id = id; + this.name = name; + } + + public Integer getId() { + return 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; + } + ImplicitEntity that = (ImplicitEntity) object; + return Objects.equals( name, that.name ); + } + + @Override + public int hashCode() { + return Objects.hashCode( name ); + } + } +} diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/JoinedSubclassInheritanceTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/JoinedSubclassInheritanceTest.java index ab64e1925..d12e4aded 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/JoinedSubclassInheritanceTest.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/JoinedSubclassInheritanceTest.java @@ -5,6 +5,8 @@ */ package org.hibernate.reactive; +import org.hibernate.reactive.annotations.DisabledFor; + import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -28,6 +30,7 @@ import static java.util.concurrent.TimeUnit.MINUTES; import static org.assertj.core.api.Assertions.assertThat; +import static org.hibernate.reactive.containers.DatabaseConfiguration.DBType.POSTGRESQL; @Timeout(value = 10, timeUnit = MINUTES) @@ -185,6 +188,53 @@ public void testQueryUpdateWithParameters(VertxTestContext context) { ); } + @Test + @DisabledFor(value = POSTGRESQL, reason = "https://github.com/hibernate/hibernate-reactive/issues/2412") + public void testHqlInsertWithTransaction(VertxTestContext context) { + final Integer id = 1; + final String title = "Spell Book: A Comprehensive Guide to Magic Spells and Incantations"; + test( context, getMutinySessionFactory().withTransaction( session -> session + .createMutationQuery( "insert into SpellBook (id, title, forbidden) values (:id, :title, :forbidden)" ) + .setParameter( "id", id ) + .setParameter( "title", title ) + .setParameter( "forbidden", true ) + .executeUpdate() ) + .call( () -> getMutinySessionFactory().withTransaction( session -> session + .createSelectionQuery( "from SpellBook g where g.id = :id ", SpellBook.class ) + .setParameter( "id", id ) + .getSingleResult() + .invoke( spellBook -> { + assertThat( spellBook.getTitle() ).isEqualTo( title ); + assertThat( spellBook.forbidden ).isTrue(); + } + ) + ) ) + ); + } + + @Test + public void testHqlDelete(VertxTestContext context) { + final Integer id = 1; + final String title = "Spell Book: A Comprehensive Guide to Magic Spells and Incantations"; + SpellBook spellBook = new SpellBook( id, title, true, new Date() ); + test( context, getMutinySessionFactory().withTransaction( session -> session.persist( spellBook ) ) + .call( () -> getMutinySessionFactory().withTransaction( session -> session + .createMutationQuery( + "delete from SpellBook where id = :id and forbidden = :forbidden and title = :title" ) + .setParameter( "id", id ) + .setParameter( "title", title ) + .setParameter( "forbidden", true ) + .executeUpdate() ) + ) + .call( () -> getMutinySessionFactory().withTransaction( session -> session + .createSelectionQuery( "from SpellBook g where g.id = :id ", SpellBook.class ) + .setParameter( "id", id ) + .getSingleResultOrNull() + .invoke( Assertions::assertNull ) ) + ) + ); + } + @Entity(name="SpellBook") @Table(name = "SpellBookJS") @DiscriminatorValue("S") diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/LazyPropertyTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/LazyPropertyTest.java index 3d22d0e71..acff15717 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/LazyPropertyTest.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/LazyPropertyTest.java @@ -146,14 +146,14 @@ private String getIsbn() { private Author author; public Book(String isbn, String title, Author author) { - super( "Book", 1, singleton( "isbn" ), null ); + super( new EntityRelatedState( "Book", singleton( "isbn" ) ), 1, null ); this.title = title; this.isbn = isbn; this.author = author; } public Book() { - super( "Book", 1, singleton( "isbn" ), null ); + super( new EntityRelatedState( Book.class.getName(), singleton( "isbn" ) ), 1, null ); } @Override diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/LockOnLoadTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/LockOnLoadTest.java new file mode 100644 index 000000000..fe0c7e20e --- /dev/null +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/LockOnLoadTest.java @@ -0,0 +1,70 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive; + +import java.util.Collection; +import java.util.List; + +import org.hibernate.LockMode; + +import org.junit.jupiter.api.Test; + +import io.vertx.junit5.Timeout; +import io.vertx.junit5.VertxTestContext; +import jakarta.persistence.Entity; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +@Timeout(value = 10, timeUnit = MINUTES) +public class LockOnLoadTest extends BaseReactiveTest{ + @Override + protected Collection> annotatedEntities() { + return List.of( Person.class ); + } + + @Test + public void testLockOnLoad(VertxTestContext context) { + Person person = new Person( 1L, "Davide" ); + + test( context, getMutinySessionFactory() + .withTransaction( session -> session.persist( person ) ) + .call( () -> getMutinySessionFactory().withSession( session -> session + .find( Person.class, person.getId() ) + // the issue occurred when trying to find the same entity but upgrading the lock mode + .chain( p -> session.find( Person.class, person.getId(), LockMode.PESSIMISTIC_WRITE ) ) + .invoke( p -> assertThat( p ).isNotNull() ) + ) ) + ); + } + + @Entity(name = "Person") + @Table(name = "LockOnLoadTest.Person") + public static class Person { + @Id + private Long id; + + private String name; + + public Person() { + } + + public Person(Long id, String name) { + this.id = id; + this.name = name; + } + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + } +} diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/ManyToOneMapsIdAndEmbeddedIdTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/ManyToOneMapsIdAndEmbeddedIdTest.java new file mode 100644 index 000000000..314c69b48 --- /dev/null +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/ManyToOneMapsIdAndEmbeddedIdTest.java @@ -0,0 +1,189 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.hibernate.annotations.EmbeddedColumnNaming; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import io.vertx.junit5.VertxTestContext; +import jakarta.persistence.CascadeType; +import jakarta.persistence.Column; +import jakarta.persistence.Embeddable; +import jakarta.persistence.Embedded; +import jakarta.persistence.EmbeddedId; +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.MapsId; +import jakarta.persistence.OneToMany; + +import static org.assertj.core.api.Assertions.assertThat; + +public class ManyToOneMapsIdAndEmbeddedIdTest extends BaseReactiveTest { + + @Override + protected Collection> annotatedEntities() { + return List.of( Link.class, GPSPoint.class, NetPoint.class ); + } + + @BeforeEach + public void populateDb(VertxTestContext context) { + NetPointKey startKey = new NetPointKey( 1, NetPointType.STOP_POINT ); + NetPointKey endKey = new NetPointKey( 2, NetPointType.STOP_POINT ); + LinkKey linkKey = new LinkKey( startKey, endKey, "123" ); + + final NetPoint start = new NetPoint(); + fillWithBogusValues( start ); + start.key = startKey; + + final NetPoint end = new NetPoint(); + fillWithBogusValues( end ); + end.key = endKey; + + final Link link = new Link(); + link.key = linkKey; + link.addPoint( new GPSPoint( new GPSPointKey( linkKey, 0 ), link, 1, 1, 1 ) ); + link.addPoint( new GPSPoint( new GPSPointKey( linkKey, 1 ), link, 1, 1, 1 ) ); + link.addPoint( new GPSPoint( new GPSPointKey( linkKey, 2 ), link, 1, 1, 1 ) ); + + test( + context, getMutinySessionFactory().withTransaction( session -> session + .persistAll( start, end, link ) ) + ); + } + + @Test + public void test(VertxTestContext context) { + test( context, getMutinySessionFactory() + .withTransaction( session -> session + .createQuery( "from Link", Link.class ).getResultList() ) + .invoke( links -> assertThat( links ).hasSize( 1 ) ) + ); + } + + void fillWithBogusValues(NetPoint start) { + start.gpsLatitude = 1; + start.gpsLongitude = 1; + start.operatingDepartmentId = "123"; + start.operatingDepartmentShortName = "123 - 123"; + } + + @Entity(name = "GPSPoint") + public static class GPSPoint { + + @EmbeddedId + public GPSPointKey key; + + @ManyToOne + @MapsId("link") + public Link link; + + @Column(nullable = false) + public Integer latitude; + + @Column(nullable = false) + public Integer longitude; + + @Column(nullable = false) + public Integer distance; + + public GPSPoint() { + } + + public GPSPoint(GPSPointKey key, Link link, Integer latitude, Integer longitude, Integer distance) { + this.key = key; + this.link = link; + this.latitude = latitude; + this.longitude = longitude; + this.distance = distance; + } + } + + @Embeddable + public record GPSPointKey( + @Embedded + LinkKey link, + + Integer position + ) { + + } + + @Entity(name = "Link") + public static class Link { + + @EmbeddedId + public LinkKey key; + + @OneToMany(mappedBy = "link", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.EAGER) + public List gpsPoints = new ArrayList<>(); + + public void addPoint(GPSPoint point) { + gpsPoints.add( point ); + point.link = this; + } + } + + @Embeddable + public record LinkKey( + @Embedded + @EmbeddedColumnNaming("start_%s") + NetPointKey start, + + @Embedded + @EmbeddedColumnNaming("end_%s") + NetPointKey end, + + String operatingDepartmentId + ) { + + } + + @Embeddable + public record NetPointKey( + Integer id, + + @Enumerated(EnumType.ORDINAL) + NetPointType type + ) { + + } + + @Entity(name = "NetPoint") + public static class NetPoint { + + @EmbeddedId + public NetPointKey key; + + @Column(nullable = false) + public String operatingDepartmentId; + + @Column(nullable = false) + public String operatingDepartmentShortName; + + @Column(nullable = false) + public Integer gpsLatitude; + + @Column(nullable = false) + public Integer gpsLongitude; + } + + public enum NetPointType { + @Deprecated UNSPECIFIED, + STOP_POINT, + DEPOT_POINT, + BEACON, + SECTION_POINT + } +} diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/MultithreadedIdentityGenerationTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/MultithreadedIdentityGenerationTest.java index a41cefd72..5134a32e5 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/MultithreadedIdentityGenerationTest.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/MultithreadedIdentityGenerationTest.java @@ -212,7 +212,7 @@ public void start(Promise startPromise) { startPromise.fail( "Thread switch detected!" ); } else { - allResults.deliverResulst( generatedIds ); + allResults.deliverResults( generatedIds ); startPromise.complete(); } } @@ -233,7 +233,7 @@ private static class ResultsCollector { private final ConcurrentMap> resultsByThread = new ConcurrentHashMap<>(); - public void deliverResulst(List generatedIds) { + public void deliverResults(List generatedIds) { final String threadName = Thread.currentThread().getName(); resultsByThread.put( threadName, generatedIds ); } diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/MultithreadedInsertionTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/MultithreadedInsertionTest.java index bd2a36d1e..7fcf1c8c4 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/MultithreadedInsertionTest.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/MultithreadedInsertionTest.java @@ -98,15 +98,7 @@ public class MultithreadedInsertionTest { @BeforeAll public static void setupSessionFactory() { - final VertxOptions vertxOptions = new VertxOptions(); - vertxOptions.setEventLoopPoolSize( N_THREADS ); - //We relax the blocked thread checks as we'll actually use latches to block them - //intentionally for the purpose of the test; functionally this isn't required - //but it's useful as self-test in the design of this, to ensure that the way - //things are setup are indeed being run in multiple, separate threads. - vertxOptions.setBlockedThreadCheckInterval( TIMEOUT_MINUTES ); - vertxOptions.setBlockedThreadCheckIntervalUnit( TimeUnit.MINUTES ); - vertx = Vertx.vertx( vertxOptions ); + vertx = Vertx.vertx( getVertxOptions() ); Configuration configuration = new Configuration(); setDefaultProperties( configuration ); configuration.addAnnotatedClass( EntityWithGeneratedId.class ); @@ -121,6 +113,18 @@ public static void setupSessionFactory() { stageSessionFactory = sessionFactory.unwrap( Stage.SessionFactory.class ); } + private static VertxOptions getVertxOptions() { + final VertxOptions vertxOptions = new VertxOptions(); + vertxOptions.setEventLoopPoolSize( N_THREADS ); + //We relax the blocked thread checks as we'll actually use latches to block them + //intentionally for the purpose of the test; functionally this isn't required, + //but it's useful as self-test in the design of this, to ensure that the way + //things are set up are indeed being run in multiple, separate threads. + vertxOptions.setBlockedThreadCheckInterval( TIMEOUT_MINUTES ); + vertxOptions.setBlockedThreadCheckIntervalUnit( TimeUnit.MINUTES ); + return vertxOptions; + } + @AfterAll public static void closeSessionFactory() { stageSessionFactory.close(); @@ -158,10 +162,12 @@ public void start(Promise startPromise) { .whenComplete( (o, throwable) -> { endLatch.reached(); if ( throwable != null ) { + prettyOut( throwable.getMessage() ); startPromise.fail( throwable ); } else { if ( !initialThreadName.equals( Thread.currentThread().getName() ) ) { + prettyOut( "Thread switch detected. Expecting " + initialThreadName + ", actual " + Thread.currentThread().getName() ); startPromise.fail( "Thread switch detected!" ); } else { @@ -203,7 +209,7 @@ public void stop() { */ @Entity @Table(name="Entity") - private static class EntityWithGeneratedId { + public static class EntityWithGeneratedId { @Id @GeneratedValue Long id; diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/MultithreadedInsertionWithLazyConnectionTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/MultithreadedInsertionWithLazyConnectionTest.java new file mode 100644 index 000000000..2948cce27 --- /dev/null +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/MultithreadedInsertionWithLazyConnectionTest.java @@ -0,0 +1,278 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive; + +import java.util.concurrent.CompletionStage; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +import org.hibernate.SessionFactory; +import org.hibernate.boot.registry.StandardServiceRegistry; +import org.hibernate.boot.registry.StandardServiceRegistryBuilder; +import org.hibernate.cfg.Configuration; +import org.hibernate.reactive.provider.ReactiveServiceRegistryBuilder; +import org.hibernate.reactive.stage.Stage; +import org.hibernate.reactive.util.impl.CompletionStages; +import org.hibernate.reactive.vertx.VertxInstance; + +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.extension.ExtendWith; + +import io.vertx.core.AbstractVerticle; +import io.vertx.core.DeploymentOptions; +import io.vertx.core.Promise; +import io.vertx.core.Vertx; +import io.vertx.core.VertxOptions; +import io.vertx.junit5.Timeout; +import io.vertx.junit5.VertxExtension; +import io.vertx.junit5.VertxTestContext; +import jakarta.persistence.Entity; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.Id; +import jakarta.persistence.Table; + +import static java.util.concurrent.TimeUnit.MINUTES; +import static org.assertj.core.api.Assertions.fail; +import static org.hibernate.cfg.AvailableSettings.SHOW_SQL; +import static org.hibernate.reactive.BaseReactiveTest.setDefaultProperties; +import static org.hibernate.reactive.provider.Settings.POOL_CONNECT_TIMEOUT; +import static org.hibernate.reactive.util.impl.CompletionStages.failedFuture; +import static org.hibernate.reactive.util.impl.CompletionStages.loop; +import static org.hibernate.reactive.util.impl.CompletionStages.voidFuture; + +/** + * This is a multi-threaded stress test, intentionally consuming some time + * that also opens the connection lazily. + * The purpose is to verify that the sequence optimizer used by Hibernate Reactive + * is indeed able to generate unique IDs backed by the database sequences, while + * running multiple operations in different threads and on multiple Vert.x eventloops. + * This is very similar to MultithreadedIdentityGenerationTest except it models + * the full operations including the insert statements, while the latter focuses + * on the generated IDs to be unique; it's useful to maintain both tests as: + * - ID generation needs to be unique so it's good to stress that aspect + * in isolation + * - insert operations are downstream events, so this allows us to test that + * such downstream events are not being unintentionally duplicated/dropped, + * which could actually happen when the id generator triggers unintended + * threading behaviours. + * + * N.B. We actually had a case in which the IDs were uniquely generated but the + * downstream event was being processed twice (or more) concurrently, so it's + * useful to have both integration tests. + * + * A typical reactive application will not require multiple threads, but we + * specifically want to test for the case in which the single ID source is being + * shared across multiple threads and also multiple eventloops. + * @see MultithreadedInsertionTest + */ +@ExtendWith(VertxExtension.class) +@TestInstance(TestInstance.Lifecycle.PER_METHOD) +@Timeout(value = MultithreadedInsertionWithLazyConnectionTest.TIMEOUT_MINUTES, timeUnit = MINUTES) +public class MultithreadedInsertionWithLazyConnectionTest { + + /** + * The number of threads should be higher than the default size of the connection pool so that + * this test is also effective in detecting problems with resource starvation. + */ + private static final int N_THREADS = 12; + private static final int ENTITIES_STORED_PER_THREAD = 2000; + + //Should finish much sooner, but generating this amount of IDs could be slow on some CIs + public static final int TIMEOUT_MINUTES = 10; + + // Keeping this disabled because it generates a lot of queries + private static final boolean LOG_SQL = false; + + /** + * If true, it will print info about the threads + */ + private static final boolean THREAD_PRETTY_MSG = true; + + private static final Latch startLatch = new Latch( "start", N_THREADS ); + private static final Latch endLatch = new Latch( "end", N_THREADS ); + + private static Stage.SessionFactory stageSessionFactory; + private static Vertx vertx; + private static SessionFactory sessionFactory; + + @BeforeAll + public static void setupSessionFactory() { + vertx = Vertx.vertx( getVertxOptions() ); + Configuration configuration = new Configuration(); + setDefaultProperties( configuration ); + configuration.addAnnotatedClass( EntityWithGeneratedId.class ); + configuration.setProperty( SHOW_SQL, String.valueOf( LOG_SQL ) ); + configuration.setProperty( POOL_CONNECT_TIMEOUT, String.valueOf( TIMEOUT_MINUTES * 60 * 1000 ) ); + StandardServiceRegistryBuilder builder = new ReactiveServiceRegistryBuilder() + .applySettings( configuration.getProperties() ) + //Inject our custom vert.x instance: + .addService( VertxInstance.class, () -> vertx ); + StandardServiceRegistry registry = builder.build(); + sessionFactory = configuration.buildSessionFactory( registry ); + stageSessionFactory = sessionFactory.unwrap( Stage.SessionFactory.class ); + } + + private static VertxOptions getVertxOptions() { + final VertxOptions vertxOptions = new VertxOptions(); + vertxOptions.setEventLoopPoolSize( N_THREADS ); + //We relax the blocked thread checks as we'll actually use latches to block them + //intentionally for the purpose of the test; functionally this isn't required + //but it's useful as self-test in the design of this, to ensure that the way + //things are setup are indeed being run in multiple, separate threads. + vertxOptions.setBlockedThreadCheckInterval( TIMEOUT_MINUTES ); + vertxOptions.setBlockedThreadCheckIntervalUnit( TimeUnit.MINUTES ); + return vertxOptions; + } + + @AfterAll + public static void closeSessionFactory() { + stageSessionFactory.close(); + } + + @Test + public void testIdentityGenerator(VertxTestContext context) { + final DeploymentOptions deploymentOptions = new DeploymentOptions(); + deploymentOptions.setInstances( N_THREADS ); + + vertx + .deployVerticle( InsertEntitiesVerticle::new, deploymentOptions ) + .onSuccess( res -> { + endLatch.waitForEveryone(); + context.completeNow(); + } ) + .onFailure( context::failNow ) + .eventually( () -> vertx.close() ); + } + + private static class InsertEntitiesVerticle extends AbstractVerticle { + + int sequentialOperation = 0; + + public InsertEntitiesVerticle() { + } + + @Override + public void start(Promise startPromise) { + startLatch.reached(); + startLatch.waitForEveryone();//Not essential, but to ensure a good level of parallelism + final String initialThreadName = Thread.currentThread().getName(); + final Stage.Session session = stageSessionFactory.createSession(); + storeMultipleEntities( session ) + .handle( CompletionStages::handle ) + .thenCompose( handler -> session + .close() + .thenCompose( handler::getResultAsCompletionStage ) + ) + .whenComplete( (o, throwable) -> { + endLatch.reached(); + if ( throwable != null ) { + prettyOut( throwable.getMessage() ); + startPromise.fail( throwable ); + } + else { + if ( !initialThreadName.equals( Thread.currentThread().getName() ) ) { + prettyOut( "Thread switch detected. Expecting " + initialThreadName + ", actual " + Thread.currentThread().getName() ); + startPromise.fail( "Thread switch detected!" ); + } + else { + startPromise.complete(); + } + } + } ); + } + + private CompletionStage storeMultipleEntities(Stage.Session s) { + return loop( 0, ENTITIES_STORED_PER_THREAD, index -> storeEntity( s ) ); + } + + private CompletionStage storeEntity(Stage.Session s) { + final Thread beforeOperationThread = Thread.currentThread(); + final int localVerticleOperationSequence = sequentialOperation++; + final EntityWithGeneratedId entity = new EntityWithGeneratedId(); + entity.name = beforeOperationThread + "__" + localVerticleOperationSequence; + + return s + .withTransaction( t -> s.persist( entity ) ) + .thenCompose( v -> beforeOperationThread != Thread.currentThread() + ? failedFuture( new IllegalStateException( "Detected an unexpected switch of carrier threads!" ) ) + : voidFuture() ); + } + + @Override + public void stop() { + prettyOut( "Verticle stopped " + super.toString() ); + } + } + + /** + * Trivial entity using default id generation + */ + @Entity + @Table(name = "Entity") + private static class EntityWithGeneratedId { + @Id + @GeneratedValue + Long id; + + String name; + + public EntityWithGeneratedId() { + } + } + + /** + * Custom latch which is rather verbose about threads reaching the milestones, to help verifying the design + */ + private static final class Latch { + private final String label; + private final CountDownLatch countDownLatch; + + public Latch(String label, int membersCount) { + this.label = label; + this.countDownLatch = new CountDownLatch( membersCount ); + } + + public void reached() { + final long count = countDownLatch.getCount(); + countDownLatch.countDown(); + prettyOut( "Reached latch '" + label + "', current countdown is " + ( count - 1 ) ); + } + + public void waitForEveryone() { + try { + boolean reachedZero = countDownLatch.await( TIMEOUT_MINUTES, MINUTES ); + if ( reachedZero ) { + prettyOut( "Everyone has now breached '" + label + "'" ); + } + else { + fail( "Time out reached" ); + } + } + catch ( InterruptedException e ) { + fail( e ); + } + } + } + + private static void prettyOut(final String message) { + if ( THREAD_PRETTY_MSG ) { + final String threadName = Thread.currentThread().getName(); + final long l = System.currentTimeMillis(); + final long seconds = ( l / 1000 ) - initialSecond; + //We prefix log messages by seconds since bootstrap; I'm preferring this over millisecond precision + //as it's not very relevant to see exactly how long each stage took (it's actually distracting) + //but it's more useful to group things coarsely when some lock or timeout introduces a significant + //divide between some operations (when a starvation or timeout happens it takes some seconds). + System.out.println( seconds + " - " + threadName + ": " + message ); + } + } + + private static final long initialSecond = ( System.currentTimeMillis() / 1000 ); + +} diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/MutinySessionTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/MutinySessionTest.java index bf46f835a..6fecfbb85 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/MutinySessionTest.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/MutinySessionTest.java @@ -10,10 +10,11 @@ import java.util.Objects; import java.util.concurrent.atomic.AtomicInteger; +import org.hibernate.HibernateException; import org.hibernate.LockMode; +import org.hibernate.exception.ConstraintViolationException; import org.hibernate.reactive.mutiny.Mutiny; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import io.smallrye.mutiny.Uni; @@ -22,15 +23,17 @@ import jakarta.persistence.Entity; import jakarta.persistence.Id; import jakarta.persistence.LockModeType; -import jakarta.persistence.PersistenceException; import jakarta.persistence.Table; +import jakarta.persistence.Version; +import jakarta.persistence.metamodel.Attribute; import jakarta.persistence.metamodel.EntityType; import static java.util.concurrent.TimeUnit.MINUTES; -import static org.junit.jupiter.api.Assertions.*; +import static org.assertj.core.api.Assertions.assertThat; +import static org.hibernate.reactive.testing.ReactiveAssertions.assertThrown; +import static org.junit.jupiter.api.Assertions.assertEquals; @Timeout(value = 10, timeUnit = MINUTES) - public class MutinySessionTest extends BaseReactiveTest { @Override @@ -46,34 +49,34 @@ private Uni populateDB() { private Uni selectNameFromId(Integer id) { return getMutinySessionFactory().withSession( - session -> session.createQuery("SELECT name FROM GuineaPig WHERE id = " + id ) + session -> session + .createSelectionQuery( "SELECT name FROM GuineaPig WHERE id = " + id, String.class ) .getResultList() .map( MutinySessionTest::nameFromResult ) ); } - private static String nameFromResult(List rowSet) { - switch ( rowSet.size() ) { - case 0: - return null; - case 1: - return (String) rowSet.get( 0 ); - default: - throw new AssertionError( "More than one result returned: " + rowSet.size() ); - } + private static String nameFromResult(List rowSet) { + return switch ( rowSet.size() ) { + case 0 -> null; + case 1 -> rowSet.get( 0 ); + default -> throw new AssertionError( "More than one result returned: " + rowSet.size() ); + }; } @Test public void reactiveFindMultipleIds(VertxTestContext context) { final GuineaPig rump = new GuineaPig( 55, "Rumpelstiltskin" ); final GuineaPig emma = new GuineaPig( 77, "Emma" ); - test( context, populateDB() - .chain( () -> getMutinySessionFactory().withTransaction( s -> s.persistAll( emma, rump ) ) ) - .chain( () -> getMutinySessionFactory().withTransaction( s -> s.find( GuineaPig.class, emma.getId(), rump.getId() ) ) - ) - .invoke( pigs -> { - org.assertj.core.api.Assertions.assertThat( pigs ).containsExactlyInAnyOrder( emma, rump ); - } ) + test( + context, populateDB() + .chain( () -> getMutinySessionFactory().withTransaction( s -> s + .persistAll( emma, rump ) ) + ) + .chain( () -> getMutinySessionFactory().withTransaction( s -> s + .find( GuineaPig.class, emma.getId(), rump.getId() ) + ) ) + .invoke( pigs -> assertThat( pigs ).containsExactlyInAnyOrder( emma, rump ) ) ); } @@ -119,42 +122,21 @@ public void reactiveWithTransactionSession(VertxTestContext context) { } @Test - public void reactiveFind1(VertxTestContext context) { + public void reactiveFind(VertxTestContext context) { final GuineaPig expectedPig = new GuineaPig( 5, "Aloi" ); test( context, populateDB() - .call( () -> getMutinySessionFactory().withSession( - session -> session.find( GuineaPig.class, expectedPig.getId() ) - .onItem().invoke( actualPig -> { - assertThatPigsAreEqual( expectedPig, actualPig ); - assertTrue( session.contains( actualPig ) ); - assertFalse( session.contains( expectedPig ) ); - assertEquals( LockMode.READ, session.getLockMode( actualPig ) ); - session.detach( actualPig ); - assertFalse( session.contains( actualPig ) ); - } ) - ) ) - - ); - } - - @Test - public void reactiveFind2(VertxTestContext context) { - final GuineaPig expectedPig = new GuineaPig( 5, "Aloi" ); - test( - context, - populateDB() - .call( () -> getMutinySessionFactory().withSession( - session -> session.find( GuineaPig.class, expectedPig.getId() ) - .invoke( actualPig -> { - assertThatPigsAreEqual( expectedPig, actualPig ); - assertTrue( session.contains( actualPig ) ); - assertFalse( session.contains( expectedPig ) ); - assertEquals( LockMode.READ, session.getLockMode( actualPig ) ); - session.detach( actualPig ); - assertFalse( session.contains( actualPig ) ); - } ) + .call( () -> getMutinySessionFactory().withTransaction( session -> session + .find( GuineaPig.class, expectedPig.getId() ) + .invoke( actualPig -> { + assertThatPigsAreEqual( expectedPig, actualPig ); + assertThat( session.contains( actualPig ) ).isTrue(); + assertThat( session.contains( expectedPig ) ).isFalse(); + assertThat( session.getLockMode( actualPig ) ).isEqualTo( LockMode.READ ); + session.detach( actualPig ); + assertThat( session.contains( actualPig ) ).isFalse(); + } ) ) ) ); } @@ -163,14 +145,13 @@ public void reactiveFind2(VertxTestContext context) { public void reactiveFindWithLock(VertxTestContext context) { final GuineaPig expectedPig = new GuineaPig( 5, "Aloi" ); test( - context, - populateDB() - .call( () -> getMutinySessionFactory().withSession( - session -> session.find( GuineaPig.class, expectedPig.getId(), LockMode.PESSIMISTIC_WRITE ) - .invoke( actualPig -> { - assertThatPigsAreEqual( expectedPig, actualPig ); - assertEquals( session.getLockMode( actualPig ), LockMode.PESSIMISTIC_WRITE ); - } ) + context, populateDB() + .call( () -> getMutinySessionFactory().withSession( session -> session + .find( GuineaPig.class, expectedPig.getId(), LockMode.PESSIMISTIC_WRITE ) + .invoke( actualPig -> { + assertThatPigsAreEqual( expectedPig, actualPig ); + assertThat( session.getLockMode( actualPig ) ).isEqualTo( LockMode.PESSIMISTIC_WRITE ); + } ) ) ) ); } @@ -179,15 +160,14 @@ public void reactiveFindWithLock(VertxTestContext context) { public void reactiveFindRefreshWithLock(VertxTestContext context) { final GuineaPig expectedPig = new GuineaPig( 5, "Aloi" ); test( - context, - populateDB() - .call( () -> getMutinySessionFactory().withSession( - session -> session.find( GuineaPig.class, expectedPig.getId() ) - .call( pig -> session.refresh(pig, LockMode.PESSIMISTIC_WRITE) ) - .invoke( actualPig -> { - assertThatPigsAreEqual( expectedPig, actualPig ); - assertEquals( session.getLockMode( actualPig ), LockMode.PESSIMISTIC_WRITE ); - } ) + context, populateDB() + .call( () -> getMutinySessionFactory().withSession( session -> session + .find( GuineaPig.class, expectedPig.getId() ) + .call( pig -> session.refresh( pig, LockMode.PESSIMISTIC_WRITE ) ) + .invoke( actualPig -> { + assertThatPigsAreEqual( expectedPig, actualPig ); + assertThat( session.getLockMode( actualPig ) ).isEqualTo( LockMode.PESSIMISTIC_WRITE ); + } ) ) ) ); } @@ -196,31 +176,30 @@ public void reactiveFindRefreshWithLock(VertxTestContext context) { public void reactiveFindReadOnlyRefreshWithLock(VertxTestContext context) { final GuineaPig expectedPig = new GuineaPig( 5, "Aloi" ); test( - context, - populateDB() - .call( () -> getMutinySessionFactory().withSession( - session -> session.find( GuineaPig.class, expectedPig.getId() ) + context, populateDB() + .call( () -> getMutinySessionFactory().withSession( session -> session + .find( GuineaPig.class, expectedPig.getId() ) .call( pig -> { - session.setReadOnly(pig, true); - pig.setName("XXXX"); + session.setReadOnly( pig, true ); + pig.setName( "XXXX" ); return session.flush() - .call( v -> session.refresh(pig) ) + .call( v -> session.refresh( pig ) ) .invoke( v -> { - assertEquals(expectedPig.name, pig.name); - assertTrue(session.isReadOnly(pig)); + assertThat( pig.name ).isEqualTo( expectedPig.name ); + assertThat( session.isReadOnly( pig ) ).isTrue(); } ); } ) ) ) - .call( () -> getMutinySessionFactory().withSession( - session -> session.find( GuineaPig.class, expectedPig.getId() ) + .call( () -> getMutinySessionFactory().withSession( session -> session + .find( GuineaPig.class, expectedPig.getId() ) .call( pig -> { - session.setReadOnly(pig, false); - pig.setName("XXXX"); + session.setReadOnly( pig, false ); + pig.setName( "XXXX" ); return session.flush() - .call( v -> session.refresh(pig) ) + .call( v -> session.refresh( pig ) ) .invoke( v -> { - assertEquals("XXXX", pig.name); - assertFalse(session.isReadOnly(pig)); + assertThat( pig.name ).isEqualTo( "XXXX" ); + assertThat( session.isReadOnly( pig ) ).isFalse(); } ); } ) ) ) @@ -231,15 +210,14 @@ public void reactiveFindReadOnlyRefreshWithLock(VertxTestContext context) { public void reactiveFindThenUpgradeLock(VertxTestContext context) { final GuineaPig expectedPig = new GuineaPig( 5, "Aloi" ); test( - context, - populateDB() - .call( () -> getMutinySessionFactory().withSession( - session -> session.find( GuineaPig.class, expectedPig.getId() ) - .call( pig -> session.lock(pig, LockMode.PESSIMISTIC_READ) ) - .invoke( actualPig -> { - assertThatPigsAreEqual( expectedPig, actualPig ); - assertEquals( session.getLockMode( actualPig ), LockMode.PESSIMISTIC_READ ); - } ) + context, populateDB() + .call( () -> getMutinySessionFactory().withSession( session -> session + .find( GuineaPig.class, expectedPig.getId() ) + .call( pig -> session.lock( pig, LockMode.PESSIMISTIC_READ ) ) + .invoke( actualPig -> { + assertThatPigsAreEqual( expectedPig, actualPig ); + assertThat( session.getLockMode( actualPig ) ).isEqualTo( LockMode.PESSIMISTIC_READ ); + } ) ) ) ); } @@ -248,169 +226,114 @@ public void reactiveFindThenUpgradeLock(VertxTestContext context) { public void reactiveFindThenWriteLock(VertxTestContext context) { final GuineaPig expectedPig = new GuineaPig( 5, "Aloi" ); test( - context, - populateDB() - .call( () -> getMutinySessionFactory().withSession( - session -> session.find( GuineaPig.class, expectedPig.getId() ) - .call( pig -> session.lock(pig, LockMode.PESSIMISTIC_WRITE) ) - .invoke( actualPig -> { - assertThatPigsAreEqual( expectedPig, actualPig ); - assertEquals( session.getLockMode( actualPig ), LockMode.PESSIMISTIC_WRITE ); - } ) + context, populateDB() + .call( () -> getMutinySessionFactory().withSession( session -> session + .find( GuineaPig.class, expectedPig.getId() ) + .call( pig -> session.lock( pig, LockMode.PESSIMISTIC_WRITE ) ) + .invoke( actualPig -> { + assertThatPigsAreEqual( expectedPig, actualPig ); + assertThat( session.getLockMode( actualPig ) ).isEqualTo( LockMode.PESSIMISTIC_WRITE ); + } ) ) ) ); } @Test - public void reactivePersist1(VertxTestContext context) { - test( - context, - getMutinySessionFactory() - .withSession( s -> s.persist( new GuineaPig( 10, "Tulip" ) ).onItem().call( s::flush ) ) - .onItem().transformToUni( v -> selectNameFromId(10) ) - .onItem().invoke( selectRes -> assertEquals( "Tulip", selectRes ) ) - ); - } - - @Test - public void reactivePersist2(VertxTestContext context) { + public void reactivePersist(VertxTestContext context) { test( - context, - getMutinySessionFactory().withSession( s -> s.persist( new GuineaPig( 10, "Tulip" ) ).chain( s::flush ) ) - .chain( () -> selectNameFromId(10) ) - .invoke( selectRes -> assertEquals( "Tulip", selectRes ) ) + context, getMutinySessionFactory() + .withSession( s -> s.persist( new GuineaPig( 10, "Tulip" ) ).call( s::flush ) ) + .chain( () -> selectNameFromId( 10 ) ) + .invoke( selectRes -> assertThat( selectRes ).isEqualTo( "Tulip" ) ) ); } @Test public void reactivePersistInTx(VertxTestContext context) { test( - context, - getMutinySessionFactory() - .withTransaction( (s,t) -> s.persist( new GuineaPig( 10, "Tulip" ) ) ) - .chain( () -> selectNameFromId(10) ) - .invoke( selectRes -> assertEquals( "Tulip", selectRes ) ) + context, getMutinySessionFactory() + .withTransaction( s -> s.persist( new GuineaPig( 10, "Tulip" ) ) ) + .chain( () -> selectNameFromId( 10 ) ) + .invoke( selectRes -> assertThat( selectRes ).isEqualTo( "Tulip" ) ) ); } @Test public void reactiveRollbackTx(VertxTestContext context) { + final RuntimeException expectedException = new RuntimeException( "For test, After flush" ); test( - context, - getMutinySessionFactory() - .withTransaction( - (s,t) -> s.persist( new ReactiveSessionTest.GuineaPig( 10, "Tulip" ) ) - .call(s::flush) - .invoke( () -> { throw new RuntimeException(); } ) - ) - .onItem().invoke( (Runnable) Assertions::fail ) - .onFailure().recoverWithItem((Void) null) - .chain( () -> selectNameFromId(10) ) - .invoke( Assertions::assertNull ) + context, assertThrown( + RuntimeException.class, getMutinySessionFactory() + .withTransaction( s -> s + .persist( new GuineaPig( 10, "Tulip" ) ) + // Flush the changes but don't commit the transaction + .call( s::flush ) + .invoke( () -> { + // Throw an exception before committing the transaction + throw expectedException; + } ) + ) + ) + .invoke( e -> assertThat( e ).hasMessage( expectedException.getMessage() ) ) + .chain( () -> selectNameFromId( 10 ) ) + .invoke( name -> assertThat( name ).isNull() ) ); } @Test public void reactiveMarkedRollbackTx(VertxTestContext context) { test( - context, - getMutinySessionFactory() - .withTransaction( - (s, t) -> s.persist( new GuineaPig( 10, "Tulip" ) ) - .call(s::flush) - .invoke(t::markForRollback) + context, getMutinySessionFactory() + .withTransaction( (s, t) -> s + .persist( new GuineaPig( 10, "Tulip" ) ) + .call( s::flush ) + .invoke( t::markForRollback ) ) - .chain( () -> selectNameFromId(10) ) - .invoke( Assertions::assertNull ) + .chain( () -> selectNameFromId( 10 ) ) + .invoke( name -> assertThat( name ).isNull() ) ); } @Test - public void reactiveRemoveTransientEntity1(VertxTestContext context) { + public void reactiveRemoveTransientEntity(VertxTestContext context) { test( - context, - populateDB() - .onItem().call( () -> selectNameFromId(5).onItem().invoke( Assertions::assertNotNull ) ) + context, populateDB() + .chain( () -> selectNameFromId( 5 ) ) + .invoke( name -> assertThat( name ).isNotNull() ) .chain( this::openMutinySession ) - .onItem().call( session -> session.remove( new GuineaPig( 5, "Aloi" ) ) ) - .onItem().invoke( (Runnable) Assertions::fail ) - .onFailure().recoverWithItem( () -> null ) -// .onItem().invokeUni( session -> session.flush() ) -// .onTermination().invoke( (session, err, c) -> session.close() ) -// .onItem().invokeUni( v -> selectNameFromId( 5 ).onItem().invoke( context::assertNull ) ) - ); - } - - @Test - public void reactiveRemoveTransientEntity2(VertxTestContext context) { - test( - context, - populateDB() - .chain( () -> selectNameFromId(5) ) - .invoke( Assertions::assertNotNull ) - .chain( this::openMutinySession ) - .call( session -> session.remove( new GuineaPig( 5, "Aloi" ) ) ) - .onItem().invoke( (Runnable) Assertions::fail ) - .onFailure().recoverWithItem( () -> null ) -// .chain( session -> session.flush().eventually(session::close) ) -// .then( () -> selectNameFromId( 5 ) ) -// .invoke( context::assertNull ) - ); - } - - @Test - public void reactiveRemoveManagedEntity1(VertxTestContext context) { - test( - context, - populateDB() - .onItem().call( () -> getMutinySessionFactory().withSession( - session -> session.find( GuineaPig.class, 5 ) - .onItem().call(session::remove) - .onItem().call(session::flush) + .chain( session -> assertThrown( + HibernateException.class, + session.remove( new GuineaPig( 5, "Aloi" ) ) ) ) - .onItem().call( () -> selectNameFromId(5).onItem().invoke( Assertions::assertNull ) ) - ); - } - - @Test - public void reactiveRemoveManagedEntity2(VertxTestContext context) { - test( - context, - populateDB() - .call( () -> getMutinySessionFactory().withSession( - session -> session.find( GuineaPig.class, 5 ) - .chain(session::remove) - .call(session::flush) - ) ) - .chain( () -> selectNameFromId(5) ) - .invoke( Assertions::assertNull ) + .invoke( e -> assertThat( e ) + .hasMessageContaining( "unmanaged instance passed to remove" ) + ) ); } @Test - public void reactiveRemoveManagedEntityWithTx1(VertxTestContext context) { + public void reactiveRemoveManagedEntity(VertxTestContext context) { test( - context, - populateDB() - .onItem().call( () -> getMutinySessionFactory().withTransaction( - (session, transaction) -> session.find( GuineaPig.class, 5 ) - .onItem().call(session::remove) + context, populateDB() + .call( () -> getMutinySessionFactory().withTransaction( session -> session + .find( GuineaPig.class, 5 ) + .call( session::remove ) ) ) - .onItem().call( () -> selectNameFromId(5).onItem().invoke( Assertions::assertNull ) ) + .chain( () -> selectNameFromId( 5 ) ) + .invoke( name -> assertThat( name ).isNull() ) ); } @Test - public void reactiveRemoveManagedEntityWithTx2(VertxTestContext context) { + public void reactiveRemoveManagedEntityWithTx(VertxTestContext context) { test( - context, - populateDB() - .call( () -> getMutinySessionFactory().withTransaction( - (session, transaction) -> session.find( GuineaPig.class, 5 ) - .call(session::remove) + context, populateDB() + .call( () -> getMutinySessionFactory().withTransaction( session -> session + .find( GuineaPig.class, 5 ) + .call( session::remove ) ) ) - .chain( () -> selectNameFromId(5) ) - .invoke( Assertions::assertNull ) + .chain( () -> selectNameFromId( 5 ) ) + .invoke( name -> assertThat( name ).isNull() ) ); } @@ -418,21 +341,19 @@ public void reactiveRemoveManagedEntityWithTx2(VertxTestContext context) { public void reactiveUpdate(VertxTestContext context) { final String NEW_NAME = "Tina"; test( - context, - populateDB() - .call( () -> getMutinySessionFactory().withSession( - session -> session.find( GuineaPig.class, 5 ) - .map( pig -> { - assertNotNull( pig ); - // Checking we are actually changing the name - assertNotEquals( pig.getName(), NEW_NAME ); - pig.setName( NEW_NAME ); - return null; - } ) - .call(session::flush) + context, populateDB() + .call( () -> getMutinySessionFactory().withSession( session -> session + .find( GuineaPig.class, 5 ) + .invoke( pig -> { + assertThat( pig ).isNotNull(); + // Checking we are actually changing the name + assertThat( pig.getName() ).isNotEqualTo( NEW_NAME ); + pig.setName( NEW_NAME ); + } ) + .call( session::flush ) ) ) - .chain( () -> selectNameFromId(5) ) - .invoke( name -> assertEquals( NEW_NAME, name ) ) + .chain( () -> selectNameFromId( 5 ) ) + .invoke( name -> assertThat( name ).isEqualTo( NEW_NAME ) ) ); } @@ -440,21 +361,22 @@ public void reactiveUpdate(VertxTestContext context) { public void reactiveUpdateVersion(VertxTestContext context) { final String NEW_NAME = "Tina"; test( - context, - populateDB() - .call( () -> getMutinySessionFactory().withSession( - session -> session.find( GuineaPig.class, 5 ) - .map( pig -> { - assertNotNull( pig ); - // Checking we are actually changing the name - assertNotEquals( pig.getName(), NEW_NAME ); - pig.setName( NEW_NAME ); - return null; - } ) - .call(session::flush) - ) ) - .chain( () -> selectNameFromId(5) ) - .invoke( name -> assertEquals( NEW_NAME, name ) ) + context, populateDB() + .call( () -> getMutinySessionFactory().withSession( session -> session + .find( GuineaPig.class, 5 ) + .invoke( pig -> { + assertThat( pig ).isNotNull(); + // Checking we are actually changing the name + assertThat( pig.getName() ).isNotEqualTo( NEW_NAME ); + assertThat( pig.version ).isEqualTo( 0 ); + pig.setName( NEW_NAME ); + pig.version = 10; //ignored by Hibernate + } ) + .call( session::flush ) ) + ) + .chain( () -> getMutinySessionFactory() + .withSession( s -> s.find( GuineaPig.class, 5 ) ) ) + .invoke( pig -> assertThat( pig.version ).isEqualTo( 1 ) ) ); } @@ -462,16 +384,15 @@ public void reactiveUpdateVersion(VertxTestContext context) { public void reactiveQueryWithLock(VertxTestContext context) { final GuineaPig expectedPig = new GuineaPig( 5, "Aloi" ); test( - context, - populateDB() - .call( () -> getMutinySessionFactory().withTransaction( - (session, tx) -> session.createSelectionQuery( "from GuineaPig pig", GuineaPig.class) - .setLockMode( LockModeType.PESSIMISTIC_WRITE ) - .getSingleResult() - .invoke( actualPig -> { - assertThatPigsAreEqual( expectedPig, actualPig ); - assertEquals( session.getLockMode( actualPig ), LockMode.PESSIMISTIC_WRITE ); - } ) + context, populateDB().call( () -> getMutinySessionFactory() + .withTransaction( session -> session + .createSelectionQuery( "from GuineaPig pig", GuineaPig.class ) + .setLockMode( LockModeType.PESSIMISTIC_WRITE ) + .getSingleResult() + .invoke( actualPig -> { + assertThatPigsAreEqual( expectedPig, actualPig ); + assertThat( session.getLockMode( actualPig ) ).isEqualTo( LockMode.PESSIMISTIC_WRITE ); + } ) ) ) ); } @@ -480,16 +401,15 @@ public void reactiveQueryWithLock(VertxTestContext context) { public void reactiveQueryWithAliasedLock(VertxTestContext context) { final GuineaPig expectedPig = new GuineaPig( 5, "Aloi" ); test( - context, - populateDB() - .call( () -> getMutinySessionFactory().withTransaction( - (session, tx) -> session.createSelectionQuery( "from GuineaPig pig", GuineaPig.class) - .setLockMode("pig", LockMode.PESSIMISTIC_WRITE ) - .getSingleResult() - .invoke( actualPig -> { - assertThatPigsAreEqual( expectedPig, actualPig ); - assertEquals( session.getLockMode( actualPig ), LockMode.PESSIMISTIC_WRITE ); - } ) + context, populateDB().call( () -> getMutinySessionFactory() + .withTransaction( session -> session + .createSelectionQuery( "from GuineaPig pig", GuineaPig.class ) + .setLockMode( "pig", LockMode.PESSIMISTIC_WRITE ) + .getSingleResult() + .invoke( actualPig -> { + assertThatPigsAreEqual( expectedPig, actualPig ); + assertThat( session.getLockMode( actualPig ) ).isEqualTo( LockMode.PESSIMISTIC_WRITE ); + } ) ) ) ); } @@ -501,144 +421,173 @@ public void reactiveMultiQuery(VertxTestContext context) { GuineaPig baz = new GuineaPig( 7, "Baz" ); AtomicInteger i = new AtomicInteger(); - test( context, - getMutinySessionFactory() - .withTransaction( (session, transaction) -> session.persistAll(foo, bar, baz) ) - .call( () -> getMutinySessionFactory().withSession( - session -> session.createSelectionQuery("from GuineaPig", GuineaPig.class) - .getResultList().onItem().disjoint() - .invoke( pig -> { - assertNotNull(pig); - i.getAndIncrement(); - } ) - .collect().asList() - .invoke( list -> { - assertEquals(3, i.get()); - assertEquals(3, list.size()); - } ) + test( + context, getMutinySessionFactory() + .withTransaction( session -> session.persistAll( foo, bar, baz ) ) + .call( () -> getMutinySessionFactory().withSession( session -> session + .createSelectionQuery( "from GuineaPig", GuineaPig.class ) + .getResultList().onItem().disjoint() + .invoke( pig -> { + assertThat( pig ).isNotNull(); + i.getAndIncrement(); + } ) + .collect().asList() + .invoke( list -> { + assertThat( i.get() ).isEqualTo( 3 ); + assertThat( list.size() ).isEqualTo( 3 ); + } ) ) ) ); } @Test public void reactiveClose(VertxTestContext context) { - test( context, openMutinySession() - .invoke( session -> assertTrue( session.isOpen() ) ) - .call( Mutiny.Session::close ) - .invoke( session -> assertFalse( session.isOpen() ) ) + test( + context, openMutinySession() + .invoke( session -> assertThat( session.isOpen() ).isTrue() ) + .call( Mutiny.Session::close ) + .invoke( session -> assertThat( session.isOpen() ).isFalse() ) ); } - @Test void testFactory(VertxTestContext context) { - test( context, getMutinySessionFactory().withSession( session -> { - session.getFactory().getCache().evictAll(); - session.getFactory().getMetamodel().entity(GuineaPig.class); - session.getFactory().getCriteriaBuilder().createQuery(GuineaPig.class); - session.getFactory().getStatistics().isStatisticsEnabled(); - return Uni.createFrom().voidItem(); - } ) ); + @Test + void testFactory(VertxTestContext context) { + test( + context, getMutinySessionFactory().withSession( session -> { + session.getFactory().getCache().evictAll(); + session.getFactory().getMetamodel().entity( GuineaPig.class ); + session.getFactory().getCriteriaBuilder().createQuery( GuineaPig.class ); + session.getFactory().getStatistics().isStatisticsEnabled(); + return Uni.createFrom().voidItem(); + } ) + ); } @Test public void testMetamodel() { - EntityType pig = getSessionFactory().getMetamodel().entity(GuineaPig.class); - assertNotNull(pig); - assertEquals( 2, pig.getAttributes().size() ); - assertEquals( "GuineaPig", pig.getName() ); + EntityType pig = getSessionFactory().getMetamodel().entity( GuineaPig.class ); + assertThat( pig ).isNotNull(); + assertThat( pig.getAttributes() ) + .map( Attribute::getName ) + .containsExactlyInAnyOrder( "id", "version", "name" ); + assertThat( pig.getName() ).isEqualTo( "GuineaPig" ); } @Test public void testTransactionPropagation(VertxTestContext context) { - test( context, getMutinySessionFactory().withTransaction( - (session, transaction) -> session.createSelectionQuery("from GuineaPig", GuineaPig.class).getResultList() - .chain( list -> { - assertNotNull( session.currentTransaction() ); - assertFalse( session.currentTransaction().isMarkedForRollback() ); - session.currentTransaction().markForRollback(); - assertTrue( session.currentTransaction().isMarkedForRollback() ); - assertTrue( transaction.isMarkedForRollback() ); - return session.withTransaction( t -> { - assertTrue( t.isMarkedForRollback() ); - return session.createSelectionQuery("from GuineaPig", GuineaPig.class).getResultList(); - } ); - } ) - ) ); + test( + context, getMutinySessionFactory() + .withTransaction( (session, transaction) -> session + .createSelectionQuery( "from GuineaPig", GuineaPig.class ) + .getResultList() + .chain( list -> { + assertThat( session.currentTransaction() ).isNotNull(); + assertThat( session.currentTransaction().isMarkedForRollback() ).isFalse(); + session.currentTransaction().markForRollback(); + assertThat( session.currentTransaction().isMarkedForRollback() ).isTrue(); + assertThat( session.currentTransaction().isMarkedForRollback() ).isTrue(); + assertThat( transaction.isMarkedForRollback() ).isTrue(); + return session.withTransaction( t -> { + assertThat( t.isMarkedForRollback() ).isTrue(); + return session + .createSelectionQuery( "from GuineaPig", GuineaPig.class ) + .getResultList(); + } ); + } ) + ) + ); } @Test public void testSessionPropagation(VertxTestContext context) { - test( context, getMutinySessionFactory().withSession( session -> { - assertFalse( session.isDefaultReadOnly() ); - session.setDefaultReadOnly(true); - return session.createSelectionQuery("from GuineaPig", GuineaPig.class).getResultList() - .chain( list -> getMutinySessionFactory().withSession(s -> { - assertTrue( s.isDefaultReadOnly() ); - return s.createSelectionQuery("from GuineaPig", GuineaPig.class).getResultList(); - } ) ); - } ) ); + test( + context, getMutinySessionFactory() + .withSession( session -> { + assertThat( session.isDefaultReadOnly() ).isFalse(); + session.setDefaultReadOnly( true ); + return session + .createSelectionQuery( "from GuineaPig", GuineaPig.class ) + .getResultList() + .chain( list -> getMutinySessionFactory().withSession( s -> { + assertThat( s.isDefaultReadOnly() ).isTrue(); + return s.createSelectionQuery( "from GuineaPig", GuineaPig.class ) + .getResultList(); + } ) ); + } ) + ); } @Test public void testDupeException(VertxTestContext context) { test( - context, - getMutinySessionFactory() - .withTransaction((s, t) -> s.persist( new GuineaPig( 10, "Tulip" ) )) - .chain(() -> getMutinySessionFactory() - .withTransaction((s, t) -> s.persist( new GuineaPig( 10, "Tulip" ) )) - ).onItemOrFailure().invoke((i, t) -> { - assertNotNull(t); - assertTrue(t instanceof PersistenceException); - }) - .onFailure().recoverWithNull() + // It would make sense to check the error message, but it changes based on the database selected. + // I think this is good enough for now. + context, assertThrown( + ConstraintViolationException.class, getMutinySessionFactory() + .withTransaction( s -> s + .persist( new GuineaPig( 10, "Tulip" ) ) + ) + .chain( () -> getMutinySessionFactory().withTransaction( s -> s + .persist( new GuineaPig( 10, "Tulip" ) ) ) + ) + ) ); } @Test public void testExceptionInWithSession(VertxTestContext context) { final Mutiny.Session[] savedSession = new Mutiny.Session[1]; - test( context, getMutinySessionFactory() - .withSession( session -> { - assertTrue( session.isOpen() ); - savedSession[0] = session; - throw new RuntimeException( "No Panic: This is just a test" ); - } ) - .onItem().invoke( () -> fail( "Test should throw an exception" ) ) - .onFailure() - .recoverWithNull() - .invoke( () -> assertFalse( savedSession[0].isOpen(), "Session should be closed" ) ) + test( + context, assertThrown( + RuntimeException.class, getMutinySessionFactory() + .withSession( session -> { + assertThat( session.isOpen() ).isTrue(); + savedSession[0] = session; + throw new RuntimeException( "No Panic: This is just a test" ); + } ) + ) + .invoke( e -> assertThat( e ).hasMessageContaining( "No Panic:" ) ) + .invoke( () -> assertThat( savedSession[0].isOpen() ) + .as( "Session should be closed" ) + .isFalse() ) ); } @Test public void testExceptionInWithTransaction(VertxTestContext context) { final Mutiny.Session[] savedSession = new Mutiny.Session[1]; - test( context, getMutinySessionFactory() - .withTransaction( (session, tx) -> { - assertTrue( session.isOpen() ); - savedSession[0] = session; - throw new RuntimeException( "No Panic: This is just a test" ); - } ) - .onItem().invoke( () -> fail( "Test should throw an exception" ) ) - .onFailure() - .recoverWithNull() - .invoke( () -> assertFalse( savedSession[0].isOpen(), "Session should be closed" ) ) + test( + context, assertThrown( + RuntimeException.class, getMutinySessionFactory() + .withTransaction( (session, tx) -> { + assertThat( session.isOpen() ).isTrue(); + savedSession[0] = session; + throw new RuntimeException( "No Panic: This is just a test" ); + } ) + ) + .invoke( e -> assertThat( e ).hasMessageContaining( "No Panic:" ) ) + .invoke( () -> assertThat( savedSession[0].isOpen() ) + .as( "Session should be closed" ) + .isFalse() ) ); } @Test public void testExceptionInWithStatelessSession(VertxTestContext context) { final Mutiny.StatelessSession[] savedSession = new Mutiny.StatelessSession[1]; - test( context, getMutinySessionFactory() - .withStatelessSession( session -> { - assertTrue( session.isOpen() ); - savedSession[0] = session; - throw new RuntimeException( "No Panic: This is just a test" ); - } ) - .onItem().invoke( () -> fail( "Test should throw an exception" ) ) - .onFailure() - .recoverWithNull() - .invoke( () -> assertFalse( savedSession[0].isOpen(), "Session should be closed" ) ) + test( + context, assertThrown( + RuntimeException.class, getMutinySessionFactory() + .withStatelessSession( session -> { + assertThat( session.isOpen() ).isTrue(); + savedSession[0] = session; + throw new RuntimeException( "No Panic: This is just a test" ); + } ) + ) + .invoke( e -> assertThat( e ).hasMessageContaining( "No Panic:" ) ) + .invoke( () -> assertThat( savedSession[0].isOpen() ) + .as( "Session should be closed" ) + .isFalse() ) ); } @@ -647,58 +596,64 @@ public void testForceFlushWithDelete(VertxTestContext context) { // Pig1 and Pig2 must have the same id final GuineaPig pig1 = new GuineaPig( 111, "Pig 1" ); final GuineaPig pig2 = new GuineaPig( 111, "Pig 2" ); - - test( context, getMutinySessionFactory() - .withTransaction( session -> session - .persist( pig1 ) - .call( () -> session.remove( pig1 ) ) - // pig 2 has the same id as pig1. - // If pig1 has not been removed from the session, - // we will have a duplicated id exception - .call( () -> session.persist( pig2 ) ) - ) - .chain( () -> getMutinySessionFactory() - .withSession( s -> s.find( GuineaPig.class, pig2.getId() ) ) ) - .invoke( result -> assertThatPigsAreEqual( pig2, result ) ) + test( + context, getMutinySessionFactory() + .withTransaction( session -> session + .persist( pig1 ) + .call( () -> session.remove( pig1 ) ) + // pig 2 has the same id as pig1. + // If pig1 has not been removed from the session, + // we will have a duplicated id exception + .call( () -> session.persist( pig2 ) ) + ) + .chain( () -> getMutinySessionFactory() + .withSession( s -> s.find( GuineaPig.class, pig2.getId() ) ) ) + .invoke( result -> assertThatPigsAreEqual( pig2, result ) ) ); } @Test public void testCurrentSession(VertxTestContext context) { - test( context, - getMutinySessionFactory().withSession(session -> - getMutinySessionFactory().withSession(s -> { - assertEquals(session, s); - Mutiny.Session currentSession = getMutinySessionFactory().getCurrentSession(); - assertNotNull(currentSession); - assertTrue(currentSession.isOpen()); - assertEquals(session, currentSession); - return Uni.createFrom().voidItem(); - }).invoke(() -> assertNotNull(getMutinySessionFactory().getCurrentSession())) - ).invoke(() -> assertNull(getMutinySessionFactory().getCurrentSession())) + test( + context, getMutinySessionFactory() + .withSession( session -> getMutinySessionFactory() + .withSession( s -> { + assertThat( s ).isEqualTo( session ); + Mutiny.Session currentSession = getMutinySessionFactory().getCurrentSession(); + assertThat( currentSession ).isNotNull(); + assertThat( currentSession.isOpen() ).isTrue(); + assertThat( currentSession ).isEqualTo( session ); + return Uni.createFrom().voidItem(); + } ) + .invoke( () -> assertThat( getMutinySessionFactory().getCurrentSession() ).isNotNull() ) + ) + .invoke( () -> assertThat( getMutinySessionFactory().getCurrentSession() ).isNull() ) ); } @Test public void testCurrentStatelessSession(VertxTestContext context) { - test( context, - getMutinySessionFactory().withStatelessSession(session -> - getMutinySessionFactory().withStatelessSession(s -> { - assertEquals(session, s); - Mutiny.StatelessSession currentSession = getMutinySessionFactory().getCurrentStatelessSession(); - assertNotNull(currentSession); - assertTrue(currentSession.isOpen()); - assertEquals(session, currentSession); - return Uni.createFrom().voidItem(); - }).invoke(() -> assertNotNull(getMutinySessionFactory().getCurrentStatelessSession())) - ).invoke(() -> assertNull(getMutinySessionFactory().getCurrentStatelessSession())) + test( + context, getMutinySessionFactory() + .withStatelessSession( session -> getMutinySessionFactory() + .withStatelessSession( s -> { + assertEquals( session, s ); + Mutiny.StatelessSession currentSession = getMutinySessionFactory().getCurrentStatelessSession(); + assertThat( currentSession ).isNotNull(); + assertThat( currentSession.isOpen() ).isTrue(); + assertThat( currentSession ).isEqualTo( session ); + return Uni.createFrom().voidItem(); + } ) + .invoke( () -> assertThat( getMutinySessionFactory().getCurrentStatelessSession() ).isNotNull() ) + ) + .invoke( () -> assertThat( getMutinySessionFactory().getCurrentStatelessSession() ).isNull() ) ); } private void assertThatPigsAreEqual(GuineaPig expected, GuineaPig actual) { - assertNotNull( actual ); - assertEquals( expected.getId(), actual.getId() ); - assertEquals( expected.getName(), actual.getName() ); + assertThat( actual ).isNotNull(); + assertThat( actual.getId() ).isEqualTo( expected.getId() ); + assertThat( actual.getName() ).isEqualTo( expected.getName() ); } @Entity(name = "GuineaPig") @@ -708,6 +663,9 @@ public static class GuineaPig { private Integer id; private String name; + @Version + private int version; + public GuineaPig() { } diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/ParametersProcessorTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/ParametersProcessorTest.java deleted file mode 100644 index ed4b3323c..000000000 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/ParametersProcessorTest.java +++ /dev/null @@ -1,67 +0,0 @@ -/* Hibernate, Relational Persistence for Idiomatic Java - * - * SPDX-License-Identifier: Apache-2.0 - * Copyright: Red Hat Inc. and Hibernate Authors - */ -package org.hibernate.reactive; - -import java.util.stream.Stream; - -import org.hibernate.reactive.pool.impl.OracleParameters; -import org.hibernate.reactive.pool.impl.PostgresParameters; -import org.hibernate.reactive.pool.impl.SQLServerParameters; - -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.Arguments; -import org.junit.jupiter.params.provider.MethodSource; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.params.provider.Arguments.arguments; - -/** - * Test the {@link org.hibernate.reactive.pool.impl.Parameters} processor for each database - */ -public class ParametersProcessorTest { - - /** - * Each test will replace this placeholder with the correct parameter prefix for the selected database - */ - private static final String PARAM_PREFIX = "__paramPrefix__"; - - - /** - * Return the strings to process and the expected result for each one - */ - static Stream expectations() { - return Stream.of( - arguments( "/* One comment */ \\?", "/* One comment */ ?" ), - arguments( "/* One comment */ ?", "/* One comment */ " + PARAM_PREFIX + "1" ), - arguments( "'Sql text ?'", "'Sql text ?'" ), - arguments( "\\?", "?" ), - arguments( "???", PARAM_PREFIX + "1" + PARAM_PREFIX + "2" + PARAM_PREFIX + "3" ), - arguments( "\\?|?", "?|" + PARAM_PREFIX + "1" ), - arguments( " ? ", " " + PARAM_PREFIX + "1 " ) - ); - } - - @ParameterizedTest - @MethodSource("expectations") - public void testPostgreSQLProcessing(String unprocessed, String expected) { - assertThat( PostgresParameters.INSTANCE.process( unprocessed ) ) - .isEqualTo( expected.replaceAll( PARAM_PREFIX, "\\$" ) ); - } - - @ParameterizedTest - @MethodSource("expectations") - public void testSqlServerProcessing(String unprocessed, String expected) { - assertThat( SQLServerParameters.INSTANCE.process( unprocessed ) ) - .isEqualTo( expected.replaceAll( PARAM_PREFIX, "@P" ) ); - } - - @ParameterizedTest - @MethodSource("expectations") - public void testOracleProcessing(String unprocessed, String expected) { - assertThat( OracleParameters.INSTANCE.process( unprocessed ) ) - .isEqualTo( expected.replaceAll( PARAM_PREFIX, ":" ) ); - } -} diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/QueryTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/QueryTest.java index c65c79ee4..1011c0b19 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/QueryTest.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/QueryTest.java @@ -43,6 +43,7 @@ import static jakarta.persistence.FetchType.LAZY; import static java.util.concurrent.TimeUnit.MINUTES; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.groups.Tuple.tuple; import static org.hibernate.reactive.QueryTest.Author.AUTHOR_TABLE; import static org.hibernate.reactive.QueryTest.Author.HQL_NAMED_QUERY; import static org.hibernate.reactive.QueryTest.Author.SQL_NAMED_QUERY; @@ -395,6 +396,42 @@ public void testNativeEntityQueryWithParam(VertxTestContext context) { ); } + // https://github.com/hibernate/hibernate-reactive/issues/2314 + @Test + public void testNativeEntityQueryWithLimit(VertxTestContext context) { + Author author1 = new Author( "Iain M. Banks" ); + Author author2 = new Author( "Neal Stephenson" ); + Book book1 = new Book( "1-85723-235-6", "Feersum Endjinn", author1 ); + Book book2 = new Book( "0-380-97346-4", "Cryptonomicon", author2 ); + Book book3 = new Book( "0-553-08853-X", "Snow Crash", author2 ); + author1.books.add( book1 ); + author2.books.add( book2 ); + author2.books.add( book3 ); + + test( + context, + openSession() + .thenCompose( session -> session.persist( author1, author2 ) + .thenCompose( v -> session.flush() ) + ) + .thenCompose( v -> openSession() ) + .thenCompose( session -> session.createNativeQuery( + "select * from " + BOOK_TABLE + " order by isbn", + Book.class + ) + .setMaxResults( 2 ) + .getResultList() ) + .thenAccept( books -> { + assertThat( books ) + .extracting( b -> b.id, b -> b.title, b -> b.isbn ) + .containsExactly( + tuple( book2.id, book2.title, book2.isbn ), + tuple( book3.id, book3.title, book3.isbn ) + ); + } ) + ); + } + @Test public void testNativeEntityQueryWithNamedParam(VertxTestContext context) { Author author1 = new Author( "Iain M. Banks" ); diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/ReactiveMultitenantNoResolverTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/ReactiveMultitenantNoResolverTest.java index 81f3469cb..e3ad22d3d 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/ReactiveMultitenantNoResolverTest.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/ReactiveMultitenantNoResolverTest.java @@ -69,27 +69,28 @@ protected Configuration constructConfiguration() { @Test public void reactivePersistFindDelete(VertxTestContext context) { final GuineaPig guineaPig = new GuineaPig( 5, "Aloi" ); - test( - context, - getSessionFactory().openSession( DEFAULT.name() ) - .thenCompose( session -> session - .persist( guineaPig ) - .thenCompose( v -> session.flush() ) - .thenAccept( v -> session.detach( guineaPig ) ) - .thenAccept( v -> assertFalse( session.contains( guineaPig ) ) ) - .thenCompose( v -> session.find( GuineaPig.class, guineaPig.getId() ) ) - .thenAccept( actualPig -> { - assertThatPigsAreEqual( guineaPig, actualPig ); - assertTrue( session.contains( actualPig ) ); - assertFalse( session.contains( guineaPig ) ); - assertEquals( LockMode.READ, session.getLockMode( actualPig ) ); - session.detach( actualPig ); - assertFalse( session.contains( actualPig ) ); - } ) - .thenCompose( v -> session.find( GuineaPig.class, guineaPig.getId() ) ) - .thenCompose( session::remove ) - .thenCompose( v -> session.flush() ) - .thenCompose( v -> session.close() ) ) + test( context, getSessionFactory() + .openSession( DEFAULT.name() ) + .thenCompose( session -> session + .withTransaction( t -> session + .persist( guineaPig ) + .thenCompose( v -> session.flush() ) + .thenAccept( v -> session.detach( guineaPig ) ) + .thenAccept( v -> assertFalse( session.contains( guineaPig ) ) ) + .thenCompose( v -> session.find( GuineaPig.class, guineaPig.getId() ) ) + .thenAccept( actualPig -> { + assertThatPigsAreEqual( guineaPig, actualPig ); + assertTrue( session.contains( actualPig ) ); + assertFalse( session.contains( guineaPig ) ); + assertEquals( LockMode.READ, session.getLockMode( actualPig ) ); + session.detach( actualPig ); + assertFalse( session.contains( actualPig ) ); + } ) + .thenCompose( v -> session.find( GuineaPig.class, guineaPig.getId() ) ) + .thenCompose( session::remove ) + ) + .thenCompose( v -> session.close() ) + ) ); } diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/ReactiveMultitenantTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/ReactiveMultitenantTest.java index 84e7e92c9..15e41716a 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/ReactiveMultitenantTest.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/ReactiveMultitenantTest.java @@ -10,8 +10,9 @@ import org.hibernate.LockMode; import org.hibernate.cfg.AvailableSettings; import org.hibernate.cfg.Configuration; -import org.hibernate.reactive.provider.Settings; import org.hibernate.reactive.annotations.EnabledFor; +import org.hibernate.reactive.provider.Settings; +import org.hibernate.reactive.stage.Stage; import org.junit.jupiter.api.Test; @@ -23,14 +24,11 @@ import jakarta.persistence.Version; import static java.util.concurrent.TimeUnit.MINUTES; +import static org.assertj.core.api.Assertions.assertThat; import static org.hibernate.reactive.MyCurrentTenantIdentifierResolver.Tenant.DEFAULT; import static org.hibernate.reactive.MyCurrentTenantIdentifierResolver.Tenant.TENANT_1; import static org.hibernate.reactive.MyCurrentTenantIdentifierResolver.Tenant.TENANT_2; import static org.hibernate.reactive.containers.DatabaseConfiguration.DBType.POSTGRESQL; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; /** * This class creates multiple additional databases so that we can check that queries run @@ -46,10 +44,8 @@ public class ReactiveMultitenantTest extends BaseReactiveTest { protected Configuration constructConfiguration() { Configuration configuration = super.constructConfiguration(); configuration.addAnnotatedClass( GuineaPig.class ); - configuration.setProperty( - AvailableSettings.MULTI_TENANT_CONNECTION_PROVIDER, - "anything" - );//FIXME this is terrible? + // FIXME this is terrible? + configuration.setProperty( AvailableSettings.MULTI_TENANT_CONNECTION_PROVIDER, "anything" ); configuration.getProperties().put( Settings.MULTI_TENANT_IDENTIFIER_RESOLVER, TENANT_RESOLVER ); // Contains the SQL scripts for the creation of the additional databases configuration.setProperty( Settings.HBM2DDL_IMPORT_FILES, "/multitenancy-test.sql" ); @@ -61,25 +57,27 @@ protected Configuration constructConfiguration() { public void reactivePersistFindDelete(VertxTestContext context) { TENANT_RESOLVER.setTenantIdentifier( DEFAULT ); final GuineaPig guineaPig = new GuineaPig( 5, "Aloi" ); - test( - context, - getSessionFactory().openSession().thenCompose( session -> session + test( context, openSession() + .thenCompose( session -> session .persist( guineaPig ) .thenCompose( v -> session.flush() ) .thenAccept( v -> session.detach( guineaPig ) ) - .thenAccept( v -> assertFalse( session.contains( guineaPig ) ) ) + .thenAccept( v -> assertThat( session.contains( guineaPig ) ).isFalse() ) .thenCompose( v -> session.find( GuineaPig.class, guineaPig.getId() ) ) .thenAccept( actualPig -> { - assertThatPigsAreEqual( guineaPig, actualPig ); - assertTrue( session.contains( actualPig ) ); - assertFalse( session.contains( guineaPig ) ); - assertEquals( LockMode.READ, session.getLockMode( actualPig ) ); + assertThat( actualPig ).isNotNull(); + assertThat( actualPig.getId() ).isEqualTo( guineaPig.getId() ); + assertThat( actualPig.getName() ).isEqualTo( guineaPig.getName() ); + assertThat( session.contains( actualPig ) ).isTrue(); + assertThat( session.contains( guineaPig ) ).isFalse(); + assertThat( session.getLockMode( actualPig ) ).isEqualTo( LockMode.READ ); session.detach( actualPig ); - assertFalse( session.contains( actualPig ) ); + assertThat( session.contains( actualPig ) ).isFalse(); } ) .thenCompose( v -> session.find( GuineaPig.class, guineaPig.getId() ) ) .thenCompose( session::remove ) - .thenCompose( v -> session.flush() ) ) + .thenCompose( v -> session.flush() ) + ) ); } @@ -90,13 +88,34 @@ public void testTenantSelection(VertxTestContext context) { .thenCompose( session -> session .createNativeQuery( "select current_database()" ) .getSingleResult() - .thenAccept( result -> assertEquals( TENANT_1.getDbName(), result ) ) ) + .thenAccept( result -> assertThat( result ).isEqualTo( TENANT_1.getDbName() ) ) ) .thenAccept( unused -> TENANT_RESOLVER.setTenantIdentifier( TENANT_2 ) ) .thenCompose( unused -> openSession() ) .thenCompose( session -> session .createNativeQuery( "select current_database()" ) .getSingleResult() - .thenAccept( result -> assertEquals( TENANT_2.getDbName(), result ) ) ) + .thenAccept( result -> assertThat( result ).isEqualTo( TENANT_2.getDbName() ) ) ) + ); + } + + @Test + public void testTenantSelectionWithProxy(VertxTestContext context) { + TENANT_RESOLVER.setTenantIdentifier( TENANT_1 ); + Stage.Session t1Session = getSessionFactory().createSession(); + test( + context, t1Session + .createNativeQuery( "select current_database()" ) + .getSingleResult() + .thenAccept( result -> assertThat( result ).isEqualTo( TENANT_1.getDbName() ) ) + .thenCompose( v -> t1Session.close() ) + .thenAccept( v -> TENANT_RESOLVER.setTenantIdentifier( TENANT_2 ) ) + .thenApply( v -> getSessionFactory().createSession() ) + .thenCompose( t2Session -> t2Session + .createNativeQuery( "select current_database()" ) + .getSingleResult() + .thenAccept( result -> assertThat( result ).isEqualTo( TENANT_2.getDbName() ) ) + .thenCompose( v -> t2Session.close() ) + ) ); } @@ -107,14 +126,14 @@ public void testTenantSelectionStatelessSession(VertxTestContext context) { .thenCompose( t1Session -> t1Session .createNativeQuery( "select current_database()" ) .getSingleResult() - .thenAccept( result -> assertEquals( TENANT_1.getDbName(), result ) ) + .thenAccept( result -> assertThat( result ).isEqualTo( TENANT_1.getDbName() ) ) .thenCompose( unused -> t1Session.close() ) ) .thenAccept( unused -> TENANT_RESOLVER.setTenantIdentifier( TENANT_2 ) ) .thenCompose( v -> getSessionFactory().openStatelessSession() ) .thenCompose( t2Session -> t2Session .createNativeQuery( "select current_database()" ) .getSingleResult() - .thenAccept( result -> assertEquals( TENANT_2.getDbName(), result ) ) + .thenAccept( result -> assertThat( result ).isEqualTo( TENANT_2.getDbName() ) ) .thenCompose( v -> t2Session.close() ) ) ); } @@ -122,26 +141,21 @@ public void testTenantSelectionStatelessSession(VertxTestContext context) { @Test public void testTenantSelectionStatelessSessionMutiny(VertxTestContext context) { TENANT_RESOLVER.setTenantIdentifier( TENANT_1 ); - test( context, getMutinySessionFactory().withStatelessSession( t1Session -> - t1Session - .createNativeQuery( "select current_database()" ) - .getSingleResult() - .invoke( result -> assertEquals( TENANT_1.getDbName(), result ) ) - ) + test( context, getMutinySessionFactory() + .withStatelessSession( t1Session -> t1Session + .createNativeQuery( "select current_database()" ) + .getSingleResult() + .invoke( result -> assertThat( result ).isEqualTo( TENANT_1.getDbName() ) ) + ) .invoke( result -> TENANT_RESOLVER.setTenantIdentifier( TENANT_2 ) ) .chain( () -> getMutinySessionFactory().withStatelessSession( t2Session -> t2Session .createNativeQuery( "select current_database()" ) .getSingleResult() - .invoke( result -> assertEquals( TENANT_2.getDbName(), result ) ) ) ) + .invoke( result -> assertThat( result ).isEqualTo( TENANT_2.getDbName() ) ) + ) ) ); } - private void assertThatPigsAreEqual( GuineaPig expected, GuineaPig actual) { - assertNotNull( actual ); - assertEquals( expected.getId(), actual.getId() ); - assertEquals( expected.getName(), actual.getName() ); - } - @Entity(name = "GuineaPig") @Table(name = "Pig") public static class GuineaPig { diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/ReactiveSessionTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/ReactiveSessionTest.java index ed0c724f1..d83cb64cd 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/ReactiveSessionTest.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/ReactiveSessionTest.java @@ -10,13 +10,17 @@ import java.util.concurrent.CompletionException; import java.util.concurrent.CompletionStage; +import org.hibernate.HibernateException; import org.hibernate.LockMode; import org.hibernate.reactive.common.AffectedEntities; +import org.hibernate.reactive.mutiny.Mutiny; +import org.hibernate.reactive.mutiny.impl.MutinySessionImpl; +import org.hibernate.reactive.mutiny.impl.MutinyStatelessSessionImpl; +import org.hibernate.reactive.pool.ReactiveConnection; import org.hibernate.reactive.stage.Stage; +import org.hibernate.reactive.stage.impl.StageSessionImpl; +import org.hibernate.reactive.stage.impl.StageStatelessSessionImpl; -import org.hibernate.reactive.util.impl.CompletionStages; - -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; @@ -32,10 +36,10 @@ import static java.util.concurrent.TimeUnit.MINUTES; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.*; +import static org.hibernate.reactive.testing.ReactiveAssertions.assertThrown; +import static org.hibernate.reactive.util.impl.CompletionStages.voidFuture; @Timeout(value = 10, timeUnit = MINUTES) - public class ReactiveSessionTest extends BaseReactiveTest { @Override @@ -57,6 +61,104 @@ private CompletionStage selectNameFromId(Integer id) { ); } + @Test + public void reactivePersistFindRemoveWithSessionProxy(VertxTestContext context) { + final GuineaPig guineaPig = new GuineaPig( 5, "Aloi" ); + Stage.Session session = getSessionFactory().createSession(); + assertConnectionIsLazy( ( (StageSessionImpl) session ).getReactiveConnection() ); + session.setBatchSize( 55 ); + + test( context, session + .persist( guineaPig ) + .thenCompose( v -> session.flush() ) + .thenAccept( v -> session.detach( guineaPig ) ) + .thenAccept( v -> assertThat( session.contains( guineaPig ) ).isFalse() ) + .thenCompose( v -> session.find( GuineaPig.class, guineaPig.getId() ) ) + .thenAccept( actualPig -> { + assertThatPigsAreEqual( guineaPig, actualPig ); + assertThat( session.contains( actualPig ) ).isTrue(); + assertThat( session.contains( guineaPig ) ).isFalse(); + assertThat( session.getLockMode( actualPig ) ).isEqualTo( LockMode.READ ); + assertThat( session.getBatchSize() ).isEqualTo( 55 ); + session.detach( actualPig ); + assertThat( session.contains( actualPig ) ).isFalse(); + } ) + .thenCompose( v -> session.find( GuineaPig.class, guineaPig.getId() ) ) + .thenCompose( session::remove ) + .thenCompose( v -> session.flush() ) + .thenCompose( v -> session.close() ) + ); + } + + @Test + public void reactiveInsertGetDeleteWithStatelessSessionProxy(VertxTestContext context) { + final GuineaPig guineaPig = new GuineaPig( 5, "Aloi" ); + Stage.StatelessSession session = getSessionFactory().createStatelessSession(); + assertConnectionIsLazy( ( (StageStatelessSessionImpl) session ).getReactiveConnection(), true ); + test( context, session + .insert( guineaPig ) + .thenCompose( v -> session.get( GuineaPig.class, guineaPig.getId() ) ) + .thenAccept( actualPig -> assertThatPigsAreEqual( guineaPig, actualPig ) ) + .thenCompose( v -> session.get( GuineaPig.class, guineaPig.getId() ) ) + .thenCompose( session::delete ) + .thenCompose( v -> session.close() ) + ); + } + + @Test + public void reactivePersistFindRemoveWithSessionProxyAndMutiny(VertxTestContext context) { + final GuineaPig guineaPig = new GuineaPig( 5, "Aloi" ); + Mutiny.Session session = getMutinySessionFactory().createSession(); + assertConnectionIsLazy( ( (MutinySessionImpl) session ).getReactiveConnection() ); + session.setBatchSize( 55 ); + test( context, session + .persist( guineaPig ) + .call( session::flush ) + .chain( () -> { + session.detach( guineaPig ); + assertThat( session.contains( guineaPig ) ).isFalse(); + return session.find( GuineaPig.class, guineaPig.getId() ); + } ) + .chain( actualPig -> { + assertThatPigsAreEqual( guineaPig, actualPig ); + assertThat( session.contains( actualPig ) ).isTrue(); + assertThat( session.contains( guineaPig ) ).isFalse(); + assertThat( session.getLockMode( actualPig ) ).isEqualTo( LockMode.READ ); + assertThat( session.getBatchSize() ).isEqualTo( 55 ); + session.detach( actualPig ); + assertThat( session.contains( actualPig ) ).isFalse(); + return session.find( GuineaPig.class, guineaPig.getId() ); + } ) + .chain( session::remove ) + .call( session::flush ) + .eventually( session::close ) + ); + } + + protected void assertConnectionIsLazy(ReactiveConnection connection, boolean stateless) { + assertConnectionIsLazy( connection ); + } + + protected void assertConnectionIsLazy(ReactiveConnection connection) { + assertThat( connection.getClass().getName() ) + .isEqualTo( org.hibernate.reactive.pool.impl.SqlClientPool.class.getName() + "$ProxyConnection" ); + } + + @Test + public void reactiveInsertGetDeleteWithStatelessSessionProxyAndMutiny(VertxTestContext context) { + final GuineaPig guineaPig = new GuineaPig( 5, "Aloi" ); + Mutiny.StatelessSession session = getMutinySessionFactory().createStatelessSession(); + assertConnectionIsLazy( ( (MutinyStatelessSessionImpl) session ).getReactiveConnection(), true ); + test( context, session + .insert( guineaPig ) + .chain( () -> session.get( GuineaPig.class, guineaPig.getId() ) ) + .invoke( actualPig -> assertThatPigsAreEqual( guineaPig, actualPig ) ) + .chain( () -> session.get( GuineaPig.class, guineaPig.getId() ) ) + .call( session::delete ) + .eventually( session::close ) + ); + } + @Test public void reactiveFind(VertxTestContext context) { final GuineaPig expectedPig = new GuineaPig( 5, "Aloi" ); @@ -67,11 +169,11 @@ public void reactiveFind(VertxTestContext context) { .thenCompose( session -> session.find( GuineaPig.class, expectedPig.getId() ) .thenAccept( actualPig -> { assertThatPigsAreEqual( expectedPig, actualPig ); - assertTrue( session.contains( actualPig ) ); - assertFalse( session.contains( expectedPig ) ); - assertEquals( LockMode.READ, session.getLockMode( actualPig ) ); + assertThat( session.contains( actualPig ) ).isTrue(); + assertThat( session.contains( expectedPig ) ).isFalse(); + assertThat( session.getLockMode( actualPig ) ).isEqualTo( LockMode.READ ); session.detach( actualPig ); - assertFalse( session.contains( actualPig ) ); + assertThat( session.contains( actualPig ) ).isFalse(); } ) ) ); @@ -87,9 +189,7 @@ context, populateDB() .thenCompose( v -> getSessionFactory().withTransaction( s -> s .find( GuineaPig.class, emma.getId(), rump.getId() ) ) ) - .thenAccept( pigs -> { - org.assertj.core.api.Assertions.assertThat( pigs ).containsExactlyInAnyOrder( emma, rump ); - } ) + .thenAccept( pigs -> assertThat( pigs ).containsExactlyInAnyOrder( emma, rump ) ) ); } @@ -145,15 +245,15 @@ public void reactivePersistFindDelete(VertxTestContext context) { .persist( guineaPig ) .thenCompose( v -> session.flush() ) .thenAccept( v -> session.detach( guineaPig ) ) - .thenAccept( v -> assertFalse( session.contains( guineaPig ) ) ) + .thenAccept( v -> assertThat( session.contains( guineaPig ) ).isFalse() ) .thenCompose( v -> session.find( GuineaPig.class, guineaPig.getId() ) ) .thenAccept( actualPig -> { assertThatPigsAreEqual( guineaPig, actualPig ); - assertTrue( session.contains( actualPig ) ); - assertFalse( session.contains( guineaPig ) ); - assertEquals( LockMode.READ, session.getLockMode( actualPig ) ); + assertThat( session.contains( actualPig ) ).isTrue(); + assertThat( session.contains( guineaPig ) ).isFalse(); + assertThat( session.getLockMode( actualPig ) ).isEqualTo( LockMode.READ ); session.detach( actualPig ); - assertFalse( session.contains( actualPig ) ); + assertThat( session.contains( actualPig ) ).isFalse(); } ) .thenCompose( v -> session.find( GuineaPig.class, guineaPig.getId() ) ) .thenCompose( session::remove ) @@ -171,7 +271,7 @@ context, populateDB().thenCompose( v -> getSessionFactory() .find( GuineaPig.class, expectedPig.getId(), LockMode.PESSIMISTIC_WRITE ) .thenAccept( actualPig -> { assertThatPigsAreEqual( expectedPig, actualPig ); - assertEquals( session.getLockMode( actualPig ), LockMode.PESSIMISTIC_WRITE ); + assertThat( session.getLockMode( actualPig ) ).isEqualTo( LockMode.PESSIMISTIC_WRITE ); } ) ) ) ); @@ -189,10 +289,7 @@ context, populateDB() .refresh( pig, LockMode.PESSIMISTIC_WRITE ) .thenAccept( vv -> { assertThatPigsAreEqual( expectedPig, pig ); - assertEquals( - session.getLockMode( pig ), - LockMode.PESSIMISTIC_WRITE - ); + assertThat( session.getLockMode( pig ) ).isEqualTo( LockMode.PESSIMISTIC_WRITE ); } ) ) ) ) @@ -213,8 +310,8 @@ public void reactiveFindReadOnlyRefreshWithLock(VertxTestContext context) { return session.flush() .thenCompose( v -> session.refresh( pig ) ) .thenAccept( v -> { - assertEquals( expectedPig.name, pig.name ); - assertTrue( session.isReadOnly( pig ) ); + assertThat( expectedPig.getName() ).isEqualTo( pig.getName() ); + assertThat( session.isReadOnly( pig ) ).isTrue(); } ); } ) ) @@ -226,8 +323,8 @@ public void reactiveFindReadOnlyRefreshWithLock(VertxTestContext context) { return session.flush() .thenCompose( v -> session.refresh( pig ) ) .thenAccept( v -> { - assertEquals( "XXXX", pig.name ); - assertFalse( session.isReadOnly( pig ) ); + assertThat( "XXXX" ).isEqualTo( pig.getName() ); + assertThat( session.isReadOnly( pig ) ).isFalse(); } ); } ) ) @@ -246,10 +343,7 @@ context, populateDB() .lock( pig, LockMode.PESSIMISTIC_READ ) .thenAccept( v -> { assertThatPigsAreEqual( expectedPig, pig ); - assertEquals( - session.getLockMode( pig ), - LockMode.PESSIMISTIC_READ - ); + assertThat( session.getLockMode( pig ) ).isEqualTo( LockMode.PESSIMISTIC_READ ); } ) ) ) @@ -268,8 +362,8 @@ context, populateDB().thenCompose( v -> getSessionFactory() .lock( pig, LockMode.PESSIMISTIC_WRITE ) .thenAccept( vv -> { assertThatPigsAreEqual( expectedPig, pig ); - assertEquals( session.getLockMode( pig ), LockMode.PESSIMISTIC_WRITE ); - assertEquals( pig.version, 0 ); + assertThat( session.getLockMode( pig ) ).isEqualTo( LockMode.PESSIMISTIC_WRITE ); + assertThat( pig.version ).isEqualTo( 0 ); } ) ) ) ) @@ -288,15 +382,12 @@ public void reactiveFindThenForceLock(VertxTestContext context) { .thenApply( v -> pig ) ) .thenAccept( actualPig -> { assertThatPigsAreEqual( expectedPig, actualPig ); - assertEquals( - session.getLockMode( actualPig ), - LockMode.PESSIMISTIC_FORCE_INCREMENT - ); - assertEquals( actualPig.version, 1 ); + assertThat( session.getLockMode( actualPig ) ).isEqualTo( LockMode.PESSIMISTIC_FORCE_INCREMENT ); + assertThat( actualPig.version ).isEqualTo( 1 ); } ) .thenCompose( v -> session.createSelectionQuery( "select version from GuineaPig", Integer.class ) .getSingleResult() ) - .thenAccept( version -> assertEquals( 1, version ) ) + .thenAccept( version -> assertThat( version ).isEqualTo( 1 ) ) ) .thenCompose( v -> openSession() ) .thenCompose( session -> session.find( GuineaPig.class, expectedPig.getId() ) @@ -304,16 +395,13 @@ public void reactiveFindThenForceLock(VertxTestContext context) { .thenApply( v -> pig ) ) .thenAccept( actualPig -> { assertThatPigsAreEqual( expectedPig, actualPig ); - assertEquals( - session.getLockMode( actualPig ), - LockMode.PESSIMISTIC_FORCE_INCREMENT - ); - assertEquals( actualPig.version, 2 ); + assertThat( session.getLockMode( actualPig ) ).isEqualTo( LockMode.PESSIMISTIC_FORCE_INCREMENT ); + assertThat( actualPig.version ).isEqualTo( 2 ); } ) .thenCompose( v -> session .createSelectionQuery( "select version from GuineaPig", Integer.class ) .getSingleResult() ) - .thenAccept( version -> assertEquals( 2, version ) ) + .thenAccept( version -> assertThat( version ).isEqualTo( 2 ) ) ) ); } @@ -328,70 +416,55 @@ public void reactiveFindWithPessimisticIncrementLock(VertxTestContext context) { .withTransaction( session -> session.find( GuineaPig.class, expectedPig.getId(), LockMode.PESSIMISTIC_FORCE_INCREMENT ) .thenAccept( actualPig -> { assertThatPigsAreEqual( expectedPig, actualPig ); - assertEquals( LockMode.PESSIMISTIC_FORCE_INCREMENT, session.getLockMode( actualPig ) ); // grrr, lame - assertEquals( 1, actualPig.version ); + assertThat( session.getLockMode( actualPig ) ).isEqualTo( LockMode.PESSIMISTIC_FORCE_INCREMENT ); + assertThat( actualPig.version ).isEqualTo( 1 ); } ) ) ) .thenCompose( v -> openSession() ) .thenCompose( session -> session.find( GuineaPig.class, expectedPig.getId() ) ) - .thenAccept( actualPig -> assertEquals( 1, actualPig.version ) ) + .thenAccept( actualPig -> assertThat( actualPig.version ).isEqualTo( 1 ) ) ); } @Test public void reactiveFindWithOptimisticIncrementLock(VertxTestContext context) { final GuineaPig expectedPig = new GuineaPig( 5, "Aloi" ); - test( - context, - populateDB() - .thenCompose( v -> getSessionFactory().withTransaction( - (session, transaction) -> session.find( - GuineaPig.class, - expectedPig.getId(), - LockMode.OPTIMISTIC_FORCE_INCREMENT - ) - .thenAccept( actualPig -> { - assertThatPigsAreEqual( expectedPig, actualPig ); - assertEquals( - LockMode.OPTIMISTIC_FORCE_INCREMENT, - session.getLockMode( actualPig ) - ); - assertEquals( 0, actualPig.version ); - } ) - ) - ) - .thenCompose( v -> openSession() ) - .thenCompose( session -> session.find( GuineaPig.class, expectedPig.getId() ) ) - .thenAccept( actualPig -> assertEquals( 1, actualPig.version ) ) + test( context, populateDB() + .thenCompose( v -> getSessionFactory().withTransaction( session -> session + .find( GuineaPig.class, expectedPig.getId(), LockMode.OPTIMISTIC_FORCE_INCREMENT ) + .thenAccept( actualPig -> { + assertThatPigsAreEqual( expectedPig, actualPig ); + assertThat( session.getLockMode( actualPig ) ).isEqualTo( LockMode.OPTIMISTIC_FORCE_INCREMENT ); + assertThat( actualPig.version ).isEqualTo( 0 ); + } ) + ) + ) + .thenCompose( v -> openSession() ) + .thenCompose( session -> session.find( GuineaPig.class, expectedPig.getId() ) ) + .thenAccept( actualPig -> assertThat( actualPig.version ).isEqualTo( 1 ) ) ); } @Test public void reactiveLockWithOptimisticIncrement(VertxTestContext context) { final GuineaPig expectedPig = new GuineaPig( 5, "Aloi" ); - test( - context, - populateDB() - .thenCompose( v -> getSessionFactory().withTransaction( - (session, transaction) -> session.find( GuineaPig.class, expectedPig.getId() ) - .thenCompose( actualPig -> session.lock( - actualPig, - LockMode.OPTIMISTIC_FORCE_INCREMENT - ) - .thenAccept( vv -> { - assertThatPigsAreEqual( expectedPig, actualPig ); - assertEquals( - session.getLockMode( actualPig ), - LockMode.OPTIMISTIC_FORCE_INCREMENT - ); - assertEquals( 0, actualPig.version ); - } ) - ) - ) + test( context, populateDB() + .thenCompose( v -> getSessionFactory() + .withTransaction( session -> session + .find( GuineaPig.class, expectedPig.getId() ) + .thenCompose( actualPig -> session + .lock( actualPig, LockMode.OPTIMISTIC_FORCE_INCREMENT ) + .thenAccept( vv -> { + assertThatPigsAreEqual( expectedPig, actualPig ); + assertThat( session.getLockMode( actualPig ) ).isEqualTo( LockMode.OPTIMISTIC_FORCE_INCREMENT ); + assertThat( actualPig.version ).isEqualTo( 0 ); + } ) + ) ) - .thenCompose( v -> openSession() ) - .thenCompose( session -> session.find( GuineaPig.class, expectedPig.getId() ) ) - .thenAccept( actualPig -> assertEquals( 1, actualPig.version ) ) + ) + .thenCompose( v -> openSession() ) + .thenCompose( session -> session.find( GuineaPig.class, expectedPig.getId() ) ) + .thenAccept( actualPig -> assertThat( actualPig.version ).isEqualTo( 1 ) ) ); } @@ -409,18 +482,15 @@ public void reactiveLockWithIncrement(VertxTestContext context) { ) .thenAccept( vv -> { assertThatPigsAreEqual( expectedPig, actualPig ); - assertEquals( - session.getLockMode( actualPig ), - LockMode.PESSIMISTIC_FORCE_INCREMENT - ); - assertEquals( 1, actualPig.version ); + assertThat( session.getLockMode( actualPig ) ).isEqualTo( LockMode.PESSIMISTIC_FORCE_INCREMENT ); + assertThat( actualPig.version ).isEqualTo( 1 ); } ) ) ) ) .thenCompose( v -> openSession() ) .thenCompose( session -> session.find( GuineaPig.class, expectedPig.getId() ) ) - .thenAccept( actualPig -> assertEquals( 1, actualPig.version ) ) + .thenAccept( actualPig -> assertThat( actualPig.version ).isEqualTo( 1 ) ) ); } @@ -435,12 +505,12 @@ public void reactiveFindWithOptimisticVerifyLock(VertxTestContext context) { .find( GuineaPig.class, expectedPig.getId(), LockMode.OPTIMISTIC ) .thenAccept( actualPig -> { assertThatPigsAreEqual( expectedPig, actualPig ); - assertEquals( LockMode.OPTIMISTIC, session.getLockMode( actualPig ) ); - assertEquals( 0, actualPig.version ); + assertThat( session.getLockMode( actualPig ) ).isEqualTo( LockMode.OPTIMISTIC ); + assertThat( actualPig.version ).isEqualTo( 0 ); } ) ) ) .thenCompose( v -> openSession() ) .thenCompose( session -> session.find( GuineaPig.class, expectedPig.getId() ) ) - .thenAccept( actualPig -> assertEquals( 0, actualPig.version ) ) + .thenAccept( actualPig -> assertThat( actualPig.version ).isEqualTo( 0 ) ) ); } @@ -455,12 +525,12 @@ public void reactiveLockWithOptimisticVerify(VertxTestContext context) { .thenCompose( actualPig -> session.lock( actualPig, LockMode.OPTIMISTIC ) .thenAccept( vv -> { assertThatPigsAreEqual( expectedPig, actualPig ); - assertEquals( LockMode.OPTIMISTIC, session.getLockMode( actualPig ) ); - assertEquals( 0, actualPig.version ); + assertThat( session.getLockMode( actualPig ) ).isEqualTo( LockMode.OPTIMISTIC ); + assertThat( actualPig.version ).isEqualTo( 0 ); } ) ) ) ) .thenCompose( v -> openSession() ) .thenCompose( session -> session.find( GuineaPig.class, expectedPig.getId() ) ) - .thenAccept( actualPig -> assertEquals( 0, actualPig.version ) ) + .thenAccept( actualPig -> assertThat( actualPig.version ).isEqualTo( 0 ) ) ); } @@ -476,12 +546,12 @@ public void reactiveFindWithPessimisticRead(VertxTestContext context) { .find( GuineaPig.class, expectedPig.getId(), LockMode.PESSIMISTIC_READ ) .thenAccept( actualPig -> { assertThatPigsAreEqual( expectedPig, actualPig ); - assertEquals( session.getLockMode( actualPig ), LockMode.PESSIMISTIC_READ ); - assertEquals( 0, actualPig.version ); + assertThat( session.getLockMode( actualPig ) ).isEqualTo( LockMode.PESSIMISTIC_READ ); + assertThat( actualPig.version ).isEqualTo( 0 ); } ) ) ) .thenCompose( v -> openSession() ) .thenCompose( session -> session.find( GuineaPig.class, expectedPig.getId() ) ) - .thenAccept( actualPig -> assertEquals( 0, actualPig.version ) ) + .thenAccept( actualPig -> assertThat( actualPig.version ).isEqualTo( 0 ) ) ); } @@ -498,12 +568,12 @@ public void reactiveLockWithPessimisticRead(VertxTestContext context) { .thenCompose( actualPig -> session.lock( actualPig, LockMode.PESSIMISTIC_READ ) .thenAccept( vv -> { assertThatPigsAreEqual( expectedPig, actualPig ); - assertEquals( LockMode.PESSIMISTIC_READ, session.getLockMode( actualPig ) ); - assertEquals( 0, actualPig.version ); + assertThat( session.getLockMode( actualPig ) ).isEqualTo( LockMode.PESSIMISTIC_READ ); + assertThat( actualPig.version ).isEqualTo( 0 ); } ) ) ) ) .thenCompose( v -> openSession() ) .thenCompose( session -> session.find( GuineaPig.class, expectedPig.getId() ) ) - .thenAccept( actualPig -> assertEquals( 0, actualPig.version ) ) + .thenAccept( actualPig -> assertThat( actualPig.version ).isEqualTo( 0 ) ) ); } @@ -519,12 +589,12 @@ public void reactiveFindWithPessimisticWrite(VertxTestContext context) { .find( GuineaPig.class, expectedPig.getId(), LockMode.PESSIMISTIC_WRITE ) .thenAccept( actualPig -> { assertThatPigsAreEqual( expectedPig, actualPig ); - assertEquals( LockMode.PESSIMISTIC_WRITE, session.getLockMode( actualPig ) ); - assertEquals( 0, actualPig.version ); + assertThat( session.getLockMode( actualPig ) ).isEqualTo( LockMode.PESSIMISTIC_WRITE ); + assertThat( actualPig.version ).isEqualTo( 0 ); } ) ) ) .thenCompose( v -> openSession() ) .thenCompose( session -> session.find( GuineaPig.class, expectedPig.getId() ) ) - .thenAccept( actualPig -> assertEquals( 0, actualPig.version ) ) + .thenAccept( actualPig -> assertThat( actualPig.version ).isEqualTo( 0 ) ) ); } @@ -541,12 +611,12 @@ public void reactiveLockWithPessimisticWrite(VertxTestContext context) { .thenCompose( actualPig -> session.lock( actualPig, LockMode.PESSIMISTIC_WRITE ) .thenAccept( vv -> { assertThatPigsAreEqual( expectedPig, actualPig ); - assertEquals( LockMode.PESSIMISTIC_WRITE, session.getLockMode( actualPig ) ); - assertEquals( 0, actualPig.version ); + assertThat( session.getLockMode( actualPig ) ).isEqualTo( LockMode.PESSIMISTIC_WRITE ); + assertThat( actualPig.version ).isEqualTo( 0 ); } ) ) ) ) .thenCompose( v -> openSession() ) .thenCompose( session -> session.find( GuineaPig.class, expectedPig.getId() ) ) - .thenAccept( actualPig -> assertEquals( 0, actualPig.version ) ) + .thenAccept( actualPig -> assertThat( actualPig.version ).isEqualTo( 0 ) ) ); } @@ -563,7 +633,7 @@ public void reactiveQueryWithLock(VertxTestContext context) { .getSingleResult() .thenAccept( actualPig -> { assertThatPigsAreEqual( expectedPig, actualPig ); - assertEquals( LockMode.PESSIMISTIC_WRITE, session.getLockMode( actualPig ) ); + assertThat( session.getLockMode( actualPig ) ).isEqualTo( LockMode.PESSIMISTIC_WRITE ); } ) ) ) ); } @@ -580,10 +650,7 @@ context, populateDB() .getSingleResult() .thenAccept( actualPig -> { assertThatPigsAreEqual( expectedPig, actualPig ); - assertEquals( - LockMode.PESSIMISTIC_WRITE, - session.getLockMode( actualPig ) - ); + assertThat( session.getLockMode( actualPig ) ).isEqualTo( LockMode.PESSIMISTIC_WRITE ); } ) ) ) @@ -600,7 +667,7 @@ public void reactivePersist(VertxTestContext context) { .thenCompose( v -> s.close() ) ) .thenCompose( v -> selectNameFromId( 10 ) ) - .thenAccept( selectRes -> assertEquals( "Tulip", selectRes ) ) + .thenAccept( selectRes -> assertThat( selectRes ).isEqualTo( "Tulip" ) ) ); } @@ -613,80 +680,70 @@ public void reactivePersistInTx(VertxTestContext context) { .withTransaction( t -> s.persist( new GuineaPig( 10, "Tulip" ) ) ) .thenCompose( v -> s.close() ) ) .thenCompose( vv -> selectNameFromId( 10 ) ) - .thenAccept( selectRes -> assertEquals( "Tulip", selectRes ) ) + .thenAccept( selectRes -> assertThat( selectRes ).isEqualTo( "Tulip" ) ) ); } @Test public void reactiveRollbackTx(VertxTestContext context) { - test( - context, - openSession() - .thenCompose( s -> s - .withTransaction( t -> s - .persist( new GuineaPig( 10, "Tulip" ) ) - .thenCompose( v -> s.flush() ) - .thenAccept( v -> { - throw new RuntimeException( "No Panic: This is just a test" ); - } ) - ) - .thenCompose( v -> s.close() ) + test( context, openSession() + .thenCompose( s -> s + .withTransaction( t -> s + .persist( new GuineaPig( 10, "Tulip" ) ) + .thenCompose( v -> s.flush() ) + .thenAccept( v -> { + throw new RuntimeException( "No Panic: This is just a test" ); + } ) ) - .handle( (v, e) -> null ) - .thenCompose( vv -> selectNameFromId( 10 ) ) - .thenAccept( Assertions::assertNull ) + .thenCompose( v -> s.close() ) + ) + .handle( (v, e) -> null ) + .thenCompose( vv -> selectNameFromId( 10 ) ) + .thenAccept( result -> assertThat( result ).isNull() ) ); } @Test public void reactiveMarkedRollbackTx(VertxTestContext context) { - test( - context, openSession() - .thenCompose( s -> s - .withTransaction( t -> s - .persist( new GuineaPig( 10, "Tulip" ) ) - .thenCompose( vv -> s.flush() ) - .thenAccept( vv -> t.markForRollback() ) - ) - .thenCompose( v -> s.close() ) + test( context, openSession() + .thenCompose( s -> s + .withTransaction( t -> s + .persist( new GuineaPig( 10, "Tulip" ) ) + .thenCompose( vv -> s.flush() ) + .thenAccept( vv -> t.markForRollback() ) ) - .thenCompose( vv -> selectNameFromId( 10 ) ) - .thenAccept( Assertions::assertNull ) + .thenCompose( v -> s.close() ) + ) + .thenCompose( vv -> selectNameFromId( 10 ) ) + .thenAccept( result -> assertThat( result ).isNull() ) ); } @Test public void reactiveRemoveTransientEntity(VertxTestContext context) { - test( - context, - populateDB() - .thenCompose( v -> selectNameFromId( 5 ) ) - .thenAccept( Assertions::assertNotNull ) - .thenCompose( v -> openSession() ) - .thenCompose( session -> session.remove( new GuineaPig( 5, "Aloi" ) ) - .thenCompose( v -> session.flush() ) - .thenCompose( v -> session.close() ) - ) - .handle( (r, e) -> { - assertNotNull( e ); - return r; - } ) - + test( context, populateDB() + .thenCompose( v -> selectNameFromId( 5 ) ) + .thenAccept( result -> assertThat( result ).isNotNull() ) + .thenCompose( v -> openSession() ) + .thenCompose( session -> assertThrown( HibernateException.class, session.remove( new GuineaPig( 5, "Aloi" ) ) ) + ) + .thenAccept( t -> assertThat( t ) + .hasCauseInstanceOf( IllegalArgumentException.class ) + .hasMessageContaining( "unmanaged instance" ) + ) ); } @Test public void reactiveRemoveManagedEntity(VertxTestContext context) { - test( - context, - populateDB() + test( context, populateDB() .thenCompose( v -> openSession() ) - .thenCompose( session -> - session.find( GuineaPig.class, 5 ) - .thenCompose( session::remove ) - .thenCompose( v -> session.flush() ) - .thenCompose( v -> selectNameFromId( 5 ) ) - .thenAccept( Assertions::assertNull ) ) + .thenCompose( session -> session + .find( GuineaPig.class, 5 ) + .thenCompose( session::remove ) + .thenCompose( v -> session.flush() ) + .thenCompose( v -> selectNameFromId( 5 ) ) + .thenAccept( result -> assertThat( result ).isNull() ) ) ); } @@ -699,16 +756,16 @@ public void reactiveUpdate(VertxTestContext context) { .thenCompose( v -> openSession() ) .thenCompose( session -> session.find( GuineaPig.class, 5 ) .thenAccept( pig -> { - assertNotNull( pig ); + assertThat( pig ).isNotNull(); // Checking we are actually changing the name - assertNotEquals( NEW_NAME, pig.getName() ); + assertThat( pig.getName() ).isNotEqualTo( NEW_NAME ); pig.setName( NEW_NAME ); } ) .thenCompose( v -> session.flush() ) .thenCompose( v -> session.close() ) ) .thenCompose( v -> selectNameFromId( 5 ) ) - .thenAccept( name -> assertEquals( NEW_NAME, name ) ) + .thenAccept( name -> assertThat( name ).isEqualTo( NEW_NAME ) ) ); } @@ -721,10 +778,10 @@ public void reactiveUpdateVersion(VertxTestContext context) { .thenCompose( v -> openSession() ) .thenCompose( session -> session.find( GuineaPig.class, 5 ) .thenAccept( pig -> { - assertNotNull( pig ); + assertThat( pig ).isNotNull(); // Checking we are actually changing the name - assertNotEquals( NEW_NAME, pig.getName() ); - assertEquals( 0, pig.version ); + assertThat( pig.getName() ).isNotEqualTo( NEW_NAME ); + assertThat( pig.version ).isEqualTo( 0 ); pig.setName( NEW_NAME ); pig.version = 10; //ignored by Hibernate } ) @@ -733,7 +790,7 @@ public void reactiveUpdateVersion(VertxTestContext context) { ) .thenCompose( v -> openSession() ) .thenCompose( s -> s.find( GuineaPig.class, 5 ) - .thenAccept( pig -> assertEquals( 1, pig.version ) ) ) + .thenAccept( pig -> assertThat( pig.version ).isEqualTo( 1 ) ) ) ); } @@ -742,9 +799,9 @@ public void reactiveClose(VertxTestContext context) { test( context, openSession() .thenCompose( session -> { - assertTrue( session.isOpen() ); + assertThat( session.isOpen() ).isTrue(); return session.close() - .thenAccept( v -> assertFalse( session.isOpen() ) ); + .thenAccept( v -> assertThat( session.isOpen() ).isFalse() ); } ) ); } @@ -763,8 +820,8 @@ public void testSessionWithNativeAffectedEntities(VertxTestContext context) { .setParameter( "n", pig.name ) .getResultList() ) .thenAccept( list -> { - assertFalse( list.isEmpty() ); - assertEquals( 1, list.size() ); + assertThat( list ).isNotEmpty(); + assertThat( list.size() ).isEqualTo( 1 ); assertThatPigsAreEqual( pig, list.get( 0 ) ); } ) .thenCompose( v -> s.find( GuineaPig.class, pig.id ) ) @@ -773,23 +830,23 @@ public void testSessionWithNativeAffectedEntities(VertxTestContext context) { p.name = "X"; } ) .thenCompose( v -> s.createNativeQuery( "update pig set name='Y' where name='X'", affectsPigs ).executeUpdate() ) - .thenAccept( rows -> assertEquals( 1, rows ) ) + .thenAccept( rows -> assertThat( rows ).isEqualTo( 1 ) ) .thenCompose( v -> s.refresh( pig ) ) - .thenAccept( v -> assertEquals( "Y", pig.name ) ) + .thenAccept( v -> assertThat( pig.name ).isEqualTo( "Y" ) ) .thenAccept( v -> pig.name = "Z" ) .thenCompose( v -> s.createNativeQuery( "delete from pig where name='Z'", affectsPigs ).executeUpdate() ) - .thenAccept( rows -> assertEquals( 1, rows ) ) + .thenAccept( rows -> assertThat( rows ).isEqualTo( 1 ) ) .thenCompose( v -> s.createNativeQuery( "select id from pig", affectsPigs ).getResultList() ) - .thenAccept( list -> assertTrue( list.isEmpty() ) ) ) + .thenAccept( list -> assertThat( list ).isEmpty() ) ) ); } @Test public void testMetamodel() { EntityType pig = getSessionFactory().getMetamodel().entity( GuineaPig.class ); - assertNotNull( pig ); - assertEquals( 3, pig.getAttributes().size() ); - assertEquals( "GuineaPig", pig.getName() ); + assertThat( pig ).isNotNull(); + assertThat( pig.getAttributes().size() ).isEqualTo( 3 ); + assertThat( pig.getName() ).isEqualTo( "GuineaPig" ); } @Test @@ -800,7 +857,7 @@ context, getSessionFactory().withSession( session -> { session.getFactory().getMetamodel().entity( GuineaPig.class ); session.getFactory().getCriteriaBuilder().createQuery( GuineaPig.class ); session.getFactory().getStatistics().isStatisticsEnabled(); - return CompletionStages.voidFuture(); + return voidFuture(); } ) ); } @@ -809,17 +866,18 @@ context, getSessionFactory().withSession( session -> { public void testTransactionPropagation(VertxTestContext context) { test( context, getSessionFactory().withTransaction( - (session, transaction) -> session.createSelectionQuery( "from GuineaPig", GuineaPig.class ) + (session, transaction) -> session + .createSelectionQuery( "from GuineaPig", GuineaPig.class ) .getResultList() .thenCompose( list -> { - assertNotNull( session.currentTransaction() ); - assertFalse( session.currentTransaction().isMarkedForRollback() ); + assertThat( session.currentTransaction() ).isNotNull(); + assertThat( session.currentTransaction().isMarkedForRollback() ).isFalse(); session.currentTransaction().markForRollback(); - assertTrue( session.currentTransaction().isMarkedForRollback() ); - assertTrue( transaction.isMarkedForRollback() ); + assertThat( session.currentTransaction().isMarkedForRollback() ).isTrue(); + assertThat( transaction.isMarkedForRollback() ).isTrue(); return session.withTransaction( t -> { - assertEquals( t, transaction ); - assertTrue( t.isMarkedForRollback() ); + assertThat( t ).isEqualTo( transaction ); + assertThat( t.isMarkedForRollback() ).isTrue(); return session.createSelectionQuery( "from GuineaPig", GuineaPig.class ).getResultList(); } ); } ) @@ -831,11 +889,11 @@ context, getSessionFactory().withTransaction( public void testSessionPropagation(VertxTestContext context) { test( context, getSessionFactory().withSession( session -> { - assertFalse( session.isDefaultReadOnly() ); + assertThat( session.isDefaultReadOnly() ).isFalse(); session.setDefaultReadOnly( true ); return session.createSelectionQuery( "from GuineaPig", GuineaPig.class ).getResultList() .thenCompose( list -> getSessionFactory().withSession( s -> { - assertTrue( s.isDefaultReadOnly() ); + assertThat( s.isDefaultReadOnly() ).isTrue(); return s.createSelectionQuery( "from GuineaPig", GuineaPig.class ).getResultList(); } ) ); } ) @@ -851,9 +909,9 @@ public void testDupeException(VertxTestContext context) { .thenCompose( v -> getSessionFactory() .withTransaction( (s, t) -> s.persist( new GuineaPig( 10, "Tulip" ) ) ) ).handle( (i, t) -> { - assertNotNull( t ); - assertInstanceOf( CompletionException.class, t ); - assertInstanceOf( PersistenceException.class, t.getCause() ); + assertThat( t ).isNotNull(); + assertThat( t ).isInstanceOf( CompletionException.class ); + assertThat( t.getCause() ).isInstanceOf( PersistenceException.class ); return null; } ) ); @@ -864,12 +922,12 @@ public void testExceptionInWithSession(VertxTestContext context) { final Stage.Session[] savedSession = new Stage.Session[1]; test( context, getSessionFactory().withSession( session -> { - assertTrue( session.isOpen() ); + assertThat( session.isOpen() ).isTrue(); savedSession[0] = session; throw new RuntimeException( "No Panic: This is just a test" ); } ).handle( (o, t) -> { - assertNotNull( t ); - assertFalse( savedSession[0].isOpen(), "Session should be closed" ); + assertThat( t ).isNotNull(); + assertThat( savedSession[0].isOpen() ).withFailMessage( "Session should be closed" ).isFalse(); return null; } ) ); @@ -880,12 +938,12 @@ public void testExceptionInWithTransaction(VertxTestContext context) { final Stage.Session[] savedSession = new Stage.Session[1]; test( context, getSessionFactory().withTransaction( (session, tx) -> { - assertTrue( session.isOpen() ); + assertThat( session.isOpen() ).isTrue(); savedSession[0] = session; throw new RuntimeException( "No Panic: This is just a test" ); } ).handle( (o, t) -> { - assertNotNull( t ); - assertFalse( savedSession[0].isOpen(), "Session should be closed" ); + assertThat( t ).isNotNull(); + assertThat( savedSession[0].isOpen() ).withFailMessage( "Session should be closed" ).isFalse(); return null; } ) ); @@ -896,12 +954,12 @@ public void testExceptionInWithStatelessSession(VertxTestContext context) { final Stage.StatelessSession[] savedSession = new Stage.StatelessSession[1]; test( context, getSessionFactory().withStatelessSession( session -> { - assertTrue( session.isOpen() ); + assertThat( session.isOpen() ).isTrue(); savedSession[0] = session; throw new RuntimeException( "No Panic: This is just a test" ); } ).handle( (o, t) -> { - assertNotNull( t ); - assertFalse( savedSession[0].isOpen(), "Session should be closed" ); + assertThat( t ).isNotNull(); + assertThat( savedSession[0].isOpen() ).withFailMessage( "Session should be closed" ).isFalse(); return null; } ) ); @@ -956,54 +1014,60 @@ public void testCreateSelectionQueryNull(VertxTestContext context) { context, openSession() .thenCompose( session -> session.createSelectionQuery( "from GuineaPig", GuineaPig.class ) .getSingleResultOrNull() - .thenAccept( Assertions::assertNull ) ) + .thenAccept( result -> assertThat( result ).isNull() ) ) .thenCompose( v -> openSession() ) .thenCompose( session -> session.createSelectionQuery( "from GuineaPig", GuineaPig.class ) .getSingleResultOrNull() - .thenAccept( Assertions::assertNull ) ) + .thenAccept( result -> assertThat( result ).isNull() ) ) ); } @Test public void testCurrentSession(VertxTestContext context) { - test( context, - getSessionFactory().withSession(session -> - getSessionFactory().withSession(s -> { - assertEquals(session, s); - Stage.Session currentSession = getSessionFactory().getCurrentSession(); - assertNotNull(currentSession); - assertTrue(currentSession.isOpen()); - assertEquals(session, currentSession); - return CompletionStages.voidFuture(); - }) - .thenAccept(v -> assertNotNull(getSessionFactory().getCurrentSession())) - ) - .thenAccept(v -> assertNull(getSessionFactory().getCurrentSession())) + test( + context, getSessionFactory() + .withSession( s1 -> getSessionFactory() + .withSession( s2 -> { + assertThat( s2 ).isEqualTo( s1 ); + Stage.Session currentSession = getSessionFactory().getCurrentSession(); + assertThat( currentSession ).isNotNull(); + assertThat( currentSession.isOpen() ).isTrue(); + assertThat( currentSession ).isEqualTo( s1 ); + return voidFuture(); + } ) + // We closed s2, not s1 + .thenAccept( v -> assertThat( getSessionFactory().getCurrentSession() ).isNotNull() ) + ) + // Both sessions are closed now + .thenAccept( v -> assertThat( getSessionFactory().getCurrentSession() ).isNull() ) ); } @Test public void testCurrentStatelessSession(VertxTestContext context) { - test( context, - getSessionFactory().withStatelessSession(session -> - getSessionFactory().withStatelessSession(s -> { - assertEquals(session, s); - Stage.StatelessSession currentSession = getSessionFactory().getCurrentStatelessSession(); - assertNotNull(currentSession); - assertTrue(currentSession.isOpen()); - assertEquals(session, currentSession); - return CompletionStages.voidFuture(); - }) - .thenAccept(v -> assertNotNull(getSessionFactory().getCurrentStatelessSession())) - ) - .thenAccept(v -> assertNull(getSessionFactory().getCurrentStatelessSession())) + test( + context, getSessionFactory() + .withStatelessSession( session -> getSessionFactory() + .withStatelessSession( s -> { + assertThat( s ).isEqualTo( session ); + Stage.StatelessSession currentSession = getSessionFactory().getCurrentStatelessSession(); + assertThat( currentSession ).isNotNull(); + assertThat( currentSession.isOpen() ).isTrue(); + assertThat( currentSession ).isEqualTo( session ); + return voidFuture(); + } ) + // We closed s2, not s1 + .thenAccept( v -> assertThat( getSessionFactory().getCurrentStatelessSession() ).isNotNull() ) + ) + // Both sessions are closed now + .thenAccept( v -> assertThat( getSessionFactory().getCurrentStatelessSession() ).isNull() ) ); } private void assertThatPigsAreEqual(GuineaPig expected, GuineaPig actual) { - assertNotNull( actual ); - assertEquals( expected.getId(), actual.getId() ); - assertEquals( expected.getName(), actual.getName() ); + assertThat( actual ).isNotNull(); + assertThat( actual.getId() ).isEqualTo( expected.getId() ); + assertThat( actual.getName() ).isEqualTo( expected.getName() ); } @Entity(name = "GuineaPig") diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/UpsertTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/UpsertTest.java index 8dcc07a6a..8cf8e6931 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/UpsertTest.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/UpsertTest.java @@ -144,14 +144,10 @@ private void assertQueries() { } private boolean hasMergeOperator() { - switch ( dbType() ) { - case SQLSERVER: - case ORACLE: - case POSTGRESQL: - return true; - default: - return false; - } + return switch ( dbType() ) { + case SQLSERVER, ORACLE, POSTGRESQL, DB2 -> true; + default -> false; + }; } @Entity(name = "Record") diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/CockroachDBDatabase.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/CockroachDBDatabase.java index e1a0928d1..172ace2bc 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/CockroachDBDatabase.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/CockroachDBDatabase.java @@ -12,7 +12,7 @@ import org.testcontainers.containers.CockroachContainer; import org.testcontainers.containers.Container; -import static org.hibernate.reactive.containers.DockerImage.imageName; +import static org.hibernate.reactive.containers.DockerImage.fromDockerfile; class CockroachDBDatabase extends PostgreSQLDatabase { @@ -25,7 +25,7 @@ class CockroachDBDatabase extends PostgreSQLDatabase { * TIP: To reuse the same containers across multiple runs, set `testcontainers.reuse.enable=true` in a file located * at `$HOME/.testcontainers.properties` (create the file if it does not exist). */ - public static final CockroachContainer cockroachDb = new CockroachContainer( imageName( "cockroachdb/cockroach", "v24.3.13" ) ) + public static final CockroachContainer cockroachDb = new CockroachContainer( fromDockerfile( "cockroachdb" ) ) // Username, password and database are not supported by test container at the moment // Testcontainers will use a database named 'postgres' and the 'root' user .withReuse( true ); diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/DB2Database.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/DB2Database.java index dfeeaf15c..c2b60e3d6 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/DB2Database.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/DB2Database.java @@ -28,7 +28,7 @@ import org.testcontainers.containers.Db2Container; -import static org.hibernate.reactive.containers.DockerImage.imageName; +import static org.hibernate.reactive.containers.DockerImage.fromDockerfile; class DB2Database implements TestableDatabase { @@ -87,7 +87,7 @@ class DB2Database implements TestableDatabase { * TIP: To reuse the same containers across multiple runs, set `testcontainers.reuse.enable=true` in a file located * at `$HOME/.testcontainers.properties` (create the file if it does not exist). */ - static final Db2Container db2 = new Db2Container( imageName( "icr.io", "db2_community/db2", "12.1.0.0" ) ) + static final Db2Container db2 = new Db2Container( fromDockerfile( "db2" ) ) .withUsername( DatabaseConfiguration.USERNAME ) .withPassword( DatabaseConfiguration.PASSWORD ) .withDatabaseName( DatabaseConfiguration.DB_NAME ) diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/DockerImage.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/DockerImage.java index 1b2f3f6a7..e8f40f34d 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/DockerImage.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/DockerImage.java @@ -5,10 +5,17 @@ */ package org.hibernate.reactive.containers; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + import org.testcontainers.utility.DockerImageName; + /** - * A utility class with methods to generate {@link DockerImageName} for testcontainers. + * A utility class with methods to generate a {@link DockerImageName} for Testcontainers. *

* Testcontainers might not work if the image required is available in multiple different registries (for example when * using podman instead of docker). @@ -17,10 +24,28 @@ */ public final class DockerImage { - public static final String DEFAULT_REGISTRY = "docker.io"; + /** + * The absolute path of the project root that we have set in Gradle. + */ + private static final String PROJECT_ROOT = System.getProperty( "hibernate.reactive.project.root" ); + + /** + * The path to the directory containing all the Dockerfile files + */ + private static final Path DOCKERFILE_DIR_PATH = Path.of( PROJECT_ROOT ).resolve( "tooling" ).resolve( "docker" ); - public static DockerImageName imageName(String image, String version) { - return imageName( DEFAULT_REGISTRY, image, version ); + /** + * Extract the image name and version from the first FROM instruction in the Dockerfile. + * Note that everything else is ignored. + */ + public static DockerImageName fromDockerfile(String databaseName) { + try { + final ImageInformation imageInformation = readFromInstruction( databaseName.toLowerCase() ); + return imageName( imageInformation.getRegistry(), imageInformation.getImage(), imageInformation.getVersion() ); + } + catch (IOException e) { + throw new RuntimeException( e ); + } } public static DockerImageName imageName(String registry, String image, String version) { @@ -28,4 +53,74 @@ public static DockerImageName imageName(String registry, String image, String ve .parse( registry + "/" + image + ":" + version ) .asCompatibleSubstituteFor( image ); } + + private static class ImageInformation { + private final String registry; + private final String image; + private final String version; + + public ImageInformation(String fullImageInfo) { + // FullImageInfo pattern: /: + // For example: "docker.io/cockroachdb/cockroach:v24.3.13" becomes registry = "docker.io", image = "cockroachdb/cockroach", version = "v24.3.13" + final int registryEndPos = fullImageInfo.indexOf( '/' ); + final int imageEndPos = fullImageInfo.lastIndexOf( ':' ); + this.registry = fullImageInfo.substring( 0, registryEndPos ); + this.image = fullImageInfo.substring( registryEndPos + 1, imageEndPos ); + this.version = fullImageInfo.substring( imageEndPos + 1 ); + } + + public String getRegistry() { + return registry; + } + + public String getImage() { + return image; + } + + public String getVersion() { + return version; + } + + @Override + public String toString() { + return registry + "/" + image + ":" + version; + } + } + + private static Path dockerFilePath(String database) { + // Get project root from system property set by Gradle, with fallback + return DOCKERFILE_DIR_PATH.resolve( database.toLowerCase() + ".Dockerfile" ); + } + + private static ImageInformation readFromInstruction(String database) throws IOException { + return readFromInstruction( dockerFilePath( database ) ); + } + + /** + * Read a Dockerfile and extract the first FROM instruction. + * + * @param dockerfilePath path to the Dockerfile + * @return the first FROM instruction found, or empty if none found + * @throws IOException if the file cannot be read + */ + private static ImageInformation readFromInstruction(Path dockerfilePath) throws IOException { + if ( !Files.exists( dockerfilePath ) ) { + throw new FileNotFoundException( "Dockerfile not found: " + dockerfilePath ); + } + + List lines = Files.readAllLines( dockerfilePath ); + for ( String line : lines ) { + // Skip comments and empty lines + String trimmedLine = line.trim(); + if ( trimmedLine.isEmpty() || trimmedLine.startsWith( "#" ) ) { + continue; + } + + if ( trimmedLine.startsWith( "FROM " ) ) { + return new ImageInformation( trimmedLine.substring( "FROM ".length() ) ); + } + } + + throw new IOException( " Missing FROM instruction in " + dockerfilePath ); + } } diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/MSSQLServerDatabase.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/MSSQLServerDatabase.java index aeb1b8fb0..eaab2e8fb 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/MSSQLServerDatabase.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/MSSQLServerDatabase.java @@ -28,7 +28,7 @@ import org.testcontainers.containers.MSSQLServerContainer; -import static org.hibernate.reactive.containers.DockerImage.imageName; +import static org.hibernate.reactive.containers.DockerImage.fromDockerfile; /** * The JDBC driver syntax is: @@ -96,7 +96,7 @@ class MSSQLServerDatabase implements TestableDatabase { * TIP: To reuse the same containers across multiple runs, set `testcontainers.reuse.enable=true` in a file located * at `$HOME/.testcontainers.properties` (create the file if it does not exist). */ - public static final MSSQLServerContainer mssqlserver = new MSSQLServerContainer<>( imageName( "mcr.microsoft.com", "mssql/server", "2022-latest" ) ) + public static final MSSQLServerContainer mssqlserver = new MSSQLServerContainer<>( fromDockerfile( "sqlserver" ) ) .acceptLicense() .withPassword( PASSWORD ) .withReuse( true ); diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/MariaDatabase.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/MariaDatabase.java index 16a5f97ff..14d60c264 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/MariaDatabase.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/MariaDatabase.java @@ -13,7 +13,7 @@ import org.testcontainers.containers.MariaDBContainer; -import static org.hibernate.reactive.containers.DockerImage.imageName; +import static org.hibernate.reactive.containers.DockerImage.fromDockerfile; class MariaDatabase extends MySQLDatabase { @@ -36,7 +36,7 @@ class MariaDatabase extends MySQLDatabase { * TIP: To reuse the same containers across multiple runs, set `testcontainers.reuse.enable=true` in a file located * at `$HOME/.testcontainers.properties` (create the file if it does not exist). */ - public static final MariaDBContainer maria = new MariaDBContainer<>( imageName( "mariadb", "11.7.2" ) ) + public static final MariaDBContainer maria = new MariaDBContainer<>( fromDockerfile( "maria" ) ) .withUsername( DatabaseConfiguration.USERNAME ) .withPassword( DatabaseConfiguration.PASSWORD ) .withDatabaseName( DatabaseConfiguration.DB_NAME ) diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/MySQLDatabase.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/MySQLDatabase.java index e0aa9ef3a..5b5ad5d2c 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/MySQLDatabase.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/MySQLDatabase.java @@ -5,7 +5,7 @@ */ package org.hibernate.reactive.containers; -import static org.hibernate.reactive.containers.DockerImage.imageName; +import static org.hibernate.reactive.containers.DockerImage.fromDockerfile; import java.io.Serializable; import java.math.BigDecimal; @@ -87,7 +87,7 @@ class MySQLDatabase implements TestableDatabase { * TIP: To reuse the same containers across multiple runs, set `testcontainers.reuse.enable=true` in a file located * at `$HOME/.testcontainers.properties` (create the file if it does not exist). */ - public static final MySQLContainer mysql = new MySQLContainer<>( imageName( "mysql", "9.2.0") ) + public static final MySQLContainer mysql = new MySQLContainer<>( fromDockerfile( "mysql" ).asCompatibleSubstituteFor( "mysql" ) ) .withUsername( DatabaseConfiguration.USERNAME ) .withPassword( DatabaseConfiguration.PASSWORD ) .withDatabaseName( DatabaseConfiguration.DB_NAME ) diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/OracleDatabase.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/OracleDatabase.java index d57f9103a..bbde95e6c 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/OracleDatabase.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/OracleDatabase.java @@ -29,7 +29,7 @@ import org.testcontainers.containers.OracleContainer; -import static org.hibernate.reactive.containers.DockerImage.imageName; +import static org.hibernate.reactive.containers.DockerImage.fromDockerfile; /** * Connection string for Oracle thin should be something like: @@ -88,9 +88,7 @@ class OracleDatabase implements TestableDatabase { } } - public static final OracleContainer oracle = new OracleContainer( - imageName( "gvenzl/oracle-free", "23-slim-faststart" ) - .asCompatibleSubstituteFor( "gvenzl/oracle-xe" ) ) + public static final OracleContainer oracle = new OracleContainer( fromDockerfile( "oracle" ).asCompatibleSubstituteFor( "gvenzl/oracle-xe" ) ) .withUsername( DatabaseConfiguration.USERNAME ) .withPassword( DatabaseConfiguration.PASSWORD ) .withDatabaseName( DatabaseConfiguration.DB_NAME ) diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/PostgreSQLDatabase.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/PostgreSQLDatabase.java index 56ef4f878..f6a974ba2 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/PostgreSQLDatabase.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/containers/PostgreSQLDatabase.java @@ -5,8 +5,6 @@ */ package org.hibernate.reactive.containers; -import static org.hibernate.reactive.containers.DockerImage.imageName; - import java.io.Serializable; import java.math.BigDecimal; import java.math.BigInteger; @@ -30,9 +28,11 @@ import org.testcontainers.containers.PostgreSQLContainer; +import static org.hibernate.reactive.containers.DockerImage.fromDockerfile; + class PostgreSQLDatabase implements TestableDatabase { - public static PostgreSQLDatabase INSTANCE = new PostgreSQLDatabase(); + public static final PostgreSQLDatabase INSTANCE = new PostgreSQLDatabase(); private static Map, String> expectedDBTypeForClass = new HashMap<>(); @@ -87,7 +87,7 @@ class PostgreSQLDatabase implements TestableDatabase { * TIP: To reuse the same containers across multiple runs, set `testcontainers.reuse.enable=true` in a file located * at `$HOME/.testcontainers.properties` (create the file if it does not exist). */ - public static final PostgreSQLContainer postgresql = new PostgreSQLContainer<>( imageName( "postgres", "17.5" ) ) + public static final PostgreSQLContainer postgresql = new PostgreSQLContainer<>( fromDockerfile( "postgresql" ) ) .withUsername( DatabaseConfiguration.USERNAME ) .withPassword( DatabaseConfiguration.PASSWORD ) .withDatabaseName( DatabaseConfiguration.DB_NAME ) diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/schema/SchemaUpdateCockroachDBTestBase.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/schema/SchemaUpdateCockroachDBTestBase.java index 44b34c154..24e8921dd 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/schema/SchemaUpdateCockroachDBTestBase.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/schema/SchemaUpdateCockroachDBTestBase.java @@ -35,8 +35,8 @@ import static java.util.concurrent.TimeUnit.MINUTES; import static org.hibernate.reactive.containers.DatabaseConfiguration.DBType.COCKROACHDB; -import static org.hibernate.tool.schema.JdbcMetadaAccessStrategy.GROUPED; -import static org.hibernate.tool.schema.JdbcMetadaAccessStrategy.INDIVIDUALLY; +import static org.hibernate.tool.schema.JdbcMetadataAccessStrategy.GROUPED; +import static org.hibernate.tool.schema.JdbcMetadataAccessStrategy.INDIVIDUALLY; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/schema/SchemaUpdateMariaDBTestBase.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/schema/SchemaUpdateMariaDBTestBase.java index 7afed941e..dde639cda 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/schema/SchemaUpdateMariaDBTestBase.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/schema/SchemaUpdateMariaDBTestBase.java @@ -34,8 +34,8 @@ import static java.util.concurrent.TimeUnit.MINUTES; import static org.hibernate.reactive.containers.DatabaseConfiguration.DBType.MARIA; -import static org.hibernate.tool.schema.JdbcMetadaAccessStrategy.GROUPED; -import static org.hibernate.tool.schema.JdbcMetadaAccessStrategy.INDIVIDUALLY; +import static org.hibernate.tool.schema.JdbcMetadataAccessStrategy.GROUPED; +import static org.hibernate.tool.schema.JdbcMetadataAccessStrategy.INDIVIDUALLY; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/schema/SchemaUpdateMySqlTestBase.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/schema/SchemaUpdateMySqlTestBase.java index 501a72282..064de5cff 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/schema/SchemaUpdateMySqlTestBase.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/schema/SchemaUpdateMySqlTestBase.java @@ -34,8 +34,8 @@ import static java.util.concurrent.TimeUnit.MINUTES; import static org.hibernate.reactive.containers.DatabaseConfiguration.DBType.MYSQL; -import static org.hibernate.tool.schema.JdbcMetadaAccessStrategy.GROUPED; -import static org.hibernate.tool.schema.JdbcMetadaAccessStrategy.INDIVIDUALLY; +import static org.hibernate.tool.schema.JdbcMetadataAccessStrategy.GROUPED; +import static org.hibernate.tool.schema.JdbcMetadataAccessStrategy.INDIVIDUALLY; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/schema/SchemaUpdateOracleTestBase.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/schema/SchemaUpdateOracleTestBase.java index 4f46641a1..42c8e592d 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/schema/SchemaUpdateOracleTestBase.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/schema/SchemaUpdateOracleTestBase.java @@ -34,8 +34,8 @@ import static java.util.concurrent.TimeUnit.MINUTES; import static org.hibernate.reactive.containers.DatabaseConfiguration.DBType.ORACLE; -import static org.hibernate.tool.schema.JdbcMetadaAccessStrategy.GROUPED; -import static org.hibernate.tool.schema.JdbcMetadaAccessStrategy.INDIVIDUALLY; +import static org.hibernate.tool.schema.JdbcMetadataAccessStrategy.GROUPED; +import static org.hibernate.tool.schema.JdbcMetadataAccessStrategy.INDIVIDUALLY; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/schema/SchemaUpdatePostgreSqlTestBase.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/schema/SchemaUpdatePostgreSqlTestBase.java index ded7f0904..7ae699b65 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/schema/SchemaUpdatePostgreSqlTestBase.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/schema/SchemaUpdatePostgreSqlTestBase.java @@ -34,8 +34,8 @@ import static java.util.concurrent.TimeUnit.MINUTES; import static org.hibernate.reactive.containers.DatabaseConfiguration.DBType.POSTGRESQL; -import static org.hibernate.tool.schema.JdbcMetadaAccessStrategy.GROUPED; -import static org.hibernate.tool.schema.JdbcMetadaAccessStrategy.INDIVIDUALLY; +import static org.hibernate.tool.schema.JdbcMetadataAccessStrategy.GROUPED; +import static org.hibernate.tool.schema.JdbcMetadataAccessStrategy.INDIVIDUALLY; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/schema/SchemaUpdateSqlServerTestBase.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/schema/SchemaUpdateSqlServerTestBase.java index 1ebcf85d7..3a1aa8fb6 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/schema/SchemaUpdateSqlServerTestBase.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/schema/SchemaUpdateSqlServerTestBase.java @@ -35,8 +35,8 @@ import static java.util.concurrent.TimeUnit.MINUTES; import static org.hibernate.reactive.containers.DatabaseConfiguration.DBType.SQLSERVER; -import static org.hibernate.tool.schema.JdbcMetadaAccessStrategy.GROUPED; -import static org.hibernate.tool.schema.JdbcMetadaAccessStrategy.INDIVIDUALLY; +import static org.hibernate.tool.schema.JdbcMetadataAccessStrategy.GROUPED; +import static org.hibernate.tool.schema.JdbcMetadataAccessStrategy.INDIVIDUALLY; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/schema/SchemaUpdateTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/schema/SchemaUpdateTest.java index febcda93b..776a53bc4 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/schema/SchemaUpdateTest.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/schema/SchemaUpdateTest.java @@ -32,8 +32,8 @@ import static org.hibernate.reactive.containers.DatabaseConfiguration.dbType; import static org.hibernate.reactive.testing.ReactiveAssertions.assertThrown; import static org.hibernate.reactive.util.impl.CompletionStages.voidFuture; -import static org.hibernate.tool.schema.JdbcMetadaAccessStrategy.GROUPED; -import static org.hibernate.tool.schema.JdbcMetadaAccessStrategy.INDIVIDUALLY; +import static org.hibernate.tool.schema.JdbcMetadataAccessStrategy.GROUPED; +import static org.hibernate.tool.schema.JdbcMetadataAccessStrategy.INDIVIDUALLY; import static org.junit.jupiter.params.provider.Arguments.arguments; /** diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/schema/SchemaValidationTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/schema/SchemaValidationTest.java index 3132b8498..1a5942e25 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/schema/SchemaValidationTest.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/schema/SchemaValidationTest.java @@ -6,13 +6,16 @@ package org.hibernate.reactive.schema; +import java.net.URL; import java.util.concurrent.CompletionStage; import java.util.stream.Stream; import org.hibernate.boot.registry.StandardServiceRegistryBuilder; +import org.hibernate.cfg.AvailableSettings; import org.hibernate.cfg.Configuration; import org.hibernate.reactive.BaseReactiveTest; import org.hibernate.reactive.annotations.DisabledFor; +import org.hibernate.reactive.annotations.EnabledFor; import org.hibernate.reactive.provider.Settings; import org.hibernate.tool.schema.spi.SchemaManagementException; @@ -23,6 +26,7 @@ import io.vertx.junit5.Timeout; import io.vertx.junit5.VertxTestContext; +import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.Id; @@ -34,8 +38,9 @@ import static org.hibernate.reactive.containers.DatabaseConfiguration.DBType.DB2; import static org.hibernate.reactive.containers.DatabaseConfiguration.DBType.MARIA; import static org.hibernate.reactive.containers.DatabaseConfiguration.DBType.MYSQL; -import static org.hibernate.tool.schema.JdbcMetadaAccessStrategy.GROUPED; -import static org.hibernate.tool.schema.JdbcMetadaAccessStrategy.INDIVIDUALLY; +import static org.hibernate.reactive.containers.DatabaseConfiguration.DBType.ORACLE; +import static org.hibernate.tool.schema.JdbcMetadataAccessStrategy.GROUPED; +import static org.hibernate.tool.schema.JdbcMetadataAccessStrategy.INDIVIDUALLY; import static org.junit.jupiter.params.provider.Arguments.arguments; /** @@ -46,7 +51,7 @@ * - TODO: Test that validation fails when a column is the wrong type */ @DisabledFor(value = DB2, reason = "We don't have an information extractor. See https://github.com/hibernate/hibernate-reactive/issues/911") -@DisabledFor(value = {MARIA, MYSQL}, reason = "HHH-18869: Schema creation creates an invalid schema") +@DisabledFor(value = { MARIA, MYSQL }, reason = "HHH-18869: Schema creation creates an invalid schema") public class SchemaValidationTest extends BaseReactiveTest { static Stream settings() { @@ -76,9 +81,18 @@ public void before(VertxTestContext context) { } public CompletionStage setupFactory(String strategy, String type) { + return setupFactory( strategy, type, null ); + } + + public CompletionStage setupFactory(String strategy, String type, String importFile) { Configuration createConf = constructConfiguration( "create", strategy, type ); createConf.addAnnotatedClass( BasicTypesTestEntity.class ); - + if ( importFile != null ) { + final URL importFileURL = Thread.currentThread() + .getContextClassLoader() + .getResource( importFile ); + createConf.setProperty( AvailableSettings.JAKARTA_HBM2DDL_LOAD_SCRIPT_SOURCE, importFileURL.getFile() ); + } // Make sure that the extra table is not in the db Configuration dropConf = constructConfiguration( "drop", strategy, type ); dropConf.addAnnotatedClass( Extra.class ); @@ -96,6 +110,21 @@ public void after(VertxTestContext context) { closeFactory( context ); } + @ParameterizedTest + @MethodSource("settings") + @EnabledFor( ORACLE ) + public void testOracleColumnTypeValidation(final String strategy, final String type, VertxTestContext context) { + test( + context, setupFactory( strategy, type, "oracle-SchemaValidationTest.sql" ) + .thenCompose( v -> { + Configuration validateConf = constructConfiguration( "validate", strategy, type ); + validateConf.addAnnotatedClass( Fruit.class ); + new StandardServiceRegistryBuilder().applySettings( validateConf.getProperties() ); + return setupSessionFactory( validateConf ); + } ) + ); + } + // When we have created the table, the validation should pass @ParameterizedTest @MethodSource("settings") @@ -112,13 +141,12 @@ context, setupFactory( strategy, type ) ); } - // Validation should fail if a table is missing @ParameterizedTest @MethodSource("settings") @Timeout(value = 10, timeUnit = MINUTES) public void testValidationFails(String strategy, String type, VertxTestContext context) { - final String errorMessage = "Schema-validation: missing table [" + Extra.TABLE_NAME + "]"; + final String errorMessage = "Schema validation: missing table [" + Extra.TABLE_NAME + "]"; test( context, setupFactory( strategy, type ) .thenCompose( v -> { @@ -151,4 +179,43 @@ public static class Extra { private String description; } + + @Entity(name = "Fruit") + public static class Fruit { + + @Id + @GeneratedValue + private Integer id; + + @Column(name = "something_name", nullable = false, updatable = false) + private String name; + + public Fruit() { + } + + public Fruit(String name) { + this.name = 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; + } + + @Override + public String toString() { + return "Fruit{" + id + "," + name + '}'; + } + } } diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/testing/TestingRegistryExtension.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/testing/TestingRegistryExtension.java index d3a0f3528..0a4e82468 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/testing/TestingRegistryExtension.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/testing/TestingRegistryExtension.java @@ -135,6 +135,11 @@ public ServiceBinding locateServiceBinding(Class servi public void destroy() { } + @Override + public boolean isActive() { + return true; + } + @Override public void registerChild(ServiceRegistryImplementor child) { } diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/types/BasicTypesAndCallbacksForAllDBsTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/types/BasicTypesAndCallbacksForAllDBsTest.java index 3ff20c2b3..62064c33f 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/types/BasicTypesAndCallbacksForAllDBsTest.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/types/BasicTypesAndCallbacksForAllDBsTest.java @@ -31,7 +31,6 @@ import org.hibernate.reactive.BaseReactiveTest; import org.hibernate.reactive.annotations.DisabledFor; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import io.vertx.junit5.VertxTestContext; @@ -60,17 +59,13 @@ import jakarta.persistence.Transient; import jakarta.persistence.Version; +import static org.assertj.core.api.Assertions.assertThat; import static org.hibernate.reactive.containers.DatabaseConfiguration.DBType.DB2; import static org.hibernate.reactive.testing.ReactiveAssertions.assertWithTruncationThat; -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; /** * Test all the types and lifecycle callbacks that we expect to work on all supported DBs */ - public class BasicTypesAndCallbacksForAllDBsTest extends BaseReactiveTest { @Override @@ -79,14 +74,15 @@ protected Set> annotatedEntities() { } private void testField(VertxTestContext context, Basic original, Consumer consumer) { - test( context, getSessionFactory() - .withTransaction( (s, t) -> s.persist( original ) ) - .thenCompose( v -> getSessionFactory().withSession( s -> s - .find( Basic.class, original.id ) - .thenAccept( found -> { - assertNotNull( found ); - consumer.accept( found ); - } ) ) ) + test( + context, getSessionFactory() + .withTransaction( (s, t) -> s.persist( original ) ) + .thenCompose( v -> getSessionFactory().withSession( s -> s + .find( Basic.class, original.id ) + .thenAccept( found -> { + assertThat( found ).isNotNull(); + consumer.accept( found ); + } ) ) ) ); } @@ -96,7 +92,7 @@ public void testStringType(VertxTestContext context) { Basic basic = new Basic(); basic.string = string; - testField( context, basic, found -> assertEquals( string, found.string ) ); + testField( context, basic, found -> assertThat( found.string ).isEqualTo( string ) ); } @Test @@ -105,10 +101,12 @@ public void testIntegerType(VertxTestContext context) { basic.primitiveInt = Integer.MIN_VALUE; basic.fieldInteger = Integer.MAX_VALUE; - testField( context, basic, found -> { - assertEquals( Integer.MIN_VALUE, found.primitiveInt ); - assertEquals( Integer.MAX_VALUE, found.fieldInteger ); - } ); + testField( + context, basic, found -> { + assertThat( found.primitiveInt ).isEqualTo( Integer.MIN_VALUE ); + assertThat( found.fieldInteger ).isEqualTo( Integer.MAX_VALUE ); + } + ); } @Test @@ -117,10 +115,12 @@ public void testLongType(VertxTestContext context) { basic.primitiveLong = Long.MIN_VALUE; basic.fieldLong = Long.MAX_VALUE; - testField( context, basic, found -> { - assertEquals( Long.MIN_VALUE, found.primitiveLong ); - assertEquals( Long.MAX_VALUE, found.fieldLong ); - } ); + testField( + context, basic, found -> { + assertThat( found.primitiveLong ).isEqualTo( Long.MIN_VALUE ); + assertThat( found.fieldLong ).isEqualTo( Long.MAX_VALUE ); + } + ); } @Test @@ -132,10 +132,12 @@ public void testFloatType(VertxTestContext context) { basic.primitiveFloat = 10.02f; basic.fieldFloat = 12.562f; - testField( context, basic, found -> { - assertEquals( primitiveFloat, found.primitiveFloat ); - assertEquals( fieldFloat, found.fieldFloat ); - } ); + testField( + context, basic, found -> { + assertThat( found.primitiveFloat ).isEqualTo( primitiveFloat ); + assertThat( found.fieldFloat ).isEqualTo( fieldFloat ); + } + ); } @Test @@ -147,10 +149,12 @@ public void testDoubleType(VertxTestContext context) { basic.primitiveDouble = primitiveDouble; basic.fieldDouble = fieldDouble; - testField( context, basic, found -> { - assertEquals( primitiveDouble, found.primitiveDouble ); - assertEquals( fieldDouble, found.fieldDouble ); - } ); + testField( + context, basic, found -> { + assertThat( found.primitiveDouble ).isEqualTo( primitiveDouble ); + assertThat( found.fieldDouble ).isEqualTo( fieldDouble ); + } + ); } @Test @@ -162,13 +166,15 @@ public void testBooleanType(VertxTestContext context) { basic.booleanYesNo = Boolean.FALSE; basic.booleanNumeric = Boolean.FALSE; - testField( context, basic, found -> { - assertEquals( true, found.primitiveBoolean ); - assertEquals( Boolean.FALSE, found.fieldBoolean ); - assertEquals( Boolean.FALSE, found.booleanTrueFalse ); - assertEquals( Boolean.FALSE, found.booleanYesNo ); - assertEquals( Boolean.FALSE, found.booleanNumeric ); - } ); + testField( + context, basic, found -> { + assertThat( found.primitiveBoolean ).isEqualTo( true ); + assertThat( found.fieldBoolean ).isEqualTo( Boolean.FALSE ); + assertThat( found.booleanTrueFalse ).isEqualTo( Boolean.FALSE ); + assertThat( found.booleanYesNo ).isEqualTo( Boolean.FALSE ); + assertThat( found.booleanNumeric ).isEqualTo( Boolean.FALSE ); + } + ); } @Test @@ -182,11 +188,13 @@ public void testBytesType(VertxTestContext context) { basic.primitiveBytes = primitiveBytes; basic.fieldByte = fieldByte; - testField( context, basic, found -> { - assertEquals( primitiveByte, found.primitiveByte ); - assertTrue( Objects.deepEquals( primitiveBytes, found.primitiveBytes ) ); - assertEquals( fieldByte, found.fieldByte ); - } ); + testField( + context, basic, found -> { + assertThat( found.primitiveByte ).isEqualTo( primitiveByte ); + assertThat( Objects.deepEquals( primitiveBytes, found.primitiveBytes ) ).isTrue(); + assertThat( found.fieldByte ).isEqualTo( fieldByte ); + } + ); } @Test @@ -195,7 +203,7 @@ public void testURL(VertxTestContext context) throws Exception { Basic basic = new Basic(); basic.url = url; - testField( context, basic, found -> assertEquals( url, found.url ) ); + testField( context, basic, found -> assertThat( found.url ).isEqualTo( url ) ); } @Test @@ -204,7 +212,7 @@ public void testDateType(VertxTestContext context) { Basic basic = new Basic(); basic.date = date; - testField( context, basic, found -> assertEquals( date, found.date ) ); + testField( context, basic, found -> assertThat( found.date ).isEqualTo( date ) ); } @Test @@ -213,10 +221,12 @@ public void testDateAsTimestampType(VertxTestContext context) { Basic basic = new Basic(); basic.dateAsTimestamp = date; - testField( context, basic, found -> { - assertTrue( found.dateAsTimestamp instanceof Timestamp ); - assertEquals( date, found.dateAsTimestamp ); - } ); + testField( + context, basic, found -> { + assertThat( found.dateAsTimestamp ).isInstanceOf( Timestamp.class ); + assertThat( found.dateAsTimestamp ).isEqualTo( new Timestamp( date.getTime() ) ); + } + ); } @Test @@ -225,15 +235,15 @@ public void testTimeZoneType(VertxTestContext context) { Basic basic = new Basic(); basic.timeZone = timeZone; - testField( context, basic, found -> assertEquals( basic.timeZone, found.timeZone ) ); + testField( context, basic, found -> assertThat( found.timeZone ).isEqualTo( basic.timeZone ) ); } @Test public void testCalendarAsDateType(VertxTestContext context) { Calendar calendar = GregorianCalendar.getInstance(); - calendar.set( Calendar.DAY_OF_MONTH, 15); - calendar.set( Calendar.MONTH, 7); - calendar.set( Calendar.YEAR, 2002); + calendar.set( Calendar.DAY_OF_MONTH, 15 ); + calendar.set( Calendar.MONTH, 7 ); + calendar.set( Calendar.YEAR, 2002 ); // TemporalType#Date only deals with year/month/day int expectedYear = calendar.get( Calendar.YEAR ); @@ -243,11 +253,13 @@ public void testCalendarAsDateType(VertxTestContext context) { Basic basic = new Basic(); basic.calendarAsDate = calendar; - testField( context, basic, found -> { - assertEquals( expectedDay, found.calendarAsDate.get( Calendar.DAY_OF_MONTH ) ); - assertEquals( expectedMonth, found.calendarAsDate.get( Calendar.MONTH ) ); - assertEquals( expectedYear, found.calendarAsDate.get( Calendar.YEAR ) ); - } ); + testField( + context, basic, found -> { + assertThat( found.calendarAsDate.get( Calendar.DAY_OF_MONTH ) ).isEqualTo( expectedDay ); + assertThat( found.calendarAsDate.get( Calendar.MONTH ) ).isEqualTo( expectedMonth ); + assertThat( found.calendarAsDate.get( Calendar.YEAR ) ).isEqualTo( expectedYear ); + } + ); } @Test @@ -260,10 +272,12 @@ public void testCalendarAsTimestampType(VertxTestContext context) { Basic basic = new Basic(); basic.calendarAsTimestamp = calendar; - testField( context, basic, found -> { - String actual = format( found.calendarAsTimestamp ); - assertEquals( expected, actual ); - } ); + testField( + context, basic, found -> { + String actual = format( found.calendarAsTimestamp ); + assertThat( actual ).isEqualTo( expected ); + } + ); } private static String format(Calendar calendar) { @@ -277,7 +291,7 @@ public void testLocalDateType(VertxTestContext context) { Basic basic = new Basic(); basic.localDate = now; - testField( context, basic, found -> assertEquals( now, found.localDate ) ); + testField( context, basic, found -> assertThat( found.localDate ).isEqualTo( now ) ); } @Test @@ -289,7 +303,7 @@ public void testLocalDateTimeType(VertxTestContext context) { Basic basic = new Basic(); basic.localDateTime = now; - testField( context, basic, found -> assertEquals( now, found.localDateTime ) ); + testField( context, basic, found -> assertThat( found.localDateTime ).isEqualTo( now ) ); } @Test @@ -299,11 +313,13 @@ public void testEnumType(VertxTestContext context) { basic.coverAsOrdinal = Cover.HARD; basic.coverAsString = Cover.SOFT; - testField( context, basic, found -> { - assertEquals( Cover.HARDER, found.cover ); - assertEquals( Cover.HARD, found.coverAsOrdinal ); - assertEquals( Cover.SOFT, found.coverAsString ); - } ); + testField( + context, basic, found -> { + assertThat( found.cover ).isEqualTo( Cover.HARDER ); + assertThat( found.coverAsOrdinal ).isEqualTo( Cover.HARD ); + assertThat( found.coverAsString ).isEqualTo( Cover.SOFT ); + } + ); } @Test @@ -312,9 +328,11 @@ public void testEmbeddableType(VertxTestContext context) { Basic basic = new Basic(); basic.embed = embed; - testField( context, basic, found -> { - assertEquals( embed, found.embed ); - } ); + testField( + context, basic, found -> { + assertThat( found.embed ).isEqualTo( embed ); + } + ); } @Test @@ -322,9 +340,11 @@ public void testBigIntegerWithConverterType(VertxTestContext context) { Basic basic = new Basic(); basic.bigIntegerAsString = BigInteger.TEN; - testField( context, basic, found -> { - assertEquals( BigInteger.TEN.floatValue(), found.bigIntegerAsString.floatValue() ); - } ); + testField( + context, basic, found -> { + assertThat( found.bigIntegerAsString.floatValue() ).isEqualTo( BigInteger.TEN.floatValue() ); + } + ); } @Test @@ -332,9 +352,11 @@ public void testBigDecimalWithUserType(VertxTestContext context) { Basic basic = new Basic(); basic.bigDecimalAsString = BigDecimal.TEN; - testField( context, basic, found -> { - assertEquals( BigInteger.TEN.floatValue(), found.bigDecimalAsString.floatValue() ); - } ); + testField( + context, basic, found -> { + assertThat( found.bigDecimalAsString.floatValue() ).isEqualTo( BigInteger.TEN.floatValue() ); + } + ); } @Test @@ -344,10 +366,12 @@ public void testSerializableType(VertxTestContext context) { Basic basic = new Basic(); basic.thing = thing; - testField( context, basic, found -> { - assertTrue( found.thing instanceof String[] ); - assertTrue( Objects.deepEquals( thing, found.thing ) ); - } ); + testField( + context, basic, found -> { + assertThat( found.thing instanceof String[] ).isTrue(); + assertThat( Objects.deepEquals( thing, found.thing ) ).isTrue(); + } + ); } @Test @@ -355,7 +379,7 @@ public void testUUIDType(VertxTestContext context) { Basic basic = new Basic(); basic.uuid = UUID.fromString( "123e4567-e89b-42d3-a456-556642440000" ); - testField( context, basic, found -> assertEquals( basic.uuid, found.uuid ) ); + testField( context, basic, found -> assertThat( found.uuid ).isEqualTo( basic.uuid ) ); } @Test @@ -363,15 +387,19 @@ public void testDecimalType(VertxTestContext context) { Basic basic = new Basic(); basic.bigDecimal = new BigDecimal( "12.12" ); - testField( context, basic, found -> assertEquals( basic.bigDecimal.floatValue(), found.bigDecimal.floatValue() ) ); + testField( + context, + basic, + found -> assertThat( found.bigDecimal.floatValue() ).isEqualTo( basic.bigDecimal.floatValue() ) + ); } @Test public void testBigIntegerType(VertxTestContext context) { Basic basic = new Basic(); - basic.bigInteger = BigInteger.valueOf( 123L); + basic.bigInteger = BigInteger.valueOf( 123L ); - testField( context, basic, found -> assertEquals( basic.bigInteger, found.bigInteger ) ); + testField( context, basic, found -> assertThat( found.bigInteger ).isEqualTo( basic.bigInteger ) ); } @Test @@ -379,10 +407,9 @@ public void testLocalTimeType(VertxTestContext context) { Basic basic = new Basic(); basic.localTime = LocalTime.now(); - testField( context, basic, found -> assertEquals( - basic.localTime.truncatedTo( ChronoUnit.MINUTES ), - found.localTime.truncatedTo( ChronoUnit.MINUTES ) - ) ); + testField( context, basic, found -> assertThat( found.localTime.truncatedTo( ChronoUnit.MINUTES ) ) + .isEqualTo( basic.localTime.truncatedTo( ChronoUnit.MINUTES ) ) + ); } @Test @@ -392,11 +419,13 @@ public void testDateAsTimeType(VertxTestContext context) { Basic basic = new Basic(); basic.dateAsTime = date; - testField( context, basic, found -> { - SimpleDateFormat timeSdf = new SimpleDateFormat( "HH:mm:ss" ); - assertTrue( found.dateAsTime instanceof Time); - assertEquals( timeSdf.format( date ), timeSdf.format( found.dateAsTime ) ); - } ); + testField( + context, basic, found -> { + SimpleDateFormat timeSdf = new SimpleDateFormat( "HH:mm:ss" ); + assertThat( found.dateAsTime instanceof Time ).isTrue(); + assertThat( timeSdf.format( found.dateAsTime ) ).isEqualTo( timeSdf.format( date ) ); + } + ); } @Test @@ -404,10 +433,12 @@ public void testDuration(VertxTestContext context) { Basic basic = new Basic(); basic.duration = Duration.ofMillis( 1894657L ); - testField( context, basic, found -> { - assertNotNull( found.duration ); - assertEquals( basic.duration, found.duration ); - } ); + testField( + context, basic, found -> { + assertThat( found.duration ).isNotNull(); + assertThat( found.duration ).isEqualTo( basic.duration ); + } + ); } @Test @@ -416,10 +447,12 @@ public void testInstant(VertxTestContext context) { Basic basic = new Basic(); basic.instant = Instant.now(); - testField( context, basic, found -> { - assertNotNull( found.instant ); - assertWithTruncationThat( found.instant ).isEqualTo( basic.instant ); - } ); + testField( + context, basic, found -> { + assertThat( found.instant ).isNotNull(); + assertWithTruncationThat( found.instant ).isEqualTo( basic.instant ); + } + ); } @Test @@ -430,61 +463,61 @@ public void testCallbacksAndVersioning(VertxTestContext context) { basik.parent = parent; test( - context, - openSession() - .thenCompose( s -> s.persist( basik.parent ).thenCompose( v -> s.persist( basik ) ) - .thenAccept( v -> assertTrue( basik.prePersisted && !basik.postPersisted ) ) - .thenAccept( v -> assertTrue( basik.parent.prePersisted && !basik.parent.postPersisted ) ) + context, getSessionFactory() + .withSession( s -> s.persist( basik.parent ).thenCompose( v -> s.persist( basik ) ) + .thenAccept( v -> assertThat( basik.prePersisted && !basik.postPersisted ).isTrue() ) + .thenAccept( v -> assertThat( basik.parent.prePersisted && !basik.parent.postPersisted ).isTrue() ) .thenCompose( v -> s.flush() ) - .thenAccept( v -> assertTrue( basik.prePersisted && basik.postPersisted ) ) - .thenAccept( v -> assertTrue( basik.parent.prePersisted && basik.parent.postPersisted ) ) + .thenAccept( v -> assertThat( basik.prePersisted && basik.postPersisted ).isTrue() ) + .thenAccept( v -> assertThat( basik.parent.prePersisted && basik.parent.postPersisted ).isTrue() ) ) - .thenCompose( v -> openSession() - .thenCompose( s2 -> s2.find( Basic.class, basik.getId() ) - .thenCompose( basic -> { - assertNotNull( basic ); - assertTrue( basic.loaded ); - assertEquals( basic.string, basik.string ); - assertEquals( basic.cover, basik.cover ); - assertEquals( basic.version, 0 ); - - basic.string = "Goodbye"; - basic.cover = Cover.SOFT; - basic.parent = new Basic( "New Parent" ); - return s2.persist( basic.parent ) - .thenCompose( vv -> s2.flush() ) - .thenAccept( vv -> { - assertNotNull( basic ); - assertTrue( basic.postUpdated && basic.preUpdated ); - assertFalse( basic.postPersisted && basic.prePersisted ); - assertTrue( basic.parent.postPersisted && basic.parent.prePersisted ); - assertEquals( basic.version, 1 ); - } ); - } ) - ) ) - .thenCompose( v -> openSession() - .thenCompose( s3 -> s3.find( Basic.class, basik.getId() ) - .thenCompose( basic -> { - assertFalse( basic.postUpdated && basic.preUpdated ); - assertFalse( basic.postPersisted && basic.prePersisted ); - assertEquals( basic.version, 1 ); - assertEquals( basic.string, "Goodbye" ); - return s3.remove( basic ) - .thenAccept( vv -> assertTrue( !basic.postRemoved && basic.preRemoved ) ) - .thenCompose( vv -> s3.flush() ) - .thenAccept( vv -> assertTrue( basic.postRemoved && basic.preRemoved ) ); - } ) - ) ) - .thenCompose( v -> openSession() - .thenCompose( s4 -> s4.find( Basic.class, basik.getId() ) ) - .thenAccept( Assertions::assertNull ) ) + .thenCompose( v -> getSessionFactory().withSession( s2 -> s2 + .find( Basic.class, basik.getId() ) + .thenCompose( basic -> { + assertThat( basic ).isNotNull(); + assertThat( basic.loaded ).isTrue(); + assertThat( basic.string ).isEqualTo( basik.string ); + assertThat( basic.cover ).isEqualTo( basik.cover ); + assertThat( basic.version ).isEqualTo( 0 ); + + basic.string = "Goodbye"; + basic.cover = Cover.SOFT; + basic.parent = new Basic( "New Parent" ); + return s2.persist( basic.parent ) + .thenCompose( vv -> s2.flush() ) + .thenAccept( vv -> { + assertThat( basic ).isNotNull(); + assertThat( basic.postUpdated && basic.preUpdated ).isTrue(); + assertThat( basic.postPersisted && basic.prePersisted ).isFalse(); + assertThat( basic.parent.postPersisted && basic.parent.prePersisted ).isTrue(); + assertThat( basic.version ).isEqualTo( 1 ); + } ); + } ) + ) ) + .thenCompose( v -> getSessionFactory().withSession( s3 -> s3 + .find( Basic.class, basik.getId() ) + .thenCompose( basic -> { + assertThat( basic.postUpdated && basic.preUpdated ).isFalse(); + assertThat( basic.postPersisted && basic.prePersisted ).isFalse(); + assertThat( basic.version ).isEqualTo( 1 ); + assertThat( basic.string ).isEqualTo( "Goodbye" ); + return s3.remove( basic ) + .thenAccept( vv -> assertThat( !basic.postRemoved && basic.preRemoved ).isTrue() ) + .thenCompose( vv -> s3.flush() ) + .thenAccept( vv -> assertThat( basic.postRemoved && basic.preRemoved ).isTrue() ); + } ) + ) ) + .thenCompose( v -> getSessionFactory().withSession( s4 -> s4 + .find( Basic.class, basik.getId() ) + .thenAccept( result -> assertThat( result ).isNull() ) + ) ) ); } enum Cover {HARDER, HARD, SOFT} @Embeddable - static class Embed { + public static class Embed { String one; String two; @@ -608,12 +641,12 @@ private static class Basic { UUID uuid; - @Column(name="dessimal") + @Column(name = "dessimal") BigDecimal bigDecimal; - @Column(name="inteja") + @Column(name = "inteja") BigInteger bigInteger; - @Column(name="localtyme") + @Column(name = "localtyme") private LocalTime localTime; @Temporal(TemporalType.TIME) Date dateAsTime; diff --git a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/types/JsonTypeTest.java b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/types/JsonTypeTest.java index 30c5a8512..2a2d0bd2d 100644 --- a/hibernate-reactive-core/src/test/java/org/hibernate/reactive/types/JsonTypeTest.java +++ b/hibernate-reactive-core/src/test/java/org/hibernate/reactive/types/JsonTypeTest.java @@ -7,12 +7,15 @@ import java.util.Collection; import java.util.List; +import java.util.Map; import java.util.Objects; -import java.util.concurrent.CompletionStage; import java.util.function.Consumer; +import org.hibernate.annotations.JdbcTypeCode; import org.hibernate.reactive.BaseReactiveTest; import org.hibernate.reactive.annotations.DisabledFor; +import org.hibernate.reactive.annotations.EnabledFor; +import org.hibernate.type.SqlTypes; import org.junit.jupiter.api.Test; @@ -26,19 +29,18 @@ import jakarta.persistence.Version; import static java.util.concurrent.TimeUnit.MINUTES; +import static org.assertj.core.api.Assertions.assertThat; import static org.hibernate.reactive.containers.DatabaseConfiguration.DBType.DB2; import static org.hibernate.reactive.containers.DatabaseConfiguration.DBType.MARIA; import static org.hibernate.reactive.containers.DatabaseConfiguration.DBType.ORACLE; +import static org.hibernate.reactive.containers.DatabaseConfiguration.DBType.POSTGRESQL; import static org.hibernate.reactive.containers.DatabaseConfiguration.DBType.SQLSERVER; -import static org.hibernate.reactive.util.impl.CompletionStages.loop; import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; /** * Test types that we expect to work only on selected DBs. */ @Timeout(value = 10, timeUnit = MINUTES) - @DisabledFor(value = DB2, reason = "java.sql.SQLException: The object 'HREACT.JSONENTITY' provided is not defined, SQLCODE=-204 SQLSTATE=42704") @DisabledFor(value = SQLSERVER, reason = "java.lang.IllegalArgumentException: Unsupported value class: class io.vertx.core.json.JsonObject") @DisabledFor(value = ORACLE, reason = "java.sql.SQLException: ORA-17004: Invalid column type: https://docs.oracle.com/error-help/db/ora-17004/") @@ -50,13 +52,37 @@ protected Collection> annotatedEntities() { return List.of( Basic.class ); } - @Override - public CompletionStage deleteEntities(Class... types) { - return getSessionFactory() - .withTransaction( s -> loop( types, entityClass -> s - .createSelectionQuery( "from JsonEntity", entityClass ) - .getResultList() - .thenCompose( list -> loop( list, entity -> s.remove( entity ) ) ) ) ); + @Test + @EnabledFor(POSTGRESQL) + public void nativeQuestionMarkOperatorForPostgres(VertxTestContext context) { + Basic basic = new Basic(); + basic.jsonAsMap = Map.of( "sport", "Cheese Rolling" ); + + test( context, getMutinySessionFactory() + .withTransaction( s -> s.persist( basic ) ) + .chain( () -> getMutinySessionFactory().withTransaction( s -> s + .createNativeQuery( "select id from JsonEntity where jsonAsMap -> 'sport' \\? 'Cheese Rolling'" ) + .getSingleResult() ) + ) + .invoke( result -> assertThat( result ).isEqualTo( basic.id ) ) + ); + } + + @Test + @EnabledFor(POSTGRESQL) + public void nativeQuestionMarkOperatorWithParameterForPostgres(VertxTestContext context) { + Basic basic = new Basic(); + basic.jsonAsMap = Map.of( "sport", "Chess boxing" ); + + test( context, getMutinySessionFactory() + .withTransaction( s -> s.persist( basic ) ) + .chain( () -> getMutinySessionFactory().withTransaction( s -> s + .createNativeQuery( "select id from JsonEntity where jsonAsMap -> 'sport' \\? ?" ) + .setParameter( 1, "Chess boxing" ) + .getSingleResult() ) + ) + .invoke( result -> assertThat( result ).isEqualTo( basic.id ) ) + ); } @Test @@ -79,16 +105,15 @@ public void testNullJsonType(VertxTestContext context) { * Persist the entity, find it and execute the assertions */ private void testField(VertxTestContext context, Basic original, Consumer consumer) { - test( - context, - getSessionFactory().withTransaction( (s, t) -> s.persist( original ) ) - .thenCompose( v -> openSession() ) - .thenCompose( s2 -> s2.find( Basic.class, original.id ) - .thenAccept( found -> { - assertNotNull( found ); - assertEquals( original, found ); - consumer.accept( found ); - } ) ) + test( context, getSessionFactory() + .withTransaction( s -> s.persist( original ) ) + .thenCompose( v -> getSessionFactory().withTransaction( s -> s + .find( Basic.class, original.id ) ) + ) + .thenAccept( found -> { + assertThat( found ).isEqualTo( original ); + consumer.accept( found ); + } ) ); } @@ -105,6 +130,9 @@ private static class Basic { private JsonObject jsonObj; + @JdbcTypeCode(SqlTypes.JSON) + Map jsonAsMap; + public Basic() { } diff --git a/hibernate-reactive-core/src/test/resources/oracle-SchemaValidationTest.sql b/hibernate-reactive-core/src/test/resources/oracle-SchemaValidationTest.sql new file mode 100644 index 000000000..69a01b357 --- /dev/null +++ b/hibernate-reactive-core/src/test/resources/oracle-SchemaValidationTest.sql @@ -0,0 +1,12 @@ +-- Import file for testing schema validation in SchemaValidationTest +drop table if exists Fruit cascade constraints +drop sequence if exists Fruit_SEQ + +-- Create the table manually, so that we can check if the validation succeeds +create sequence fruit_seq start with 1 increment by 50; +create table Fruit (id number(10,0) not null, something_name nvarchar2(20) not null, primary key (id)) + +INSERT INTO fruit(id, something_name) VALUES (1, 'Cherry'); +INSERT INTO fruit(id, something_name) VALUES (2, 'Apple'); +INSERT INTO fruit(id, something_name) VALUES (3, 'Banana'); +ALTER SEQUENCE fruit_seq RESTART start with 4; \ No newline at end of file diff --git a/integration-tests/bytecode-enhancements-it/build.gradle b/integration-tests/bytecode-enhancements-it/build.gradle index 17ffdf10e..c29c0f376 100644 --- a/integration-tests/bytecode-enhancements-it/build.gradle +++ b/integration-tests/bytecode-enhancements-it/build.gradle @@ -10,41 +10,39 @@ buildscript { } plugins { - id "org.hibernate.orm" version "${hibernateOrmGradlePluginVersion}" + alias(libs.plugins.org.hibernate.orm) } description = 'Bytecode enhancements integration tests' -ext { - log4jVersion = '2.20.0' - assertjVersion = '3.27.3' -} - dependencies { implementation project(':hibernate-reactive-core') // JPA metamodel generation for criteria queries (optional) - annotationProcessor "org.hibernate.orm:hibernate-jpamodelgen:${hibernateOrmVersion}" + annotationProcessor(libs.org.hibernate.orm.hibernate.jpamodelgen) // Testing on one database should be enough - runtimeOnly "io.vertx:vertx-pg-client:${vertxSqlClientVersion}" + runtimeOnly(libs.io.vertx.vertx.pg.client) // Allow authentication to PostgreSQL using SCRAM: - runtimeOnly 'com.ongres.scram:client:2.1' + runtimeOnly(libs.com.ongres.scram.client) // logging - runtimeOnly "org.apache.logging.log4j:log4j-core:${log4jVersion}" + runtimeOnly(libs.org.apache.logging.log4j.log4j.core) // Testcontainers - testImplementation "org.testcontainers:postgresql:${testcontainersVersion}" + testImplementation(libs.org.testcontainers.postgresql) // Testing - testImplementation "org.assertj:assertj-core:${assertjVersion}" - testImplementation "io.vertx:vertx-junit5:${vertxSqlClientVersion}" + testImplementation(libs.org.assertj.assertj.core) + testImplementation(libs.io.vertx.vertx.junit5) } -// Optional: enable the bytecode enhancements -hibernate { enhancement } +hibernate { + enhancement { + // We want everything enabled for the tests + } +} // Print a summary of the results of the tests (number of failures, successes and skipped) // This is the same as the one in hibernate-reactive-core @@ -113,4 +111,4 @@ tasks.addRule( "Pattern testDb" ) { String taskName -> } } -} \ No newline at end of file +} diff --git a/integration-tests/bytecode-enhancements-it/src/test/java/org/hibernate/reactive/it/BaseReactiveIT.java b/integration-tests/bytecode-enhancements-it/src/test/java/org/hibernate/reactive/it/BaseReactiveIT.java index 2388cb421..8f6081a8a 100644 --- a/integration-tests/bytecode-enhancements-it/src/test/java/org/hibernate/reactive/it/BaseReactiveIT.java +++ b/integration-tests/bytecode-enhancements-it/src/test/java/org/hibernate/reactive/it/BaseReactiveIT.java @@ -52,9 +52,7 @@ public abstract class BaseReactiveIT { // These properties are in DatabaseConfiguration in core public static final boolean USE_DOCKER = Boolean.getBoolean( "docker" ); - public static final DockerImageName IMAGE_NAME = DockerImageName - .parse( "docker.io/postgres:17.5" ) - .asCompatibleSubstituteFor( "postgres" ); + public static final DockerImageName IMAGE_NAME = DockerImage.fromDockerfile( "postgresql" ); public static final String USERNAME = "hreact"; public static final String PASSWORD = "hreact"; diff --git a/integration-tests/bytecode-enhancements-it/src/test/java/org/hibernate/reactive/it/DockerImage.java b/integration-tests/bytecode-enhancements-it/src/test/java/org/hibernate/reactive/it/DockerImage.java new file mode 100644 index 000000000..b5621249d --- /dev/null +++ b/integration-tests/bytecode-enhancements-it/src/test/java/org/hibernate/reactive/it/DockerImage.java @@ -0,0 +1,125 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.it; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import org.testcontainers.utility.DockerImageName; + +/** + * A utility class with methods to generate a {@link DockerImageName} for Testcontainers. + *

+ * Testcontainers might not work if the image required is available in multiple different registries (for example when + * using podman instead of docker). + * These methods make sure to pick a registry as default. + *

+ */ +public final class DockerImage { + + /** + * The absolute path of the project root that we have set in Gradle. + */ + private static final String PROJECT_ROOT = System.getProperty( "hibernate.reactive.project.root" ); + + /** + * The path to the directory containing all the Dockerfile files + */ + private static final Path DOCKERFILE_DIR_PATH = Path.of( PROJECT_ROOT ).resolve( "tooling" ).resolve( "docker" ); + + /** + * Extract the image name and version from the first FROM instruction in the Dockerfile. + * Note that everything else is ignored. + */ + public static DockerImageName fromDockerfile(String databaseName) { + try { + final ImageInformation imageInformation = readFromInstruction( databaseName.toLowerCase() ); + return imageName( imageInformation.getRegistry(), imageInformation.getImage(), imageInformation.getVersion() ); + } + catch (IOException e) { + throw new RuntimeException( e ); + } + } + + public static DockerImageName imageName(String registry, String image, String version) { + return DockerImageName + .parse( registry + "/" + image + ":" + version ) + .asCompatibleSubstituteFor( image ); + } + + private static class ImageInformation { + private final String registry; + private final String image; + private final String version; + + public ImageInformation(String fullImageInfo) { + // FullImageInfo pattern: /: + // For example: "docker.io/cockroachdb/cockroach:v24.3.13" becomes registry = "docker.io", image = "cockroachdb/cockroach", version = "v24.3.13" + final int registryEndPos = fullImageInfo.indexOf( '/' ); + final int imageEndPos = fullImageInfo.lastIndexOf( ':' ); + this.registry = fullImageInfo.substring( 0, registryEndPos ); + this.image = fullImageInfo.substring( registryEndPos + 1, imageEndPos ); + this.version = fullImageInfo.substring( imageEndPos + 1 ); + } + + public String getRegistry() { + return registry; + } + + public String getImage() { + return image; + } + + public String getVersion() { + return version; + } + + @Override + public String toString() { + return registry + "/" + image + ":" + version; + } + } + + private static Path dockerFilePath(String database) { + // Get project root from system property set by Gradle, with fallback + return DOCKERFILE_DIR_PATH.resolve( database.toLowerCase() + ".Dockerfile" ); + } + + private static ImageInformation readFromInstruction(String database) throws IOException { + return readFromInstruction( dockerFilePath( database ) ); + } + + /** + * Read a Dockerfile and extract the first FROM instruction. + * + * @param dockerfilePath path to the Dockerfile + * @return the first FROM instruction found, or empty if none found + * @throws IOException if the file cannot be read + */ + private static ImageInformation readFromInstruction(Path dockerfilePath) throws IOException { + if ( !Files.exists( dockerfilePath ) ) { + throw new FileNotFoundException( "Dockerfile not found: " + dockerfilePath ); + } + + List lines = Files.readAllLines( dockerfilePath ); + for ( String line : lines ) { + // Skip comments and empty lines + String trimmedLine = line.trim(); + if ( trimmedLine.isEmpty() || trimmedLine.startsWith( "#" ) ) { + continue; + } + + if ( trimmedLine.startsWith( "FROM " ) ) { + return new ImageInformation( trimmedLine.substring( "FROM ".length() ) ); + } + } + + throw new IOException( " Missing FROM instruction in " + dockerfilePath ); + } +} diff --git a/integration-tests/bytecode-enhancements-it/src/test/java/org/hibernate/reactive/it/LazyBasicFieldTest.java b/integration-tests/bytecode-enhancements-it/src/test/java/org/hibernate/reactive/it/LazyBasicFieldTest.java index 68f234b26..fe69f3534 100644 --- a/integration-tests/bytecode-enhancements-it/src/test/java/org/hibernate/reactive/it/LazyBasicFieldTest.java +++ b/integration-tests/bytecode-enhancements-it/src/test/java/org/hibernate/reactive/it/LazyBasicFieldTest.java @@ -5,10 +5,18 @@ */ package org.hibernate.reactive.it; +import org.hibernate.LazyInitializationException; +import org.hibernate.boot.registry.StandardServiceRegistryBuilder; +import org.hibernate.cfg.Configuration; + +import org.hibernate.testing.SqlStatementTracker; + +import io.smallrye.mutiny.Uni; import java.util.Collection; import java.util.List; import java.util.concurrent.TimeUnit; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import io.vertx.junit5.Timeout; @@ -22,6 +30,33 @@ */ @Timeout(value = 10, timeUnit = TimeUnit.MINUTES) public class LazyBasicFieldTest extends BaseReactiveIT { + + private static SqlStatementTracker sqlTracker; + + @Override + protected Configuration constructConfiguration() { + Configuration configuration = super.constructConfiguration(); + + // Construct a tracker that collects query statements via the SqlStatementLogger framework. + // Pass in configuration properties to hand off any actual logging properties + sqlTracker = new SqlStatementTracker( LazyBasicFieldTest::selectQueryFilter, configuration.getProperties() ); + return configuration; + } + + @BeforeEach + public void clearTracker() { + sqlTracker.clear(); + } + + @Override + protected void addServices(StandardServiceRegistryBuilder builder) { + sqlTracker.registerService( builder ); + } + + private static boolean selectQueryFilter(String s) { + return s.toLowerCase().startsWith( "select " ); + } + @Override protected Collection> annotatedEntities() { return List.of( Crew.class ); @@ -47,4 +82,86 @@ public void testFetchBasicField(VertxTestContext context) { ) ) ) ); } + + @Test + public void testFetchBasicFieldAlsoInitializesIt(VertxTestContext context) { + final Crew emily = new Crew(); + emily.setId( 21L ); + emily.setName( "Emily Jackson" ); + emily.setRole( "Passenger" ); + emily.setFate( "Unknown" ); + + test( context, getMutinySessionFactory() + .withTransaction( session -> session.persist( emily ) ) + .chain( () -> getMutinySessionFactory() + .withSession( session -> session + .find( Crew.class, emily.getId() ) + .call( crew -> session.fetch( crew, Crew_.role ) + .invoke( role -> assertThat( role ).isEqualTo( emily.getRole() ) ) ) + .invoke( sqlTracker::clear ) + .call( crew -> session + .fetch( crew, Crew_.role ) + .invoke( role -> { + // No select query expected, the previous fetch must have initialized the role attribute + assertThat( sqlTracker.getLoggedQueries() ).hasSize( 0 ); + assertThat( role ).isEqualTo( emily.getRole() ); + } ) + ) ) ) + ); + } + + @Test + public void testTransparentLazyFetching(VertxTestContext context) { + final Crew emily = new Crew(); + emily.setId( 21L ); + emily.setName( "Emily Jackson" ); + emily.setRole( "Passenger" ); + emily.setFate( "Unknown" ); + + test( context, assertThrown( LazyInitializationException.class, + getMutinySessionFactory().withTransaction( session -> session.persist( emily ) ) + .chain( () -> getMutinySessionFactory().withSession( session -> session + .find( Crew.class, emily.getId() ) + // getRole() must throw a LazyInitializationException because we are not using + // Mutiny.fetch to load a lazy field + .map( Crew::getRole ) ) ) + ).invoke( exception -> assertThat( exception ) + .as( "Expected LazyInitializationException not thrown" ) + .hasMessageContaining( "Reactive sessions do not support transparent lazy fetching" ) + ) + ); + } + + @Test + public void testGetReferenceAndTransparentLazyFetching(VertxTestContext context) { + final Crew emily = new Crew(); + emily.setId( 21L ); + emily.setName( "Emily Jackson" ); + emily.setRole( "Passenger" ); + emily.setFate( "Unknown" ); + + test( + context, assertThrown( + LazyInitializationException.class, getMutinySessionFactory() + .withTransaction( session -> session.persist( emily ) ) + .chain( () -> getMutinySessionFactory().withSession( session -> { + Crew crew = session.getReference( Crew.class, emily.getId() ); + // getRole() must throw a LazyInitializationException because we are not using + // Mutiny.fetch to load a lazy field + String role = crew.getRole(); + return Uni.createFrom() + .failure( new AssertionError( "Expected LazyInitializationException not thrown" ) ); + } ) ) + ).invoke( exception -> assertThat( exception ) + .as( "Expected LazyInitializationException not thrown" ) + .hasMessageContaining( "Reactive sessions do not support transparent lazy fetching" ) ) + ); + } + + public static Uni assertThrown(Class expectedException, Uni uni) { + return uni.onItemOrFailure().transform( (s, e) -> { + assertThat( e ).isInstanceOf( expectedException ); + return (U) e; + } ); + } } diff --git a/integration-tests/bytecode-enhancements-it/src/test/java/org/hibernate/testing/SqlStatementTracker.java b/integration-tests/bytecode-enhancements-it/src/test/java/org/hibernate/testing/SqlStatementTracker.java new file mode 100644 index 000000000..bfe415809 --- /dev/null +++ b/integration-tests/bytecode-enhancements-it/src/test/java/org/hibernate/testing/SqlStatementTracker.java @@ -0,0 +1,107 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.testing; + +import org.hibernate.boot.registry.StandardServiceInitiator; +import org.hibernate.boot.registry.StandardServiceRegistryBuilder; +import org.hibernate.engine.jdbc.internal.Formatter; +import org.hibernate.engine.jdbc.spi.SqlStatementLogger; +import org.hibernate.reactive.provider.Settings; +import org.hibernate.service.spi.ServiceRegistryImplementor; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.function.Predicate; + +/** + * Track sql queries. + *

+ * Check {@link #registerService(StandardServiceRegistryBuilder)} to register an instance of this class. + * It's not thread-safe. + *

+ * @see #registerService(StandardServiceRegistryBuilder) + */ +public class SqlStatementTracker extends SqlStatementLogger { + + private final Predicate filter; + + private final List statements = new ArrayList<>(); + + public SqlStatementTracker(Predicate predicate) { + this.filter = predicate; + } + + public SqlStatementTracker(Predicate predicate, Properties properties) { + super( + toBoolean( properties.get( Settings.SHOW_SQL ) ), + toBoolean( properties.get( Settings.FORMAT_SQL ) ), + toBoolean( properties.get( Settings.HIGHLIGHT_SQL ) ), + toLong( properties.get( Settings.LOG_SLOW_QUERY ) ) + ); + this.filter = predicate; + } + + private static boolean toBoolean(Object obj) { + return Boolean.parseBoolean( (String) obj ); + } + + private static long toLong(Object obj) { + if ( obj == null ) { + return 0; + } + return Long.parseLong( (String) obj ); + } + + @Override + public void logStatement(String statement, Formatter formatter) { + addSql( statement ); + super.logStatement( statement, formatter ); + } + + private void addSql(String sql) { + if ( filter.test( sql ) ) { + statements.add( sql ); + } + } + + public List getLoggedQueries() { + return statements; + } + + public void clear() { + statements.clear(); + } + + /** + * Register the current sql tracker to the {@link StandardServiceRegistryBuilder}. + * + * @param builder the {@link StandardServiceRegistryBuilder} for the creation of the factory + */ + public void registerService(StandardServiceRegistryBuilder builder) { + Initiator initiator = new Initiator( this ); + builder.addInitiator( initiator ); + } + + private static class Initiator implements StandardServiceInitiator { + private final SqlStatementTracker sqlStatementTracker; + + public Initiator(SqlStatementTracker sqlStatementTracker) { + this.sqlStatementTracker = sqlStatementTracker; + } + + @Override + public SqlStatementLogger initiateService(Map configurationValues, ServiceRegistryImplementor registry) { + return sqlStatementTracker; + } + + @Override + public Class getServiceInitiated() { + return SqlStatementLogger.class; + } + } +} diff --git a/integration-tests/hibernate-validator-postgres-it/build.gradle b/integration-tests/hibernate-validator-postgres-it/build.gradle index 27689b24d..61500ef81 100644 --- a/integration-tests/hibernate-validator-postgres-it/build.gradle +++ b/integration-tests/hibernate-validator-postgres-it/build.gradle @@ -10,39 +10,34 @@ buildscript { } plugins { - id "org.hibernate.orm" version "${hibernateOrmGradlePluginVersion}" + alias(libs.plugins.org.hibernate.orm) } description = 'Quarkus QE integration tests' -ext { - log4jVersion = '2.20.0' - assertjVersion = '3.27.3' -} - dependencies { implementation project(':hibernate-reactive-core') - implementation "org.hibernate.validator:hibernate-validator:8.0.2.Final" - runtimeOnly 'org.glassfish.expressly:expressly:5.0.0' + implementation(libs.org.hibernate.validator.hibernate.validator) + runtimeOnly(libs.org.glassfish.expressly.expressly) // JPA metamodel generation for criteria queries (optional) - annotationProcessor "org.hibernate.orm:hibernate-jpamodelgen:${hibernateOrmVersion}" + annotationProcessor(libs.org.hibernate.orm.hibernate.jpamodelgen) // Testing on one database should be enough - runtimeOnly "io.vertx:vertx-pg-client:${vertxSqlClientVersion}" + runtimeOnly(libs.io.vertx.vertx.pg.client) // Allow authentication to PostgreSQL using SCRAM: - runtimeOnly 'com.ongres.scram:client:2.1' + runtimeOnly(libs.com.ongres.scram.client) // logging - runtimeOnly "org.apache.logging.log4j:log4j-core:${log4jVersion}" + runtimeOnly(libs.org.apache.logging.log4j.log4j.core) // Testcontainers - testImplementation "org.testcontainers:postgresql:${testcontainersVersion}" + testImplementation(libs.org.testcontainers.postgresql) // Testing - testImplementation "org.assertj:assertj-core:${assertjVersion}" - testImplementation "io.vertx:vertx-junit5:${vertxSqlClientVersion}" + testImplementation(libs.org.assertj.assertj.core) + testImplementation(libs.io.vertx.vertx.junit5) } // Optional: enable the bytecode enhancements @@ -116,4 +111,4 @@ tasks.addRule( "Pattern testDb" ) { String taskName -> } } -} \ No newline at end of file +} diff --git a/integration-tests/hibernate-validator-postgres-it/src/test/java/org/hibernate/reactive/it/quarkus/qe/database/BaseReactiveIT.java b/integration-tests/hibernate-validator-postgres-it/src/test/java/org/hibernate/reactive/it/quarkus/qe/database/BaseReactiveIT.java index 7debdbb2c..fcefdaa84 100644 --- a/integration-tests/hibernate-validator-postgres-it/src/test/java/org/hibernate/reactive/it/quarkus/qe/database/BaseReactiveIT.java +++ b/integration-tests/hibernate-validator-postgres-it/src/test/java/org/hibernate/reactive/it/quarkus/qe/database/BaseReactiveIT.java @@ -52,9 +52,7 @@ public abstract class BaseReactiveIT { // These properties are in DatabaseConfiguration in core public static final boolean USE_DOCKER = Boolean.getBoolean( "docker" ); - public static final DockerImageName IMAGE_NAME = DockerImageName - .parse( "docker.io/postgres:17.5" ) - .asCompatibleSubstituteFor( "postgres" ); + public static final DockerImageName IMAGE_NAME = DockerImage.fromDockerfile( "postgresql" ); public static final String USERNAME = "hreact"; public static final String PASSWORD = "hreact"; diff --git a/integration-tests/hibernate-validator-postgres-it/src/test/java/org/hibernate/reactive/it/quarkus/qe/database/DockerImage.java b/integration-tests/hibernate-validator-postgres-it/src/test/java/org/hibernate/reactive/it/quarkus/qe/database/DockerImage.java new file mode 100644 index 000000000..7770da76c --- /dev/null +++ b/integration-tests/hibernate-validator-postgres-it/src/test/java/org/hibernate/reactive/it/quarkus/qe/database/DockerImage.java @@ -0,0 +1,125 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.it.quarkus.qe.database; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import org.testcontainers.utility.DockerImageName; + +/** + * A utility class with methods to generate a {@link DockerImageName} for Testcontainers. + *

+ * Testcontainers might not work if the image required is available in multiple different registries (for example when + * using podman instead of docker). + * These methods make sure to pick a registry as default. + *

+ */ +public final class DockerImage { + + /** + * The absolute path of the project root that we have set in Gradle. + */ + private static final String PROJECT_ROOT = System.getProperty( "hibernate.reactive.project.root" ); + + /** + * The path to the directory containing all the Dockerfile files + */ + private static final Path DOCKERFILE_DIR_PATH = Path.of( PROJECT_ROOT ).resolve( "tooling" ).resolve( "docker" ); + + /** + * Extract the image name and version from the first FROM instruction in the Dockerfile. + * Note that everything else is ignored. + */ + public static DockerImageName fromDockerfile(String databaseName) { + try { + final ImageInformation imageInformation = readFromInstruction( databaseName.toLowerCase() ); + return imageName( imageInformation.getRegistry(), imageInformation.getImage(), imageInformation.getVersion() ); + } + catch (IOException e) { + throw new RuntimeException( e ); + } + } + + public static DockerImageName imageName(String registry, String image, String version) { + return DockerImageName + .parse( registry + "/" + image + ":" + version ) + .asCompatibleSubstituteFor( image ); + } + + private static class ImageInformation { + private final String registry; + private final String image; + private final String version; + + public ImageInformation(String fullImageInfo) { + // FullImageInfo pattern: /: + // For example: "docker.io/cockroachdb/cockroach:v24.3.13" becomes registry = "docker.io", image = "cockroachdb/cockroach", version = "v24.3.13" + final int registryEndPos = fullImageInfo.indexOf( '/' ); + final int imageEndPos = fullImageInfo.lastIndexOf( ':' ); + this.registry = fullImageInfo.substring( 0, registryEndPos ); + this.image = fullImageInfo.substring( registryEndPos + 1, imageEndPos ); + this.version = fullImageInfo.substring( imageEndPos + 1 ); + } + + public String getRegistry() { + return registry; + } + + public String getImage() { + return image; + } + + public String getVersion() { + return version; + } + + @Override + public String toString() { + return registry + "/" + image + ":" + version; + } + } + + private static Path dockerFilePath(String database) { + // Get project root from system property set by Gradle, with fallback + return DOCKERFILE_DIR_PATH.resolve( database.toLowerCase() + ".Dockerfile" ); + } + + private static ImageInformation readFromInstruction(String database) throws IOException { + return readFromInstruction( dockerFilePath( database ) ); + } + + /** + * Read a Dockerfile and extract the first FROM instruction. + * + * @param dockerfilePath path to the Dockerfile + * @return the first FROM instruction found, or empty if none found + * @throws IOException if the file cannot be read + */ + private static ImageInformation readFromInstruction(Path dockerfilePath) throws IOException { + if ( !Files.exists( dockerfilePath ) ) { + throw new FileNotFoundException( "Dockerfile not found: " + dockerfilePath ); + } + + List lines = Files.readAllLines( dockerfilePath ); + for ( String line : lines ) { + // Skip comments and empty lines + String trimmedLine = line.trim(); + if ( trimmedLine.isEmpty() || trimmedLine.startsWith( "#" ) ) { + continue; + } + + if ( trimmedLine.startsWith( "FROM " ) ) { + return new ImageInformation( trimmedLine.substring( "FROM ".length() ) ); + } + } + + throw new IOException( " Missing FROM instruction in " + dockerfilePath ); + } +} diff --git a/integration-tests/techempower-postgres-it/build.gradle b/integration-tests/techempower-postgres-it/build.gradle index 5c4ecda47..edfc4ae4d 100644 --- a/integration-tests/techempower-postgres-it/build.gradle +++ b/integration-tests/techempower-postgres-it/build.gradle @@ -11,37 +11,25 @@ buildscript { description = 'TechEmpower integration tests' -ext { - jacksonDatabindVersion = '2.15.2' - jbossLoggingVersion = '3.5.0.Final' - assertjVersion = '3.27.3' - vertxWebVersion = project.hasProperty( 'vertxWebVersion' ) - ? project.property( 'vertxWebVersion' ) - : vertxSqlClientVersion - vertxWebClientVersion = project.hasProperty( 'vertxWebClientVersion' ) - ? project.property( 'vertxWebClientVersion' ) - : vertxSqlClientVersion -} - dependencies { implementation project( ':hibernate-reactive-core' ) - implementation "io.vertx:vertx-web:${vertxWebVersion}" - implementation "io.vertx:vertx-web-client:${vertxWebClientVersion}" + implementation(libs.io.vertx.vertx.web) + implementation(libs.io.vertx.vertx.web.client) - runtimeOnly "io.vertx:vertx-pg-client:${vertxSqlClientVersion}" + runtimeOnly(libs.io.vertx.vertx.pg.client) // The Pg client requires this dependency - runtimeOnly "com.ongres.scram:client:2.1" - runtimeOnly "com.fasterxml.jackson.core:jackson-databind:${jacksonDatabindVersion}" + runtimeOnly(libs.com.ongres.scram.client) + runtimeOnly(libs.com.fasterxml.jackson.core.jackson.databind) // logging - implementation "org.jboss.logging:jboss-logging:${jbossLoggingVersion}" + implementation(libs.org.jboss.logging.jboss.logging) // Testcontainers - implementation "org.testcontainers:postgresql:${testcontainersVersion}" + implementation(libs.org.testcontainers.postgresql) // Testing - testImplementation "org.assertj:assertj-core:${assertjVersion}" - testImplementation "io.vertx:vertx-junit5:${vertxSqlClientVersion}" + testImplementation(libs.org.assertj.assertj.core) + testImplementation(libs.io.vertx.vertx.junit5) } // Configuration for the tests diff --git a/integration-tests/verticle-postgres-it/build.gradle b/integration-tests/verticle-postgres-it/build.gradle index 0be234620..2cd6edfec 100644 --- a/integration-tests/verticle-postgres-it/build.gradle +++ b/integration-tests/verticle-postgres-it/build.gradle @@ -11,37 +11,25 @@ buildscript { description = 'Bytecode enhancements integration tests' -ext { - jacksonDatabindVersion = '2.15.2' - jbossLoggingVersion = '3.5.0.Final' - assertjVersion = '3.27.3' - vertxWebVersion = project.hasProperty( 'vertxWebVersion' ) - ? project.property( 'vertxWebVersion' ) - : vertxSqlClientVersion - vertxWebClientVersion = project.hasProperty( 'vertxWebClientVersion' ) - ? project.property( 'vertxWebClientVersion' ) - : vertxSqlClientVersion -} - dependencies { implementation project(':hibernate-reactive-core') - implementation "io.vertx:vertx-web:${vertxWebVersion}" - implementation "io.vertx:vertx-web-client:${vertxWebClientVersion}" + implementation(libs.io.vertx.vertx.web) + implementation(libs.io.vertx.vertx.web.client) - runtimeOnly "io.vertx:vertx-pg-client:${vertxSqlClientVersion}" + runtimeOnly(libs.io.vertx.vertx.pg.client) // The Pg client requires this dependency - runtimeOnly "com.ongres.scram:client:2.1" - runtimeOnly "com.fasterxml.jackson.core:jackson-databind:${jacksonDatabindVersion}" + runtimeOnly(libs.com.ongres.scram.client) + runtimeOnly(libs.com.fasterxml.jackson.core.jackson.databind) // logging - implementation "org.jboss.logging:jboss-logging:${jbossLoggingVersion}" + implementation(libs.org.jboss.logging.jboss.logging) // Testcontainers - implementation "org.testcontainers:postgresql:${testcontainersVersion}" + implementation(libs.org.testcontainers.postgresql) // Testing - testImplementation "org.assertj:assertj-core:${assertjVersion}" - testImplementation "io.vertx:vertx-junit5:${vertxSqlClientVersion}" + testImplementation(libs.org.assertj.assertj.core) + testImplementation(libs.io.vertx.vertx.junit5) } // Configuration for the tests diff --git a/integration-tests/verticle-postgres-it/src/main/java/org/hibernate/reactive/it/verticle/DockerImage.java b/integration-tests/verticle-postgres-it/src/main/java/org/hibernate/reactive/it/verticle/DockerImage.java new file mode 100644 index 000000000..10026d020 --- /dev/null +++ b/integration-tests/verticle-postgres-it/src/main/java/org/hibernate/reactive/it/verticle/DockerImage.java @@ -0,0 +1,125 @@ +/* Hibernate, Relational Persistence for Idiomatic Java + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright: Red Hat Inc. and Hibernate Authors + */ +package org.hibernate.reactive.it.verticle; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; + +import org.testcontainers.utility.DockerImageName; + +/** + * A utility class with methods to generate a {@link DockerImageName} for Testcontainers. + *

+ * Testcontainers might not work if the image required is available in multiple different registries (for example when + * using podman instead of docker). + * These methods make sure to pick a registry as default. + *

+ */ +public final class DockerImage { + + /** + * The absolute path of the project root that we have set in Gradle. + */ + private static final String PROJECT_ROOT = System.getProperty( "hibernate.reactive.project.root" ); + + /** + * The path to the directory containing all the Dockerfile files + */ + private static final Path DOCKERFILE_DIR_PATH = Path.of( PROJECT_ROOT ).resolve( "tooling" ).resolve( "docker" ); + + /** + * Extract the image name and version from the first FROM instruction in the Dockerfile. + * Note that everything else is ignored. + */ + public static DockerImageName fromDockerfile(String databaseName) { + try { + final ImageInformation imageInformation = readFromInstruction( databaseName.toLowerCase() ); + return imageName( imageInformation.getRegistry(), imageInformation.getImage(), imageInformation.getVersion() ); + } + catch (IOException e) { + throw new RuntimeException( e ); + } + } + + public static DockerImageName imageName(String registry, String image, String version) { + return DockerImageName + .parse( registry + "/" + image + ":" + version ) + .asCompatibleSubstituteFor( image ); + } + + private static class ImageInformation { + private final String registry; + private final String image; + private final String version; + + public ImageInformation(String fullImageInfo) { + // FullImageInfo pattern: /: + // For example: "docker.io/cockroachdb/cockroach:v24.3.13" becomes registry = "docker.io", image = "cockroachdb/cockroach", version = "v24.3.13" + final int registryEndPos = fullImageInfo.indexOf( '/' ); + final int imageEndPos = fullImageInfo.lastIndexOf( ':' ); + this.registry = fullImageInfo.substring( 0, registryEndPos ); + this.image = fullImageInfo.substring( registryEndPos + 1, imageEndPos ); + this.version = fullImageInfo.substring( imageEndPos + 1 ); + } + + public String getRegistry() { + return registry; + } + + public String getImage() { + return image; + } + + public String getVersion() { + return version; + } + + @Override + public String toString() { + return registry + "/" + image + ":" + version; + } + } + + private static Path dockerFilePath(String database) { + // Get project root from system property set by Gradle, with fallback + return DOCKERFILE_DIR_PATH.resolve( database.toLowerCase() + ".Dockerfile" ); + } + + private static ImageInformation readFromInstruction(String database) throws IOException { + return readFromInstruction( dockerFilePath( database ) ); + } + + /** + * Read a Dockerfile and extract the first FROM instruction. + * + * @param dockerfilePath path to the Dockerfile + * @return the first FROM instruction found, or empty if none found + * @throws IOException if the file cannot be read + */ + private static ImageInformation readFromInstruction(Path dockerfilePath) throws IOException { + if ( !Files.exists( dockerfilePath ) ) { + throw new FileNotFoundException( "Dockerfile not found: " + dockerfilePath ); + } + + List lines = Files.readAllLines( dockerfilePath ); + for ( String line : lines ) { + // Skip comments and empty lines + String trimmedLine = line.trim(); + if ( trimmedLine.isEmpty() || trimmedLine.startsWith( "#" ) ) { + continue; + } + + if ( trimmedLine.startsWith( "FROM " ) ) { + return new ImageInformation( trimmedLine.substring( "FROM ".length() ) ); + } + } + + throw new IOException( " Missing FROM instruction in " + dockerfilePath ); + } +} diff --git a/integration-tests/verticle-postgres-it/src/main/java/org/hibernate/reactive/it/verticle/ProductVerticle.java b/integration-tests/verticle-postgres-it/src/main/java/org/hibernate/reactive/it/verticle/ProductVerticle.java index 882231ad1..462db5f98 100644 --- a/integration-tests/verticle-postgres-it/src/main/java/org/hibernate/reactive/it/verticle/ProductVerticle.java +++ b/integration-tests/verticle-postgres-it/src/main/java/org/hibernate/reactive/it/verticle/ProductVerticle.java @@ -40,19 +40,10 @@ public ProductVerticle(Supplier emfSupplier) { this.emfSupplier = emfSupplier; } - private void startHibernate(Promise p) { - try { - this.emf = emfSupplier.get(); - p.complete(); - } - catch (Throwable t) { - p.fail( t ); - } - } - @Override public void start(Promise startPromise) { - final Future startHibernate = vertx.executeBlocking( this::startHibernate ) + final Future startHibernate = vertx + .executeBlocking( this::startHibernate ) .onSuccess( s -> LOG.infof( "✅ Hibernate Reactive is ready" ) ); Router router = Router.router( vertx ); @@ -74,9 +65,15 @@ public void start(Promise startPromise) { .onFailure( startPromise::fail ); } + private Mutiny.SessionFactory startHibernate() { + this.emf = emfSupplier.get(); + return this.emf; + } + @Override public void stop(Promise stopPromise) { - httpServer.close().onComplete( unused -> emf.close() ) + httpServer.close() + .onComplete( unused -> emf.close() ) .onSuccess( s -> stopPromise.complete() ) .onFailure( stopPromise::fail ); } diff --git a/integration-tests/verticle-postgres-it/src/main/java/org/hibernate/reactive/it/verticle/VertxServer.java b/integration-tests/verticle-postgres-it/src/main/java/org/hibernate/reactive/it/verticle/VertxServer.java index 990fef754..f11254c6d 100644 --- a/integration-tests/verticle-postgres-it/src/main/java/org/hibernate/reactive/it/verticle/VertxServer.java +++ b/integration-tests/verticle-postgres-it/src/main/java/org/hibernate/reactive/it/verticle/VertxServer.java @@ -21,6 +21,7 @@ import io.vertx.core.Vertx; import io.vertx.core.VertxOptions; import org.testcontainers.containers.PostgreSQLContainer; +import org.testcontainers.utility.DockerImageName; import static java.lang.invoke.MethodHandles.lookup; import static org.hibernate.reactive.logging.impl.LoggerFactory.make; @@ -36,7 +37,8 @@ public class VertxServer { // These properties are in DatabaseConfiguration in core public static final boolean USE_DOCKER = Boolean.getBoolean( "docker" ); - public static final String IMAGE_NAME = "postgres:17.5"; + public static final DockerImageName IMAGE_NAME = DockerImage.fromDockerfile( "postgresql" ); + public static final String USERNAME = "hreact"; public static final String PASSWORD = "hreact"; public static final String DB_NAME = "hreact"; diff --git a/integration-tests/verticle-postgres-it/src/test/java/org/hibernate/reactive/it/LocalContextTest.java b/integration-tests/verticle-postgres-it/src/test/java/org/hibernate/reactive/it/LocalContextTest.java index 0c819a393..4c332341b 100644 --- a/integration-tests/verticle-postgres-it/src/test/java/org/hibernate/reactive/it/LocalContextTest.java +++ b/integration-tests/verticle-postgres-it/src/test/java/org/hibernate/reactive/it/LocalContextTest.java @@ -31,7 +31,6 @@ import io.vertx.junit5.VertxExtension; import io.vertx.junit5.VertxTestContext; -import static io.vertx.core.CompositeFuture.all; import static io.vertx.core.Future.all; import static io.vertx.core.Future.failedFuture; import static io.vertx.core.Future.succeededFuture; @@ -50,7 +49,7 @@ * that's been close ahead of time by someone else. * Theoretically, everything could happen in the right order because of chance, * but it's unlikely and at the moment I don't have a better solution. - * See the the related issue + * See the related issue * for more details. *

*/ @@ -88,7 +87,7 @@ public void testProductsGeneration(VertxTestContext context) { .compose( this::findProducts ) .onSuccess( res -> context.completeNow() ) .onFailure( context::failNow ) - .eventually( unused -> vertx.close() ); + .eventually( () -> vertx.close() ); } /** @@ -97,7 +96,7 @@ public void testProductsGeneration(VertxTestContext context) { * @see #REQUEST_NUMBER */ private Future createProducts(WebClient webClient) { - List postRequests = new ArrayList<>(); + List> postRequests = new ArrayList<>(); for ( int i = 0; i < REQUEST_NUMBER; i++ ) { final Product product = new Product( i + 1 ); diff --git a/local-build-plugins/src/main/java/org/hibernate/reactive/env/VersionsPlugin.java b/local-build-plugins/src/main/java/org/hibernate/reactive/env/VersionsPlugin.java index 36b5ae10a..c34a4b479 100644 --- a/local-build-plugins/src/main/java/org/hibernate/reactive/env/VersionsPlugin.java +++ b/local-build-plugins/src/main/java/org/hibernate/reactive/env/VersionsPlugin.java @@ -28,6 +28,7 @@ public class VersionsPlugin implements Plugin { public static final String SKIP_ORM_VERSION_PARSING = "skipOrmVersionParsing"; public static final String RELATIVE_FILE = "gradle/version.properties"; + public static final String RELATIVE_CATALOG = "gradle/libs.versions.toml"; @Override public void apply(Project project) { @@ -57,12 +58,13 @@ public void apply(Project project) { project.getLogger().lifecycle( "Development version: n/a" ); } - final String ormVersionString = determineOrmVersion( project ); + final VersionsTomlParser tomlParser = new VersionsTomlParser( RELATIVE_CATALOG ); + final String ormVersionString = determineOrmVersion( project, tomlParser ); final Object ormVersion = resolveOrmVersion( ormVersionString, project ); project.getLogger().lifecycle( "ORM version: {}", ormVersion ); project.getExtensions().add( ORM_VERSION, ormVersion ); - final Object ormPluginVersion = determineOrmPluginVersion( ormVersion, project ); + final Object ormPluginVersion = determineOrmPluginVersion( ormVersion, project, tomlParser ); project.getLogger().lifecycle( "ORM Gradle plugin version: {}", ormPluginVersion ); project.getExtensions().add( ORM_PLUGIN_VERSION, ormPluginVersion ); } @@ -123,10 +125,17 @@ private static void withInputStream(File file, Consumer action) { } } - private String determineOrmVersion(Project project) { + private String determineOrmVersion(Project project, VersionsTomlParser parser) { + // Check if it has been set in the project if ( project.hasProperty( ORM_VERSION ) ) { return (String) project.property( ORM_VERSION ); } + + // Check in the catalog + final String version = parser.read( ORM_VERSION ); + if ( version != null ) { + return version; + } throw new IllegalStateException( "Hibernate ORM version not specified on project" ); } @@ -138,10 +147,18 @@ private Object resolveOrmVersion(String stringForm, Project project) { return new ProjectVersion( stringForm ); } - private Object determineOrmPluginVersion(Object ormVersion, Project project) { + private Object determineOrmPluginVersion(Object ormVersion, Project project, VersionsTomlParser parser) { + // Check if it has been set in the project if ( project.hasProperty( ORM_PLUGIN_VERSION ) ) { return project.property( ORM_PLUGIN_VERSION ); } - return ormVersion; + + // Check in the catalog + final String version = parser.read( ORM_PLUGIN_VERSION ); + if ( version != null ) { + return version; + } + + throw new IllegalStateException( "Hibernate ORM Gradle plugin version not specified on project" ); } } diff --git a/local-build-plugins/src/main/java/org/hibernate/reactive/env/VersionsTomlParser.java b/local-build-plugins/src/main/java/org/hibernate/reactive/env/VersionsTomlParser.java new file mode 100644 index 000000000..b0adafd6d --- /dev/null +++ b/local-build-plugins/src/main/java/org/hibernate/reactive/env/VersionsTomlParser.java @@ -0,0 +1,68 @@ +package org.hibernate.reactive.env; + +import java.io.BufferedReader; +import java.io.FileReader; +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +/** + * Read the versions section of the library catalog + */ +public class VersionsTomlParser { + + private final Map data = new HashMap<>(); + + public VersionsTomlParser(String filePath) { + parse( filePath ); + } + + private void parse(String filePath) { + try ( BufferedReader reader = new BufferedReader( new FileReader( filePath ) ) ) { + String line; + String currentSection = null; + while ( ( line = reader.readLine() ) != null ) { + line = line.trim(); + + // Skip comments and blank lines + if ( line.isEmpty() || line.startsWith( "#" ) ) { + continue; + } + + // Handle [section] + if ( line.startsWith( "[" ) && line.endsWith( "]" ) ) { + currentSection = line.substring( 1, line.length() - 1 ).trim(); + continue; + } + + if ( "versions".equalsIgnoreCase( currentSection ) ) { + // Handle key = value + int equalsIndex = line.indexOf( '=' ); + if ( equalsIndex == -1 ) { + continue; + } + + String key = line.substring( 0, equalsIndex ).trim(); + String value = line.substring( equalsIndex + 1 ).trim(); + + // Remove optional quotes around string values + if ( value.startsWith( "\"" ) && value.endsWith( "\"" ) ) { + value = value.substring( 1, value.length() - 1 ); + } + + data.put( key, value ); + } + } + } + catch (IOException e) { + throw new RuntimeException( e ); + } + } + + /** + * Read the value of the property in the versions section of a toml file + */ + public String read(String property) { + return data.get( property ); + } +} diff --git a/podman.md b/podman.md index bc4d96ba7..9b9a71af2 100644 --- a/podman.md +++ b/podman.md @@ -158,7 +158,7 @@ and schema to run the tests: podman run --rm -e LICENSE=accept --privileged=true --group-add keep-groups \ --name HibernateTestingDB2 -e DBNAME=hreact -e DB2INSTANCE=hreact \ -e DB2INST1_PASSWORD=hreact -e PERSISTENT_HOME=false -e ARCHIVE_LOGS=false \ - -e AUTOCONFIG=false -p 50000:50000 docker.io/icr.io/db2_community/db2:12.1.0.0 + -e AUTOCONFIG=false -p 50000:50000 icr.io/db2_community/db2:12.1.2.0 ``` When the database has started, you can run the tests on Db2 with: diff --git a/publish.gradle b/publish.gradle index f77b26959..747415921 100644 --- a/publish.gradle +++ b/publish.gradle @@ -21,7 +21,7 @@ jar { 'Implementation-Version': project.version, 'Implementation-Vendor': 'Hibernate.org', 'Implementation-Vendor-Id': 'org.hibernate', - 'Implementation-Url': 'http://hibernate.org/reactive', + 'Implementation-Url': 'https://hibernate.org/reactive', ) } } @@ -50,7 +50,7 @@ publishing { license { name = 'Apache License Version 2.0' url = 'https://opensource.org/licenses/Apache-2.0' - comments = 'See discussion at http://hibernate.org/community/license/ for more details.' + comments = 'See discussion at https://hibernate.org/community/license/ for more details.' distribution = 'repo' } } @@ -81,10 +81,18 @@ publishing { } maven { name = 'snapshots' - url = "https://oss.sonatype.org/content/repositories/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) } } } + +def releasePrepareTask = tasks.register("releasePrepare") { + description "Prepares all the artifacts and documentation for a JReleaser release." + group "Release" + + // Create all the published artifacts (i.e. jars) and move them to the staging directory (for JReleaser): + dependsOn publishAllPublicationsToStagingRepository +} diff --git a/release/build.gradle b/release/build.gradle index 690534750..ff80aff49 100644 --- a/release/build.gradle +++ b/release/build.gradle @@ -1,28 +1,13 @@ import java.nio.charset.StandardCharsets -ext { - // Select which repository to use for publishing the documentation - // Example: - // ./gradlew uploadDocumentation \ - // -PdocPublishRepoUri="git@github.com:DavideD/hibernate.org.git" \ - // -PdocPublishBranch="staging" - if ( !project.hasProperty('docPublishRepoUri') ) { - docPublishRepoUri = 'git@github.com:hibernate/hibernate.org.git' - } - if ( !project.hasProperty('docPublishBranch') ) { - docPublishBranch = 'staging' - } -} - description = 'Release module' // (Optional) before uploading the documentation, you can check // the generated website under release/build/hibernate.org with: // ./gradlew gitPublishCopy -// To publish the documentation: -// 1. Add the relevant SSH key to your SSH agent. -// 2. Execute this: -// ./gradlew uploadDocumentation -PdocPublishBranch=production +// The generated documentation are copied to the +// rootProject.layout.buildDirectory.dir("staging-deploy/documentation") by the updateDocumentationTask +// while the publishing is delegated to the https://github.com/hibernate/hibernate-release-scripts // To tag a version and trigger a release on CI (which will include publishing to Bintray and publishing documentation): // ./gradlew ciRelease -PreleaseVersion=x.y.z.Final -PdevelopmentVersion=x.y.z-SNAPSHOT -PgitRemote=origin -PgitBranch=main @@ -30,64 +15,6 @@ description = 'Release module' // The folder containing the rendered documentation final Directory documentationDir = project(":documentation").layout.buildDirectory.get() -// Relative path on the static website where the documentation is located -final String docWebsiteRelativePath = "reactive/documentation/${projectVersion.family}" - -// The location of the docs when the website has been cloned -final Directory docWebsiteReactiveFolder = project.layout.buildDirectory.dir( "docs-website/${docWebsiteRelativePath}" ).get() - -def releaseChecksTask = tasks.register( "releaseChecks" ) { - description 'Checks and preparation for release' - group '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 = project.hasProperty( 'gitBranch' ) && !project.property( 'gitBranch' ).isEmpty() - ? project.property( 'gitBranch' ) - : executeGitCommand( 'branch', '--show-current' ).trim() - - String gitRemoteLocal - 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." - ) - } - } -} - /** * Assembles all documentation into the {buildDir}/documentation directory. */ @@ -98,280 +25,36 @@ def assembleDocumentationTask = tasks.register( 'assembleDocumentation' ) { } assemble.dependsOn assembleDocumentationTask -/** -* Clone the website -*/ -def removeDocsWebsiteTask = tasks.register( 'removeDocsWebsite', Delete ) { - delete project.layout.buildDirectory.dir( "docs-website" ) -} - -def cloneDocsWebsiteTask = tasks.register( 'cloneDocsWebsite', Exec ) { - dependsOn removeDocsWebsiteTask - // Assure that the buildDir exists. Otherwise this task will fail. - dependsOn compileJava - workingDir project.layout.buildDirectory - commandLine 'git', 'clone', docPublishRepoUri, '-b', docPublishBranch, '--sparse', '--depth', '1', 'docs-website' -} - -def sparseCheckoutDocumentationTask = tasks.register( 'sparseCheckoutDocumentation', Exec ) { - dependsOn cloneDocsWebsiteTask - workingDir project.layout.buildDirectory.dir( "docs-website" ) - commandLine 'git', 'sparse-checkout', 'set', docWebsiteRelativePath -} - -/** -* Update the docs on the cloned website -*/ -def changeToReleaseVersionTask = tasks.register( 'changeToReleaseVersion' ) { - description 'Updates `gradle/version.properties` file to the specified release-version' - group 'Release' - - dependsOn releaseChecksTask - - doFirst { - logger.lifecycle( "Updating version-file to release-version : `${project.releaseVersion}`" ) - updateVersionFile( "${project.releaseVersion}" ) - } -} - def updateDocumentationTask = tasks.register( 'updateDocumentation' ) { description "Update the documentation on the cloned static website" - dependsOn assembleDocumentationTask, sparseCheckoutDocumentationTask - mustRunAfter changeToReleaseVersion + dependsOn assembleDocumentationTask // copy documentation outputs into target/documentation: // * this is used in building the dist bundles // * it is also used as a base to build a staged directory for documentation upload doLast { - // delete the folders in case some files have been removed - delete docWebsiteReactiveFolder.dir("javadocs"), docWebsiteReactiveFolder.dir("reference") - // Aggregated JavaDoc - copy { - from documentationDir.dir("javadocs") - into docWebsiteReactiveFolder.dir("javadocs") - } + copy { + from documentationDir.dir("javadocs") + into rootProject.layout.buildDirectory.dir("staging-deploy/documentation/javadocs") + } // Reference Documentation - copy { - from documentationDir.dir("asciidoc/reference/html_single") - into docWebsiteReactiveFolder.dir("reference/html_single") - } - } -} - -def stageDocChangesTask = tasks.register( 'stageDocChanges', Exec ) { - dependsOn updateDocumentationTask - workingDir project.layout.buildDirectory.dir( "docs-website" ) - commandLine 'git', 'add', '-A', '.' -} - -def commitDocChangesTask = tasks.register( 'commitDocChanges', Exec ) { - dependsOn stageDocChangesTask - workingDir project.layout.buildDirectory.dir( "docs-website" ) - commandLine 'git', 'commit', '-m', "[HR] Hibernate Reactive documentation for ${projectVersion}" -} - -def pushDocChangesTask = tasks.register( 'pushDocChanges', Exec ) { - description "Push documentation changes on the remote repository" - dependsOn commitDocChangesTask - workingDir project.layout.buildDirectory.dir( "docs-website" ) - commandLine 'git', 'push', '--atomic', 'origin', docPublishBranch -} - -def uploadDocumentationTask = tasks.register( 'uploadDocumentation' ) { - description "Upload documentation on the website" - group "Release" - dependsOn pushDocChangesTask - - doLast { - logger.lifecycle "Documentation published on '${docPublishRepoUri}' branch '${docPublishBranch}'" - } -} - -def gitPreparationForReleaseTask = tasks.register( 'gitPreparationForRelease' ) { - dependsOn releaseChecksTask, changeToReleaseVersionTask - finalizedBy updateDocumentationTask - - doLast { - logger.lifecycle( "Performing pre-steps Git commit : `${project.releaseVersion}`" ) - executeGitCommand( 'add', '.' ) - executeGitCommand( 'commit', '-m', "Update project version to : `${project.releaseVersion}`" ) - } -} - -def changeToDevelopmentVersionTask = tasks.register( 'changeToDevelopmentVersion' ) { - description 'Updates `gradle/version.properties` file to the specified development-version' - group 'Release' - - 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.projectVersion" ) - } - - logger.lifecycle( "Performing post-steps Git commit : `${project.releaseVersion}`" ) - executeGitCommand( 'add', '.' ) - executeGitCommand( 'commit', '-m', "Update project version to : `${project.developmentVersion}`" ) - } -} - -void updateVersionFile(var version) { - logger.lifecycle( "Updating `gradle/version.properties` version to `${version}`" ) - project.versionFile.text = "projectVersion=${version}" - project.version = version -} - -def publishReleaseArtifactsTask = tasks.register( 'publishReleaseArtifacts' ) { - dependsOn uploadDocumentationTask - - mustRunAfter gitPreparationForReleaseTask -} - -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 ) - } + copy { + from documentationDir.dir("asciidoc/reference/html_single") + into rootProject.layout.buildDirectory.dir("staging-deploy/documentation/reference/html_single") + } } } def releasePrepareTask = tasks.register( "releasePrepare" ) { - description "On a local checkout, performs all the changes required for the release, website updates included" + description "Prepares all the artifacts and documentation for a JReleaser release." group "Release" - 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 -} - -/* -* Release everything -*/ -def releaseTask = tasks.register( 'release' ) { - description 'Performs a release on local check-out' - group 'Release' - - dependsOn releasePrepareTask - dependsOn releasePerformTask -} - -def ciReleaseTask = tasks.register( 'ciRelease' ) { - description "Triggers the release on CI: creates commits to change the version (release, then development), creates a tag, pushes everything. Then CI will take over and perform the release." - group "Release" - - 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()) - } - } - } -} - -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 + // Render the Documentation/Javadocs and move them to the staging directory (for JReleaser): + dependsOn updateDocumentationTask + // Create all the published artifacts (i.e. jars) and move them to the staging directory (for JReleaser): + // this one is defined in the publish.gradle + // dependsOn project.getTasksByName(publishAllPublicationsToStagingRepository, false) } diff --git a/settings.gradle b/settings.gradle index e997f3e73..f1c542d37 100644 --- a/settings.gradle +++ b/settings.gradle @@ -19,6 +19,44 @@ gradle.ext.baselineJavaVersion = JavaLanguageVersion.of( 17 ) // You can't use bytecode higher than what Gradle supports, even with toolchains. def GRADLE_MAX_SUPPORTED_BYTECODE_VERSION = 23 +// This overrides the default version catalog in gradle/libs.versions.toml, which can be +// useful to monitor compatibility for upcoming versions on CI: +dependencyResolutionManagement { + versionCatalogs { + create("libs") { + // ./gradlew build -PhibernateOrmVersion=7.1.0.Final + def hibernateOrmVersion = settings.ext.find("hibernateOrmVersion") ?: "" + if ( hibernateOrmVersion != "" ) { + version("hibernateOrmVersion", hibernateOrmVersion) + } + + // ./gradlew build -PhibernateOrmGradlePluginVersion=7.1.0.Final + def hibernateOrmGradlePluginVersion = settings.ext.find("hibernateOrmGradlePluginVersion") ?: "" + if ( hibernateOrmGradlePluginVersion ) { + version("hibernateOrmGradlePluginVersion", hibernateOrmGradlePluginVersion) + } + + // ./gradlew build -PvertxSqlClientVersion=4.0.0-SNAPSHOT + def vertxSqlClientVersion = settings.ext.find("vertxSqlClientVersion") ?: "" + if ( vertxSqlClientVersion ) { + version("vertxSqlClientVersion", vertxSqlClientVersion) + } + + // ./gradlew build -PvertxWebVersion=4.0.0-SNAPSHOT + def vertxWebVersion = settings.ext.find("vertxWebVersion") ?: vertxSqlClientVersion + if ( vertxWebVersion ) { + version("vertxWebVersion", vertxWebVersion) + } + + // ./gradlew build -PvertxWebClientVersion=4.0.0-SNAPSHOT + def vertxWebClientVersion = settings.ext.find("vertxWebClientVersion") ?: vertxSqlClientVersion + if ( vertxWebClientVersion ) { + version("vertxWebClientVersion", vertxWebClientVersion) + } + } + } +} + // If either 'main.jdk.version' or 'test.jdk.version' is set, enable the toolchain and use the selected jdk. // If only one property is set, the other defaults to the baseline Java version (8). // Note that when toolchain is enabled, you also need to specify diff --git a/tooling/docker/README.md b/tooling/docker/README.md new file mode 100644 index 000000000..4de2451e0 --- /dev/null +++ b/tooling/docker/README.md @@ -0,0 +1,6 @@ +Our test suite will only read the first FROM instruction from each Dockerfile to extract the base image and the version +of the container to run. It will ignore everything else. + +The reason we have these files is that we want to automate the upgrade of the containers using dependabot. + +See the class `DockerImage`. diff --git a/tooling/docker/cockroachdb.Dockerfile b/tooling/docker/cockroachdb.Dockerfile new file mode 100644 index 000000000..0b441f7ca --- /dev/null +++ b/tooling/docker/cockroachdb.Dockerfile @@ -0,0 +1,3 @@ +# CockroachDB +# See https://hub.docker.com/r/cockroachdb/cockroach +FROM docker.io/cockroachdb/cockroach:v25.3.4 diff --git a/tooling/docker/db2.Dockerfile b/tooling/docker/db2.Dockerfile new file mode 100644 index 000000000..fbd438115 --- /dev/null +++ b/tooling/docker/db2.Dockerfile @@ -0,0 +1,3 @@ +# IBM DB2 +# See https://hub.docker.com/r/ibmcom/db2 +FROM icr.io/db2_community/db2:12.1.3.0 diff --git a/tooling/docker/maria.Dockerfile b/tooling/docker/maria.Dockerfile new file mode 100644 index 000000000..6aa9bf898 --- /dev/null +++ b/tooling/docker/maria.Dockerfile @@ -0,0 +1,3 @@ +# MariaDB +# See https://hub.docker.com/_/mariadb +FROM docker.io/mariadb:12.0.2 diff --git a/tooling/docker/mysql.Dockerfile b/tooling/docker/mysql.Dockerfile new file mode 100644 index 000000000..03918b8e3 --- /dev/null +++ b/tooling/docker/mysql.Dockerfile @@ -0,0 +1,3 @@ +# MySQL +# See https://hub.docker.com/_/mysql +FROM container-registry.oracle.com/mysql/community-server:9.5.0 diff --git a/tooling/docker/oracle.Dockerfile b/tooling/docker/oracle.Dockerfile new file mode 100644 index 000000000..7dfc6447d --- /dev/null +++ b/tooling/docker/oracle.Dockerfile @@ -0,0 +1,3 @@ +# Oracle Database Free +# See https://hub.docker.com/r/gvenzl/oracle-free +FROM docker.io/gvenzl/oracle-free:23-slim-faststart diff --git a/tooling/docker/postgresql.Dockerfile b/tooling/docker/postgresql.Dockerfile new file mode 100644 index 000000000..f2daff3c7 --- /dev/null +++ b/tooling/docker/postgresql.Dockerfile @@ -0,0 +1,3 @@ +# PostgreSQL +# See https://hub.docker.com/_/postgres +FROM docker.io/postgres:18.0 diff --git a/tooling/docker/sqlserver.Dockerfile b/tooling/docker/sqlserver.Dockerfile new file mode 100644 index 000000000..94fae1429 --- /dev/null +++ b/tooling/docker/sqlserver.Dockerfile @@ -0,0 +1,3 @@ +# Microsoft SQL Server +# See https://hub.docker.com/_/microsoft-mssql-server +FROM mcr.microsoft.com/mssql/server:2025-latest diff --git a/tooling/jbang/CockroachDBReactiveTest.java.qute b/tooling/jbang/CockroachDBReactiveTest.java.qute index 908c828e5..54996e38d 100755 --- a/tooling/jbang/CockroachDBReactiveTest.java.qute +++ b/tooling/jbang/CockroachDBReactiveTest.java.qute @@ -5,12 +5,12 @@ * Copyright: Red Hat Inc. and Hibernate Authors */ -//DEPS io.vertx:vertx-pg-client:$\{vertx.version:4.5.15} -//DEPS io.vertx:vertx-unit:$\{vertx.version:4.5.15} +//DEPS io.vertx:vertx-pg-client:$\{vertx.version:4.5.16} +//DEPS io.vertx:vertx-unit:$\{vertx.version:4.5.16} //DEPS org.hibernate.reactive:hibernate-reactive-core:$\{hibernate-reactive.version:3.0.0.Final} //DEPS org.assertj:assertj-core:3.27.3 //DEPS junit:junit:4.13.2 -//DEPS org.testcontainers:cockroachdb:1.21.0 +//DEPS org.testcontainers:cockroachdb:1.21.3 //DEPS org.slf4j:slf4j-simple:2.0.7 //// Testcontainer needs the JDBC drivers to start the container diff --git a/tooling/jbang/Db2ReactiveTest.java.qute b/tooling/jbang/Db2ReactiveTest.java.qute index 1b4f599e5..ed3c993d1 100755 --- a/tooling/jbang/Db2ReactiveTest.java.qute +++ b/tooling/jbang/Db2ReactiveTest.java.qute @@ -5,12 +5,12 @@ * Copyright: Red Hat Inc. and Hibernate Authors */ -//DEPS io.vertx:vertx-db2-client:$\{vertx.version:4.5.15} -//DEPS io.vertx:vertx-unit:$\{vertx.version:4.5.15} +//DEPS io.vertx:vertx-db2-client:$\{vertx.version:4.5.16} +//DEPS io.vertx:vertx-unit:$\{vertx.version:4.5.16} //DEPS org.hibernate.reactive:hibernate-reactive-core:$\{hibernate-reactive.version:3.0.0.Final} //DEPS org.assertj:assertj-core:3.27.3 //DEPS junit:junit:4.13.2 -//DEPS org.testcontainers:db2:1.21.0 +//DEPS org.testcontainers:db2:1.21.3 //DEPS org.slf4j:slf4j-simple:2.0.7 import jakarta.persistence.Entity; diff --git a/tooling/jbang/Example.java b/tooling/jbang/Example.java index 4342974f8..a807fb33f 100644 --- a/tooling/jbang/Example.java +++ b/tooling/jbang/Example.java @@ -6,9 +6,9 @@ */ //DEPS com.ongres.scram:client:2.1 -//DEPS io.vertx:vertx-pg-client:${vertx.version:4.5.15} -//DEPS io.vertx:vertx-mysql-client:${vertx.version:4.5.15} -//DEPS io.vertx:vertx-db2-client:${vertx.version:4.5.15} +//DEPS io.vertx:vertx-pg-client:${vertx.version:4.5.16} +//DEPS io.vertx:vertx-mysql-client:${vertx.version:4.5.16} +//DEPS io.vertx:vertx-db2-client:${vertx.version:4.5.16} //DEPS org.hibernate.reactive:hibernate-reactive-core:${hibernate-reactive.version:3.0.0.Final} //DEPS org.slf4j:slf4j-simple:2.0.7 //DESCRIPTION Allow authentication to PostgreSQL using SCRAM: diff --git a/tooling/jbang/MariaDBReactiveTest.java.qute b/tooling/jbang/MariaDBReactiveTest.java.qute index bef838391..611f142c0 100755 --- a/tooling/jbang/MariaDBReactiveTest.java.qute +++ b/tooling/jbang/MariaDBReactiveTest.java.qute @@ -5,12 +5,12 @@ * Copyright: Red Hat Inc. and Hibernate Authors */ -//DEPS io.vertx:vertx-mysql-client:$\{vertx.version:4.5.15} -//DEPS io.vertx:vertx-unit:$\{vertx.version:4.5.15} +//DEPS io.vertx:vertx-mysql-client:$\{vertx.version:4.5.16} +//DEPS io.vertx:vertx-unit:$\{vertx.version:4.5.16} //DEPS org.hibernate.reactive:hibernate-reactive-core:$\{hibernate-reactive.version:3.0.0.Final} //DEPS org.assertj:assertj-core:3.27.3 //DEPS junit:junit:4.13.2 -//DEPS org.testcontainers:mariadb:1.21.0 +//DEPS org.testcontainers:mariadb:1.21.3 //DEPS org.slf4j:slf4j-simple:2.0.7 //// Testcontainer needs the JDBC drivers to start the container diff --git a/tooling/jbang/MySQLReactiveTest.java.qute b/tooling/jbang/MySQLReactiveTest.java.qute index 5047f8a4d..c258612ae 100755 --- a/tooling/jbang/MySQLReactiveTest.java.qute +++ b/tooling/jbang/MySQLReactiveTest.java.qute @@ -5,12 +5,12 @@ * Copyright: Red Hat Inc. and Hibernate Authors */ -//DEPS io.vertx:vertx-mysql-client:$\{vertx.version:4.5.15} -//DEPS io.vertx:vertx-unit:$\{vertx.version:4.5.15} +//DEPS io.vertx:vertx-mysql-client:$\{vertx.version:4.5.16} +//DEPS io.vertx:vertx-unit:$\{vertx.version:4.5.16} //DEPS org.hibernate.reactive:hibernate-reactive-core:$\{hibernate-reactive.version:3.0.0.Final} //DEPS org.assertj:assertj-core:3.27.3 //DEPS junit:junit:4.13.2 -//DEPS org.testcontainers:mysql:1.21.0 +//DEPS org.testcontainers:mysql:1.21.3 //DEPS org.slf4j:slf4j-simple:2.0.7 //// Testcontainer needs the JDBC drivers to start the container @@ -72,7 +72,7 @@ public class {baseName} { } @ClassRule - public final static MySQLContainer database = new MySQLContainer<>( imageName( "docker.io", "mysql", "9.2.0" ) ); + public final static MySQLContainer database = new MySQLContainer<>( imageName( "container-registry.oracle.com", "mysql/community-server", "9.3.0" ).asCompatibleSubstituteFor( "mysql" ) ); private Mutiny.SessionFactory sessionFactory; diff --git a/tooling/jbang/PostgreSQLReactiveTest.java.qute b/tooling/jbang/PostgreSQLReactiveTest.java.qute index dfd7f4a67..1e6e094d5 100755 --- a/tooling/jbang/PostgreSQLReactiveTest.java.qute +++ b/tooling/jbang/PostgreSQLReactiveTest.java.qute @@ -5,12 +5,12 @@ * Copyright: Red Hat Inc. and Hibernate Authors */ -//DEPS io.vertx:vertx-pg-client:$\{vertx.version:4.5.15} -//DEPS io.vertx:vertx-unit:$\{vertx.version:4.5.15} +//DEPS io.vertx:vertx-pg-client:$\{vertx.version:4.5.16} +//DEPS io.vertx:vertx-unit:$\{vertx.version:4.5.16} //DEPS org.hibernate.reactive:hibernate-reactive-core:$\{hibernate-reactive.version:3.0.0.Final} //DEPS org.assertj:assertj-core:3.27.3 //DEPS junit:junit:4.13.2 -//DEPS org.testcontainers:postgresql:1.21.0 +//DEPS org.testcontainers:postgresql:1.21.3 //DEPS org.slf4j:slf4j-simple:2.0.7 //DESCRIPTION Allow authentication to PostgreSQL using SCRAM: //DEPS com.ongres.scram:client:2.1 diff --git a/tooling/jbang/ReactiveTest.java b/tooling/jbang/ReactiveTest.java index 5b732b70f..ed6011433 100755 --- a/tooling/jbang/ReactiveTest.java +++ b/tooling/jbang/ReactiveTest.java @@ -5,19 +5,19 @@ */ ///usr/bin/env jbang "$0" "$@" ; exit $? -//DEPS io.vertx:vertx-pg-client:${vertx.version:4.5.15} +//DEPS io.vertx:vertx-pg-client:${vertx.version:4.5.16} //DEPS com.ongres.scram:client:2.1 -//DEPS io.vertx:vertx-db2-client:${vertx.version:4.5.15} -//DEPS io.vertx:vertx-mysql-client:${vertx.version:4.5.15} -//DEPS io.vertx:vertx-unit:${vertx.version:4.5.15} +//DEPS io.vertx:vertx-db2-client:${vertx.version:4.5.16} +//DEPS io.vertx:vertx-mysql-client:${vertx.version:4.5.16} +//DEPS io.vertx:vertx-unit:${vertx.version:4.5.16} //DEPS org.hibernate.reactive:hibernate-reactive-core:${hibernate-reactive.version:3.0.0.Final} //DEPS org.assertj:assertj-core:3.27.3 //DEPS junit:junit:4.13.2 -//DEPS org.testcontainers:postgresql:1.21.0 -//DEPS org.testcontainers:mysql:1.21.0 -//DEPS org.testcontainers:db2:1.21.0 -//DEPS org.testcontainers:mariadb:1.21.0 -//DEPS org.testcontainers:cockroachdb:1.21.0 +//DEPS org.testcontainers:postgresql:1.21.3 +//DEPS org.testcontainers:mysql:1.21.3 +//DEPS org.testcontainers:db2:1.21.3 +//DEPS org.testcontainers:mariadb:1.21.3 +//DEPS org.testcontainers:cockroachdb:1.21.3 // //// Testcontainer needs the JDBC drivers to start the containers //// Hibernate Reactive doesn't use them @@ -229,7 +229,7 @@ public String toString() { */ enum Database { POSTGRESQL( () -> new PostgreSQLContainer( "postgres:17.5" ) ), - MYSQL( () -> new MySQLContainer( "mysql:9.2.0" ) ), + MYSQL( () -> new MySQLContainer( "container-registry.oracle.com/mysql/community-server:9.3.0" ) ), DB2( () -> new Db2Container( "docker.io/icr.io/db2_community/db2:12.1.0.0" ).acceptLicense() ), MARIADB( () -> new MariaDBContainer( "mariadb:11.7.2" ) ), COCKROACHDB( () -> new CockroachContainer( "cockroachdb/cockroach:v24.3.13" ) );