HHH-15118 Fix duplicate ids with PooledOptimizer when sequence value is initialValue

This commit is contained in:
CHAPEL Guillaume 2022-03-12 11:42:10 +01:00 committed by Christian Beikov
parent 3b9fbdcaab
commit 6d21f7eed2
6 changed files with 148 additions and 90 deletions

View File

@ -9,7 +9,6 @@ package org.hibernate.userguide.envers;
import java.util.Arrays; import java.util.Arrays;
import java.util.Date; import java.util.Date;
import java.util.HashSet; import java.util.HashSet;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import javax.persistence.Column; import javax.persistence.Column;

View File

@ -70,22 +70,21 @@ public class PooledOptimizer extends AbstractOptimizer implements InitialValueAw
final GenerationState generationState = locateGenerationState( callback.getTenantIdentifier() ); final GenerationState generationState = locateGenerationState( callback.getTenantIdentifier() );
if ( generationState.hiValue == null ) { if ( generationState.hiValue == null ) {
generationState.value = callback.getNextValue(); generationState.hiValue = callback.getNextValue();
// unfortunately not really safe to normalize this // unfortunately not really safe to normalize this
// to 1 as an initial value like we do for the others // to 1 as an initial value like we do for the others
// because we would not be able to control this if // because we would not be able to control this if
// we are using a sequence... // we are using a sequence...
if ( generationState.value.lt( 1 ) ) { if ( generationState.hiValue.lt( 1 ) ) {
log.pooledOptimizerReportedInitialValue( generationState.value ); log.pooledOptimizerReportedInitialValue( generationState.hiValue );
} }
// the call to obtain next-value just gave us the initialValue // the call to obtain next-value just gave us the initialValue
if ( ( initialValue == -1 if ( ( initialValue == -1
&& generationState.value.lt( incrementSize ) ) && generationState.hiValue.lt( incrementSize ) )
|| generationState.value.eq( initialValue ) ) { || generationState.hiValue.eq( initialValue ) ) {
generationState.hiValue = callback.getNextValue(); generationState.value = generationState.hiValue.copy();
} }
else { else {
generationState.hiValue = generationState.value;
generationState.value = generationState.hiValue.copy().subtract( incrementSize - 1 ); generationState.value = generationState.hiValue.copy().subtract( incrementSize - 1 );
} }
} }

View File

