Skip to content

Commit 8c07fc9

Browse files
committed
HHH-19498 - improve upserts on MySQL and MariaDB
Signed-off-by: Jan Schatteman <[email protected]>
1 parent 562c5c6 commit 8c07fc9

File tree

9 files changed

+357
-11
lines changed

9 files changed

+357
-11
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
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/*
2+
* SPDX-License-Identifier: Apache-2.0
3+
* Copyright Red Hat Inc. and Hibernate Authors
4+
*/
5+
package org.hibernate.dialect;
6+
7+
8+
import org.hibernate.StaleStateException;
9+
import org.hibernate.engine.jdbc.mutation.JdbcValueBindings;
10+
import org.hibernate.engine.spi.SharedSessionContractImplementor;
11+
import org.hibernate.jdbc.Expectation;
12+
import org.hibernate.persister.entity.mutation.EntityMutationTarget;
13+
import org.hibernate.persister.entity.mutation.EntityTableMapping;
14+
import org.hibernate.sql.model.ValuesAnalysis;
15+
import org.hibernate.sql.model.internal.OptionalTableUpdate;
16+
import org.hibernate.sql.model.jdbc.DeleteOrUpsertOperation;
17+
import org.hibernate.sql.model.jdbc.UpsertOperation;
18+
19+
import java.sql.PreparedStatement;
20+
21+
22+
/**
23+
* @author Jan Schatteman
24+
*/
25+
public class MySQLDeleteOrUpsertOperation extends DeleteOrUpsertOperation {
26+
27+
private Expectation customExpectation;
28+
29+
public MySQLDeleteOrUpsertOperation(EntityMutationTarget mutationTarget, EntityTableMapping tableMapping, UpsertOperation upsertOperation, OptionalTableUpdate optionalTableUpdate) {
30+
super( mutationTarget, tableMapping, upsertOperation, optionalTableUpdate );
31+
}
32+
33+
@Override
34+
public void performMutation(JdbcValueBindings jdbcValueBindings, ValuesAnalysis valuesAnalysis, SharedSessionContractImplementor session) {
35+
customExpectation = new MySQLRowCountExpectation();
36+
super.performMutation( jdbcValueBindings, valuesAnalysis, session );
37+
}
38+
39+
@Override
40+
protected Expectation getExpectation() {
41+
return customExpectation;
42+
}
43+
44+
private class MySQLRowCountExpectation implements Expectation {
45+
@Override
46+
public final void verifyOutcome(int rowCount, PreparedStatement statement, int batchPosition, String sql) {
47+
if ( getOptionalTableUpdate().getNumberOfOptimisticLockBindings() > 0 && rowCount == 0) {
48+
throw new StaleStateException(
49+
"Unexpected row count"
50+
+ " (for a versioned entity the expected row count should != 0)"
51+
+ " [" + sql + "]"
52+
);
53+
}
54+
if ( rowCount > 2 ) {
55+
throw new StaleStateException(
56+
"Unexpected row count"
57+
+ " (the expected row count for an ON DUPLICATE KEY UPDATE statement should be either 0, 1 or 2 )"
58+
+ " [" + sql + "]"
59+
);
60+
}
61+
}
62+
}
63+
64+
}

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: 164 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,164 @@
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.dialect.MySQLDeleteOrUpsertOperation;
9+
import org.hibernate.engine.spi.SessionFactoryImplementor;
10+
import org.hibernate.persister.entity.mutation.EntityTableMapping;
11+
import org.hibernate.sql.ast.spi.SqlAstTranslatorWithUpsert;
12+
import org.hibernate.sql.ast.tree.Statement;
13+
import org.hibernate.sql.exec.spi.JdbcOperation;
14+
import org.hibernate.sql.model.MutationOperation;
15+
import org.hibernate.sql.model.ast.ColumnValueBinding;
16+
import org.hibernate.sql.model.internal.OptionalTableUpdate;
17+
import org.hibernate.sql.model.jdbc.UpsertOperation;
18+
19+
import java.util.List;
20+
21+
/**
22+
* @author Jan Schatteman
23+
*/
24+
public class SqlAstTranslatorWithOnDuplicateKeyUpdate<T extends JdbcOperation> extends SqlAstTranslatorWithUpsert<T> {
25+
26+
public SqlAstTranslatorWithOnDuplicateKeyUpdate(SessionFactoryImplementor sessionFactory, Statement statement) {
27+
super( sessionFactory, statement );
28+
}
29+
30+
@Override
31+
public MutationOperation createMergeOperation(OptionalTableUpdate optionalTableUpdate) {
32+
renderUpsertStatement( optionalTableUpdate );
33+
34+
final UpsertOperation upsertOperation = new UpsertOperation(
35+
optionalTableUpdate.getMutatingTable().getTableMapping(),
36+
optionalTableUpdate.getMutationTarget(),
37+
getSql(),
38+
getParameterBinders()
39+
);
40+
41+
return new MySQLDeleteOrUpsertOperation(
42+
optionalTableUpdate.getMutationTarget(),
43+
(EntityTableMapping) optionalTableUpdate.getMutatingTable().getTableMapping(),
44+
upsertOperation,
45+
optionalTableUpdate
46+
);
47+
}
48+
49+
@Override
50+
protected void renderUpsertStatement(OptionalTableUpdate optionalTableUpdate) {
51+
/*
52+
Template: (for an entity with @Version, and without using values() - but this might require changes in parameter binding)
53+
INSERT INTO employees (id, name, salary, version)
54+
VALUES (?, ?, ?, ?) AS t
55+
ON DUPLICATE KEY UPDATE
56+
name = IF(employees.version=?,t.name,employees.name),
57+
salary = IF(employees.version=?,t.salary,employees.salary),
58+
version = IF(employees.version=?,t.version,employees.version),
59+
60+
So, initially we'll have:
61+
INSERT INTO employees (id, name, salary, version)
62+
VALUES (?, ?, ?, ?)
63+
ON DUPLICATE KEY UPDATE
64+
name = IF(version=@oldversion:=?,VALUES(name), employees.name),
65+
salary = IF(version=@oldversion?,VALUES(salary),employees.salary),
66+
version = IF(version=@oldversion?,VALUES(version),employees.version),
67+
*/
68+
renderInsertInto( optionalTableUpdate );
69+
appendSql( " " );
70+
renderOnDuplicateKeyUpdate( optionalTableUpdate );
71+
}
72+
73+
protected void renderInsertInto(OptionalTableUpdate optionalTableUpdate) {
74+
appendSql( "insert into " );
75+
appendSql( optionalTableUpdate.getMutatingTable().getTableName() );
76+
appendSql( " (" );
77+
78+
final List<ColumnValueBinding> keyBindings = optionalTableUpdate.getKeyBindings();
79+
for ( ColumnValueBinding keyBinding : keyBindings ) {
80+
appendSql( keyBinding.getColumnReference().getColumnExpression() );
81+
appendSql( ',' );
82+
}
83+
84+
optionalTableUpdate.forEachValueBinding( (columnPosition, columnValueBinding) -> {
85+
appendSql( columnValueBinding.getColumnReference().getColumnExpression() );
86+
if ( columnPosition != optionalTableUpdate.getValueBindings().size() - 1 ) {
87+
appendSql( ',' );
88+
}
89+
} );
90+
91+
appendSql( ") values (" );
92+
93+
for ( ColumnValueBinding keyBinding : keyBindings ) {
94+
keyBinding.getValueExpression().accept( this );
95+
appendSql( ',' );
96+
}
97+
98+
optionalTableUpdate.forEachValueBinding( (columnPosition, columnValueBinding) -> {
99+
if ( columnPosition > 0 ) {
100+
appendSql( ',' );
101+
}
102+
columnValueBinding.getValueExpression().accept( this );
103+
} );
104+
appendSql( ")" );
105+
}
106+
107+
protected void renderOnDuplicateKeyUpdate(OptionalTableUpdate optionalTableUpdate) {
108+
appendSql( "on duplicate key update " );
109+
optionalTableUpdate.forEachValueBinding( (columnPosition, columnValueBinding) -> {
110+
final String columnName = columnValueBinding.getColumnReference().getColumnExpression();
111+
if ( columnPosition > 0 ) {
112+
appendSql( ',' );
113+
}
114+
appendSql( columnName );
115+
append( " = " );
116+
117+
if ( optionalTableUpdate.getNumberOfOptimisticLockBindings() > 0 ) {
118+
renderVersionedUpdate( optionalTableUpdate, columnPosition, columnValueBinding );
119+
}
120+
else {
121+
renderNonVersionedUpdate( columnValueBinding );
122+
}
123+
} );
124+
}
125+
126+
private void renderVersionedUpdate(OptionalTableUpdate optionalTableUpdate, Integer columnPosition, ColumnValueBinding columnValueBinding) {
127+
final String tableName = optionalTableUpdate.getMutatingTable().getTableName();
128+
appendSql( "if(" );
129+
renderVersionRestriction( tableName, optionalTableUpdate.getOptimisticLockBindings(), columnPosition );
130+
appendSql( "," );
131+
appendSql( "values(" );
132+
appendSql( columnValueBinding.getColumnReference().getColumnExpression() );
133+
appendSql( ")" );
134+
appendSql( "," );
135+
columnValueBinding.getColumnReference().appendColumnForWrite( this, tableName );
136+
appendSql( ")" );
137+
}
138+
139+
private void renderNonVersionedUpdate(ColumnValueBinding columnValueBinding) {
140+
appendSql( "values(" );
141+
appendSql( columnValueBinding.getColumnReference().getColumnExpression() );
142+
appendSql( ")" );
143+
}
144+
145+
private void renderVersionRestriction(String tableName, List<ColumnValueBinding> optimisticLockBindings, int index) {
146+
final String operator = index == 0 ? ":=" : "";
147+
final String versionVariable = "@oldversion" + operator;
148+
for (int i = 0; i < optimisticLockBindings.size(); i++) {
149+
// if ( i>0 ) {
150+
// appendSql(" and ");
151+
// }
152+
final ColumnValueBinding binding = optimisticLockBindings.get( i );
153+
binding.getColumnReference().appendColumnForWrite( this, tableName );
154+
appendSql( "=" );
155+
appendSql( versionVariable );
156+
// if ( i == 0 ) {
157+
if ( index == 0) {
158+
binding.getValueExpression().accept( this );
159+
}
160+
// }
161+
}
162+
}
163+
164+
}

