Skip to content

Commit 4a39789

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

File tree

6 files changed

+203
-8
lines changed

6 files changed

+203
-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.
Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
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+
/*
29+
Template: (for an entity with @Version, and without using values() - but this might require changes in parameter binding)
30+
INSERT INTO employees (id, name, salary, version)
31+
VALUES (?, ?, ?, ?) AS t
32+
ON DUPLICATE KEY UPDATE
33+
name = IF(employees.version=?,t.name,employees.name),
34+
salary = IF(employees.version=?,t.salary,employees.salary),
35+
version = IF(employees.version=?,t.version,employees.version),
36+
37+
So, initially we'll have:
38+
INSERT INTO employees (id, name, salary, version)
39+
VALUES (?, ?, ?, ?)
40+
ON DUPLICATE KEY UPDATE
41+
name = IF(version=@oldversion:=?,VALUES(name), employees.name),
42+
salary = IF(version=@oldversion?,VALUES(salary),employees.salary),
43+
version = IF(version=@oldversion?,VALUES(version),employees.version),
44+
*/
45+
renderInsertInto( optionalTableUpdate );
46+
appendSql( " " );
47+
renderOnDuplicateKeyUpdate( optionalTableUpdate );
48+
}
49+
50+
protected void renderInsertInto(OptionalTableUpdate optionalTableUpdate) {
51+
appendSql( "insert into " );
52+
appendSql( optionalTableUpdate.getMutatingTable().getTableName() );
53+
appendSql( " (" );
54+
55+
final List<ColumnValueBinding> keyBindings = optionalTableUpdate.getKeyBindings();
56+
for ( ColumnValueBinding keyBinding : keyBindings ) {
57+
appendSql( keyBinding.getColumnReference().getColumnExpression() );
58+
appendSql( ',' );
59+
}
60+
61+
optionalTableUpdate.forEachValueBinding( (columnPosition, columnValueBinding) -> {
62+
appendSql( columnValueBinding.getColumnReference().getColumnExpression() );
63+
if ( columnPosition != optionalTableUpdate.getValueBindings().size() - 1 ) {
64+
appendSql( ',' );
65+
}
66+
} );
67+
68+
appendSql( ") values (" );
69+
70+
for ( ColumnValueBinding keyBinding : keyBindings ) {
71+
keyBinding.getValueExpression().accept( this );
72+
appendSql( ',' );
73+
}
74+
75+
optionalTableUpdate.forEachValueBinding( (columnPosition, columnValueBinding) -> {
76+
if ( columnPosition > 0 ) {
77+
appendSql( ',' );
78+
}
79+
columnValueBinding.getValueExpression().accept( this );
80+
} );
81+
appendSql( ")" );
82+
}
83+
84+
protected void renderOnDuplicateKeyUpdate(OptionalTableUpdate optionalTableUpdate) {
85+
86+
87+
appendSql( "on duplicate key update " );
88+
optionalTableUpdate.forEachValueBinding( (columnPosition, columnValueBinding) -> {
89+
final String columnName = columnValueBinding.getColumnReference().getColumnExpression();
90+
if ( columnPosition > 0 ) {
91+
appendSql( ',' );
92+
}
93+
appendSql( columnName );
94+
append( " = " );
95+
96+
if ( optionalTableUpdate.getNumberOfOptimisticLockBindings() > 0 ) {
97+
renderVersionedUpdate( optionalTableUpdate, columnPosition, columnValueBinding );
98+
}
99+
else {
100+
renderNonVersionedUpdate( optionalTableUpdate, columnValueBinding );
101+
}
102+
} );
103+
}
104+
105+
private void renderVersionedUpdate(OptionalTableUpdate optionalTableUpdate, Integer columnPosition, ColumnValueBinding columnValueBinding) {
106+
final String tableName = optionalTableUpdate.getMutatingTable().getTableName();
107+
appendSql( "if(" );
108+
renderVersionRestriction( tableName, optionalTableUpdate.getOptimisticLockBindings(), columnPosition );
109+
appendSql( "," );
110+
appendSql( "values(" );
111+
appendSql( columnValueBinding.getColumnReference().getColumnExpression() );
112+
appendSql( ")" );
113+
appendSql( "," );
114+
columnValueBinding.getColumnReference().appendColumnForWrite( this, tableName );
115+
appendSql( ")" );
116+
}
117+
118+
private void renderNonVersionedUpdate(OptionalTableUpdate optionalTableUpdate, ColumnValueBinding columnValueBinding) {
119+
appendSql( "values(" );
120+
appendSql( columnValueBinding.getColumnReference().getColumnExpression() );
121+
appendSql( ")" );
122+
}
123+
124+
private void renderVersionRestriction(String tableName, List<ColumnValueBinding> optimisticLockBindings, int index) {
125+
final String operator = index == 0 ? ":=" : "";
126+
final String versionVariable = "@oldversion" + operator;
127+
for (int i = 0; i < optimisticLockBindings.size(); i++) {
128+
// if ( i>0 ) {
129+
// appendSql(" and ");
130+
// }
131+
final ColumnValueBinding binding = optimisticLockBindings.get( i );
132+
binding.getColumnReference().appendColumnForWrite( this, tableName );
133+
appendSql( "=" );
134+
appendSql( versionVariable );
135+
// if ( i == 0 ) {
136+
if ( index == 0) {
137+
binding.getValueExpression().accept( this );
138+
}
139+
// }
140+
}
141+
}
142+
143+
}

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)