Spring批处理jpaPagingItemReader为什么有些行没有被读取?

时间:2014-10-22 14:31:18

标签: jpa spring-batch

我正在使用Spring Batch(3.0.1.RELEASE)/ JPA和HSQLBD服务器数据库。 我需要浏览整个表(使用分页)和更新项目(逐个)。所以我使用了jpaPagingItemReader。但是当我运行该作业时,我可以看到跳过了一些行,并且跳过的行数等于页面大小。例如,如果我的表有12行而jpaPagingItemReader.pagesize = 3,则作业将显示为:1,2,3行,然后是第7,8,9行(因此跳过第4,5,6行)... 你能告诉我我的代码/配置有什么问题,或者这可能是HSQLDB分页的问题吗? 以下是我的代码:

[编辑] :问题在于我的ItemProcessor执行对POJO实体的修改。由于JPAPagingItemReader在每次读取之间进行刷新,因此实体会更新((这就是我想要的)。但似乎游标分页也会增加(如日志中所示:行ID 4,5和6已经过跳过)。我该如何处理这个问题?

@Configuration
@EnableBatchProcessing(modular=true)
public class AppBatchConfig {
  @Inject
  private InfrastructureConfiguration infrastructureConfiguration;  
  @Inject private JobBuilderFactory jobs;
  @Inject private StepBuilderFactory steps;

  @Bean  public Job job() {
     return jobs.get("Myjob1").start(step1()).build();
  }
  @Bean  public Step step1() {  
      return steps.get("step1")
                .<SNUserPerCampaign, SNUserPerCampaign> chunk(0)
                .reader(reader()).processor(processor()).build();   
  }
  @Bean(destroyMethod = "")
@JobScope 
public ItemStreamReader<SNUserPerCampaign> reader() String trigramme) {
    JpaPagingItemReader reader = new JpaPagingItemReader();
    reader.setEntityManagerFactory(infrastructureConfiguration.getEntityManagerFactory());
    reader.setQueryString("select t from SNUserPerCampaign t where t.isactive=true");
    reader.setPageSize(3));
    return reader;
}
 @Bean @JobScope
 public ItemProcessor<SNUserPerCampaign, SNUserPerCampaign> processor() {   
     return new MyItemProcessor();
 }
}

@Configuration
@EnableBatchProcessing
public class StandaloneInfrastructureConfiguration implements InfrastructureConfiguration {
 @Inject private EntityManagerFactory emf;  
 @Override
public EntityManagerFactory getEntityManagerFactory() {
    return emf;
}
}  

来自我的ItemProcessor:

@Override
public SNUserPerCampaign process(SNUserPerCampaign item) throws Exception {
    //do some stuff …
   //then if (condition) update the Entity pojo :   
   item.setModificationDate(new Timestamp(System.currentTimeMillis());
   item.setIsactive = false;

}

来自Spring xml配置文件:

<tx:annotation-driven transaction-manager="transactionManager" />     
<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
    <property name="entityManagerFactory" ref="entityManagerFactory" />
</bean>

<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
    <property name="dataSource" ref="dataSource" />
</bean>

<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
    <property name="driverClassName" value="org.hsqldb.jdbcDriver" />
    <property name="url" value="jdbc:hsqldb:hsql://localhost:9001/MYAppDB" />
    <property name="username" value="sa" />
    <property name="password" value="" />
</bean>

跟踪/日志汇总:

11:16:05.728 TRACE MyItemProcessor - item processed: snUserInternalId=1]
11:16:06.038 TRACE MyItemProcessor - item processed: snUserInternalId=2]
11:16:06.350 TRACE MyItemProcessor - item processed: snUserInternalId=3]

11:16:06.674 DEBUG SQL- update SNUSER_CAMPAIGN  set ...etc...
11:16:06.677 DEBUG SQL- update SNUSER_CAMPAIGN  set ...etc...
11:16:06.679 DEBUG SQL- update SNUSER_CAMPAIGN  set ...etc...

11:16:06.681 DEBUG SQL- select ...etc... from  SNUSER_CAMPAIGN snuserperc0_ 

11:16:06.687 TRACE MyItemProcessor - item processed: snUserInternalId=7]
11:16:06.998 TRACE MyItemProcessor - item processed: snUserInternalId=8]
11:16:07.314 TRACE MyItemProcessor - item processed: snUserInternalId=9]

4 个答案:

答案 0 :(得分:10)

org.springframework.batch.item.database.JpaPagingItemReader创建自己的entityManager实例

