diff --git a/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc b/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc index e3c2df79f9..ed0d000e62 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc +++ b/documentation/src/main/asciidoc/userguide/chapters/query/hql/QueryLanguage.adoc @@ -276,6 +276,11 @@ It's usually better to just use `persist()`. On the other hand, you might consider using it to set up test data. ==== +[NOTE] +==== +`insert` statements are not available in JPQL. +==== + The BNF for an `insert` statement is: [[hql-insert-bnf-example]] @@ -331,9 +336,37 @@ It's not available for entities whose id generator is implemented in Java, nor f The same two options are available for a `@Version` attribute. When no version is explicitly specified, the version for a new entity instance is used. +To implement "upsert" semantics i.e. insert-or-update, the `on conflict` clause can be used. +Reacting on conflicts can be either based on the name or the list of attribute paths of a unique constraint. +Using the unique constraint name as conflict target requires either native database support, +which at the time of writing is only available in PostgreSQL, or that the statement is a single row insert. +A single row insert can be ensured by specifying only a single values tuple in case of an insert-values statement, +or using `fetch first 1 rows only` in case of an insert-select statement. + +Possible conflict actions are to ignore the conflict or update conflicting objects/rows. + +[[hql-insert-conflict-example]] +==== +[source, SQL, indent=0] +---- +include::{example-dir-hql}/../query/hql/InsertConflictTests.java[tags=hql-insert-conflict-example] +---- +==== + +The special alias `excluded` is available in the `update set` clause of the `conflict clause` +and refers to the values that failed insertion due to a unique constraint conflict. + [NOTE] ==== -`insert ... select` statements are not available in JPQL. +The MySQL/MariaDB implementation leverages the native `on duplicate key` clause +which does not support specifying an explicit column list or constraint name. +Beware that this implementation might produce different results than on other databases +if a table has more than a single unique constraint. + +Another quirk of this implementation is that the MySQL/MariaDB JDBC driver returns surprising update counts. +For every row that is inserted, the update count is incremented by 1, but for rows that are updated, +the update count is incremented by 2. +To learn more about this, refer to the https://dev.mysql.com/doc/refman/8.0/en/insert-on-duplicate.html[MySQL documentation]. ==== [[hql-literals]] diff --git a/documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/statement_insert_bnf.txt b/documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/statement_insert_bnf.txt index 4f2e4caf90..811da6bab6 100644 --- a/documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/statement_insert_bnf.txt +++ b/documentation/src/main/asciidoc/userguide/chapters/query/hql/extras/statement_insert_bnf.txt @@ -1,5 +1,5 @@ insertStatement - : "INSERT" "INTO"? targetEntity targetFields (queryExpression | valuesList) + : "INSERT" "INTO"? targetEntity targetFields (queryExpression | valuesList) conflictClause? targetEntity : entityName variable? @@ -12,3 +12,14 @@ valuesList values : "(" expression ("," expression)* ")" + +conflictClause + : "on conflict" conflictTarget? conflictAction + +conflictTarget + : "on constraint" identifier + | "(" simplePath ("," simplePath)* ")"; + +conflictAction + : "do nothing" + | "do update" setClause whereClause? \ No newline at end of file diff --git a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java index 6edf0e2e54..d2688342dc 100644 --- a/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java +++ b/hibernate-core/src/main/java/org/hibernate/sql/ast/spi/AbstractSqlAstTranslator.java @@ -1966,7 +1966,7 @@ public abstract class AbstractSqlAstTranslator implemen // To allow meaningful usage, we simply ignore the constraint column names in this emulation. // A possible problem with this is when the constraint column names contain the primary key columns, // but the insert fails due to a unique constraint violation. This emulation will not cause a failure to be - // propagated, but instead will run the respective conflict action. todo: document this + // propagated, but instead will run the respective conflict action. final String constraintName = conflictClause.getConstraintName(); if ( constraintName != null ) { throw new IllegalQueryOperationException( "Dialect does not support constraint name in conflict clause" ); diff --git a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/InsertConflictTests.java b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/InsertConflictTests.java index d4a08fc326..6fc71aa494 100644 --- a/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/InsertConflictTests.java +++ b/hibernate-core/src/test/java/org/hibernate/orm/test/query/hql/InsertConflictTests.java @@ -18,7 +18,6 @@ import org.hibernate.testing.orm.domain.StandardDomainModel; import org.hibernate.testing.orm.domain.contacts.Contact; import org.hibernate.testing.orm.domain.contacts.Contact.Name; import org.hibernate.testing.orm.domain.gambit.BasicEntity; -import org.hibernate.testing.orm.junit.DialectFeatureCheck; import org.hibernate.testing.orm.junit.DialectFeatureChecks; import org.hibernate.testing.orm.junit.DomainModel; import org.hibernate.testing.orm.junit.JiraKey; @@ -90,12 +89,14 @@ public class InsertConflictTests { public void testOnConflictDoUpdate(SessionFactoryScope scope) { scope.inTransaction( session -> { + //tag::hql-insert-conflict-example[] int updated = session.createMutationQuery( "insert into BasicEntity (id, data) " + "values (1, 'John') " + "on conflict(id) do update " + "set data = excluded.data" ).executeUpdate(); + //end::hql-insert-conflict-example[] if ( scope.getSessionFactory().getJdbcServices().getDialect() instanceof MySQLDialect ) { // Strange MySQL returns 2 if the conflict action updates a row // Also see https://dev.mysql.com/doc/refman/8.0/en/insert-on-duplicate.html