Announcement Announcement Module
Collapse
No announcement yet.
Flush in JpaPagingItemReader causing StaleObjectStateException. Page Title Module
Move Remove Collapse
X
Conversation Detail Module
Collapse
  • Filter
  • Time
  • Show
Clear All
new posts

  • Flush in JpaPagingItemReader causing StaleObjectStateException.

    I was running into a similar problem as described here: http://forum.springsource.org/showth...r-and-pageSize and thought I'd do some more investigation on my own.

    I feel like my use case is pretty straight-forward; I want to:
    1. Read domain objects from database by page
    2. Process domain object; update one of it's values
    3. Write updated domain object back to database

    However, if the domain object is @Version'ed, a StaleObjectException is thrown when trying to read the second page. This is because the domain object in the persistence context of the reader is out of date (the writer has already flushed the changes to the database) when the reader tries to flush the session.

    I have put together a simple example to show my point:

    TheItem.java:
    Code:
    package example;
    
    import javax.persistence.Column;
    import javax.persistence.Entity;
    import javax.persistence.GeneratedValue;
    import javax.persistence.Id;
    import javax.persistence.SequenceGenerator;
    import javax.persistence.Table;
    import javax.persistence.Version;
    
    @Entity
    @Table(name = "THE_ITEM")
    public class TheItem {
    
        @Id
        @Column(name = "THE_ITEM_ID")
        @GeneratedValue(generator = "THE_ITEM_ID")
        @SequenceGenerator(name = "THE_ITEM_ID", sequenceName = "THE_ITEM_ID", allocationSize = 1)
        private Integer id;
    
        @Column(name = "the_value")
        private Integer theValue;
    
        @Column(name = "version", nullable = false)
        @Version
        private Integer version;
    
        public Integer getId() {
            return id;
        }
    
        public void setId(Integer id) {
            this.id = id;
        }
    
        public Integer getTheValue() {
            return theValue;
        }
    
        public void setTheValue(Integer theValue) {
            this.theValue = theValue;
        }
    
        public Integer getVersion() {
            return version;
        }
    
        public void setVersion(Integer version) {
            this.version = version;
        }
    }
    TheItemProcessor.java:
    Code:
    package example;
    
    import org.springframework.batch.item.ItemProcessor;
    
    public class TheItemProcessor implements ItemProcessor<TheItem, TheItem> {
    
        @Override
        public TheItem process(TheItem item) throws Exception {
            item.setTheValue(item.getTheValue() + 1);
            return item;
        }
    }
    StaleReaderExampleTest.java:
    Code:
    package example;
    
    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.batch.core.BatchStatus;
    import org.springframework.batch.core.JobExecution;
    import org.springframework.batch.core.JobParameters;
    import org.springframework.batch.test.JobLauncherTestUtils;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.test.context.ContextConfiguration;
    import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
    
    import static org.junit.Assert.assertEquals;
    import static org.junit.Assert.assertNotNull;
    import static org.junit.Assert.fail;
    
    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration(locations = { "/example/StaleReaderExample.xml" })
    public class StaleReaderExampleTest {
    
        @Autowired
        private JobLauncherTestUtils jobLauncherTestUtils;
    
        @Test
        public void testRun() {
    
            JobParameters params = new JobParameters();
    
            JobExecution jobExecution = null;
    
            try {
                jobExecution = jobLauncherTestUtils.launchJob(params);
            } catch (Exception e) {
                e.printStackTrace();
                fail("launchJob threw exception.");
            }
    
            //Assert Job completes with no exceptions
            assertNotNull(jobExecution);
            assertEquals(BatchStatus.COMPLETED, jobExecution.getStatus());
    
        }
    }
    StaleReaderExample.xml:
    Code:
    <?xml version="1.0" encoding="UTF-8"?>
    
    <beans xmlns="http://www.springframework.org/schema/beans"
    	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    	xmlns:util="http://www.springframework.org/schema/util"
    	xmlns:jdbc="http://www.springframework.org/schema/jdbc"
    	xmlns:batch="http://www.springframework.org/schema/batch"
    	xmlns:context="http://www.springframework.org/schema/context"
    	xmlns:p="http://www.springframework.org/schema/p"
    	xsi:schemaLocation="http://www.springframework.org/schema/jdbc http://www.springframework.org/schema/jdbc/spring-jdbc.xsd
    		http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
    		http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd
    		http://www.springframework.org/schema/batch http://www.springframework.org/schema/batch/spring-batch.xsd
    		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
    
    	<jdbc:initialize-database data-source="dataSource">
    		<jdbc:script execution="INIT" location="classpath:example/init.sql" />
    	</jdbc:initialize-database>
    	
    	<bean id="testUtils" class="org.springframework.batch.test.JobLauncherTestUtils"/>
    	
    	<bean id="jobRepository" class="org.springframework.batch.core.repository.support.MapJobRepositoryFactoryBean">
    	    <property name="transactionManager" ref="transactionManager"/>
    	    <property name="isolationLevelForCreate" value="ISOLATION_DEFAULT"/>
    	</bean>
    
        <bean id="transactionManager" 
        	class="org.springframework.orm.jpa.JpaTransactionManager"
            p:entityManagerFactory-ref="entityManagerFactory" />
    	
    	<bean id="jpaAdapter" 
    		class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter"
    		p:generateDdl="true"
            p:databasePlatform="org.hibernate.dialect.HSQLDialect"
            p:showSql="false" />
            
        <bean id="dataSource" class="org.hsqldb.jdbc.JDBCDataSource">
    		<property name="url" value="jdbc:hsqldb:mem:testdb" />
    		<property name="user" value="sa" />
    		<property name="password" value="" />
        </bean>
    			
        <bean id="entityManagerFactory"
        	class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean"
            p:jpaVendorAdapter-ref="jpaAdapter"
            p:packagesToScan="example">
            <property name="dataSource" ref="dataSource"/>
            <property name="jpaProperties">
    			<props>
    		        <prop key="hibernate.dialect">org.hibernate.dialect.HSQLDialect</prop>
    	    	</props>
    	 	</property>
        </bean>
    
    
    	<bean id="jobLauncher" class="org.springframework.batch.core.launch.support.SimpleJobLauncher">
    		<property name="jobRepository" ref="jobRepository" />
    	</bean>	
    	
    	<bean id="runIdIncrementer" class="org.springframework.batch.core.launch.support.RunIdIncrementer" />
    	
    	<batch:job id="StaleReaderExample" incrementer="runIdIncrementer" job-repository="jobRepository">
    	
    		<batch:step id="staleRead">
    			<tasklet transaction-manager="transactionManager">
    					<chunk	commit-interval="1">
    					
    						<batch:reader>
    							<bean id="jpaReader" class="org.springframework.batch.item.database.JpaPagingItemReader">
    								<property name="entityManagerFactory" ref="entityManagerFactory"/>
    						    	<property name="pageSize" value="1"/>
    						    	<property name="queryString" value="from TheItem i order by i.id"/>
    							</bean>
    						</batch:reader>
    						
    						<batch:processor>
    							<bean id="theItemProcessor" class="example.TheItemProcessor"/>
    						</batch:processor>
    						
    						<batch:writer>
    							<bean class="org.springframework.batch.item.database.JpaItemWriter">
    								<property name="entityManagerFactory" ref="entityManagerFactory"/>
    							</bean>
    						</batch:writer>
    						
    					</chunk>
    				</tasklet>
    		</batch:step>
    	</batch:job>
    	
    </beans>
    init.sql:
    Code:
    CREATE SEQUENCE THE_ITEM_ID;
    
    insert into the_item (the_item_id, the_value, version) values (NEXT VALUE FOR THE_ITEM_ID, 0, 0);
    insert into the_item (the_item_id, the_value, version) values (NEXT VALUE FOR THE_ITEM_ID, 0, 0);
    insert into the_item (the_item_id, the_value, version) values (NEXT VALUE FOR THE_ITEM_ID, 0, 0);
    insert into the_item (the_item_id, the_value, version) values (NEXT VALUE FOR THE_ITEM_ID, 0, 0);

    So my question is; should this be logged as a bug? Or is there an assumption in this design that is incorrect?

    The closest bugs I could find to this were https://jira.springsource.org/browse/BATCH-1880 or possibly https://jira.springsource.org/browse/BATCH-1166.

  • #2
    I should mention, I was able to work around this problem by having the reader return a list of id's instead of domain objects. Then passing the ID to a composite processor where the first processor simply does a findById to return the managed entity and hands that off to the second (unchanged) processor.

    Comment

    Working...
    X