8000 HHH-18818 Fix ID conflicts between CTE batch inserts and optimizer st… · hibernate/hibernate-orm@8623656 · GitHub
[go: up one dir, main page]

8000
Skip to content

Commit 8623656

Browse files
committed
HHH-18818 Fix ID conflicts between CTE batch inserts and optimizer strategies
1 parent c6dc40f commit 8623656

15 files changed

+603
-36
lines changed

hibernate-core/src/main/java/org/hibernate/id/enhanced/HiLoOptimizer.java

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,22 @@
44
*/
55
package org.hibernate.id.enhanced;
66

7+
import org.hibernate.HibernateException;
8+
import org.hibernate.engine.spi.SessionFactoryImplementor;
9+
import org.hibernate.id.IntegralDataTypeHolder;
10+
import org.hibernate.metamodel.mapping.BasicValuedMapping;
11+
import org.hibernate.query.sqm.BinaryArithmeticOperator;
12+
import org.hibernate.sql.ast.tree.expression.BinaryArithmeticExpression;
13+
import org.hibernate.sql.ast.tree.expression.Expression;
14+
import org.hibernate.sql.ast.tree.expression.QueryLiteral;
15+
import org.jboss.logging.Logger;
16+
717
import java.io.Serializable;
818
import java.util.Map;
919
import java.util.concurrent.ConcurrentHashMap;
1020
import java.util.concurrent.locks.Lock;
1121
import java.util.concurrent.locks.ReentrantLock;
1222< 8000 /code>

13-
import org.hibernate.HibernateException;
14-
import org.hibernate.id.IntegralDataTypeHolder;
15-
16-
import org.jboss.logging.Logger;
17-
1823
/**
1924
* Optimizer which applies a 'hilo' algorithm in memory to achieve
2025
* optimization.
@@ -197,4 +202,20 @@ public IntegralDataTypeHolder getHiValue() {
197202
lock.unlock();
198203
}
199204
}
205+
206+
@Override
207+
public Expression createLowValueExpression(Expression databaseValue, SessionFactoryImplementor sessionFactory) {
208+
BasicValuedMapping integerType = sessionFactory.getTypeConfiguration().getBasicTypeForJavaType( Integer.class );
209+
return new BinaryArithmeticExpression(
210+
new BinaryArithmeticExpression(
211+
databaseValue,
212+
BinaryArithmeticOperator.MULTIPLY,
213+
new QueryLiteral<>( incrementSize, integerType ),
214+
integerType
215+
),
216+
BinaryArithmeticOperator.SUBTRACT,
217+
new QueryLiteral<>( incrementSize - 1, integerType ),
218+
integerType
219+
);
220+
}
200221
}

hibernate-core/src/main/java/org/hibernate/id/enhanced/LegacyHiLoAlgorithmOptimizer.java

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,22 @@
44
*/
55
package org.hibernate.id.enhanced;
66

7+
import org.hibernate.HibernateException;
8+
import org.hibernate.engine.spi.SessionFactoryImplementor;
9+
import org.hibernate.id.IntegralDataTypeHolder;
10+
import org.hibernate.metamodel.mapping.BasicValuedMapping;
11+
import org.hibernate.query.sqm.BinaryArithmeticOperator;
12+
import org.hibernate.sql.ast.tree.expression.BinaryArithmeticExpression;
13+
import org.hibernate.sql.ast.tree.expression.Expression;
14+
import org.hibernate.sql.ast.tree.expression.QueryLiteral;
15+
import org.jboss.logging.Logger;
16+
717
import java.io.Serializable;
818
import java.util.Map;
919
import java.util.concurrent.ConcurrentHashMap;
1020
import java.util.concurrent.locks.Lock;
1121
import java.util.concurrent.locks.ReentrantLock;
1222

13-
import org.hibernate.HibernateException;
14-
import org.hibernate.id.IntegralDataTypeHolder;
15-
16-
import org.jboss.logging.Logger;
17-
1823
/**
1924
* Slight variation from {@link HiLoOptimizer}, maintaining compatibility with the values generated by the
2025
* legacy Hibernate hilo based generators.
@@ -150,4 +155,15 @@ public IntegralDataTypeHolder getLastValue() {
150155
lock.unlock();
151156
}
152157
}
158+
159+
@Override
160+
public Expression createLowValueExpression(Expression databaseValue, SessionFactoryImplementor sessionFactory) {
161+
BasicValuedMapping integerType = sessionFactory.getTypeConfiguration().getBasicTypeForJavaType( Integer.class );
162+
return new BinaryArithmeticExpression(
163+
databaseValue,
164+
BinaryArithmeticOperator.MULTIPLY,
165+
new QueryLiteral<>( getIncrementSize() + 1, integerType ),
166+
integerType
167+
);
168+
}
153169
}

hibernate-core/src/main/java/org/hibernate/id/enhanced/NoopOptimizer.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44
*/
55
package org.hibernate.id.enhanced;
66

