Skip to content

Commit 6193371

Browse files
committed
HHH-19498 - improve upserts on MySQL and Maria
Signed-off-by: Jan Schatteman <[email protected]>
1 parent f6c7d64 commit 6193371

File tree

6 files changed

+147
-8
lines changed

6 files changed

+147
-8
lines changed

hibernate-core/src/main/java/org/hibernate/dialect/MariaDBDialect.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,13 +35,16 @@
3535
import org.hibernate.exception.spi.SQLExceptionConversionDelegate;
3636
import org.hibernate.exception.spi.TemplatedViolatedConstraintNameExtractor;
3737
import org.hibernate.exception.spi.ViolatedConstraintNameExtractor;
38+
import org.hibernate.persister.entity.mutation.EntityMutationTarget;
3839
import org.hibernate.query.sqm.CastType;
3940
import org.hibernate.service.ServiceRegistry;
4041
import org.hibernate.sql.ast.SqlAstTranslator;
4142
import org.hibernate.sql.ast.SqlAstTranslatorFactory;
4243
import org.hibernate.sql.ast.spi.StandardSqlAstTranslatorFactory;
4344
import org.hibernate.sql.ast.tree.Statement;
4445
import org.hibernate.sql.exec.spi.JdbcOperation;
46+
import org.hibernate.sql.model.MutationOperation;
47+
import org.hibernate.sql.model.internal.OptionalTableUpdate;
4548
import org.hibernate.tool.schema.extract.internal.SequenceInformationExtractorMariaDBDatabaseImpl;
4649
import org.hibernate.tool.schema.extract.spi.SequenceInformationExtractor;
4750
import org.hibernate.type.SqlTypes;
@@ -421,4 +424,10 @@ public boolean supportsWithClauseInSubquery() {
421424
return false;
422425
}
423426

427+
@Override
428+
public MutationOperation createOptionalTableUpdateOperation(EntityMutationTarget mutationTarget, OptionalTableUpdate optionalTableUpdate, SessionFactoryImplementor factory) {
429+
final MariaDBSqlAstTranslator<?> translator = new MariaDBSqlAstTranslator<>( factory, optionalTableUpdate, MariaDBDialect.this );
430+
return translator.createMergeOperation( optionalTableUpdate );
431+
}
432+
424433
}

hibernate-core/src/main/java/org/hibernate/dialect/MySQLDialect.java

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
import org.hibernate.mapping.CheckConstraint;
4646
import org.hibernate.metamodel.mapping.EntityMappingType;
4747
import org.hibernate.metamodel.spi.RuntimeModelCreationContext;
48+
import org.hibernate.persister.entity.mutation.EntityMutationTarget;
4849
import org.hibernate.query.common.TemporalUnit;
4950
import org.hibernate.query.sqm.CastType;
5051
import org.hibernate.query.sqm.IntervalType;
@@ -63,6 +64,8 @@
6364
import org.hibernate.sql.ast.spi.StandardSqlAstTranslatorFactory;
6465
import org.hibernate.sql.ast.tree.Statement;
6566
import org.hibernate.sql.exec.spi.JdbcOperation;
67+
import org.hibernate.sql.model.MutationOperation;
68+
import org.hibernate.sql.model.internal.OptionalTableUpdate;
6669
import org.hibernate.type.BasicTypeRegistry;
6770
import org.hibernate.type.NullType;
6871
import org.hibernate.type.SqlTypes;
@@ -1668,4 +1671,10 @@ public boolean supportsRowValueConstructorSyntaxInQuantifiedPredicates() {
16681671
return false;
16691672
}
16701673

1674+
@Override
1675+
public MutationOperation createOptionalTableUpdateOperation(EntityMutationTarget mutationTarget, OptionalTableUpdate optionalTableUpdate, SessionFactoryImplementor factory) {
1676+
final MySQLSqlAstTranslator<?> translator = new MySQLSqlAstTranslator<>( factory, optionalTableUpdate, MySQLDialect.this );
1677+
return translator.createMergeOperation( optionalTableUpdate );
1678+
}
1679+
16711680
}

hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/MariaDBSqlAstTranslator.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
import org.hibernate.metamodel.mapping.JdbcMappingContainer;
1515
import org.hibernate.query.sqm.ComparisonOperator;
1616
import org.hibernate.sql.ast.Clause;
17-
import org.hibernate.sql.ast.spi.AbstractSqlAstTranslator;
1817
import org.hibernate.sql.ast.tree.Statement;
1918
import org.hibernate.sql.ast.tree.delete.DeleteStatement;
2019
import org.hibernate.sql.ast.tree.expression.BinaryArithmeticExpression;
@@ -44,7 +43,7 @@
4443
*
4544
* @author Christian Beikov
4645
*/
47-
public class MariaDBSqlAstTranslator<T extends JdbcOperation> extends AbstractSqlAstTranslator<T> {
46+
public class MariaDBSqlAstTranslator<T extends JdbcOperation> extends SqlAstTranslatorWithOnDuplicateKeyUpdate<T> {
4847

4948
private final MariaDBDialect dialect;
5049

hibernate-core/src/main/java/org/hibernate/dialect/sql/ast/MySQLSqlAstTranslator.java

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
import org.hibernate.internal.util.collections.Stack;
1313
import org.hibernate.query.sqm.ComparisonOperator;
1414
import org.hibernate.sql.ast.Clause;
15-
import org.hibernate.sql.ast.spi.AbstractSqlAstTranslator;
1615
import org.hibernate.sql.ast.tree.Statement;
1716
import org.hibernate.sql.ast.tree.delete.DeleteStatement;
1817
import org.hibernate.sql.ast.tree.expression.BinaryArithmeticExpression;
@@ -46,7 +45,7 @@
4645
*
4746
* @author Christian Beikov
4847
*/
49-
public class MySQLSqlAstTranslator<T extends JdbcOperation> extends AbstractSqlAstTranslator<T> {
48+
public class MySQLSqlAstTranslator<T extends JdbcOperation> extends SqlAstTranslatorWithOnDuplicateKeyUpdate<T> {
5049

5150
/**
5251
* On MySQL, 1GB or {@code 2^30 - 1} is the maximum size that a char value can be casted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
* Copyright Red Hat Inc. and Hibernate Authors
4+
*/
5+
package org.hibernate.dialect.sql.ast;
6+
7+
8+
import org.hibernate.engine.spi.SessionFactoryImplementor;
9+
import org.hibernate.sql.ast.spi.SqlAstTranslatorWithUpsert;
10+
import org.hibernate.sql.ast.tree.Statement;
11+
import org.hibernate.sql.exec.spi.JdbcOperation;
12+
import org.hibernate.sql.model.ast.ColumnValueBinding;
13+
import org.hibernate.sql.model.internal.OptionalTableUpdate;
14+
15+
import java.util.List;
16+
17+
/**
18+
* @author Jan Schatteman
19+
*/
20+
public class SqlAstTranslatorWithOnDuplicateKeyUpdate<T extends JdbcOperation> extends SqlAstTranslatorWithUpsert<T> {
21+
22+
public SqlAstTranslatorWithOnDuplicateKeyUpdate(SessionFactoryImplementor sessionFactory, Statement statement) {
23+
super( sessionFactory, statement );
24+
}
25+
26+
@Override
27+
protected void renderUpsertStatement(OptionalTableUpdate optionalTableUpdate) {
28+
// INSERT INTO employees (id, name, salary)
29+
// VALUES (1, 'Alice', 50000)
30+
// ON DUPLICATE KEY UPDATE
31+
// name = VALUES(name),
32+
// salary = VALUES(salary)
33+
renderInsertInto( optionalTableUpdate );
34+
appendSql( " " );
35+
renderOnDuplicateKeyUpdate( optionalTableUpdate );
36+
}
37+
38+
protected void renderInsertInto(OptionalTableUpdate optionalTableUpdate) {
39+
appendSql( "insert into " );
40+
appendSql( optionalTableUpdate.getMutatingTable().getTableName() );
41+
appendSql( " (" );
42+
43+
final List<ColumnValueBinding> keyBindings = optionalTableUpdate.getKeyBindings();
44+
for ( ColumnValueBinding keyBinding : keyBindings ) {
45+
appendSql( keyBinding.getColumnReference().getColumnExpression() );
46+
appendSql( ',' );
47+
}
48+
49+
optionalTableUpdate.forEachValueBinding( (columnPosition, columnValueBinding) -> {
50+
appendSql( columnValueBinding.getColumnReference().getColumnExpression() );
51+
if ( columnPosition != optionalTableUpdate.getValueBindings().size() - 1 ) {
52+
appendSql( ',' );
53+
}
54+
} );
55+
56+
appendSql( ") values (" );
57+
58+
for ( ColumnValueBinding keyBinding : keyBindings ) {
59+
keyBinding.getValueExpression().accept( this );
60+
appendSql( ',' );
61+
}
62+
63+
optionalTableUpdate.forEachValueBinding( (columnPosition, columnValueBinding) -> {
64+
if ( columnPosition > 0 ) {
65+
appendSql( ',' );
66+
}
67+
columnValueBinding.getValueExpression().accept( this );
68+
} );
69+
70+
appendSql( ")" );
71+
}
72+
73+
protected void renderOnDuplicateKeyUpdate(OptionalTableUpdate optionalTableUpdate) {
74+
appendSql( "on duplicate key update " );
75+
optionalTableUpdate.forEachValueBinding( (columnPosition, columnValueBinding) -> {
76+
if ( columnPosition > 0 ) {
77+
appendSql( ',' );
78+
}
79+
appendSql( columnValueBinding.getColumnReference().getColumnExpression() );
80+
append( " = " );
81+
appendSql( "values (" );
82+
appendSql( columnValueBinding.getColumnReference().getColumnExpression() );
83+
appendSql( ")" );
84+
} );
85+
}
86+
87+
}

hibernate-core/src/test/java/org/hibernate/orm/test/stateless/UpsertTest.java

Lines changed: 40 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,19 @@
66

77
import jakarta.persistence.Entity;
88
import jakarta.persistence.Id;
9+
import org.hibernate.dialect.MariaDBDialect;
10+
import org.hibernate.dialect.MySQLDialect;
11+
import org.hibernate.testing.jdbc.SQLStatementInspector;
912
import org.hibernate.testing.orm.junit.DomainModel;
13+
import org.hibernate.testing.orm.junit.RequiresDialect;
14+
import org.hibernate.testing.orm.junit.RequiresDialects;
1015
import org.hibernate.testing.orm.junit.SessionFactory;
1116
import org.hibernate.testing.orm.junit.SessionFactoryScope;
1217
import org.junit.jupiter.api.Test;
1318

1419
import static org.junit.jupiter.api.Assertions.assertEquals;
1520

16-
@SessionFactory
21+
@SessionFactory(useCollectingStatementInspector = true)
1722
@DomainModel(annotatedClasses = UpsertTest.Record.class)
1823
public class UpsertTest {
1924
@Test void test(SessionFactoryScope scope) {
@@ -25,14 +30,45 @@ public class UpsertTest {
2530
assertEquals("hello earth", s.get( Record.class,123L).message);
2631
assertEquals("hello mars", s.get( Record.class,456L).message);
2732
});
28-
scope.inStatelessTransaction(s-> {
29-
s.upsert(new Record(123L,"goodbye earth"));
30-
});
33+
scope.inStatelessTransaction(s-> s.upsert(new Record(123L,"goodbye earth")) );
3134
scope.inStatelessTransaction(s-> {
3235
assertEquals("goodbye earth", s.get( Record.class,123L).message);
3336
assertEquals("hello mars", s.get( Record.class,456L).message);
3437
});
3538
}
39+
40+
@RequiresDialects(
41+
value = {
42+
@RequiresDialect( MySQLDialect.class ),
43+
@RequiresDialect( MariaDBDialect.class )
44+
}
45+
)
46+
@Test void testMySQL(SessionFactoryScope scope) {
47+
SQLStatementInspector statementInspector = scope.getCollectingStatementInspector();
48+
statementInspector.clear();
49+
50+
scope.inStatelessTransaction(s-> {
51+
s.upsert(new Record(123L,"hello earth"));
52+
s.upsert(new Record(456L,"hello mars"));
53+
});
54+
// Verify that only a single query is executed for each upsert, in contrast to the former update+insert
55+
statementInspector.assertExecutedCount( 2 );
56+
57+
scope.inStatelessTransaction(s-> {
58+
assertEquals("hello earth",s.get(Record.class,123L).message);
59+
assertEquals("hello mars",s.get(Record.class,456L).message);
60+
});
61+
statementInspector.clear();
62+
63+
scope.inStatelessTransaction(s-> s.upsert(new Record(123L,"goodbye earth")) );
64+
statementInspector.assertExecutedCount( 1 );
65+
66+
scope.inStatelessTransaction(s-> {
67+
assertEquals("goodbye earth",s.get(Record.class,123L).message);
68+
assertEquals("hello mars",s.get(Record.class,456L).message);
69+
});
70+
}
71+
3672
@Entity
3773
static class Record {
3874
@Id Long id;

0 commit comments

Comments
 (0)