@ -226,10 +226,15 @@ public class OptimizerUnitTest extends BaseUnitTestCase {
Long next = ( Long ) optimizer.generate( sequence ); Long next = ( Long ) optimizer.generate( sequence );
assertEquals( 1, next.intValue() ); assertEquals( 1, next.intValue() );
assertEquals( 1, sequence.getTimesCalled() );
assertEquals( 1, sequence.getCurrentValue() );
next = ( Long ) optimizer.generate( sequence );
assertEquals( 2, next.intValue() );
assertEquals( 2, sequence.getTimesCalled() ); assertEquals( 2, sequence.getTimesCalled() );
assertEquals( 4, sequence.getCurrentValue() ); assertEquals( 4, sequence.getCurrentValue() );
// app ends, and starts back up (we should "lose" only 2 and 3 as id values) // app ends, and starts back up (we should "lose" only 3 and 4 as id values)
final Optimizer optimizer2 = buildPooledOptimizer( 1, 3 ); final Optimizer optimizer2 = buildPooledOptimizer( 1, 3 );
next = ( Long ) optimizer2.generate( sequence ); next = ( Long ) optimizer2.generate( sequence );
assertEquals( 5, next.intValue() ); assertEquals( 5, next.intValue() );

View File

@ -7,6 +7,7 @@
package org.hibernate.test.idgen.enhanced.forcedtable; package org.hibernate.test.idgen.enhanced.forcedtable;
import org.junit.Test; import org.junit.Test;
import org.junit.jupiter.api.Assertions;
import org.hibernate.Session; import org.hibernate.Session;
import org.hibernate.id.IdentifierGeneratorHelper.BasicHolder; import org.hibernate.id.IdentifierGeneratorHelper.BasicHolder;
@ -15,6 +16,7 @@ import org.hibernate.id.enhanced.SequenceStyleGenerator;
import org.hibernate.id.enhanced.TableStructure; import org.hibernate.id.enhanced.TableStructure;
import org.hibernate.persister.entity.EntityPersister; import org.hibernate.persister.entity.EntityPersister;
import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase; import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase;
import org.hibernate.testing.transaction.TransactionUtil;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
@ -23,6 +25,9 @@ import static org.junit.Assert.assertTrue;
* @author Steve Ebersole * @author Steve Ebersole
*/ */
public class PooledForcedTableSequenceTest extends BaseCoreFunctionalTestCase { public class PooledForcedTableSequenceTest extends BaseCoreFunctionalTestCase {
private static final long INITIAL_VALUE = 1;
public String[] getMappings() { public String[] getMappings() {
return new String[] { "idgen/enhanced/forcedtable/Pooled.hbm.xml" }; return new String[] { "idgen/enhanced/forcedtable/Pooled.hbm.xml" };
} }
@ -46,37 +51,47 @@ public class PooledForcedTableSequenceTest extends BaseCoreFunctionalTestCase {
PooledOptimizer optimizer = (PooledOptimizer) generator.getOptimizer(); PooledOptimizer optimizer = (PooledOptimizer) generator.getOptimizer();
int increment = optimizer.getIncrementSize(); int increment = optimizer.getIncrementSize();
Entity[] entities = new Entity[ increment + 2 ];
Session s = openSession(); TransactionUtil.doInHibernate(
s.beginTransaction(); this::sessionFactory,
for ( int i = 0; i <= increment; i++ ) { s -> {
entities[i] = new Entity( "" + ( i + 1 ) ); // The value that we get from the callback is the high value (PooledOptimizer by default)
s.save( entities[i] ); // When first increment is initialValue, we can only generate one id from it -> id 1
long expectedId = i + 1; Entity entity = new Entity( "" + INITIAL_VALUE );
assertEquals( expectedId, entities[i].getId().longValue() ); s.save( entity );
// NOTE : initialization calls table twice
long expectedId = INITIAL_VALUE;
assertEquals( expectedId, entity.getId().longValue() );
assertEquals( 1, generator.getDatabaseStructure().getTimesAccessed() );
assertEquals( INITIAL_VALUE, ( (BasicHolder) optimizer.getLastSourceValue() ).getActualLongValue() );
assertEquals( INITIAL_VALUE, ( (BasicHolder) optimizer.getLastValue() ).getActualLongValue() );
assertEquals( INITIAL_VALUE, ( (BasicHolder) optimizer.getLastSourceValue() ).getActualLongValue() );
// now start a full range of values, callback give us hiValue 11
// id : 2,3,4...,11
for ( int i = 1; i <= increment; i++ ) {
entity = new Entity( "" + ( i + INITIAL_VALUE ) );
s.save( entity );
expectedId = i + INITIAL_VALUE;
assertEquals( expectedId, entity.getId().longValue() );
assertEquals( 2, generator.getDatabaseStructure().getTimesAccessed() ); assertEquals( 2, generator.getDatabaseStructure().getTimesAccessed() );
assertEquals( increment + 1, ( (BasicHolder) optimizer.getLastSourceValue() ).getActualLongValue() ); assertEquals( increment + 1, ( (BasicHolder) optimizer.getLastSourceValue() ).getActualLongValue() );
assertEquals( i + 1, ( (BasicHolder) optimizer.getLastValue() ).getActualLongValue() ); assertEquals( expectedId, ( (BasicHolder) optimizer.getLastValue() ).getActualLongValue() );
assertEquals( increment + 1, ( (BasicHolder) optimizer.getLastSourceValue() ).getActualLongValue() );
} }
// now force a "clock over"
entities[increment + 1] = new Entity( "" + increment );
s.save( entities[increment + 1] );
long expectedId = optimizer.getIncrementSize() + 2;
assertEquals( expectedId, entities[ increment + 1 ].getId().longValue() );
// initialization (2) + clock over
assertEquals( 3, generator.getDatabaseStructure().getTimesAccessed() );
assertEquals( ( increment * 2 ) + 1, ( (BasicHolder) optimizer.getLastSourceValue() ).getActualLongValue() );
assertEquals( increment + 2, ( (BasicHolder) optimizer.getLastValue() ).getActualLongValue() );
s.getTransaction().commit();
s.beginTransaction(); // now force a "clock over"
for ( int i = 0; i < entities.length; i++ ) { expectedId++;
assertEquals( i + 1, entities[i].getId().intValue() ); entity = new Entity( "" + expectedId );
s.delete( entities[i] ); s.save( entity );
assertEquals( expectedId, entity.getId().longValue() );
assertEquals( 3, generator.getDatabaseStructure().getTimesAccessed() );
assertEquals( increment * 2L + 1, ( (BasicHolder) optimizer.getLastSourceValue() ).getActualLongValue() );
assertEquals( expectedId, ( (BasicHolder) optimizer.getLastValue() ).getActualLongValue() );
s.createQuery( "delete Entity" ).executeUpdate();
} }
s.getTransaction().commit(); );
s.close();
} }
} }

