From d1ec2cfa9ea197e074fd8f7b3b13255c9e2b8ee5 Mon Sep 17 00:00:00 2001 From: trisberg Date: Thu, 11 Dec 2008 21:24:51 +0000 Subject: [PATCH] BATCH-958: added an IbatisBatchItemWriter --- .../item/database/IbatisBatchItemWriter.java | 160 +++++++++++++ .../database/IbatisBatchItemWriterTests.java | 222 ++++++++++++++++++ 2 files changed, 382 insertions(+) create mode 100644 spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/IbatisBatchItemWriter.java create mode 100644 spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/IbatisBatchItemWriterTests.java diff --git a/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/IbatisBatchItemWriter.java b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/IbatisBatchItemWriter.java new file mode 100644 index 000000000..4dee0dc7e --- /dev/null +++ b/spring-batch-infrastructure/src/main/java/org/springframework/batch/item/database/IbatisBatchItemWriter.java @@ -0,0 +1,160 @@ +/* + * 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.SQLException; +import java.util.List; + +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.EmptyResultDataAccessException; +import org.springframework.dao.InvalidDataAccessResourceUsageException; +import org.springframework.orm.ibatis.SqlMapClientCallback; +import org.springframework.orm.ibatis.SqlMapClientOperations; +import org.springframework.orm.ibatis.SqlMapClientTemplate; +import org.springframework.util.Assert; + +import com.ibatis.sqlmap.client.SqlMapClient; +import com.ibatis.sqlmap.client.SqlMapExecutor; +import com.ibatis.sqlmap.engine.execution.BatchException; +import com.ibatis.sqlmap.engine.execution.BatchResult; + +/** + * {@link ItemWriter} that uses the batching features from + * {@link SqlMapClientTemplate} to execute a batch of statements for all items + * provided.
+ * + * The user must provide an iBATIS statement id that points to the SQL statement defined + * in the iBATIS SqlMap configuration.
+ * + * It is expected that {@link #write(List)} is called inside a transaction.
+ * + * The writer is thread safe after its properties are set (normal singleton + * behavior), so it can be used to write in multiple concurrent transactions. + * + * @author Thomas Risberg + * @since 2.0 + */ +public class IbatisBatchItemWriter implements ItemWriter, InitializingBean { + + protected static final Log logger = LogFactory.getLog(IbatisBatchItemWriter.class); + + private SqlMapClientTemplate sqlMapClientTemplate; + + private String statementId; + + 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 {@link SqlMapClient} for injection purposes. + * + * @param sqlMapClient the SqlMapClient + */ + public void setSqlMapClient(SqlMapClient sqlMapClient) { + if (sqlMapClientTemplate == null) { + this.sqlMapClientTemplate = new SqlMapClientTemplate(sqlMapClient); + } + } + + /** + * Public setter for the {@link SqlMapClientOperations}. + * + * @param sqlMapClientTemplate the SqlMapClientOperations + */ + public void setSqlMapClientTemplate(SqlMapClientTemplate sqlMapClientTemplate) { + this.sqlMapClientTemplate = sqlMapClientTemplate; + } + + /** + * Public setter for the statement id identifying the statement in the SqlMap + * configuration file. + * + * @param statementId the id for the statement + */ + public void setStatementId(String statementId) { + this.statementId = statementId; + } + + /** + * Check mandatory properties - there must be an SqlMapCLient and a statementId. + */ + public void afterPropertiesSet() throws Exception { + Assert.notNull(sqlMapClientTemplate, "A SqlMapClient or a SqlMapClientTemplate is required."); + Assert.notNull(statementId, "A statementId is required."); + } + + /* (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."); + } + + @SuppressWarnings("unchecked") + List results = (List) sqlMapClientTemplate.execute( + new SqlMapClientCallback() { + public Object doInSqlMapClient(SqlMapExecutor executor) + throws SQLException { + executor.startBatch(); + for (T item : items) { + executor.update(statementId, item); + } + try { + return executor.executeBatchDetailed(); + } catch (BatchException e) { + throw e.getBatchUpdateException(); + } + } + }); + + if (assertUpdates) { + if (results.size() != 1) { + throw new InvalidDataAccessResourceUsageException("Batch execution returned invalid results. " + + "Expected 1 but number of BatchResult objects returned was " + results.size()); + } + + int[] updateCounts = results.get(0).getUpdateCounts(); + + for (int i = 0; i < updateCounts.length; i++) { + int value = updateCounts[i]; + if (value == 0) { + throw new EmptyResultDataAccessException("Item " + i + " of " + updateCounts.length + + " did not update any rows: [" + items.get(i) + "]", 1); + } + } + } + + } + + } + +} diff --git a/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/IbatisBatchItemWriterTests.java b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/IbatisBatchItemWriterTests.java new file mode 100644 index 000000000..c2411fadf --- /dev/null +++ b/spring-batch-infrastructure/src/test/java/org/springframework/batch/item/database/IbatisBatchItemWriterTests.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 java.util.List; + +import javax.sql.DataSource; + +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.orm.ibatis.SqlMapClientTemplate; + +import com.ibatis.sqlmap.client.SqlMapClient; +import com.ibatis.sqlmap.client.SqlMapSession; +import com.ibatis.sqlmap.engine.execution.BatchResult; + +/** + * @author Thomas Risberg + */ +public class IbatisBatchItemWriterTests { + + private IbatisBatchItemWriter writer = new IbatisBatchItemWriter(); + + private DataSource ds; + + private SqlMapClientTemplate smct; + + private SqlMapClient smc; + + private String statementId = "updateFoo"; + + 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; + } + + @Override + public boolean equals(Object obj) { + if (!(obj instanceof Foo)) { + return false; + } + Foo compare = (Foo) obj; + if (this.bar.equals(compare.getBar())) { + return true; + } + return super.equals(obj); + } + + @Override + public int hashCode() { + return bar.hashCode(); + } + + } + + /* + * (non-Javadoc) + * + * @see junit.framework.TestCase#setUp() + */ + @Before + public void setUp() throws Exception { + smc = createMock(SqlMapClient.class); + ds = createNiceMock(DataSource.class); + smct = new SqlMapClientTemplate(ds, smc); + writer.setStatementId(statementId); + writer.setSqlMapClientTemplate(smct); + 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 IbatisBatchItemWriter(); + try { + writer.afterPropertiesSet(); + fail("Expected IllegalArgumentException"); + } + catch (IllegalArgumentException e) { + // expected + String message = e.getMessage(); + assertTrue("Message does not contain 'SqlMapClient'.", message.indexOf("SqlMapClient") >= 0); + } + writer.setSqlMapClientTemplate(smct); + try { + writer.afterPropertiesSet(); + fail("Expected IllegalArgumentException"); + } + catch (IllegalArgumentException e) { + // expected + String message = e.getMessage(); + assertTrue("Message does not contain 'statementId'.", message.indexOf("statementId") >= 0); + } + writer.setStatementId("statementId"); + writer.afterPropertiesSet(); + } + + @Test + public void testWriteAndFlush() throws Exception { + SqlMapSession sms = createMock(SqlMapSession.class); + expect(smc.openSession()).andReturn(sms); + sms.close(); + expect(sms.getCurrentConnection()).andReturn(null); + sms.setUserConnection(null); + sms.startBatch(); + expect(sms.update("updateFoo", new Foo("bar"))).andReturn(-2); + List results = Collections.singletonList(new BatchResult("updateFoo", "update foo")); + results.get(0).setUpdateCounts(new int[] {1}); + expect(sms.executeBatchDetailed()).andReturn(results); + replay(sms); + replay(smc); + writer.write(Collections.singletonList(new Foo("bar"))); + verify(sms); + verify(smc); + } + + @Test + public void testWriteAndFlushWithEmptyUpdate() throws Exception { + SqlMapSession sms = createMock(SqlMapSession.class); + expect(smc.openSession()).andReturn(sms); + sms.close(); + expect(sms.getCurrentConnection()).andReturn(null); + sms.setUserConnection(null); + sms.startBatch(); + expect(sms.update("updateFoo", new Foo("bar"))).andReturn(1); + List results = Collections.singletonList(new BatchResult("updateFoo", "update foo")); + results.get(0).setUpdateCounts(new int[] {0}); + expect(sms.executeBatchDetailed()).andReturn(results); + replay(sms); + replay(smc); + 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(sms); + verify(smc); + } + + @Test + public void testWriteAndFlushWithFailure() throws Exception { + final RuntimeException ex = new RuntimeException("ERROR"); + SqlMapSession sms = createMock(SqlMapSession.class); + expect(smc.openSession()).andReturn(sms); + sms.close(); + expect(sms.getCurrentConnection()).andReturn(null); + sms.setUserConnection(null); + sms.startBatch(); + expect(sms.update("updateFoo", new Foo("bar"))).andThrow(ex); + replay(sms); + replay(smc); + try { + writer.write(Collections.singletonList(new Foo("bar"))); + fail("Expected RuntimeException"); + } + catch (RuntimeException e) { + assertEquals("ERROR", e.getMessage()); + } + verify(sms); + verify(smc); + } + +}