From 75873629af91b8a9fbca7d03637edfb52283e68f Mon Sep 17 00:00:00 2001 From: Michael Minella Date: Thu, 23 Mar 2017 09:52:47 -0500 Subject: [PATCH] Created builder for HibernateCursorItemReader This commit creates a builder for the HibernateCursorItemReader Resolves BATCH-2584 --- .../database/HibernateCursorItemReader.java | 4 +- .../HibernateCursorItemReaderBuilder.java | 251 ++++++++++++++++++ .../HibernateCursorItemReaderCommonTests.java | 2 +- ...HibernateCursorItemReaderBuilderTests.java | 236 ++++++++++++++++ .../batch/item/database/Foo.hbm.xml | 2 +- 5 files changed, 491 insertions(+), 4 deletions(-) create mode 100644 spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/builder/HibernateCursorItemReaderBuilder.java create mode 100644 spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/builder/HibernateCursorItemReaderBuilderTests.java diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/HibernateCursorItemReader.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/HibernateCursorItemReader.java index 3b7b8692b..e4eb0378a 100644 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/HibernateCursorItemReader.java +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/HibernateCursorItemReader.java @@ -1,5 +1,5 @@ /* - * Copyright 2006-2013 the original author or authors. + * Copyright 2006-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. @@ -57,7 +57,7 @@ import org.springframework.util.ClassUtils; public class HibernateCursorItemReader extends AbstractItemCountingItemStreamItemReader implements InitializingBean { - private HibernateItemReaderHelper helper = new HibernateItemReaderHelper(); + private HibernateItemReaderHelper helper = new HibernateItemReaderHelper<>(); public HibernateCursorItemReader() { setName(ClassUtils.getShortName(HibernateCursorItemReader.class)); 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 new file mode 100644 index 000000000..ccc6185c7 --- /dev/null +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/builder/HibernateCursorItemReaderBuilder.java @@ -0,0 +1,251 @@ +/* + * 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.HibernateCursorItemReader; +import org.springframework.batch.item.database.orm.HibernateQueryProvider; +import org.springframework.util.Assert; +import org.springframework.util.StringUtils; + +/** + * This is a builder for the {@link HibernateCursorItemReader}. When configuring, one of + * the following should be provided: + *
    + *
  • {@link #queryString(String)}
  • + *
  • {@link #queryName(String)}
  • + *
  • {@link #queryProvider(HibernateQueryProvider)}
  • + *
+ * + * @author Michael Minella + * @since 4.0 + * @see HibernateCursorItemReader + */ +public class HibernateCursorItemReaderBuilder { + + private Map parameterValues; + + private String queryName; + + private int fetchSize; + + private HibernateQueryProvider queryProvider; + + private String queryString; + + private SessionFactory sessionFactory; + + private boolean useStatelessSession; + + private int currentItem; + + private int maxItemCount = Integer.MAX_VALUE; + + private boolean saveState = true; + + private String name; + + /** + * 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 HibernateCursorItemReader#setName(String) + */ + public HibernateCursorItemReaderBuilder name(String name) { + this.name = name; + + 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 HibernateCursorItemReader#setParameterValues(Map) + */ + public HibernateCursorItemReaderBuilder 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 HibernateCursorItemReader#setQueryName(String) + */ + public HibernateCursorItemReaderBuilder queryName(String queryName) { + this.queryName = queryName; + + return this; + } + + /** + * The number of items to be returned with each round trip to the database. Used + * internally by Hibernate. + * + * @param fetchSize number of records to return per fetch + * @return this instance for method chaining + * @see HibernateCursorItemReader#setFetchSize(int) + */ + public HibernateCursorItemReaderBuilder 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 HibernateCursorItemReader#setQueryProvider(HibernateQueryProvider) + */ + public HibernateCursorItemReaderBuilder 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 HibernateCursorItemReader#setQueryString(String) + */ + public HibernateCursorItemReaderBuilder queryString(String queryString) { + this.queryString = queryString; + + return this; + } + + /** + * The Hibernate {@link SessionFactory} to execute the query against. + * + * @param sessionFactory the session factorry + * @return this instance for method chaining + * @see HibernateCursorItemReader#setSessionFactory(SessionFactory) + */ + public HibernateCursorItemReaderBuilder 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 HibernateCursorItemReader#setUseStatelessSession(boolean) + */ + public HibernateCursorItemReaderBuilder useSatelessSession(boolean useStatelessSession) { + this.useStatelessSession = useStatelessSession; + + 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 HibernateCursorItemReader#setCurrentItemCount(int) + */ + public HibernateCursorItemReaderBuilder 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 HibernateCursorItemReader#setMaxItemCount(int) + */ + public HibernateCursorItemReaderBuilder 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 HibernateCursorItemReader#setSaveState(boolean) + */ + public HibernateCursorItemReaderBuilder saveState(boolean saveState) { + this.saveState = saveState; + + return this; + } + + /** + * Returns a fully constructed {@link HibernateCursorItemReader}. + * + * @return a new {@link HibernateCursorItemReader} + */ + public HibernateCursorItemReader build() { + Assert.state(this.fetchSize >= 0, "fetchSize must not be negative"); + Assert.state(this.sessionFactory != null, "A SessionFactory must be provided"); + + if(this.saveState) { + Assert.state(StringUtils.hasText(this.name), + "A name is required when saveState is set to true."); + } + + if (queryProvider == null) { + Assert.state(StringUtils.hasText(queryString) ^ StringUtils.hasText(queryName), + "queryString or queryName must be set"); + } + + HibernateCursorItemReader reader = new HibernateCursorItemReader<>(); + + reader.setFetchSize(this.fetchSize); + reader.setParameterValues(this.parameterValues); + reader.setQueryName(this.queryName); + reader.setQueryProvider(this.queryProvider); + reader.setQueryString(this.queryString); + reader.setSessionFactory(this.sessionFactory); + reader.setUseStatelessSession(this.useStatelessSession); + reader.setCurrentItemCount(this.currentItem); + reader.setMaxItemCount(this.maxItemCount); + reader.setName(this.name); + reader.setSaveState(this.saveState); + + return reader; + } + + } diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/HibernateCursorItemReaderCommonTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/HibernateCursorItemReaderCommonTests.java index 529a5dc03..1584c751c 100644 --- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/HibernateCursorItemReaderCommonTests.java +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/HibernateCursorItemReaderCommonTests.java @@ -32,7 +32,7 @@ public class HibernateCursorItemReaderCommonTests extends AbstractDatabaseItemSt String hsqlQuery = "from Foo"; - HibernateCursorItemReader reader = new HibernateCursorItemReader(); + HibernateCursorItemReader reader = new HibernateCursorItemReader<>(); reader.setQueryString(hsqlQuery); reader.setSessionFactory(sessionFactory); reader.setUseStatelessSession(true); diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/builder/HibernateCursorItemReaderBuilderTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/builder/HibernateCursorItemReaderBuilderTests.java new file mode 100644 index 000000000..f7d44a5f1 --- /dev/null +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/builder/HibernateCursorItemReaderBuilderTests.java @@ -0,0 +1,236 @@ +/* + * 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.HibernateCursorItemReader; +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 static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNull; +import static org.junit.Assert.fail; + +/** + * @author Michael Minella + */ +public class HibernateCursorItemReaderBuilderTests { + + private SessionFactory sessionFactory; + + private ConfigurableApplicationContext context; + + @Before + public void setUp() { + this.context = new AnnotationConfigApplicationContext(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 { + HibernateCursorItemReader reader = new HibernateCursorItemReaderBuilder() + .name("fooReader") + .sessionFactory(this.sessionFactory) + .fetchSize(2) + .currentItem(2) + .maxItemCount(4) + .queryName("allFoos") + .useSatelessSession(true) + .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()); + } + + @Test + public void testConfigurationNoSaveState() throws Exception { + Map parameters = new HashMap<>(); + parameters.put("value", 2); + + HibernateCursorItemReader reader = new HibernateCursorItemReaderBuilder() + .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(); + + + HibernateCursorItemReader reader = new HibernateCursorItemReaderBuilder() + .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().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 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(); + + } + } +} diff --git a/spring-batch-infrastructure/src/test/resources/org/springframework/batch/item/database/Foo.hbm.xml b/spring-batch-infrastructure/src/test/resources/org/springframework/batch/item/database/Foo.hbm.xml index 4c4b7bbbb..2fddd0b1c 100644 --- a/spring-batch-infrastructure/src/test/resources/org/springframework/batch/item/database/Foo.hbm.xml +++ b/spring-batch-infrastructure/src/test/resources/org/springframework/batch/item/database/Foo.hbm.xml @@ -1,7 +1,7 @@ + "http://www.hibernate.org/dtd/hibernate-mapping-3.0.dtd">