如何重置Hibernate序列生成器?

时间:2017-04-24 11:01:56

标签: java spring hibernate junit

我使用Hibernate 3.5.6-Final与Oracle数据库进行生产,并使用H2数据库进行集成测试。用于ID创建的Hibernate映射看起来像这样,每个实体都扩展了EasyPersistentObject:

@MappedSuperclass
public class EasyPersistentObject implements Serializable {

@Id
@SequenceGenerator(name = "hibernate_seq", sequenceName = "hibernate_id_seq", allocationSize = 1)
@GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "hibernate_seq")
protected Integer id;

在每次JUnit集成测试之前,我将使用

从数据库中删除所有数据
new SchemaExport(configuration).create(false, true);

一切正常,直到我增加allocationSize以生成序列。将此提高到例如在插入测试数据时,10将使用UniqueKeyConstraintViolations打破几个测试。

例如:

  1. Test1:创建8个测试对象(Hibernate仍然分配了id值9和10)
  2. 重新创建数据库
  3. 测试2:创建12个测试对象(Hibernate使用9和10作为ID,然后从数据库序列加载新ID,同时重置(1-10)并在尝试插入ID为9的第2个实体时失败)
  4. 所以我的问题:有没有办法在每次测试之前重置Hibernates分配的ID?

    增加: 我没有使用JPA中的PersistenceManager,而是使用纯Hibernate SessionFactory,它是这样创建的:

    @Bean(name = "easySF")
    public SessionFactory easySessionFactory(@Qualifier("easyConfig") AnnotationConfiguration configuration) {
        configuration.setInterceptor(new HibernateInterceptor());
        return configuration.buildSessionFactory();
    }
    
    @Bean(name = "easyConfig")
    protected AnnotationConfiguration easyHibernateConfiguration() {
        AnnotationConfiguration configuration = new AnnotationConfiguration();
        configuration.setProperties(createHibernateProperties());
        configuration.addResource("NamedQueries.hbm.xml");  
        for (Class annotatedClass : getAnnotatedClasses()) {
            configuration.addAnnotatedClass(annotatedClass);
        }
        configuration.setListener("post-load", new PreloadEventListener());
        return configuration;
    }
    

    我是否真的需要重新加载我的整个Spring上下文以重置Hibernate ID生成器?

5 个答案:

答案 0 :(得分:3)

我遇到了同样的问题,并没有找到一种内置的方法来做到这一点。我们正在使用hibernate 4.2.7。

深入调试到hibernate后,我最终扩展了序列生成器。我们使用标准序列生成器创建实体:

@SequenceGenerator(name = "SomeSeq", sequenceName = "DB_SEQ", allocationSize = 50)

Hibernate为每个实体创建一个org.hibernate.id.SequenceHiLoGenerator。 SequnceHiLoGenerator委托给OptimizerFactory.LegacyHiLoAlgorithmOptimizer实例。

为了重置序列计数器并强制与数据库序列同步,您必须重置LegacyHiLoAlgorithmOptimizer中的内部变量。不幸的是,这些变量是私有的,不可修改。我试图找到一种使用继承的方法,但我没有找到一个优雅的解决方案。最后我是 创建了SequenceHiLoGenerator的源副本,并使用简单的重置功能对其进行了扩展:

