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; + } + } +}