7-
import java.io.Serializable;
8-
7+
import org.hibernate.engine.spi.SessionFactoryImplementor;
98
import org.hibernate.id.IntegralDataTypeHolder;
9+
import org.hibernate.sql.ast.tree.expression.Expression;
10+
11+
import java.io.Serializable;
1012

1113
/**
1214
* An optimizer that performs no optimization. A round trip to
@@ -52,4 +54,9 @@ public boolean applyIncrementSizeToSourceValues() {
5254
// We don't apply an increment size of 1, since it is already the default.
5355
return getIncrementSize() != 0 && getIncrementSize() != 1;
5456
}
57+
58+
@Override
59+
public Expression createLowValueExpression(Expression databaseValue, SessionFactoryImplementor sessionFactory) {
60+
return databaseValue;
61+
}
5562
}

hibernate-core/src/main/java/org/hibernate/id/enhanced/Optimizer.java

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@
44
*/
55
package org.hibernate.id.enhanced;
66

7-
import java.io.Serializable;
8-
7+
import org.hibernate.engine.spi.SessionFactoryImplementor;
98
import org.hibernate.id.IntegralDataTypeHolder;
9+
import org.hibernate.sql.ast.tree.expression.Expression;
10+
11+
import java.io.Serializable;
1012

1113
/**
1214
* Performs optimization on an optimizable identifier generator. Typically
@@ -59,4 +61,16 @@ public interface Optimizer {
5961
* case the increment is totally an in memory construct.
6062
*/
6163
boolean applyIncrementSizeToSourceValues();
64+
65+
/**
66+
* Creates an expression representing the low/base value for ID allocation in batch insert operations.
67+
* <p>
68+
* Each optimizer implementation should define its own
69+
* strategy for calculating the starting value of a sequence range.
70+
*
71+
* @param databaseValue The expression representing the next value from database sequence
72+
* @param sessionFactory
73+
* @return An expression that calculates the low/base value according to the optimizer strategy
74+
*/
75+
Expression createLowValueExpression(Expression databaseValue, SessionFactoryImplementor sessionFactory);
6276
}

hibernate-core/src/main/java/org/hibernate/id/enhanced/PooledLoOptimizer.java

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,19 @@
44
*/
55
package org.hibernate.id.enhanced;
66

7+
import org.hibernate.HibernateException;
8+
import org.hibernate.engine.spi.SessionFactoryImplementor;
9+
import org.hibernate.id.IntegralDataTypeHolder;
10+
import org.hibernate.internal.CoreMessageLogger;
11+
import org.hibernate.sql.ast.tree.expression.Expression;
12+
import org.jboss.logging.Logger;
13+
714
import java.io.Serializable;
815
import java.lang.invoke.MethodHandles;
916
import java.util.Map;
1017
import java.util.concurrent.ConcurrentHashMap;
11-
import java.util.concurrent.locks.ReentrantLock;
1218
import java.util.concurrent.locks.Lock;
13-
14-
import org.hibernate.HibernateException;
15-
import org.hibernate.id.IntegralDataTypeHolder;
16-
import org.hibernate.internal.CoreMessageLogger;
17-
import org.jboss.logging.Logger;
19+
import java.util.concurrent.locks.ReentrantLock;
1820

1921
/**
2022
* Variation of {@link PooledOptimizer} which interprets the incoming database
@@ -125,4 +127,9 @@ public IntegralDataTypeHolder getLastSourceValue() {
125127
public boolean applyIncrementSizeToSourceValues() {
126128
return true;
127129
}
130+
131+
@Override
132+
public Expression createLowValueExpression(Expression databaseValue, SessionFactoryImplementor sessionFactory) {
133+
return databaseValue;
134+
}
128135
}

hibernate-core/src/main/java/org/hibernate/id/enhanced/PooledLoThreadLocalOptimizer.java

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,18 @@
44
*/
55
package org.hibernate.id.enhanced;
66

