From 91a0df5ba71664e3ba1479b4297926c968f21181 Mon Sep 17 00:00:00 2001 From: trisberg Date: Thu, 11 Dec 2008 17:46:59 +0000 Subject: [PATCH] BATCH-965: renamed batch writer to JdbcBatchItemWriter and added named parameter support, added JPA and Hibernate writers --- .../database/BatchSqlUpdateItemWriter.java | 140 ----------- ...ropertyItemSqlParameterSourceProvider.java | 39 +++ .../item/database/HibernateItemWriter.java | 115 +++++++++ .../ItemSqlParameterSourceProvider.java | 35 +++ .../item/database/JdbcBatchItemWriter.java | 205 ++++++++++++++++ .../batch/item/database/JpaItemWriter.java | 115 +++++++++ .../database/HibernateItemWriterTests.java | 118 ++++++++++ ...a => JdbcBatchItemWriterClassicTests.java} | 68 ++++-- ...dbcBatchItemWriterNamedParameterTests.java | 222 ++++++++++++++++++ .../item/database/JpaItemWriterTests.java | 122 ++++++++++ 10 files changed, 1023 insertions(+), 156 deletions(-) delete mode 100644 spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/BatchSqlUpdateItemWriter.java create mode 100644 spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/BeanPropertyItemSqlParameterSourceProvider.java create mode 100644 spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/HibernateItemWriter.java create mode 100644 spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/ItemSqlParameterSourceProvider.java create mode 100644 spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/JdbcBatchItemWriter.java create mode 100644 spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/JpaItemWriter.java create mode 100644 spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/HibernateItemWriterTests.java rename spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/{BatchSqlUpdateItemWriterTests.java => JdbcBatchItemWriterClassicTests.java} (69%) create mode 100644 spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/JdbcBatchItemWriterNamedParameterTests.java create mode 100644 spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/JpaItemWriterTests.java diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/BatchSqlUpdateItemWriter.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/BatchSqlUpdateItemWriter.java deleted file mode 100644 index 9c22172a7..000000000 --- a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/BatchSqlUpdateItemWriter.java +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Copyright 2006-2007 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; - -import java.sql.PreparedStatement; -import java.sql.SQLException; -import java.util.List; - -import org.springframework.batch.item.ItemWriter; -import org.springframework.beans.factory.InitializingBean; -import org.springframework.dao.DataAccessException; -import org.springframework.dao.EmptyResultDataAccessException; -import org.springframework.jdbc.core.JdbcOperations; -import org.springframework.jdbc.core.PreparedStatementCallback; -import org.springframework.util.Assert; - -/** - * {@link ItemWriter} that uses the batching features from - * {@link PreparedStatement} if available and can take some rudimentary steps to - * locate a failure during a flush, and identify the items that failed. When one - * of those items is encountered again the batch is flushed aggressively so that - * the bad item is eventually identified and can be dealt with in - * isolation.
- * - * The user must provide an SQL query and a special callback - * {@link ItemPreparedStatementSetter}, which is responsible for mapping the - * item to a PreparedStatement.
- * - * It is expected that {@link #write(List)} is called inside a transaction.
- * - * The writer is thread safe after its properties are set (normal singleton - * behaviour), so it can be used to write in multiple concurrent transactions. - * Note, however, that the set of failed items is stored in a collection - * internally, and this collection is never cleared, so it is not a great idea - * to go on using the writer indefinitely. Normally it would be used for the - * duration of a batch job and then discarded. - * - * @author Dave Syer - * - */ -public class BatchSqlUpdateItemWriter implements ItemWriter, InitializingBean { - - private JdbcOperations jdbcTemplate; - - private ItemPreparedStatementSetter preparedStatementSetter; - - private String sql; - - private boolean assertUpdates = true; - - /** - * Public setter for the flag that determines whether an assertion is made - * that all items cause at least one row to be updated. - * @param assertUpdates the flag to set. Defaults to true; - */ - public void setAssertUpdates(boolean assertUpdates) { - this.assertUpdates = assertUpdates; - } - - /** - * Public setter for the query string to execute on write. The parameters - * should correspond to those known to the - * {@link ItemPreparedStatementSetter}. - * @param sql the query to set - */ - public void setSql(String sql) { - this.sql = sql; - } - - /** - * Public setter for the {@link ItemPreparedStatementSetter}. - * @param preparedStatementSetter the {@link ItemPreparedStatementSetter} to - * set - */ - public void setItemPreparedStatementSetter(ItemPreparedStatementSetter preparedStatementSetter) { - this.preparedStatementSetter = preparedStatementSetter; - } - - /** - * Public setter for the {@link JdbcOperations}. - * @param jdbcTemplate the {@link JdbcOperations} to set - */ - public void setJdbcTemplate(JdbcOperations jdbcTemplate) { - this.jdbcTemplate = jdbcTemplate; - } - - /** - * Check mandatory properties - there must be a delegate. - */ - public void afterPropertiesSet() throws Exception { - Assert.notNull(jdbcTemplate, "BatchSqlUpdateItemWriter requires an data source."); - Assert.notNull(preparedStatementSetter, "BatchSqlUpdateItemWriter requires a ItemPreparedStatementSetter"); - } - - /* (non-Javadoc) - * @see org.springframework.batch.item.ItemWriter#write(java.util.List) - */ - public void write(final List items) throws Exception { - - if (!items.isEmpty()) { - - int[] values = (int[]) jdbcTemplate.execute(sql, new PreparedStatementCallback() { - public Object doInPreparedStatement(PreparedStatement ps) throws SQLException, DataAccessException { - - for (T item : items) { - preparedStatementSetter.setValues(item, ps); - ps.addBatch(); - } - return ps.executeBatch(); - } - }); - - if (assertUpdates) { - for (int i = 0; i < values.length; i++) { - int value = values[i]; - if (value == 0) { - throw new EmptyResultDataAccessException("Item " + i + " of " + values.length - + " did not update any rows: [" + items.get(i) + "]", 1); - } - } - } - - } - - } - -} diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/BeanPropertyItemSqlParameterSourceProvider.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/BeanPropertyItemSqlParameterSourceProvider.java new file mode 100644 index 000000000..3710b4182 --- /dev/null +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/BeanPropertyItemSqlParameterSourceProvider.java @@ -0,0 +1,39 @@ +/* + * Copyright 2006-2008 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; + +import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource; +import org.springframework.jdbc.core.namedparam.SqlParameterSource; + +/** + * A convenient implementation for providing BeanPropertySqlParameterSource when the item has JavaBean properties + * that correspond to names used for parameters in the SQL statement. + * + * @author Thomas Risberg + * @since 2.0 + */ +public class BeanPropertyItemSqlParameterSourceProvider implements ItemSqlParameterSourceProvider { + + /** + * Provide parameter values in an {@link BeanPropertySqlParameterSource} based on values from + * the provided item. + * @param item the item to use for parameter values + */ + public SqlParameterSource createSqlParameterSource(T item) { + return new BeanPropertySqlParameterSource(item); + } + +} diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/HibernateItemWriter.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/HibernateItemWriter.java new file mode 100644 index 000000000..db3f81f77 --- /dev/null +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/HibernateItemWriter.java @@ -0,0 +1,115 @@ +/* + * Copyright 2006-2007 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; + +import java.util.List; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.hibernate.SessionFactory; +import org.springframework.batch.item.ItemWriter; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.orm.hibernate3.HibernateOperations; +import org.springframework.orm.hibernate3.HibernateTemplate; +import org.springframework.util.Assert; + +/** + * {@link ItemWriter} that uses a Hibernate session to save or update entities + * that are not part of the current Hibernate session. It will also flush + * and clear the session at chunk boundaries.

+ * + * The writer is thread safe after its properties are set (normal singleton + * behaviour), so it can be used to write in multiple concurrent transactions. + * + * @author Dave Syer + * @author Thomas Risberg + * + */ +public class HibernateItemWriter implements ItemWriter, InitializingBean { + + protected static final Log logger = LogFactory.getLog(HibernateItemWriter.class); + + private HibernateOperations hibernateTemplate; + + /** + * Public setter for the {@link HibernateOperations} property. + * + * @param hibernateTemplate the hibernateTemplate to set + */ + public void setHibernateTemplate(HibernateOperations hibernateTemplate) { + this.hibernateTemplate = hibernateTemplate; + } + + /** + * Set the Hibernate SessionFactory to be used internally. Will + * automatically create a HibernateTemplate for the given SessionFactory. + * + * @see #setHibernateTemplate + */ + public final void setSessionFactory(SessionFactory sessionFactory) { + this.hibernateTemplate = new HibernateTemplate(sessionFactory); + } + + /** + * Check mandatory properties - there must be a hibernateTemplate. + */ + public void afterPropertiesSet() throws Exception { + Assert.notNull(hibernateTemplate, "HibernateItemWriter requires a HibernateOperations"); + } + + /** + * Save or update any entities not in the current hibernate session and then flush and + * clear the hibernate session. + * + * @see org.springframework.batch.item.ItemWriter#write(java.util.List) + */ + public final void write(List items) throws Exception { + doWrite(hibernateTemplate, items); + try { + hibernateTemplate.flush(); + } + finally { + // This should happen when the transaction commits anyway, but to be + // sure... + hibernateTemplate.clear(); + } + } + + /** + * Do perform the actual write operation. This can be overridden in a subclass if necessary. + * + * @param hibernateTemplate the HibernateTemplate to use for the operation + * @param items the list of items to use for the write + */ + protected void doWrite(HibernateOperations hibernateTemplate, List items) { + + if (!items.isEmpty()) { + long saveOrUpdateCount = 0; + for (T item : items) { + if (!hibernateTemplate.contains(item)) { + hibernateTemplate.saveOrUpdate(item); + saveOrUpdateCount++; + } + } + if (logger.isDebugEnabled()) { + logger.debug(saveOrUpdateCount + " entities saved/updated."); + logger.debug((items.size() - saveOrUpdateCount) + " entities found in session."); + } + } + + } + +} diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/ItemSqlParameterSourceProvider.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/ItemSqlParameterSourceProvider.java new file mode 100644 index 000000000..b0d30045e --- /dev/null +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/ItemSqlParameterSourceProvider.java @@ -0,0 +1,35 @@ +/* + * Copyright 2006-2008 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; + +import org.springframework.jdbc.core.namedparam.SqlParameterSource; + +/** + * A convenient strategy for providing SqlParameterSource for named parameter SQL updates. + * + * @author Thomas Risberg + * @since 2.0 + */ +public interface ItemSqlParameterSourceProvider { + + /** + * Provide parameter values in an {@link SqlParameterSource} based on values from + * the provided item. + * @param item the item to use for parameter values + */ + SqlParameterSource createSqlParameterSource(T item); + +} diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/JdbcBatchItemWriter.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/JdbcBatchItemWriter.java new file mode 100644 index 000000000..277abe320 --- /dev/null +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/JdbcBatchItemWriter.java @@ -0,0 +1,205 @@ +/* + * Copyright 2006-2008 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; + +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.ArrayList; +import java.util.List; + +import javax.sql.DataSource; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.batch.item.ItemWriter; +import org.springframework.batch.item.database.support.JdbcParameterUtils; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.dao.DataAccessException; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.dao.InvalidDataAccessApiUsageException; +import org.springframework.jdbc.core.JdbcOperations; +import org.springframework.jdbc.core.PreparedStatementCallback; +import org.springframework.jdbc.core.namedparam.SqlParameterSource; +import org.springframework.jdbc.core.simple.SimpleJdbcOperations; +import org.springframework.jdbc.core.simple.SimpleJdbcTemplate; +import org.springframework.util.Assert; + +/** + * {@link ItemWriter} that uses the batching features from + * {@link SimpleJdbcTemplate} to execute a batch of statements for all items + * provided.
+ * + * The user must provide an SQL query and a special callback in the for of either + * {@link ItemPreparedStatementSetter}, or a {@link ItemSqlParameterSourceProvider}. + * You can use either named parameters or the traditional '?' placeholders. If you use the + * named parameter support then you should provide a {@link ItemSqlParameterSourceProvider}, + * otherwise you should provide a {@link ItemPreparedStatementSetter}. + * This callback would be responsible for mapping the item to the parameters needed to + * execute the SQL statement.
+ * + * It is expected that {@link #write(List)} is called inside a transaction.
+ * + * The writer is thread safe after its properties are set (normal singleton + * behaviour), so it can be used to write in multiple concurrent transactions. + * + * @author Dave Syer + * @author Thomas Risberg + * @since 2.0 + */ +public class JdbcBatchItemWriter implements ItemWriter, InitializingBean { + + protected static final Log logger = LogFactory.getLog(JdbcBatchItemWriter.class); + + private SimpleJdbcOperations simpleJdbcTemplate; + + private ItemPreparedStatementSetter itemPreparedStatementSetter; + + private ItemSqlParameterSourceProvider itemSqlParameterSourceProvider; + + private String sql; + + private boolean assertUpdates = true; + + private int parameterCount; + + private boolean usingNamedParameters; + + /** + * Public setter for the flag that determines whether an assertion is made + * that all items cause at least one row to be updated. + * @param assertUpdates the flag to set. Defaults to true; + */ + public void setAssertUpdates(boolean assertUpdates) { + this.assertUpdates = assertUpdates; + } + + /** + * Public setter for the query string to execute on write. The parameters + * should correspond to those known to the + * {@link ItemPreparedStatementSetter}. + * @param sql the query to set + */ + public void setSql(String sql) { + this.sql = sql; + } + + /** + * Public setter for the {@link ItemPreparedStatementSetter}. + * @param preparedStatementSetter the {@link ItemPreparedStatementSetter} to + * set. This is required when using traditional '?' placeholders for the SQL statement. + */ + public void setItemPreparedStatementSetter(ItemPreparedStatementSetter preparedStatementSetter) { + this.itemPreparedStatementSetter = preparedStatementSetter; + } + + /** + * Public setter for the {@link ItemSqlParameterSourceProvider}. + * @param itemSqlParameterSourceProvider the {@link ItemSqlParameterSourceProvider} to + * set. This is required when using named parameters for the SQL statement. + */ + public void setItemSqlParameterSourceProvider(ItemSqlParameterSourceProvider itemSqlParameterSourceProvider) { + this.itemSqlParameterSourceProvider = itemSqlParameterSourceProvider; + } + + /** + * Public setter for the data source for injection purposes. + * + * @param dataSource + */ + public void setDataSource(DataSource dataSource) { + if (simpleJdbcTemplate == null) { + this.simpleJdbcTemplate = new SimpleJdbcTemplate(dataSource); + } + } + + /** + * Public setter for the {@link JdbcOperations}. + * @param simpleJdbcTemplate the {@link JdbcOperations} to set + */ + public void setSimpleJdbcTemplate(SimpleJdbcOperations simpleJdbcTemplate) { + this.simpleJdbcTemplate = simpleJdbcTemplate; + } + + /** + * Check mandatory properties - there must be a delegate. + */ + public void afterPropertiesSet() throws Exception { + Assert.notNull(simpleJdbcTemplate, "A DataSource or a SimpleJdbcTemplate is required."); + Assert.notNull(sql, "An SQL statement is required."); + List namedParameters = new ArrayList(); + parameterCount = JdbcParameterUtils.countParameterPlaceholders(sql, namedParameters); + if (namedParameters.size() > 0) { + if (parameterCount != namedParameters.size()) { + throw new InvalidDataAccessApiUsageException("You can't use both named parameters and classic \"?\" placeholders: " + sql); + } + usingNamedParameters = true; + } + if (usingNamedParameters) { + Assert.notNull(itemSqlParameterSourceProvider, "Using SQL statement with named parameters requires an ItemSqlParameterSourceProvider"); + } + else { + Assert.notNull(itemPreparedStatementSetter, "Using SQL statement with '?' placeholders requires an ItemPreparedStatementSetter"); + } + } + + /* (non-Javadoc) + * @see org.springframework.batch.item.ItemWriter#write(java.util.List) + */ + public void write(final List items) throws Exception { + + if (!items.isEmpty()) { + + if (logger.isDebugEnabled()) { + logger.debug("Executing batch with " + items.size() + " items."); + } + + int[] values = null; + + if (usingNamedParameters) { + SqlParameterSource[] batchArgs = new SqlParameterSource[items.size()]; + int i = 0; + for (T item : items) { + batchArgs[i++] = itemSqlParameterSourceProvider.createSqlParameterSource(item); + } + values = simpleJdbcTemplate.batchUpdate(sql, batchArgs); + } + else { + values = (int[]) simpleJdbcTemplate.getJdbcOperations().execute(sql, new PreparedStatementCallback() { + public Object doInPreparedStatement(PreparedStatement ps) throws SQLException, DataAccessException { + for (T item : items) { + itemPreparedStatementSetter.setValues(item, ps); + ps.addBatch(); + } + return ps.executeBatch(); + } + }); + } + + if (assertUpdates) { + for (int i = 0; i < values.length; i++) { + int value = values[i]; + if (value == 0) { + throw new EmptyResultDataAccessException("Item " + i + " of " + values.length + + " did not update any rows: [" + items.get(i) + "]", 1); + } + } + } + + } + + } + +} diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/JpaItemWriter.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/JpaItemWriter.java new file mode 100644 index 000000000..117766d70 --- /dev/null +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/JpaItemWriter.java @@ -0,0 +1,115 @@ +/* + * Copyright 2006-2008 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; + +import java.util.List; + +import javax.persistence.EntityManager; +import javax.persistence.EntityManagerFactory; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.batch.item.ItemWriter; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.dao.DataAccessResourceFailureException; +import org.springframework.orm.jpa.EntityManagerFactoryUtils; +import org.springframework.util.Assert; + +/** + * {@link org.springframework.batch.item.ItemWriter} that is using a JPA + * EntityManagerFactory to merge any Entities that aren't part of the + * persistence context. + * + * It is required that {@link #write(List)} is called inside a transaction.
+ * + * The reader must be configured with an + * {@link javax.persistence.EntityManagerFactory} that is capable of + * participating in Spring managed transactions. + * + * The writer is thread safe after its properties are set (normal singleton + * behaviour), so it can be used to write in multiple concurrent transactions. + * + * @author Thomas Risberg + * + */ +public class JpaItemWriter implements ItemWriter, InitializingBean { + + protected static final Log logger = LogFactory.getLog(JpaItemWriter.class); + + private EntityManagerFactory entityManagerFactory; + + /** + * Set the EntityManager to be used internally. + * + * @param entityManagerFactory the entityManagerFactory to set + */ + public void setEntityManagerFactory(EntityManagerFactory entityManagerFactory) { + this.entityManagerFactory = entityManagerFactory; + } + + /** + * Check mandatory properties - there must be an entityManagerFactory. + */ + public void afterPropertiesSet() throws Exception { + Assert.notNull(entityManagerFactory, "An EntityManagerFactory is required"); + } + + /** + * Merge all provided items that aren't already in the persistence context + * and then flush and clear the entity manager. + * + * @see org.springframework.batch.item.ItemWriter#write(java.util.List) + */ + public final void write(List items) throws Exception { + EntityManager entityManager = EntityManagerFactoryUtils.getTransactionalEntityManager(entityManagerFactory); + if (entityManager == null) { + throw new DataAccessResourceFailureException("Unable to obtain a transactional EntityManager"); + } + doWrite(entityManager, items); + try { + entityManager.flush(); + } + finally { + entityManager.clear(); + } + } + + /** + * Do perform the actual write operation. This can be overridden in a subclass if necessary. + * + * @param entityManager the EntityManager to use for the operation + * @param items the list of items to use for the write + */ + protected void doWrite(EntityManager entityManager, List items) { + + if (!items.isEmpty()) { + long mergeCount = 0; + for (T item : items) { + if (!entityManager.contains(item)) { + entityManager.merge(item); + mergeCount++; + } + } + if (logger.isDebugEnabled()) { + logger.debug(mergeCount + " entities merged."); + logger.debug((items.size() - mergeCount) + " entities found in persistence context."); + } + } + + } + +} diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/HibernateItemWriterTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/HibernateItemWriterTests.java new file mode 100644 index 000000000..24f4287c6 --- /dev/null +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/HibernateItemWriterTests.java @@ -0,0 +1,118 @@ +/* + * Copyright 2006-2008 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; + +import static org.easymock.EasyMock.*; +import static org.junit.Assert.*; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.batch.repeat.support.RepeatSynchronizationManager; +import org.springframework.orm.hibernate3.HibernateOperations; + +/** + * @author Dave Syer + * @author Thomas Risberg + */ +public class HibernateItemWriterTests { + + HibernateOperations ht; + + HibernateItemWriter writer; + + @Before + public void setUp() throws Exception { + writer = new HibernateItemWriter(); + ht = createMock("ht", HibernateOperations.class); + writer.setHibernateTemplate(ht); + } + + @After + public void tearDown() throws Exception { + RepeatSynchronizationManager.clear(); + } + + /** + * Test method for + * {@link org.springframework.batch.item.database.HibernateItemWriter#afterPropertiesSet()} + * + * @throws Exception + */ + @Test + public void testAfterPropertiesSet() throws Exception { + writer = new HibernateItemWriter(); + try { + writer.afterPropertiesSet(); + fail("Expected IllegalArgumentException"); + } + catch (IllegalArgumentException e) { + // expected + assertTrue("Wrong message for exception: " + e.getMessage(), e.getMessage().indexOf("HibernateOperations") >= 0); + } + } + + /** + * Test method for + * {@link org.springframework.batch.item.database.HibernateItemWriter#afterPropertiesSet()} + * + * @throws Exception + */ + @Test + public void testAfterPropertiesSetWithDelegate() throws Exception { + writer.afterPropertiesSet(); + } + + @Test + public void testWriteAndFlushSunnyDay() throws Exception { + ht.contains("foo"); + expectLastCall().andReturn(true); + ht.contains("bar"); + expectLastCall().andReturn(false); + ht.saveOrUpdate("bar"); + ht.flush(); + ht.clear(); + replay(ht); + + List items = Arrays.asList(new String[] { "foo", "bar" }); + writer.write(items); + + verify(ht); + } + + @Test + public void testWriteAndFlushWithFailure() throws Exception { + final RuntimeException ex = new RuntimeException("ERROR"); + ht.contains("foo"); + expectLastCall().andThrow(ex); + replay(ht); + + try { + writer.write(Collections.singletonList("foo")); + fail("Expected RuntimeException"); + } + catch (RuntimeException e) { + assertEquals("ERROR", e.getMessage()); + } + + verify(ht); + } + +} diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/BatchSqlUpdateItemWriterTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/JdbcBatchItemWriterClassicTests.java similarity index 69% rename from spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/BatchSqlUpdateItemWriterTests.java rename to spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/JdbcBatchItemWriterClassicTests.java index a62a9f424..2962c8b6a 100644 --- a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/BatchSqlUpdateItemWriterTests.java +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/JdbcBatchItemWriterClassicTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2006-2007 the original author or authors. + * Copyright 2006-2008 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. @@ -15,11 +15,8 @@ */ package org.springframework.batch.item.database; -import static org.easymock.EasyMock.createMock; -import static org.easymock.EasyMock.expect; -import static org.easymock.EasyMock.expectLastCall; -import static org.easymock.EasyMock.replay; -import static org.easymock.EasyMock.verify; +import static org.junit.Assert.*; +import static org.easymock.EasyMock.*; import java.sql.PreparedStatement; import java.sql.SQLException; @@ -27,22 +24,24 @@ import java.util.ArrayList; import java.util.Collections; import java.util.List; -import junit.framework.TestCase; - +import org.junit.After; +import org.junit.Before; +import org.junit.Test; import org.springframework.batch.repeat.support.RepeatSynchronizationManager; import org.springframework.dao.DataAccessException; import org.springframework.dao.EmptyResultDataAccessException; import org.springframework.jdbc.UncategorizedSQLException; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.jdbc.core.PreparedStatementCallback; +import org.springframework.jdbc.core.simple.SimpleJdbcTemplate; /** * @author Dave Syer - * + * @author Thomas Risberg */ -public class BatchSqlUpdateItemWriterTests extends TestCase { +public class JdbcBatchItemWriterClassicTests { - private BatchSqlUpdateItemWriter writer = new BatchSqlUpdateItemWriter(); + private JdbcBatchItemWriter writer = new JdbcBatchItemWriter(); private JdbcTemplate jdbcTemplate; @@ -55,7 +54,8 @@ public class BatchSqlUpdateItemWriterTests extends TestCase { * * @see junit.framework.TestCase#setUp() */ - protected void setUp() throws Exception { + @Before + public void setUp() throws Exception { ps = createMock(PreparedStatement.class); jdbcTemplate = new JdbcTemplate() { public Object execute(String sql, PreparedStatementCallback action) throws DataAccessException { @@ -69,12 +69,13 @@ public class BatchSqlUpdateItemWriterTests extends TestCase { } }; writer.setSql("SQL"); - writer.setJdbcTemplate(jdbcTemplate); + writer.setSimpleJdbcTemplate(new SimpleJdbcTemplate(jdbcTemplate)); writer.setItemPreparedStatementSetter(new ItemPreparedStatementSetter() { public void setValues(String item, PreparedStatement ps) throws SQLException { list.add(item); } }); + writer.afterPropertiesSet(); } /* @@ -82,27 +83,60 @@ public class BatchSqlUpdateItemWriterTests extends TestCase { * * @see junit.framework.TestCase#tearDown() */ - protected void tearDown() throws Exception { + @After + public void tearDown() throws Exception { RepeatSynchronizationManager.clear(); } /** * Test method for - * {@link org.springframework.batch.item.database.BatchSqlUpdateItemWriter#afterPropertiesSet()} + * {@link org.springframework.batch.item.database.JdbcBatchItemWriter#afterPropertiesSet()} * . * @throws Exception */ + @Test public void testAfterPropertiesSet() throws Exception { + writer = new JdbcBatchItemWriter(); try { writer.afterPropertiesSet(); + fail("Expected IllegalArgumentException"); + } + catch (IllegalArgumentException e) { + // expected + String message = e.getMessage(); + assertTrue("Message does not contain 'SimpleJdbcTemplate'.", message.indexOf("SimpleJdbcTemplate") >= 0); + } + writer.setSimpleJdbcTemplate(new SimpleJdbcTemplate(jdbcTemplate)); + try { + writer.afterPropertiesSet(); + fail("Expected IllegalArgumentException"); } catch (IllegalArgumentException e) { // expected String message = e.getMessage().toLowerCase(); - assertTrue("Message does not contain 'query'.", message.indexOf("query") >= 0); + assertTrue("Message does not contain 'sql'.", message.indexOf("sql") >= 0); } + writer.setSql("select * from foo where id = ?"); + try { + writer.afterPropertiesSet(); + fail("Expected IllegalArgumentException"); + } + catch (IllegalArgumentException e) { + // expected + String message = e.getMessage(); + assertTrue("Message does not contain 'ItemPreparedStatementSetter'.", message.indexOf("ItemPreparedStatementSetter") >= 0); + } + writer.setItemPreparedStatementSetter( + new ItemPreparedStatementSetter() { + public void setValues(String item, PreparedStatement ps) + throws SQLException { + } + + }); + writer.afterPropertiesSet(); } + @Test public void testWriteAndFlush() throws Exception { ps.addBatch(); expectLastCall(); @@ -113,6 +147,7 @@ public class BatchSqlUpdateItemWriterTests extends TestCase { assertTrue(list.contains("SQL")); } + @Test public void testWriteAndFlushWithEmptyUpdate() throws Exception { ps.addBatch(); expectLastCall(); @@ -131,6 +166,7 @@ public class BatchSqlUpdateItemWriterTests extends TestCase { assertTrue(list.contains("SQL")); } + @Test public void testWriteAndFlushWithFailure() throws Exception { final RuntimeException ex = new RuntimeException("bar"); writer.setItemPreparedStatementSetter(new ItemPreparedStatementSetter() { diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/JdbcBatchItemWriterNamedParameterTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/JdbcBatchItemWriterNamedParameterTests.java new file mode 100644 index 000000000..dacd4da50 --- /dev/null +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/JdbcBatchItemWriterNamedParameterTests.java @@ -0,0 +1,222 @@ +/* + * Copyright 2006-2008 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; + +import static org.junit.Assert.*; +import static org.easymock.EasyMock.*; + +import java.util.Collections; + +import org.easymock.EasyMock; +import org.easymock.IArgumentMatcher; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.batch.repeat.support.RepeatSynchronizationManager; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.namedparam.BeanPropertySqlParameterSource; +import org.springframework.jdbc.core.namedparam.SqlParameterSource; +import org.springframework.jdbc.core.simple.SimpleJdbcOperations; + +/** + * @author Thomas Risberg + */ +public class JdbcBatchItemWriterNamedParameterTests { + + private JdbcBatchItemWriter writer = new JdbcBatchItemWriter(); + + private SimpleJdbcOperations sjt; + + private String sql = "update foo set bar = :bar where id = :id"; + + private class Foo { + private Long id; + private String bar; + + public Foo(String bar) { + this.id = 1L; + this.bar = bar; + } + + public Long getId() { + return id; + } + + public void setId(Long id) { + this.id = id; + } + + public String getBar() { + return bar; + } + + public void setBar(String bar) { + this.bar = bar; + } + + } + + /* + * (non-Javadoc) + * + * @see junit.framework.TestCase#setUp() + */ + @Before + public void setUp() throws Exception { + sjt = createMock(SimpleJdbcOperations.class); + writer.setSql(sql); + writer.setSimpleJdbcTemplate(sjt); + writer.setItemSqlParameterSourceProvider( + new BeanPropertyItemSqlParameterSourceProvider()); + writer.afterPropertiesSet(); + } + + /* + * (non-Javadoc) + * + * @see junit.framework.TestCase#tearDown() + */ + @After + public void tearDown() throws Exception { + RepeatSynchronizationManager.clear(); + } + + /** + * Test method for + * {@link org.springframework.batch.item.database.JdbcBatchItemWriter#afterPropertiesSet()} + * . + * @throws Exception + */ + @Test + public void testAfterPropertiesSet() throws Exception { + writer = new JdbcBatchItemWriter(); + try { + writer.afterPropertiesSet(); + fail("Expected IllegalArgumentException"); + } + catch (IllegalArgumentException e) { + // expected + String message = e.getMessage(); + assertTrue("Message does not contain 'SimpleJdbcTemplate'.", message.indexOf("SimpleJdbcTemplate") >= 0); + } + writer.setSimpleJdbcTemplate(sjt); + try { + writer.afterPropertiesSet(); + fail("Expected IllegalArgumentException"); + } + catch (IllegalArgumentException e) { + // expected + String message = e.getMessage().toLowerCase(); + assertTrue("Message does not contain 'sql'.", message.indexOf("sql") >= 0); + } + writer.setSql("select * from foo where id = :id"); + try { + writer.afterPropertiesSet(); + fail("Expected IllegalArgumentException"); + } + catch (IllegalArgumentException e) { + // expected + String message = e.getMessage(); + assertTrue("Message does not contain 'ItemSqlParameterSourceProvider'.", message.indexOf("ItemSqlParameterSourceProvider") >= 0); + } + writer.setItemSqlParameterSourceProvider( + new BeanPropertyItemSqlParameterSourceProvider()); + writer.afterPropertiesSet(); + } + + @Test + public void testWriteAndFlush() throws Exception { + expect(sjt.batchUpdate(eq(sql), + eqSqlParameterSourceArray(new SqlParameterSource[] {new BeanPropertySqlParameterSource(new Foo("bar"))}))) + .andReturn(new int[] {1}); + replay(sjt); + writer.write(Collections.singletonList(new Foo("bar"))); + verify(sjt); + } + + @Test + public void testWriteAndFlushWithEmptyUpdate() throws Exception { + expect(sjt.batchUpdate(eq(sql), + eqSqlParameterSourceArray(new SqlParameterSource[] {new BeanPropertySqlParameterSource(new Foo("bar"))}))) + .andReturn(new int[] {0}); + replay(sjt); + try { + writer.write(Collections.singletonList(new Foo("bar"))); + fail("Expected EmptyResultDataAccessException"); + } + catch (EmptyResultDataAccessException e) { + // expected + String message = e.getMessage(); + assertTrue("Wrong message: " + message, message.indexOf("did not update") >= 0); + } + verify(sjt); + } + + @Test + public void testWriteAndFlushWithFailure() throws Exception { + final RuntimeException ex = new RuntimeException("ERROR"); + expect(sjt.batchUpdate(eq(sql), + eqSqlParameterSourceArray(new SqlParameterSource[] {new BeanPropertySqlParameterSource(new Foo("bar"))}))) + .andThrow(ex); + replay(sjt); + try { + writer.write(Collections.singletonList(new Foo("bar"))); + fail("Expected RuntimeException"); + } + catch (RuntimeException e) { + assertEquals("ERROR", e.getMessage()); + } + verify(sjt); + } + + public static SqlParameterSource[] eqSqlParameterSourceArray(SqlParameterSource[] in) { + EasyMock.reportMatcher(new SqlParameterSourceArrayEquals(in)); + return null; + } + + public static class SqlParameterSourceArrayEquals implements IArgumentMatcher { + private SqlParameterSource[] expected; + + public SqlParameterSourceArrayEquals(SqlParameterSource[] expected) { + this.expected = expected; + } + + public boolean matches(Object actual) { + if (!(actual instanceof SqlParameterSource[])) { + return false; + } + SqlParameterSource[] actualArray = (SqlParameterSource[])actual; + if (expected.length != actualArray.length) { + return false; + } + for (int i = 0; i < expected.length; i++) { + if (!expected[i].getClass().equals(actualArray[i].getClass())) { + return false; + } + } + return true; + } + + public void appendTo(StringBuffer buffer) { + buffer.append("eqSqlParameterSourceArray("); + buffer.append(expected.getClass().getName()); + buffer.append(" with length \""); + buffer.append(expected.length); + buffer.append("\")"); + + } + } +} diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/JpaItemWriterTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/JpaItemWriterTests.java new file mode 100644 index 000000000..c70cbbbfc --- /dev/null +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/JpaItemWriterTests.java @@ -0,0 +1,122 @@ +/* + * Copyright 2006-2008 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; + +import static org.easymock.EasyMock.createMock; +import static org.easymock.EasyMock.expectLastCall; +import static org.easymock.EasyMock.replay; +import static org.easymock.EasyMock.verify; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import java.util.Arrays; +import java.util.List; + +import javax.persistence.EntityManager; +import javax.persistence.EntityManagerFactory; + +import org.junit.Before; +import org.junit.Test; +import org.springframework.orm.jpa.EntityManagerHolder; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +/** + * @author Thomas Risberg + * + */ +public class JpaItemWriterTests { + + EntityManagerFactory emf; + + JpaItemWriter writer; + + @Before + public void setUp() throws Exception { + if (TransactionSynchronizationManager.isSynchronizationActive()) { + TransactionSynchronizationManager.clearSynchronization(); + } + writer = new JpaItemWriter(); + emf = createMock("emf", EntityManagerFactory.class); + writer.setEntityManagerFactory(emf); + } + + @Test + public void testAfterPropertiesSet() throws Exception { + writer = new JpaItemWriter(); + try { + writer.afterPropertiesSet(); + fail("Expected IllegalArgumentException"); + } + catch (IllegalArgumentException e) { + // expected + assertTrue("Wrong message for exception: " + e.getMessage(), + e.getMessage().indexOf("EntityManagerFactory") >= 0); + } + } + + @Test + public void testWriteAndFlushSunnyDay() throws Exception { + EntityManager em = createMock("em", EntityManager.class); + em.contains("foo"); + expectLastCall().andReturn(true); + em.contains("bar"); + expectLastCall().andReturn(false); + em.merge("bar"); + expectLastCall().andReturn("bar"); + em.flush(); + em.clear(); + replay(em); + replay(emf); + TransactionSynchronizationManager.bindResource(emf, new EntityManagerHolder(em)); + + List items = Arrays.asList(new String[] { "foo", "bar" }); + + writer.write(items); + + verify(em); + TransactionSynchronizationManager.unbindResource(emf); + } + + @Test + public void testWriteAndFlushWithFailure() throws Exception { + final RuntimeException ex = new RuntimeException("ERROR"); + EntityManager em = createMock("em", EntityManager.class); + em.contains("foo"); + expectLastCall().andReturn(true); + em.contains("bar"); + expectLastCall().andReturn(false); + em.merge("bar"); + expectLastCall().andThrow(ex); + replay(em); + replay(emf); + TransactionSynchronizationManager.bindResource(emf, new EntityManagerHolder(em)); + List items = Arrays.asList(new String[] { "foo", "bar" }); + + try { + writer.write(items); + fail("Expected RuntimeException"); + } + catch (RuntimeException e) { + assertEquals("ERROR", e.getMessage()); + } + + verify(em); + TransactionSynchronizationManager.unbindResource(emf); + } + +}