diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/HibernateItemReaderHelper.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/HibernateItemReaderHelper.java index 1fcc3b03f..a0c7dec92 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/HibernateItemReaderHelper.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/HibernateItemReaderHelper.java @@ -49,7 +49,7 @@ public class HibernateItemReaderHelper implements InitializingBean { private String queryName = ""; - private HibernateQueryProvider queryProvider; + private HibernateQueryProvider queryProvider; private boolean useStatelessSession = true; @@ -74,7 +74,7 @@ public class HibernateItemReaderHelper implements InitializingBean { /** * @param queryProvider Hibernate query provider */ - public void setQueryProvider(HibernateQueryProvider queryProvider) { + public void setQueryProvider(HibernateQueryProvider queryProvider) { this.queryProvider = queryProvider; } @@ -108,11 +108,6 @@ public class HibernateItemReaderHelper implements InitializingBean { Assert.state(StringUtils.hasText(queryString) ^ StringUtils.hasText(queryName), "queryString or queryName must be set"); } - // making sure that the appropriate (Hibernate) query provider is set - else { - Assert.state(queryProvider != null, "Hibernate query provider must be set"); - } - } /** diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/HibernatePagingItemReader.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/HibernatePagingItemReader.java index 2f4172c30..b8c5517f8 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/HibernatePagingItemReader.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/HibernatePagingItemReader.java @@ -60,7 +60,7 @@ import org.springframework.util.ClassUtils; public class HibernatePagingItemReader extends AbstractPagingItemReader implements InitializingBean { - private HibernateItemReaderHelper helper = new HibernateItemReaderHelper(); + private HibernateItemReaderHelper helper = new HibernateItemReaderHelper<>(); private Map parameterValues; @@ -108,7 +108,7 @@ public class HibernatePagingItemReader extends AbstractPagingItemReader * * @param queryProvider Hibernate query provider */ - public void setQueryProvider(HibernateQueryProvider queryProvider) { + public void setQueryProvider(HibernateQueryProvider queryProvider) { helper.setQueryProvider(queryProvider); } diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/builder/HibernateCursorItemReaderBuilder.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/builder/HibernateCursorItemReaderBuilder.java index fa347bf20..239eb373d 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/builder/HibernateCursorItemReaderBuilder.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/builder/HibernateCursorItemReaderBuilder.java @@ -148,7 +148,7 @@ public class HibernateCursorItemReaderBuilder { /** * The Hibernate {@link SessionFactory} to execute the query against. * - * @param sessionFactory the session factorry + * @param sessionFactory the session factory * @return this instance for method chaining * @see HibernateCursorItemReader#setSessionFactory(SessionFactory) */ 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 new file mode 100644 index 000000000..51fc4b922 --- /dev/null +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/builder/HibernatePagingItemReaderBuilder.java @@ -0,0 +1,268 @@ +/* + * 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 org.hibernate.SessionFactory; + +import org.springframework.batch.item.database.HibernatePagingItemReader; +import org.springframework.batch.item.database.orm.HibernateQueryProvider; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * A builder for the {@link HibernatePagingItemReader}. When configuring, only one of the + * following should be provided: + *
    + *
  • {@link #queryString(String)}
  • + *
  • {@link #queryName(String)}
  • + *
  • {@link #queryProvider(HibernateQueryProvider)}
  • + *
+ * + * @author Michael Minella + * @since 4.0 + * @see HibernatePagingItemReader + */ +public class HibernatePagingItemReaderBuilder { + + private String name; + + private int currentItem = 0; + + private int maxItemCount = Integer.MAX_VALUE; + + private boolean saveState = true; + + private int pageSize = 10; + + private Map parameterValues; + + private String queryName; + + private int fetchSize; + + private HibernateQueryProvider queryProvider; + + private String queryString; + + private SessionFactory sessionFactory; + + private boolean statelessSession = true; + + /** + * 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 HibernatePagingItemReader#setName(String) + */ + public HibernatePagingItemReaderBuilder 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 HibernatePagingItemReader#setCurrentItemCount(int) + */ + + public HibernatePagingItemReaderBuilder 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 HibernatePagingItemReader#setMaxItemCount(int) + */ + public HibernatePagingItemReaderBuilder 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 HibernatePagingItemReader#setSaveState(boolean) + */ + public HibernatePagingItemReaderBuilder 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 HibernatePagingItemReader#setPageSize(int) + */ + public HibernatePagingItemReaderBuilder 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 HibernatePagingItemReader#setParameterValues(Map) + */ + public HibernatePagingItemReaderBuilder parameterValues(Map parameterValues) { + this.parameterValues = parameterValues; + + return this; + } + + /** + * The name of the Hibernate named query to be executed for this reader. + * + * @param queryName name of the query to execute + * @return this instance for method chaining + * @see HibernatePagingItemReader#setQueryName(String) + */ + public HibernatePagingItemReaderBuilder queryName(String queryName) { + this.queryName = queryName; + + return this; + } + + /** + * Fetch size used internally by Hibernate to limit amount of data fetched + * from database per round trip. + * + * @param fetchSize number of records + * @return this instance for method chaining + * @see HibernatePagingItemReader#setFetchSize(int) + */ + public HibernatePagingItemReaderBuilder fetchSize(int fetchSize) { + this.fetchSize = fetchSize; + + return this; + } + + /** + * A query provider. This should be set only if {@link #queryString(String)} and + * {@link #queryName(String)} have not been set. + * + * @param queryProvider the query provider + * @return this instance for method chaining + * @see HibernatePagingItemReader#setQueryProvider(HibernateQueryProvider) + */ + public HibernatePagingItemReaderBuilder queryProvider(HibernateQueryProvider queryProvider) { + this.queryProvider = queryProvider; + + return this; + } + + /** + * The HQL query string to execute. This should only be set if + * {@link #queryProvider(HibernateQueryProvider)} and {@link #queryName(String)} have + * not been set. + * + * @param queryString the HQL query + * @return this instance for method chaining + * @see HibernatePagingItemReader#setQueryString(String) + */ + public HibernatePagingItemReaderBuilder queryString(String queryString) { + this.queryString = queryString; + + return this; + } + + /** + * The Hibernate {@link SessionFactory} to execute the query against. + * + * @param sessionFactory the session factory + * @return this instance for method chaining + * @see HibernatePagingItemReader#setSessionFactory(SessionFactory) + */ + public HibernatePagingItemReaderBuilder sessionFactory(SessionFactory sessionFactory) { + this.sessionFactory = sessionFactory; + + return this; + } + + /** + * Indicator for whether to use a {@link org.hibernate.StatelessSession} + * (true) or a {@link org.hibernate.Session} (false). + * + * @param useStatelessSession Defaults to false + * @return this instance for method chaining + * @see HibernatePagingItemReader#setUseStatelessSession(boolean) + */ + public HibernatePagingItemReaderBuilder useSatelessSession(boolean useStatelessSession) { + this.statelessSession = useStatelessSession; + + return this; + } + + /** + * Returns a fully constructed {@link HibernatePagingItemReader}. + * + * @return a new {@link HibernatePagingItemReader} + */ + public HibernatePagingItemReader build() { + Assert.notNull(this.sessionFactory, "A SessionFactory must be provided"); + Assert.state(this.fetchSize >= 0, "fetchSize must not be negative"); + + if(this.saveState) { + Assert.hasText(this.name, + "A name is required when saveState is set to true"); + } + + if(this.queryProvider == null) { + Assert.state(StringUtils.hasText(queryString) ^ StringUtils.hasText(queryName), + "queryString or queryName must be set"); + } + + HibernatePagingItemReader reader = new HibernatePagingItemReader<>(); + + reader.setSessionFactory(this.sessionFactory); + reader.setSaveState(this.saveState); + reader.setMaxItemCount(this.maxItemCount); + reader.setCurrentItemCount(this.currentItem); + reader.setName(this.name); + reader.setFetchSize(this.fetchSize); + reader.setParameterValues(this.parameterValues); + reader.setQueryName(this.queryName); + reader.setQueryProvider(this.queryProvider); + reader.setQueryString(this.queryString); + reader.setPageSize(this.pageSize); + reader.setUseStatelessSession(this.statelessSession); + + return reader; + } +} diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/builder/HibernatePagingItemReaderBuilderTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/builder/HibernatePagingItemReaderBuilderTests.java new file mode 100644 index 000000000..2e0fd292f --- /dev/null +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/builder/HibernatePagingItemReaderBuilderTests.java @@ -0,0 +1,245 @@ +/* + * 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.sql.DataSource; + +import org.hibernate.SessionFactory; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; + +import org.springframework.batch.item.ExecutionContext; +import org.springframework.batch.item.database.HibernateItemReaderHelper; +import org.springframework.batch.item.database.HibernatePagingItemReader; +import org.springframework.batch.item.database.orm.HibernateNativeQueryProvider; +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.hibernate5.LocalSessionFactoryBean; +import org.springframework.test.util.ReflectionTestUtils; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; + +/** + * @author Michael Minella + */ +public class HibernatePagingItemReaderBuilderTests { + + private SessionFactory sessionFactory; + + private ConfigurableApplicationContext context; + + @Before + public void setUp() { + this.context = new AnnotationConfigApplicationContext(HibernatePagingItemReaderBuilderTests.TestDataSourceConfiguration.class); + this.sessionFactory = (SessionFactory) context.getBean("sessionFactory"); + } + + @After + public void tearDown() { + if(this.context != null) { + this.context.close(); + } + } + + @Test + public void testConfiguration() throws Exception { + HibernatePagingItemReader reader = new HibernatePagingItemReaderBuilder() + .name("fooReader") + .sessionFactory(this.sessionFactory) + .fetchSize(2) + .currentItem(2) + .maxItemCount(4) + .pageSize(5) + .queryName("allFoos") + .useSatelessSession(false) + .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")); + + HibernateItemReaderHelper helper = (HibernateItemReaderHelper) ReflectionTestUtils.getField(reader, "helper"); + assertEquals(false, ReflectionTestUtils.getField(helper, "useStatelessSession")); + } + + @Test + public void testConfigurationNoSaveState() throws Exception { + Map parameters = new HashMap<>(); + parameters.put("value", 2); + + HibernatePagingItemReader reader = new HibernatePagingItemReaderBuilder() + .name("fooReader") + .sessionFactory(this.sessionFactory) + .queryString("from Foo foo where foo.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 { + + HibernateNativeQueryProvider provider = new HibernateNativeQueryProvider<>(); + provider.setEntityClass(Foo.class); + provider.setSqlQuery("select * from T_FOOS"); + provider.afterPropertiesSet(); + + HibernatePagingItemReader reader = new HibernatePagingItemReaderBuilder() + .name("fooReader") + .sessionFactory(this.sessionFactory) + .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 HibernateCursorItemReaderBuilder() + .sessionFactory(this.sessionFactory) + .fetchSize(-2) + .build(); + fail("fetch size must be >= 0"); + } + catch (IllegalStateException ise) { + assertEquals("fetchSize must not be negative", ise.getMessage()); + } + + try { + new HibernateCursorItemReaderBuilder().build(); + fail("sessionFactory is required"); + } + catch (IllegalStateException ise) { + assertEquals("A SessionFactory must be provided", ise.getMessage()); + } + + try { + new HibernateCursorItemReaderBuilder() + .sessionFactory(this.sessionFactory) + .saveState(true) + .build(); + fail("name is required when saveState is set to true"); + } + catch (IllegalStateException ise) { + assertEquals("A name is required when saveState is set to true.", ise.getMessage()); + } + + try { + new HibernateCursorItemReaderBuilder() + .sessionFactory(this.sessionFactory) + .saveState(false) + .build(); + fail("queryString or queryName must be set"); + } + catch (IllegalStateException ise) { + assertEquals("queryString or queryName must be set", ise.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 SessionFactory sessionFactory() throws Exception { + LocalSessionFactoryBean factoryBean = new LocalSessionFactoryBean(); + factoryBean.setDataSource(dataSource()); + factoryBean.setMappingLocations(new ClassPathResource("/org/springframework/batch/item/database/Foo.hbm.xml", getClass())); + factoryBean.afterPropertiesSet(); + + return factoryBean.getObject(); + + } + } +}