7-
import java.io.Serializable;
8-
import java.lang.invoke.MethodHandles;
9-
import java.util.HashMap;
10-
import java.util.Map;
11-
127
import org.hibernate.HibernateException;
8+
import org.hibernate.engine.spi.SessionFactoryImplementor;
139
import org.hibernate.id.IntegralDataTypeHolder;
1410
import org.hibernate.internal.CoreMessageLogger;
15-
11+
import org.hibernate.sql.ast.tree.expression.Expression;
1612
import org.jboss.logging.Logger;
1713

14+
import java.io.Serializable;
15+
import java.lang.invoke.MethodHandles;
16+
import java.util.HashMap;
17+
import java.util.Map;
18+
1819
/**
1920
* Variation of {@link PooledOptimizer} which interprets the incoming database
2021
* value as the lo value, rather than the hi value, as well as using thread local
@@ -112,4 +113,9 @@ private Serializable generate(AccessCallback callback, int incrementSize) {
112113
return value.makeValueThenIncrement();
113114
}
114115
}
116+
117+
@Override
118+
public Expression createLowValueExpression(Expression databaseValue, SessionFactoryImplementor sessionFactory) {
119+
return databaseValue;
120+
}
115121
}

hibernate-core/src/main/java/org/hibernate/id/enhanced/PooledOptimizer.java

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,24 @@
44
*/
55
package org.hibernate.id.enhanced;
66

7+
import org.hibernate.HibernateException;
8+
import org.hibernate.engine.spi.SessionFactoryImplementor;
9+
import org.hibernate.id.IntegralDataTypeHolder;
10+
import org.hibernate.internal.CoreMessageLogger;
11+
import org.hibernate.metamodel.mapping.BasicValuedMapping;
12+
import org.hibernate.query.sqm.BinaryArithmeticOperator;
13+
import org.hibernate.sql.ast.tree.expression.BinaryArithmeticExpression;
14+
import org.hibernate.sql.ast.tree.expression.Expression;
15+
import org.hibernate.sql.ast.tree.expression.QueryLiteral;
16+
import org.jboss.logging.Logger;
17+
718
import java.io.Serializable;
819
import java.lang.invoke.MethodHandles;
920
import java.util.Map;
1021
import java.util.concurrent.ConcurrentHashMap;
1122
import java.util.concurrent.locks.Lock;
1223
import java.util.concurrent.locks.ReentrantLock;
1324