public class ResetableIdGenerator extends SequenceGenerator {
               public static int cycle = 0; // global, indicating current test cycle
               protected int startingCycle = 0; // instance, indicating the test cycle the LegacyHiLoAlgorithmOptimizer was used the last time
    [...]
        public synchronized Serializable generate(final SessionImplementor session, Object obj) {
            // create a new HiLoOptimizer if there's a new test cycle
            if (startingCycle < cycle) {
                hiloOptimizer = new OptimizerFactory.LegacyHiLoAlgorithmOptimizer(getIdentifierType().getReturnedClass(),
                        maxLo);
                startingCycle = cycle;
            }
[....]

修改实体以使用自定义生成器:

@GenericGenerator(name = "SomeSeq", strategy = "yourpackage.ResetableIdGenerator", parameters = {
        @Parameter(name = "sequence", value = "DB_SEQ"), @Parameter(name = "max_lo", value = "49") })

在测试之间重置序列生成器(@before或@after):

// reset Hibernate Sequences
ResetableIdGenerator.cycle++;

我知道这不是一个好的解决方案 - 这是一个黑客攻击。但它有效,也许有助于找到更好的解决方案。

编辑20170504:我的初始帖子包含一个错误:参数&#34; sequenceName&#34;和&#34; allocationSize&#34;是JPA参数。 GenericGenerator来自hibernate。 而不是&#34; sequenceName&#34;你必须使用&#34; sequence&#34;而不是&#34; allocationSize&#34;你必须使用&#34; max_lo&#34;并将其设置为allocationSize-1。 我更新了代码示例。遗憾!

答案 1 :(得分:1)

或许其中一个解决方案是在新的sessionfactory中运行每个JUNIT测试。所以使用@Before@After

打开和关闭会话工厂

赞成

  • 从第一个
  • 获得序列生成器

缺点

  • 所有JUNIT测试用例需要几秒钟

<强>更新

根据评论,另一种方法是在@Before方法

中的每个JUNIT测试中重置序列
ALTER SEQUENCE Test.sequence RESTART WITH 1

答案 2 :(得分:1)

今天我遇到了同样的问题。由于找不到其他解决方案,因此我尝试从OleG获得该解决方案。不幸的是,与此同时org.hibernate.id.SequenceGenerator被标记为已弃用。因此,我使用了org.hibernate.id.enhanced.SequenceStyleGenerator。如果其他人需要它,这是我的定制解决方案:

public class ResettableSequenceStyleGenerator extends SequenceStyleGenerator {

    private static int cycle = 0;
    private int instanceCycle = cycle;

    private Type configure_type = null;
    private Properties configure_params = null;
    private ServiceRegistry configure_serviceRegistry = null;

    private Database registerExportables_database = null;

    @Override
    public void configure(Type type, Properties params, ServiceRegistry serviceRegistry) throws MappingException {

        configure_type = type;
        configure_params = params;
        configure_serviceRegistry = serviceRegistry;

        super.configure(type, params, serviceRegistry);
    }

    @Override
    public Serializable generate(SharedSessionContractImplementor session, Object object) throws HibernateException {

        if (instanceCycle != cycle) {
            super.configure(configure_type, configure_params, configure_serviceRegistry);
            super.registerExportables(registerExportables_database);
            instanceCycle = cycle;
        }

        return super.generate(session, object);
    }

    @Override
    public void registerExportables(Database database) {

        registerExportables_database = database;

        super.registerExportables(database);
    }

    public static void resetAllInstances() {
        cycle++;
    }
}

如OleG的文章所述,在ResettableSequenceStyleGenerator批注中将GenericGenerator设置为策略:

@GenericGenerator(name = "SomeSeq", strategy = "yourpackage.ResettableSequenceStyleGenerator", parameters = ...)

然后在我的IntegrationTest类中,在每个测试方法之前重置序列:

@Before
public void resetSequences() {
    ResettableSequenceStyleGenerator.resetAllInstances();
}

答案 3 :(得分:0)

这将强制hibernate重新加载其ID生成器:

myEntityManagerFactory.close();

myEntityManagerFactory = Persistence.createEntityManagerFactory(...);

答案 4 :(得分:0)

伙计们。就我而言,我不想使用自定义类更改模型生成器,因为序列重置对于集成测试是必要的,并且为测试调整代码并不是很好。 我费了好大劲才找到解决办法,终于找到了。首先,我在data.sql 中创建了一个名为truncate_tables_and_sequences 的函数。此函数截断所有表并使用 EXECUTE 'ALTER SEQUENCE ' || 重置所有序列quote_ident(stmt.sequencename) || '从 1 开始'

然后,在我有一个用于 JUnit 测试的自定义侦听器之后,该侦听器在每次测试运行之前执行,并且此侦听器运行我的 PostgresTestDataInitializer,它重置所有实体中所有 SequenceStyleGenerator 的值。反射用于重置计数器。不理想,但对于集成测试来说还可以。

这是代码示例:

@Component
public class PostgresTestDataInitializer implements TestDataInitializer {

    private final TransactionUtils transactionUtils;

    private final EntityManager entityManager;

    @Value("${spring.datasource.username:postgres}")
    private String databaseUserName;

    public PostgresTestDataInitializer(TransactionUtils transactionUtils,
                                       EntityManager entityManager) {
        this.transactionUtils = transactionUtils;
        this.entityManager = entityManager;
    }

    public void resetAllDbData() throws Exception {
        transactionUtils.executeInNewTransaction(this::clearAllDataAndSequences);
        resetSequenceGenerators();
    }

    private void resetSequenceGenerators() throws IllegalAccessException {
        MetamodelImpl metamodel = (MetamodelImpl)entityManager.getMetamodel();
        for(EntityPersister entityPersister : metamodel.entityPersisters().values()) {
            if (entityPersister.hasIdentifierProperty()
                    && entityPersister.getIdentifierGenerator() != null
                    && entityPersister.getIdentifierGenerator() instanceof SequenceStyleGenerator sequenceStyleGenerator
                    && sequenceStyleGenerator.getOptimizer() instanceof PooledOptimizer pooledOptimizer) {

                Field noTenantStateField = ReflectionUtils.findField(PooledOptimizer.class, "noTenantState");
                noTenantStateField.setAccessible(true);
                noTenantStateField.set(pooledOptimizer, null);
            }
        }
    }

    private void clearAllDataAndSequences() {
        entityManager
                .createStoredProcedureQuery("truncate_tables_and_sequences")
                .registerStoredProcedureParameter("username", String.class, ParameterMode.IN)
                .setParameter("username", databaseUserName)
                .execute();
    }
}