hibernate-core/src/main/java/org/hibernate/sql/model/jdbc/DeleteOrUpsertOperation.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ public class DeleteOrUpsertOperation implements SelfExecutingUpdateOperation {
4141

4242
private final OptionalTableUpdate optionalTableUpdate;
4343

44-
private final Expectation expectation = new Expectation.RowCount();
44+
private final Expectation expectation = getExpectation();
4545

4646
public DeleteOrUpsertOperation(
4747
EntityMutationTarget mutationTarget,
@@ -127,7 +127,7 @@ private void performDelete(JdbcValueBindings jdbcValueBindings, SharedSessionCon
127127
.executeUpdate( upsertDeleteStatement, statementDetails.getSqlString() );
128128
MODEL_MUTATION_LOGGER.tracef( "`%s` rows upsert-deleted from `%s`", rowCount, tableMapping.getTableName() );
129129
try {
130-
expectation.verifyOutcome( rowCount, upsertDeleteStatement, -1, statementDetails.getSqlString() );
130+
getExpectation().verifyOutcome( rowCount, upsertDeleteStatement, -1, statementDetails.getSqlString() );
131131
}
132132
catch (SQLException e) {
133133
throw jdbcServices.getSqlExceptionHelper().convert(
@@ -203,7 +203,7 @@ private void performUpsert(JdbcValueBindings jdbcValueBindings, SharedSessionCon
203203
.executeUpdate( updateStatement, statementDetails.getSqlString() );
204204
MODEL_MUTATION_LOGGER.tracef( "`%s` rows upserted into `%s`", rowCount, tableMapping.getTableName() );
205205
try {
206-
expectation.verifyOutcome( rowCount, updateStatement, -1, statementDetails.getSqlString() );
206+
getExpectation().verifyOutcome( rowCount, updateStatement, -1, statementDetails.getSqlString() );
207207
}
208208
catch (SQLException e) {
209209
throw jdbcServices.getSqlExceptionHelper().convert(
@@ -231,4 +231,8 @@ public UpsertOperation getUpsertOperation() {
231231
public OptionalTableUpdate getOptionalTableUpdate() {
232232
return optionalTableUpdate;
233233
}
234+
235+
protected Expectation getExpectation() {
236+
return new Expectation.RowCount();
237+
}
234238
}

0 commit comments

Comments
 (0)