HHH-16084 - MERGE (upsert) for optional table updates - H2
This commit is contained in:
parent
998f2ef21f
commit
21b7745768
|
@ -43,6 +43,7 @@ import org.hibernate.exception.spi.ViolatedConstraintNameExtractor;
|
||||||
import org.hibernate.internal.util.JdbcExceptionHelper;
|
import org.hibernate.internal.util.JdbcExceptionHelper;
|
||||||
import org.hibernate.metamodel.mapping.EntityMappingType;
|
import org.hibernate.metamodel.mapping.EntityMappingType;
|
||||||
import org.hibernate.metamodel.spi.RuntimeModelCreationContext;
|
import org.hibernate.metamodel.spi.RuntimeModelCreationContext;
|
||||||
|
import org.hibernate.persister.entity.mutation.EntityMutationTarget;
|
||||||
import org.hibernate.query.sqm.FetchClauseType;
|
import org.hibernate.query.sqm.FetchClauseType;
|
||||||
import org.hibernate.query.sqm.IntervalType;
|
import org.hibernate.query.sqm.IntervalType;
|
||||||
import org.hibernate.query.sqm.NullOrdering;
|
import org.hibernate.query.sqm.NullOrdering;
|
||||||
|
@ -60,6 +61,8 @@ import org.hibernate.sql.ast.spi.SqlAppender;
|
||||||
import org.hibernate.sql.ast.spi.StandardSqlAstTranslatorFactory;
|
import org.hibernate.sql.ast.spi.StandardSqlAstTranslatorFactory;
|
||||||
import org.hibernate.sql.ast.tree.Statement;
|
import org.hibernate.sql.ast.tree.Statement;
|
||||||
import org.hibernate.sql.exec.spi.JdbcOperation;
|
import org.hibernate.sql.exec.spi.JdbcOperation;
|
||||||
|
import org.hibernate.sql.model.MutationOperation;
|
||||||
|
import org.hibernate.sql.model.internal.TableUpsert;
|
||||||
import org.hibernate.tool.schema.extract.internal.SequenceInformationExtractorH2DatabaseImpl;
|
import org.hibernate.tool.schema.extract.internal.SequenceInformationExtractorH2DatabaseImpl;
|
||||||
import org.hibernate.tool.schema.extract.internal.SequenceInformationExtractorLegacyImpl;
|
import org.hibernate.tool.schema.extract.internal.SequenceInformationExtractorLegacyImpl;
|
||||||
import org.hibernate.tool.schema.extract.spi.SequenceInformationExtractor;
|
import org.hibernate.tool.schema.extract.spi.SequenceInformationExtractor;
|
||||||
|
@ -867,4 +870,13 @@ public class H2Dialect extends Dialect {
|
||||||
public int rowIdSqlType() {
|
public int rowIdSqlType() {
|
||||||
return BIGINT;
|
return BIGINT;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MutationOperation createUpsertOperation(
|
||||||
|
EntityMutationTarget mutationTarget,
|
||||||
|
TableUpsert tableUpsert,
|
||||||
|
SessionFactoryImplementor factory) {
|
||||||
|
final H2SqlAstTranslator<?> translator = new H2SqlAstTranslator<>( factory, tableUpsert );
|
||||||
|
return translator.visitUpsert( tableUpsert );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,7 +35,11 @@ import org.hibernate.sql.ast.tree.predicate.LikePredicate;
|
||||||
import org.hibernate.sql.ast.tree.select.QueryPart;
|
import org.hibernate.sql.ast.tree.select.QueryPart;
|
||||||
import org.hibernate.sql.ast.tree.select.SelectClause;
|
import org.hibernate.sql.ast.tree.select.SelectClause;
|
||||||
import org.hibernate.sql.exec.spi.JdbcOperation;
|
import org.hibernate.sql.exec.spi.JdbcOperation;
|
||||||
|
import org.hibernate.sql.model.MutationOperation;
|
||||||
|
import org.hibernate.sql.model.ast.ColumnValueBinding;
|
||||||
import org.hibernate.sql.model.internal.TableInsertStandard;
|
import org.hibernate.sql.model.internal.TableInsertStandard;
|
||||||
|
import org.hibernate.sql.model.internal.TableUpsert;
|
||||||
|
import org.hibernate.sql.model.jdbc.UpsertOperation;
|
||||||
|
|
||||||
import static org.hibernate.internal.util.collections.CollectionHelper.isNotEmpty;
|
import static org.hibernate.internal.util.collections.CollectionHelper.isNotEmpty;
|
||||||
|
|
||||||
|
@ -307,4 +311,162 @@ public class H2SqlAstTranslator<T extends JdbcOperation> extends AbstractSqlAstT
|
||||||
// Introduction of PERCENT support https://github.com/h2database/h2database/commit/f45913302e5f6ad149155a73763c0c59d8205849
|
// Introduction of PERCENT support https://github.com/h2database/h2database/commit/f45913302e5f6ad149155a73763c0c59d8205849
|
||||||
return getDialect().getVersion().isSameOrAfter( 1, 4, 198 );
|
return getDialect().getVersion().isSameOrAfter( 1, 4, 198 );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public MutationOperation visitUpsert(TableUpsert tableUpsert) {
|
||||||
|
// template:
|
||||||
|
//
|
||||||
|
// merge into [table] as t
|
||||||
|
// using values([bindings]) as s ([column-names])
|
||||||
|
// on t.[key] = s.[key]
|
||||||
|
// and t.[key] = ?
|
||||||
|
// when not matched
|
||||||
|
// then insert ...
|
||||||
|
// when matched
|
||||||
|
// and s.[columns] is null
|
||||||
|
// then delete
|
||||||
|
// when matched
|
||||||
|
// then update set ...
|
||||||
|
|
||||||
|
renderMergeInto( tableUpsert );
|
||||||
|
appendSql( " " );
|
||||||
|
renderMergeUsing( tableUpsert );
|
||||||
|
appendSql( " " );
|
||||||
|
renderMergeOn( tableUpsert );
|
||||||
|
appendSql( " " );
|
||||||
|
renderMergeInsert( tableUpsert );
|
||||||
|
appendSql( " " );
|
||||||
|
renderMergeDelete( tableUpsert );
|
||||||
|
appendSql( " " );
|
||||||
|
renderMergeUpdate( tableUpsert );
|
||||||
|
|
||||||
|
return new UpsertOperation(
|
||||||
|
tableUpsert.getMutatingTable().getTableMapping(),
|
||||||
|
tableUpsert.getMutationTarget(),
|
||||||
|
getSql(),
|
||||||
|
getParameterBinders()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void renderMergeInto(TableUpsert tableUpsert) {
|
||||||
|
appendSql( "merge into " );
|
||||||
|
appendSql( tableUpsert.getMutatingTable().getTableName() );
|
||||||
|
appendSql( " as t" );
|
||||||
|
}
|
||||||
|
|
||||||
|
private void renderMergeUsing(TableUpsert tableUpsert) {
|
||||||
|
final List<ColumnValueBinding> valueBindings = tableUpsert.getValueBindings();
|
||||||
|
final List<ColumnValueBinding> keyBindings = tableUpsert.getKeyBindings();
|
||||||
|
|
||||||
|
appendSql( "using values (" );
|
||||||
|
|
||||||
|
for ( int i = 0; i < keyBindings.size(); i++ ) {
|
||||||
|
final ColumnValueBinding keyBinding = keyBindings.get( i );
|
||||||
|
if ( i > 0 ) {
|
||||||
|
appendSql( ", " );
|
||||||
|
}
|
||||||
|
keyBinding.getValueExpression().accept( this );
|
||||||
|
}
|
||||||
|
for ( int i = 0; i < valueBindings.size(); i++ ) {
|
||||||
|
appendSql( ", " );
|
||||||
|
final ColumnValueBinding valueBinding = valueBindings.get( i );
|
||||||
|
valueBinding.getValueExpression().accept( this );
|
||||||
|
}
|
||||||
|
|
||||||
|
appendSql( ") as s(" );
|
||||||
|
|
||||||
|
for ( int i = 0; i < keyBindings.size(); i++ ) {
|
||||||
|
final ColumnValueBinding keyBinding = keyBindings.get( i );
|
||||||
|
if ( i > 0 ) {
|
||||||
|
appendSql( ", " );
|
||||||
|
}
|
||||||
|
appendSql( keyBinding.getColumnReference().getColumnExpression() );
|
||||||
|
}
|
||||||
|
for ( int i = 0; i < valueBindings.size(); i++ ) {
|
||||||
|
appendSql( ", " );
|
||||||
|
final ColumnValueBinding valueBinding = valueBindings.get( i );
|
||||||
|
appendSql( valueBinding.getColumnReference().getColumnExpression() );
|
||||||
|
}
|
||||||
|
|
||||||
|
appendSql( ")" );
|
||||||
|
}
|
||||||
|
|
||||||
|
private void renderMergeOn(TableUpsert tableUpsert) {
|
||||||
|
appendSql( "on " );
|
||||||
|
|
||||||
|
// todo : optimistic locks?
|
||||||
|
|
||||||
|
final List<ColumnValueBinding> keyBindings = tableUpsert.getKeyBindings();
|
||||||
|
for ( int i = 0; i < keyBindings.size(); i++ ) {
|
||||||
|
final ColumnValueBinding keyBinding = keyBindings.get( i );
|
||||||
|
if ( i > 0 ) {
|
||||||
|
appendSql( " and " );
|
||||||
|
}
|
||||||
|
keyBinding.getColumnReference().appendReadExpression( this, "t" );
|
||||||
|
appendSql( "=" );
|
||||||
|
keyBinding.getColumnReference().appendReadExpression( this, "s" );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void renderMergeInsert(TableUpsert tableUpsert) {
|
||||||
|
final List<ColumnValueBinding> valueBindings = tableUpsert.getValueBindings();
|
||||||
|
final List<ColumnValueBinding> keyBindings = tableUpsert.getKeyBindings();
|
||||||
|
|
||||||
|
appendSql( "when not matched then insert (" );
|
||||||
|
for ( int i = 0; i < keyBindings.size(); i++ ) {
|
||||||
|
if ( i > 0 ) {
|
||||||
|
appendSql( ", " );
|
||||||
|
}
|
||||||
|
final ColumnValueBinding keyBinding = keyBindings.get( i );
|
||||||
|
appendSql( keyBinding.getColumnReference().getColumnExpression() );
|
||||||
|
}
|
||||||
|
for ( int i = 0; i < valueBindings.size(); i++ ) {
|
||||||
|
appendSql( ", " );
|
||||||
|
final ColumnValueBinding valueBinding = valueBindings.get( i );
|
||||||
|
appendSql( valueBinding.getColumnReference().getColumnExpression() );
|
||||||
|
}
|
||||||
|
|
||||||
|
appendSql( ") values (" );
|
||||||
|
for ( int i = 0; i < keyBindings.size(); i++ ) {
|
||||||
|
if ( i > 0 ) {
|
||||||
|
appendSql( ", " );
|
||||||
|
}
|
||||||
|
final ColumnValueBinding keyBinding = keyBindings.get( i );
|
||||||
|
keyBinding.getColumnReference().appendReadExpression( this, "s" );
|
||||||
|
}
|
||||||
|
for ( int i = 0; i < valueBindings.size(); i++ ) {
|
||||||
|
appendSql( ", " );
|
||||||
|
final ColumnValueBinding valueBinding = valueBindings.get( i );
|
||||||
|
valueBinding.getColumnReference().appendReadExpression( this, "s" );
|
||||||
|
}
|
||||||
|
|
||||||
|
appendSql( ")" );
|
||||||
|
}
|
||||||
|
|
||||||
|
private void renderMergeDelete(TableUpsert tableUpsert) {
|
||||||
|
final List<ColumnValueBinding> valueBindings = tableUpsert.getValueBindings();
|
||||||
|
|
||||||
|
appendSql( " when matched " );
|
||||||
|
for ( int i = 0; i < valueBindings.size(); i++ ) {
|
||||||
|
final ColumnValueBinding binding = valueBindings.get( i );
|
||||||
|
appendSql( " and " );
|
||||||
|
binding.getColumnReference().appendReadExpression( this, "s" );
|
||||||
|
appendSql( " is null" );
|
||||||
|
}
|
||||||
|
appendSql( " then delete" );
|
||||||
|
}
|
||||||
|
|
||||||
|
private void renderMergeUpdate(TableUpsert tableUpsert) {
|
||||||
|
final List<ColumnValueBinding> valueBindings = tableUpsert.getValueBindings();
|
||||||
|
|
||||||
|
appendSql( " when matched then update set " );
|
||||||
|
for ( int i = 0; i < valueBindings.size(); i++ ) {
|
||||||
|
final ColumnValueBinding binding = valueBindings.get( i );
|
||||||
|
if ( i > 0 ) {
|
||||||
|
appendSql( ", " );
|
||||||
|
}
|
||||||
|
binding.getColumnReference().appendColumnForWrite( this, "t" );
|
||||||
|
appendSql( "=" );
|
||||||
|
binding.getColumnReference().appendColumnForWrite( this, "s" );
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,37 @@
|
||||||
|
/*
|
||||||
|
* Hibernate, Relational Persistence for Idiomatic Java
|
||||||
|
*
|
||||||
|
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
|
||||||
|
* See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html.
|
||||||
|
*/
|
||||||
|
package org.hibernate.sql.model.jdbc;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.hibernate.jdbc.Expectations;
|
||||||
|
import org.hibernate.sql.exec.spi.JdbcParameterBinder;
|
||||||
|
import org.hibernate.sql.model.MutationTarget;
|
||||||
|
import org.hibernate.sql.model.MutationType;
|
||||||
|
import org.hibernate.sql.model.TableMapping;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* JdbcMutation implementation for MERGE/UPSERT handling
|
||||||
|
*
|
||||||
|
* @author Steve Ebersole
|
||||||
|
*/
|
||||||
|
public class UpsertOperation extends AbstractJdbcMutation {
|
||||||
|
public UpsertOperation(
|
||||||
|
TableMapping tableDetails,
|
||||||
|
MutationTarget<?> mutationTarget,
|
||||||
|
String sql,
|
||||||
|
List<? extends JdbcParameterBinder> parameterBinders) {
|
||||||
|
super( tableDetails, mutationTarget, sql, false, Expectations.NONE, parameterBinders );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public MutationType getMutationType() {
|
||||||
|
return MutationType.UPDATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
|
@ -1,12 +1,15 @@
|
||||||
package org.hibernate.orm.test.secondarytable;
|
package org.hibernate.orm.test.secondarytable;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
|
||||||
|
import org.hibernate.dialect.H2Dialect;
|
||||||
|
|
||||||
import org.hibernate.testing.orm.junit.DomainModel;
|
import org.hibernate.testing.orm.junit.DomainModel;
|
||||||
import org.hibernate.testing.orm.junit.SessionFactory;
|
import org.hibernate.testing.orm.junit.SessionFactory;
|
||||||
import org.hibernate.testing.orm.junit.SessionFactoryScope;
|
import org.hibernate.testing.orm.junit.SessionFactoryScope;
|
||||||
|
import org.hibernate.testing.orm.junit.SkipForDialect;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
@ -15,7 +18,17 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
@SessionFactory
|
@SessionFactory
|
||||||
public class SecondaryRowTest {
|
public class SecondaryRowTest {
|
||||||
@Test
|
@Test
|
||||||
|
@SkipForDialect(
|
||||||
|
dialectClass = H2Dialect.class,
|
||||||
|
reason = "This test relies on SQL execution counts which is based on the legacy multi-statement solution. " +
|
||||||
|
"HHH-16084 adds support for H2 MERGE which handles those cases in one statement, so the counts are off. " +
|
||||||
|
"Need to change this test to physically check the number of rows. " +
|
||||||
|
"See e.g. org.hibernate.orm.test.write.UpsertTests"
|
||||||
|
)
|
||||||
public void testSecondaryRow(SessionFactoryScope scope) {
|
public void testSecondaryRow(SessionFactoryScope scope) {
|
||||||
|
// we need to check the actual number of rows.
|
||||||
|
// because we now support merge/upsert SQL statements, the
|
||||||
|
// because HHH-16084 implements support for usage
|
||||||
int seq = scope.getSessionFactory().getJdbcServices().getDialect().getSequenceSupport().supportsSequences()
|
int seq = scope.getSessionFactory().getJdbcServices().getDialect().getSequenceSupport().supportsSequences()
|
||||||
? 1 : 0;
|
? 1 : 0;
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,134 @@
|
||||||
|
/*
|
||||||
|
* Hibernate, Relational Persistence for Idiomatic Java
|
||||||
|
*
|
||||||
|
* License: GNU Lesser General Public License (LGPL), version 2.1 or later.
|
||||||
|
* See the lgpl.txt file in the root directory or http://www.gnu.org/licenses/lgpl-2.1.html.
|
||||||
|
*/
|
||||||
|
package org.hibernate.orm.test.write;
|
||||||
|
|
||||||
|
import org.hibernate.testing.orm.junit.DomainModel;
|
||||||
|
import org.hibernate.testing.orm.junit.SessionFactory;
|
||||||
|
import org.hibernate.testing.orm.junit.SessionFactoryScope;
|
||||||
|
import org.junit.jupiter.api.AfterEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import jakarta.persistence.Basic;
|
||||||
|
import jakarta.persistence.Column;
|
||||||
|
import jakarta.persistence.Entity;
|
||||||
|
import jakarta.persistence.Id;
|
||||||
|
import jakarta.persistence.SecondaryTable;
|
||||||
|
import jakarta.persistence.Table;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Steve Ebersole
|
||||||
|
*/
|
||||||
|
@DomainModel(annotatedClasses = UpsertTests.TheEntity.class)
|
||||||
|
@SessionFactory
|
||||||
|
public class UpsertTests {
|
||||||
|
@Test
|
||||||
|
void testUpsertInsert(SessionFactoryScope scope) {
|
||||||
|
scope.inTransaction( (session) -> {
|
||||||
|
session.persist( new TheEntity( 1, "name", null ) );
|
||||||
|
} );
|
||||||
|
|
||||||
|
verifySecondaryRows( scope, 0 );
|
||||||
|
|
||||||
|
scope.inTransaction( (session) -> {
|
||||||
|
final TheEntity loaded = session.byId( TheEntity.class ).load( 1 );
|
||||||
|
loaded.setDetails( "non-null" );
|
||||||
|
} );
|
||||||
|
|
||||||
|
verifySecondaryRows( scope, 1 );
|
||||||
|
}
|
||||||
|
|
||||||
|
private void verifySecondaryRows(SessionFactoryScope scope, int expectedCount) {
|
||||||
|
scope.inTransaction( (session) -> {
|
||||||
|
final int count = session.createNativeQuery( "select count(1) from supplements", Integer.class ).getSingleResult();
|
||||||
|
assertThat( count ).isEqualTo( expectedCount );
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testUpsertUpdate(SessionFactoryScope scope) {
|
||||||
|
scope.inTransaction( (session) -> {
|
||||||
|
session.persist( new TheEntity( 1, "name", "non-null" ) );
|
||||||
|
} );
|
||||||
|
|
||||||
|
verifySecondaryRows( scope, 1 );
|
||||||
|
|
||||||
|
scope.inTransaction( (session) -> {
|
||||||
|
final TheEntity loaded = session.byId( TheEntity.class ).load( 1 );
|
||||||
|
loaded.setDetails( "non-non-null" );
|
||||||
|
} );
|
||||||
|
|
||||||
|
verifySecondaryRows( scope, 1 );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void testUpsertDelete(SessionFactoryScope scope) {
|
||||||
|
scope.inTransaction( (session) -> {
|
||||||
|
session.persist( new TheEntity( 1, "name", "non-null" ) );
|
||||||
|
} );
|
||||||
|
|
||||||
|
verifySecondaryRows( scope, 1 );
|
||||||
|
|
||||||
|
scope.inTransaction( (session) -> {
|
||||||
|
final TheEntity loaded = session.byId( TheEntity.class ).load( 1 );
|
||||||
|
loaded.setDetails( null );
|
||||||
|
} );
|
||||||
|
|
||||||
|
verifySecondaryRows( scope, 0 );
|
||||||
|
}
|
||||||
|
|
||||||
|
@AfterEach
|
||||||
|
void cleanUpTestData(SessionFactoryScope scope) {
|
||||||
|
scope.inTransaction( (session) -> {
|
||||||
|
session.createMutationQuery( "delete TheEntity" ).executeUpdate();
|
||||||
|
} );
|
||||||
|
}
|
||||||
|
|
||||||
|
@Entity( name = "TheEntity" )
|
||||||
|
@Table( name = "entities" )
|
||||||
|
@SecondaryTable( name = "supplements" )
|
||||||
|
public static class TheEntity {
|
||||||
|
@Id
|
||||||
|
private Integer id;
|
||||||
|
@Basic
|
||||||
|
private String name;
|
||||||
|
@Basic
|
||||||
|
@Column( table = "supplements" )
|
||||||
|
private String details;
|
||||||
|
|
||||||
|
private TheEntity() {
|
||||||
|
// for use by Hibernate
|
||||||
|
}
|
||||||
|
|
||||||
|
public TheEntity(Integer id, String name, String details) {
|
||||||
|
this.id = id;
|
||||||
|
this.name = name;
|
||||||
|
this.details = details;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Integer getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getName() {
|
||||||
|
return name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setName(String name) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getDetails() {
|
||||||
|
return details;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDetails(String details) {
|
||||||
|
this.details = details;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue