diff --git a/spring-batch-infrastructure-tests/src/test/resources/org/springframework/batch/item/database/JpaPagingItemReaderCommonTests-context.xml b/spring-batch-infrastructure-tests/src/test/resources/org/springframework/batch/item/database/JpaPagingItemReaderCommonTests-context.xml
index 0e1d8476a..1fe06055a 100644
--- a/spring-batch-infrastructure-tests/src/test/resources/org/springframework/batch/item/database/JpaPagingItemReaderCommonTests-context.xml
+++ b/spring-batch-infrastructure-tests/src/test/resources/org/springframework/batch/item/database/JpaPagingItemReaderCommonTests-context.xml
@@ -1,8 +1,7 @@
-
+
diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/data/RepositoryItemWriter.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/data/RepositoryItemWriter.java
index 706fecc97..754da4cf2 100644
--- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/data/RepositoryItemWriter.java
+++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/data/RepositoryItemWriter.java
@@ -36,7 +36,7 @@ import org.springframework.util.MethodInvoker;
*
*
*
- * It depends on {@link org.springframework.data.repository.CrudRepository#save(Iterable)}
+ * It depends on {@link org.springframework.data.repository.CrudRepository#saveAll(Iterable)}
* method to store the items for the chunk. Performance will be determined by that
* implementation more than this writer.
*
diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/builder/HibernatePagingItemReaderBuilder.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/builder/HibernatePagingItemReaderBuilder.java
index 51fc4b922..f610bb7db 100644
--- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/builder/HibernatePagingItemReaderBuilder.java
+++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/builder/HibernatePagingItemReaderBuilder.java
@@ -84,7 +84,6 @@ public class HibernatePagingItemReaderBuilder {
* @return this instance for method chaining
* @see HibernatePagingItemReader#setCurrentItemCount(int)
*/
-
public HibernatePagingItemReaderBuilder currentItem(int currentItem) {
this.currentItem = currentItem;
diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/builder/JpaPagingItemReaderBuilder.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/builder/JpaPagingItemReaderBuilder.java
new file mode 100644
index 000000000..042d25ce7
--- /dev/null
+++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/builder/JpaPagingItemReaderBuilder.java
@@ -0,0 +1,221 @@
+/*
+ * Copyright 2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.batch.item.database.builder;
+
+import java.util.Map;
+import javax.persistence.EntityManagerFactory;
+
+import org.springframework.batch.item.database.JpaPagingItemReader;
+import org.springframework.batch.item.database.orm.JpaQueryProvider;
+import org.springframework.util.Assert;
+
+/**
+ * @author Michael Minella
+ */
+public class JpaPagingItemReaderBuilder {
+
+ private String name;
+
+ private int currentItem = 0;
+
+ private int maxItemCount = Integer.MAX_VALUE;
+
+ private boolean saveState = true;
+
+ private int pageSize = 10;
+
+ private EntityManagerFactory entityManagerFactory;
+
+ private Map parameterValues;
+
+ private boolean transacted = true;
+
+ private String queryString;
+
+ private JpaQueryProvider queryProvider;
+
+ /**
+ * A name used to prevent key collisions while saving the state in the
+ * {@link org.springframework.batch.item.ExecutionContext}
+ *
+ * @param name unique name for this reader instance
+ * @return this instance for method chaining
+ * @see JpaPagingItemReader#setName(String)
+ */
+ public JpaPagingItemReaderBuilder name(String name) {
+ this.name = name;
+
+ return this;
+ }
+
+ /**
+ * Index for the current item. Used on restarts to indicate where to start from.
+ *
+ * @param currentItem current index
+ * @return this instance for method chaining
+ * @see JpaPagingItemReader#setCurrentItemCount(int)
+ */
+ public JpaPagingItemReaderBuilder currentItem(int currentItem) {
+ this.currentItem = currentItem;
+
+ return this;
+ }
+
+ /**
+ * The index of the max item to be read.
+ *
+ * @param maxItemCount max index
+ * @return this instance for method chaining
+ * @see JpaPagingItemReader#setMaxItemCount(int)
+ */
+ public JpaPagingItemReaderBuilder maxItemCount(int maxItemCount) {
+ this.maxItemCount = maxItemCount;
+
+ return this;
+ }
+
+ /**
+ * Indicates if the state should be saved. If set to false, restarts will begin at
+ * the beginning of the dataset. Defaults to true
+ *
+ * @param saveState indicator
+ * @return this instance for method chaining
+ * @see JpaPagingItemReader#setSaveState(boolean)
+ */
+ public JpaPagingItemReaderBuilder saveState(boolean saveState) {
+ this.saveState = saveState;
+
+ return this;
+ }
+
+ /**
+ * The number of records to request per page/query. Defaults to 10. Must be greater
+ * than zero.
+ *
+ * @param pageSize number of items
+ * @return this instance for method chaining
+ * @see JpaPagingItemReader#setPageSize(int)
+ */
+ public JpaPagingItemReaderBuilder pageSize(int pageSize) {
+ this.pageSize = pageSize;
+
+ return this;
+ }
+
+ /**
+ * A map of parameter values to be set on the query. The key of the map is the name
+ * of the parameter to be set with the value being the value to be set.
+ *
+ * @param parameterValues map of values
+ * @return this instance for method chaining
+ * @see JpaPagingItemReader#setParameterValues(Map)
+ */
+ public JpaPagingItemReaderBuilder parameterValues(Map parameterValues) {
+ this.parameterValues = parameterValues;
+
+ return this;
+ }
+
+ /**
+ * A query provider. This should be set only if {@link #queryString(String)} have not
+ * been set.
+ *
+ * @param queryProvider the query provider
+ * @return this instance for method chaining
+ * @see JpaPagingItemReader#setQueryProvider(JpaQueryProvider)
+ */
+ public JpaPagingItemReaderBuilder queryProvider(JpaQueryProvider queryProvider) {
+ this.queryProvider = queryProvider;
+
+ return this;
+ }
+
+ /**
+ * The HQL query string to execute. This should only be set if
+ * {@link #queryProvider(JpaQueryProvider)} has not been set.
+ *
+ * @param queryString the HQL query
+ * @return this instance for method chaining
+ * @see JpaPagingItemReader#setQueryString(String)
+ */
+ public JpaPagingItemReaderBuilder queryString(String queryString) {
+ this.queryString = queryString;
+
+ return this;
+ }
+
+ /**
+ * Indicates if a transaction should be created around the read (true by default).
+ * Can be set to false in cases where JPA implementation doesn't support a particular
+ * transaction, however this may cause object inconsistency in the EntityManagerFactory.
+ *
+ * @param transacted defaults to true
+ * @return this instance for method chaining
+ * @see JpaPagingItemReader#setTransacted(boolean)
+ */
+ public JpaPagingItemReaderBuilder transacted(boolean transacted) {
+ this.transacted = transacted;
+
+ return this;
+ }
+
+ /**
+ * The {@link EntityManagerFactory} to be used for executing the configured
+ * {@link #queryString}.
+ *
+ * @param entityManagerFactory
+ * @return
+ */
+ public JpaPagingItemReaderBuilder entityManagerFactory(EntityManagerFactory entityManagerFactory) {
+ this.entityManagerFactory = entityManagerFactory;
+
+ return this;
+ }
+
+ /**
+ * Returns a fully constructed {@link JpaPagingItemReader}.
+ *
+ * @return a new {@link JpaPagingItemReader}
+ */
+ public JpaPagingItemReader build() {
+ Assert.isTrue(this.pageSize > 0, "pageSize must be greater than zero");
+ Assert.notNull(this.entityManagerFactory, "An EntityManagerFactory is required");
+
+ if(this.saveState) {
+ Assert.hasText(this.name,
+ "A name is required when saveState is set to true");
+ }
+
+ if(this.queryProvider == null) {
+ Assert.hasLength(this.queryString, "Query string is required when queryProvider is null");
+ }
+
+ JpaPagingItemReader reader = new JpaPagingItemReader<>();
+
+ reader.setQueryString(this.queryString);
+ reader.setPageSize(this.pageSize);
+ reader.setParameterValues(this.parameterValues);
+ reader.setEntityManagerFactory(this.entityManagerFactory);
+ reader.setQueryProvider(this.queryProvider);
+ reader.setTransacted(this.transacted);
+ reader.setCurrentItemCount(this.currentItem);
+ reader.setMaxItemCount(this.maxItemCount);
+ reader.setSaveState(this.saveState);
+ reader.setName(this.name);
+
+ return reader;
+ }
+}
diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/builder/JpaPagingItemReaderBuilderTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/builder/JpaPagingItemReaderBuilderTests.java
new file mode 100644
index 000000000..fe7436a87
--- /dev/null
+++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/builder/JpaPagingItemReaderBuilderTests.java
@@ -0,0 +1,243 @@
+/*
+ * Copyright 2017 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.springframework.batch.item.database.builder;
+
+import java.util.HashMap;
+import java.util.Map;
+import javax.persistence.EntityManagerFactory;
+import javax.sql.DataSource;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import org.springframework.batch.item.ExecutionContext;
+import org.springframework.batch.item.database.JpaPagingItemReader;
+import org.springframework.batch.item.database.orm.JpaNativeQueryProvider;
+import org.springframework.batch.item.sample.Foo;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.context.annotation.AnnotationConfigApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.core.io.ClassPathResource;
+import org.springframework.core.io.Resource;
+import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseFactory;
+import org.springframework.jdbc.datasource.init.DataSourceInitializer;
+import org.springframework.jdbc.datasource.init.ResourceDatabasePopulator;
+import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
+import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.fail;
+
+/**
+ * @author Michael Minella
+ */
+public class JpaPagingItemReaderBuilderTests {
+
+ private EntityManagerFactory entityManagerFactory;
+
+ private ConfigurableApplicationContext context;
+
+ @Before
+ public void setUp() {
+ this.context = new AnnotationConfigApplicationContext(JpaPagingItemReaderBuilderTests.TestDataSourceConfiguration.class);
+ this.entityManagerFactory = (EntityManagerFactory) context.getBean("entityManagerFactory");
+ }
+
+ @After
+ public void tearDown() {
+ if(this.context != null) {
+ this.context.close();
+ }
+ }
+
+ @Test
+ public void testConfiguration() throws Exception {
+ JpaPagingItemReader reader = new JpaPagingItemReaderBuilder()
+ .name("fooReader")
+ .entityManagerFactory(this.entityManagerFactory)
+ .currentItem(2)
+ .maxItemCount(4)
+ .pageSize(5)
+ .transacted(false)
+ .queryString("select f from Foo f ")
+ .build();
+
+ reader.afterPropertiesSet();
+
+ ExecutionContext executionContext = new ExecutionContext();
+
+ reader.open(executionContext);
+ Foo item1 = reader.read();
+ Foo item2 = reader.read();
+ assertNull(reader.read());
+ reader.update(executionContext);
+ reader.close();
+
+ assertEquals(3, item1.getId());
+ assertEquals("bar3", item1.getName());
+ assertEquals(3, item1.getValue());
+ assertEquals(4, item2.getId());
+ assertEquals("bar4", item2.getName());
+ assertEquals(4, item2.getValue());
+
+ assertEquals(2, executionContext.size());
+ assertEquals(5, ReflectionTestUtils.getField(reader, "pageSize"));
+ assertFalse((Boolean) ReflectionTestUtils.getField(reader, "transacted"));
+ }
+
+ @Test
+ public void testConfigurationNoSaveState() throws Exception {
+ Map parameters = new HashMap<>();
+ parameters.put("value", 2);
+
+ JpaPagingItemReader reader = new JpaPagingItemReaderBuilder()
+ .name("fooReader")
+ .entityManagerFactory(this.entityManagerFactory)
+ .queryString("select f from Foo f where f.id > :value")
+ .parameterValues(parameters)
+ .saveState(false)
+ .build();
+
+ reader.afterPropertiesSet();
+
+ ExecutionContext executionContext = new ExecutionContext();
+
+ reader.open(executionContext);
+
+ int i = 0;
+ while(reader.read() != null) {
+ i++;
+ }
+
+ reader.update(executionContext);
+ reader.close();
+
+ assertEquals(3, i);
+ assertEquals(0, executionContext.size());
+ }
+
+ @Test
+ public void testConfigurationQueryProvider() throws Exception {
+
+ JpaNativeQueryProvider provider = new JpaNativeQueryProvider<>();
+ provider.setEntityClass(Foo.class);
+ provider.setSqlQuery("select * from T_FOOS");
+ provider.afterPropertiesSet();
+
+ JpaPagingItemReader reader = new JpaPagingItemReaderBuilder()
+ .name("fooReader")
+ .entityManagerFactory(this.entityManagerFactory)
+ .queryProvider(provider)
+ .build();
+
+ reader.afterPropertiesSet();
+
+ ExecutionContext executionContext = new ExecutionContext();
+
+ reader.open(executionContext);
+
+ int i = 0;
+ while(reader.read() != null) {
+ i++;
+ }
+
+ reader.update(executionContext);
+ reader.close();
+
+ assertEquals(5, i);
+ }
+
+ @Test
+ public void testValidation() {
+ try {
+ new JpaPagingItemReaderBuilder()
+ .entityManagerFactory(this.entityManagerFactory)
+ .pageSize(-2)
+ .build();
+ fail("pageSize must be >= 0");
+ }
+ catch (IllegalArgumentException iae) {
+ assertEquals("pageSize must be greater than zero", iae.getMessage());
+ }
+
+ try {
+ new JpaPagingItemReaderBuilder().build();
+ fail("An EntityManagerFactory is required");
+ }
+ catch (IllegalArgumentException iae) {
+ assertEquals("An EntityManagerFactory is required", iae.getMessage());
+ }
+
+ try {
+ new JpaPagingItemReaderBuilder()
+ .entityManagerFactory(this.entityManagerFactory)
+ .saveState(true)
+ .build();
+ fail("A name is required when saveState is set to true");
+ }
+ catch (IllegalArgumentException iae) {
+ assertEquals("A name is required when saveState is set to true", iae.getMessage());
+ }
+
+ try {
+ new JpaPagingItemReaderBuilder()
+ .entityManagerFactory(this.entityManagerFactory)
+ .saveState(false)
+ .build();
+ fail("Query string is required when queryProvider is null");
+ }
+ catch (IllegalArgumentException iae) {
+ assertEquals("Query string is required when queryProvider is null", iae.getMessage());
+ }
+ }
+
+ @Configuration
+ public static class TestDataSourceConfiguration {
+
+ @Bean
+ public DataSource dataSource() {
+ return new EmbeddedDatabaseFactory().getDatabase();
+ }
+
+ @Bean
+ public DataSourceInitializer initializer(DataSource dataSource) {
+ DataSourceInitializer dataSourceInitializer = new DataSourceInitializer();
+ dataSourceInitializer.setDataSource(dataSource);
+
+ Resource create = new ClassPathResource("org/springframework/batch/item/database/init-foo-schema-hsqldb.sql");
+ dataSourceInitializer.setDatabasePopulator(new ResourceDatabasePopulator(create));
+
+ return dataSourceInitializer;
+ }
+
+ @Bean
+ public LocalContainerEntityManagerFactoryBean entityManagerFactory() throws Exception {
+ LocalContainerEntityManagerFactoryBean entityManagerFactoryBean =
+ new LocalContainerEntityManagerFactoryBean();
+
+ entityManagerFactoryBean.setDataSource(dataSource());
+ entityManagerFactoryBean.setPersistenceUnitName("bar");
+ entityManagerFactoryBean.setJpaVendorAdapter(new HibernateJpaVendorAdapter());
+
+ return entityManagerFactoryBean;
+ }
+ }
+}