14-
import org.hibernate.HibernateException;
15-
import org.hibernate.id.IntegralDataTypeHolder;
16-
import org.hibernate.internal.CoreMessageLogger;
17-
18-
import org.jboss.logging.Logger;
19-
2025
/**
2126
* Optimizer which uses a pool of values, storing the next low value of the range
2227
* in the database.
@@ -167,4 +172,15 @@ public IntegralDataTypeHolder getLastValue() {
167172
public void injectInitialValue(long initialValue) {
168173
this.initialValue = initialValue;
169174
}
175+
176+
@Override
177+
public Expression createLowValueExpression(Expression databaseValue, SessionFactoryImplementor sessionFactory) {
178+
BasicValuedMapping integerType = sessionFactory.getTypeConfiguration().getBasicTypeForJavaType( Integer.class );
179+
return new BinaryArithmeticExpression(
180+
databaseValue,
181+
BinaryArithmeticOperator.SUBTRACT,
182+
new QueryLiteral<>( incrementSize - 1, integerType ),
183+
integerType
184+
);
185+
}
170186
}

hibernate-core/src/main/java/org/hibernate/query/sqm/mutation/internal/cte/CteInsertHandler.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -384,12 +384,13 @@ public int execute(DomainQueryExecutionContext executionContext) {
384384
generator.determineBulkInsertionIdentifierGenerationSelectFragment(
385385
sessionFactory.getSqlStringGenerationContext()
386386
);
387+
388+
Expression databaseValue = new SelfRenderingSqlFragmentExpression( fragment );
387389
rowsWithSequenceQuery.getSelectClause().addSqlSelection(
388-
new SqlSelectionImpl(
389-
1,
390-
new SelfRenderingSqlFragmentExpression( fragment )
391-
)
390+
new SqlSelectionImpl( 1,
391+
optimizer.createLowValueExpression( databaseValue, sessionFactory ) )
392392
);
393+
393394
rowsWithSequenceQuery.applyPredicate(
394395
new ComparisonPredicate(
395396
rowNumberMinusOneModuloIncrement,
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/*
2+
* SPDX-License-Identifier: LGPL-2.1-or-later
3+
* Copyright Red Hat Inc. and Hibernate Authors
4+
*/
5+
package org.hibernate.orm.test.id.cte;
6+
7+
import org.hibernate.cfg.AvailableSettings;
8+
import org.hibernate.dialect.DB2Dialect;
9+
import org.hibernate.dialect.PostgreSQLDialect;
10+
import org.hibernate.id.enhanced.HiLoOptimizer;
11+
import org.hibernate.testing.orm.junit.DomainModel;
12+
import org.hibernate.testing.orm.junit.JiraKey;
13+
import org.hibernate.testing.orm.junit.RequiresDialect;
14+
import org.hibernate.testing.orm.junit.RequiresDialects;
15+
import org.hibernate.testing.orm.junit.ServiceRegistry;
16+
import org.hibernate.testing.orm.junit.SessionFactory;
17+
import org.hibernate.testing.orm.junit.SessionFactoryScope;
18+
import org.hibernate.testing.orm.junit.Setting;
19+
import org.junit.jupiter.api.Test;
20+
21+
import static org.junit.jupiter.api.Assertions.assertEquals;
22+
23+
/**
24+
* Tests sequence ID conflicts between entity persists and CTE batch inserts with {@link HiLoOptimizer},
25+
* ensuring proper ID allocation and prevention of duplicates across both operations.
26+
*
27+
* @author Kowsar Atazadeh
28+
*/
29+
@JiraKey("HHH-18818")
30+
@SessionFactory
31+
@RequiresDialects({
32+
@RequiresDialect(PostgreSQLDialect.class),
33+
@RequiresDialect(DB2Dialect.class)
34+
})
35+
@ServiceRegistry(settings = @Setting(name = AvailableSettings.PREFERRED_POOLED_OPTIMIZER, value = "hilo"))
36+
@DomainModel(annotatedClasses = Dummy.class)
37+
public class CteInsertWithHiLoOptimizerTest {
38+
@Test
39+
void test(SessionFactoryScope scope) {
40+
// 7 rows inserted with IDs 1-7
41+
// Database sequence calls:
42+
// - First returns 1 (allocates IDs 1-5)
43+
// - Second returns 2 (allocates IDs 6-10)
44+
// IDs 8-10 reserved from current allocation
45+
scope.inTransaction( session -> {
46+
for ( var id = 1; id <= 7; id++ ) {
47+
Dummy d = new Dummy( "d" + id );
48+
session.persist( d );
49+
assertEquals( id, d.getId() );
50+
}
51+
} );
52+
53+
// 7 rows inserted with IDs 11-17
54+
// Database sequence calls:
55+
// - First returns 3 (allocates IDs 11-15)
56+
// - Second returns 4 (allocates IDs 16-20)
57+
// IDs 18-20 reserved from current allocation
58+
scope.inTransaction( session -> {
59+
session.createMutationQuery( "INSERT INTO Dummy (name) SELECT d.name FROM Dummy d" ).
60+
executeUpdate();
61+
var inserted = session.createSelectionQuery(
62+
"SELECT d.id FROM Dummy d WHERE d.id > 7 ORDER BY d.id", Long.class )
63+
.getResultList();
64+
assertEquals( 7, inserted.size() );
65+
for ( int i = 0; i < inserted.size(); i++ ) {
66+
assertEquals( 11 + i, inserted.get( i ) );
67+
}
68+
} );
69+
70+
// 5 rows inserted with IDs 8-10, 21-22
71+
// Database sequence call returns 5 (allocates IDs 21-25)
72+
// Using previously reserved IDs 8-10 and new allocation IDs 21-22
73+
// IDs 23-25 reserved from current allocation
74+
scope.inTransaction( session -> {
75+
for ( var id = 8; id <= 10; id++ ) {
76+
Dummy d = new Dummy( "d" + id );
77+
session.persist( d );
78+
assertEquals( id, d.getId() );
79+
}
80+
81+
for ( var id = 21; id <= 22; id++ ) {
82+
Dummy d = new Dummy( "d" + id );
83+
session.persist( d );
84+
assertEquals( id, d.getId() );
85+
}
86+
} );
87+
}
88+
}

0 commit comments

Comments
 (0)
0