(来自org.springframework.batch.item.database.JpaPagingItemReader #doOpen):

entityManager = entityManagerFactory.createEntityManager(jpaPropertyMap);

如果您处于交易中,就像看起来一样,读者实体不会分离 (来自org.springframework.batch.item.database.JpaPagingItemReader #doReadPage):

    if (!transacted) {
        List<T> queryResult = query.getResultList();
        for (T entity : queryResult) {
            entityManager.detach(entity);
            results.add(entity);
        }//end if
    } else {
        results.addAll(query.getResultList());
        tx.commit();
    }

因此,当您将项目更新为处理器或编写器时,此项目仍由读者的entityManager管理。

当项目阅读器读取下一个数据块时,它会将上下文刷新到数据库。

所以,如果我们看看你的情况,在第一块数据处理之后,我们就有了数据库:

|id|active
|1 | false
|2 | false
|3 | false

org.springframework.batch.item.database.JpaPagingItemReader使用limit&amp; offset以检索分页数据。所以读者创建的下一个选择如下:

select * from table where active = true offset 3 limits 3. 

Reader会遗漏ID为4,5,6的项目,因为它们现在是数据库检索的第一行。

作为一种解决方法,您可以使用jdbc实现(org.springframework.batch.item.database.JdbcPagingItemReader),因为它不使用limit&amp;偏移。它基于有序列(通常是id列),因此您不会遗漏任何数据。 当然,您必须将数据更新到编写器中(使用JPA纯JDBC实现)

读者会更加冗长:

@Bean
public ItemReader<? extends Entity> reader() {
    JdbcPagingItemReader<Entity> reader = new JdbcPagingItemReader<Entity>();
    final SqlPagingQueryProviderFactoryBean sqlPagingQueryProviderFactoryBean = new SqlPagingQueryProviderFactoryBean();
    sqlPagingQueryProviderFactoryBean.setDataSource(dataSource);
    sqlPagingQueryProviderFactoryBean.setSelectClause("select *");
    sqlPagingQueryProviderFactoryBean.setFromClause("from <your table name>");
    sqlPagingQueryProviderFactoryBean.setWhereClause("where active = true");
    sqlPagingQueryProviderFactoryBean.setSortKey("id");
    try {
        reader.setQueryProvider(sqlPagingQueryProviderFactoryBean.getObject());
    } catch (Exception e) {
        e.printStackTrace();
    }
    reader.setDataSource(dataSource);
    reader.setPageSize(3);
    reader.setRowMapper(new BeanPropertyRowMapper<Entity>(Entity.class));
    return reader;

答案 1 :(得分:2)

有几点需要注意:

  1. JpaPaginingItemReader返回的所有实体都已分离。我们以两种方式之一完成此任务。我们要么在查询页面之前创建一个事务,然后提交事务(它会分离与该事务的EntityManager相关联的所有实体),或者我们显式调用entityManager.detach。我们这样做是为了可以正确执行重试和跳过等功能。
  2. 虽然您没有在处理器中发布所有代码,但我的预感是在//do some stuff部分,您的项目正在重新附加,这就是更新发生的原因。但是,由于无法看到该代码,我无法确定。
  3. 在任何一种情况下,都应该使用明确的ItemWriter。事实上,我认为这是一个错误,我们在使用java配置时不需要ItemWriter(我们为XML做)。
  4. 对于缺少记录的特定问题,您需要记住,任何*PagingItemReader都不使用游标。它们都为每个数据页执行独立查询。因此,如果您更新每个页面之间的基础数据,则会对将来页面中返回的项目产生影响。例如,如果我的分页查询指定where val1 > 4并且我有一个记录,即val1为1为5,则在块2中,该项可能会返回,因为它现在符合条件。如果您需要更新where子句中的值(从而影响您正在处理的数据集中的内容),最好添加一些您可以查询的处理过的标记。

答案 2 :(得分:0)

基于pageSize我跳过行的问题也一样。 例如,如果我将pageSize设置为2,它将读取2,忽略2,读取2,忽略2等。

我正在构建一个守护程序处理器来轮询“请求”数据库表,以获取处于“等待处理”状态的记录。该守护进程旨在在后台运行。

我有一个'status'字段,它在@NamedQuery中定义,并会选择状态为'10'的记录:等待处理。处理完记录后,状态字段将更新为“20”:错误或“30”:成功。 事实证明这是问题的原因 - 我正在更新查询中定义的字段。如果我引入了'processedField'并更新了那个而不是'status'字段那么没问题 - 所有的记录都会被读取。

作为更新状态字段的可能解决方案,我将setMaxItemCount设置为与PageSize相同;这在步骤完成之前正确更新了记录。然后我继续执行该步骤,直到请求停止守护程序。好吧,可能不是最有效的方法(但我仍然受益于JPA提供的易用性),但我认为使用JdbcPagingItemReader可能会更好(如上所述 - 谢谢!)。关于批量数据库轮询问题的最佳方法的意见将受到欢迎:)

答案 3 :(得分:0)

我遇到了同样的情况,我的读者是一个JpaPagingItemReader,它查询了编写器中更新的字段。因此,由于页面窗口正在进行而已读取的项目不再在阅读器范围内,因此跳过需要更新的一半项目。

对我来说最简单的解决方法是覆盖JpaPagingItemReader上的getPage方法,以便始终返回第一页。

JpaPagingItemReader<XXXXX> jpaPagingItemReader = new JpaPagingItemReader() {
    @Override
    public int getPage() {
        return 0;
    }
};