View File

@ -13,6 +13,7 @@ import org.hibernate.id.enhanced.PooledOptimizer;
import org.hibernate.id.enhanced.SequenceStyleGenerator; import org.hibernate.id.enhanced.SequenceStyleGenerator;
import org.hibernate.persister.entity.EntityPersister; import org.hibernate.persister.entity.EntityPersister;
import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase; import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase;
import org.hibernate.testing.transaction.TransactionUtil;
import static org.hibernate.id.IdentifierGeneratorHelper.BasicHolder; import static org.hibernate.id.IdentifierGeneratorHelper.BasicHolder;
import static org.hibernate.testing.junit4.ExtraAssertions.assertClassAssignability; import static org.hibernate.testing.junit4.ExtraAssertions.assertClassAssignability;
@ -22,6 +23,9 @@ import static org.junit.Assert.assertEquals;
* @author Steve Ebersole * @author Steve Ebersole
*/ */
public class PooledSequenceTest extends BaseCoreFunctionalTestCase { public class PooledSequenceTest extends BaseCoreFunctionalTestCase {
private static final long INITIAL_VALUE = 1;
@Override @Override
public String[] getMappings() { public String[] getMappings() {
return new String[] { "idgen/enhanced/sequence/Pooled.hbm.xml" }; return new String[] { "idgen/enhanced/sequence/Pooled.hbm.xml" };
@ -36,31 +40,47 @@ public class PooledSequenceTest extends BaseCoreFunctionalTestCase {
PooledOptimizer optimizer = (PooledOptimizer) generator.getOptimizer(); PooledOptimizer optimizer = (PooledOptimizer) generator.getOptimizer();
int increment = optimizer.getIncrementSize(); int increment = optimizer.getIncrementSize();
Entity[] entities = new Entity[ increment + 2 ];
Session s = openSession();
s.beginTransaction();
for ( int i = 0; i <= increment; i++ ) {
entities[i] = new Entity( "" + ( i + 1 ) );
s.save( entities[i] );
assertEquals( 2, generator.getDatabaseStructure().getTimesAccessed() ); // initialization calls seq twice
assertEquals( increment + 1, ( (BasicHolder) optimizer.getLastSourceValue() ).getActualLongValue() ); // initialization calls seq twice
assertEquals( i + 1, ( (BasicHolder) optimizer.getLastValue() ).getActualLongValue() );
assertEquals( increment + 1, ( (BasicHolder) optimizer.getLastSourceValue() ).getActualLongValue() );
}
// now force a "clock over"
entities[ increment + 1 ] = new Entity( "" + increment );
s.save( entities[ increment + 1 ] );
assertEquals( 3, generator.getDatabaseStructure().getTimesAccessed() ); // initialization (2) + clock over
assertEquals( ( increment * 2 ) + 1, ( (BasicHolder) optimizer.getLastSourceValue() ).getActualLongValue() ); // initialization (2) + clock over
assertEquals( increment + 2, ( (BasicHolder) optimizer.getLastValue() ).getActualLongValue() );
s.getTransaction().commit();
s.beginTransaction(); TransactionUtil.doInHibernate(
for ( int i = 0; i < entities.length; i++ ) { this::sessionFactory,
assertEquals( i + 1, entities[i].getId().intValue() ); s -> {
s.delete( entities[i] ); // The value that we get from the callback is the high value (PooledOptimizer by default)
// When first increment is initialValue, we can only generate one id from it -> id 1
Entity entity = new Entity( "" + INITIAL_VALUE );
s.save( entity );
long expectedId = INITIAL_VALUE;
assertEquals( expectedId, entity.getId().longValue() );
assertEquals( 1, generator.getDatabaseStructure().getTimesAccessed() );
assertEquals( INITIAL_VALUE, ( (BasicHolder) optimizer.getLastSourceValue() ).getActualLongValue() );
assertEquals( INITIAL_VALUE, ( (BasicHolder) optimizer.getLastValue() ).getActualLongValue() );
assertEquals( INITIAL_VALUE, ( (BasicHolder) optimizer.getLastSourceValue() ).getActualLongValue() );
// now start a full range of values, callback give us hiValue 11
// id : 2,3,4...,11
for ( int i = 1; i <= increment; i++ ) {
entity = new Entity( "" + ( i + INITIAL_VALUE ) );
s.save( entity );
expectedId = i + INITIAL_VALUE;
assertEquals( expectedId, entity.getId().longValue() );
assertEquals( 2, generator.getDatabaseStructure().getTimesAccessed() );
assertEquals( increment + 1, ( (BasicHolder) optimizer.getLastSourceValue() ).getActualLongValue() );
assertEquals( expectedId, ( (BasicHolder) optimizer.getLastValue() ).getActualLongValue() );
} }
s.getTransaction().commit();
s.close(); // now force a "clock over"
expectedId++;
entity = new Entity( "" + expectedId );
s.save( entity );
assertEquals( expectedId, entity.getId().longValue() );
assertEquals( 3, generator.getDatabaseStructure().getTimesAccessed() );
assertEquals( increment * 2L + 1, ( (BasicHolder) optimizer.getLastSourceValue() ).getActualLongValue() );
assertEquals( expectedId, ( (BasicHolder) optimizer.getLastValue() ).getActualLongValue() );
s.createQuery( "delete Entity" ).executeUpdate();
}
);
} }
} }

View File

@ -13,6 +13,7 @@ import org.hibernate.id.enhanced.PooledOptimizer;
import org.hibernate.id.enhanced.TableGenerator; import org.hibernate.id.enhanced.TableGenerator;
import org.hibernate.persister.entity.EntityPersister; import org.hibernate.persister.entity.EntityPersister;
import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase; import org.hibernate.testing.junit4.BaseCoreFunctionalTestCase;
import org.hibernate.testing.transaction.TransactionUtil;
import static org.hibernate.id.IdentifierGeneratorHelper.BasicHolder; import static org.hibernate.id.IdentifierGeneratorHelper.BasicHolder;
import static org.hibernate.testing.junit4.ExtraAssertions.assertClassAssignability; import static org.hibernate.testing.junit4.ExtraAssertions.assertClassAssignability;
@ -22,6 +23,9 @@ import static org.junit.Assert.assertEquals;
* @author Steve Ebersole * @author Steve Ebersole
*/ */
public class PooledTableTest extends BaseCoreFunctionalTestCase { public class PooledTableTest extends BaseCoreFunctionalTestCase {
private static final long INITIAL_VALUE = 1;
@Override @Override
public String[] getMappings() { public String[] getMappings() {
return new String[] { "idgen/enhanced/table/Pooled.hbm.xml" }; return new String[] { "idgen/enhanced/table/Pooled.hbm.xml" };
@ -36,31 +40,47 @@ public class PooledTableTest extends BaseCoreFunctionalTestCase {
PooledOptimizer optimizer = (PooledOptimizer) generator.getOptimizer(); PooledOptimizer optimizer = (PooledOptimizer) generator.getOptimizer();
int increment = optimizer.getIncrementSize(); int increment = optimizer.getIncrementSize();
Entity[] entities = new Entity[ increment + 2 ];
Session s = openSession();
s.beginTransaction();
for ( int i = 0; i <= increment; i++ ) {
entities[i] = new Entity( "" + ( i + 1 ) );
s.save( entities[i] );
assertEquals( 2, generator.getTableAccessCount() ); // initialization calls seq twice
assertEquals( increment + 1, ( (BasicHolder) optimizer.getLastSourceValue() ).getActualLongValue() ); // initialization calls seq twice
assertEquals( i + 1, ( (BasicHolder) optimizer.getLastValue() ).getActualLongValue() );
assertEquals( increment + 1, ( (BasicHolder) optimizer.getLastSourceValue() ).getActualLongValue() );
}
// now force a "clock over"
entities[ increment + 1 ] = new Entity( "" + increment );
s.save( entities[ increment + 1 ] );
assertEquals( 3, generator.getTableAccessCount() ); // initialization (2) + clock over
assertEquals( ( increment * 2 ) + 1, ( (BasicHolder) optimizer.getLastSourceValue() ).getActualLongValue() ); // initialization (2) + clock over
assertEquals( increment + 2, ( (BasicHolder) optimizer.getLastValue() ).getActualLongValue() );
s.getTransaction().commit();
s.beginTransaction(); TransactionUtil.doInHibernate(
for ( int i = 0; i < entities.length; i++ ) { this::sessionFactory,
assertEquals( i + 1, entities[i].getId().intValue() ); s -> {
s.delete( entities[i] ); // The value that we get from the callback is the high value (PooledOptimizer by default)
// When first increment is initialValue, we can only generate one id from it -> id 1
Entity entity = new Entity( "" + INITIAL_VALUE );
s.save( entity );
long expectedId = INITIAL_VALUE;
assertEquals( expectedId, entity.getId().longValue() );
assertEquals( 1, generator.getTableAccessCount() );
assertEquals( INITIAL_VALUE, ( (BasicHolder) optimizer.getLastSourceValue() ).getActualLongValue() );
assertEquals( INITIAL_VALUE, ( (BasicHolder) optimizer.getLastValue() ).getActualLongValue() );
assertEquals( INITIAL_VALUE, ( (BasicHolder) optimizer.getLastSourceValue() ).getActualLongValue() );
// now start a full range of values, callback give us hiValue 11
// id : 2,3,4...,11
for ( int i = 1; i <= increment; i++ ) {
entity = new Entity( "" + ( i + INITIAL_VALUE ) );
s.save( entity );
expectedId = i + INITIAL_VALUE;
assertEquals( expectedId, entity.getId().longValue() );
assertEquals( 2, generator.getTableAccessCount() );
assertEquals( increment + 1, ( (BasicHolder) optimizer.getLastSourceValue() ).getActualLongValue() );
assertEquals( expectedId, ( (BasicHolder) optimizer.getLastValue() ).getActualLongValue() );
} }
s.getTransaction().commit();
s.close(); // now force a "clock over"
expectedId++;
entity = new Entity( "" + expectedId );
s.save( entity );
assertEquals( expectedId, entity.getId().longValue() );
assertEquals( 3, generator.getTableAccessCount() );
assertEquals( increment * 2L + 1, ( (BasicHolder) optimizer.getLastSourceValue() ).getActualLongValue() );
assertEquals( expectedId, ( (BasicHolder) optimizer.getLastValue() ).getActualLongValue() );
s.createQuery( "delete Entity" ).executeUpdate();
}
);
} }
} }