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.metamodel.mapping.EntityMappingType;
|
||||
import org.hibernate.metamodel.spi.RuntimeModelCreationContext;
|
||||
import org.hibernate.persister.entity.mutation.EntityMutationTarget;
|
||||
import org.hibernate.query.sqm.FetchClauseType;
|
||||
import org.hibernate.query.sqm.IntervalType;
|
||||
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.tree.Statement;
|
||||
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.SequenceInformationExtractorLegacyImpl;
|
||||
import org.hibernate.tool.schema.extract.spi.SequenceInformationExtractor;
|
||||
|
@ -867,4 +870,13 @@ public class H2Dialect extends Dialect {
|
|||
public int rowIdSqlType() {
|
||||
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.SelectClause;
|
||||
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.TableUpsert;
|
||||
import org.hibernate.sql.model.jdbc.UpsertOperation;
|
||||
|
||||
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
|
||||
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;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import org.hibernate.dialect.H2Dialect;
|
||||
|
||||
import org.hibernate.testing.orm.junit.DomainModel;
|
||||
import org.hibernate.testing.orm.junit.SessionFactory;
|
||||
import org.hibernate.testing.orm.junit.SessionFactoryScope;
|
||||
import org.hibernate.testing.orm.junit.SkipForDialect;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
@ -15,7 +18,17 @@ import static org.junit.jupiter.api.Assertions.assertTrue;
|
|||
@SessionFactory
|
||||
public class SecondaryRowTest {
|
||||
@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) {
|
||||
// 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()
|
||||
? 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