From 3915aeb1e9c7338548ad67fafb6a0c14533d3ed4 Mon Sep 17 00:00:00 2001 From: dsyer Date: Thu, 22 Nov 2007 13:51:53 +0000 Subject: [PATCH] IN PROGRESS - issue BATCH-194: Incorrect exception handling when using Hibernate http://opensource.atlassian.com/projects/spring/browse/BATCH-194 Implemented a pretty good workaround in HibernateCreditWriter, which can probably be tightened up and made into a base class HibernateItemWriter. Also had to make HibernateCursorInputSource Skippable (and it isn't yet Restartable so it's kind of broken - work in progress). Plus the samples were configured to use SimpleStepExecutor which doesn't skip by default (makes me wonder if the restart sample actually does anything sensible). --- .../tasklet/ItemProviderProcessTasklet.java | 2 +- .../io/cursor/HibernateCursorInputSource.java | 129 +++++++++----- .../RethrowOnThresholdExceptionHandler.java | 2 +- .../policy/CountingCompletionPolicy.java | 2 +- ...atchTransactionSynchronizationManager.java | 2 +- .../callback/ItemProviderRetryCallback.java | 2 +- .../retry/policy/ItemProviderRetryPolicy.java | 2 +- ...throwOnThresholdExceptionHandlerTests.java | 2 +- .../sample/dao/HibernateCreditWriter.java | 35 ++-- .../sample/dao/SqlNflPlayerSummaryDao.java | 4 +- .../src/main/resources/jobs/hibernateJob.xml | 157 ++++++++++-------- .../resources/simple-container-definition.xml | 2 +- .../AbstractCustomerCreditIncreaseTests.java | 27 ++- .../HibernateFailureJobFunctionalTests.java | 27 ++- 14 files changed, 255 insertions(+), 140 deletions(-) diff --git a/execution/src/main/java/org/springframework/batch/execution/tasklet/ItemProviderProcessTasklet.java b/execution/src/main/java/org/springframework/batch/execution/tasklet/ItemProviderProcessTasklet.java index 1d92c9f64..31f4984bb 100644 --- a/execution/src/main/java/org/springframework/batch/execution/tasklet/ItemProviderProcessTasklet.java +++ b/execution/src/main/java/org/springframework/batch/execution/tasklet/ItemProviderProcessTasklet.java @@ -96,7 +96,7 @@ public class ItemProviderProcessTasklet implements Tasklet, Recoverable, Skippab * item being processed. Needed to provide recoverable behavior if * {@link RetryOperations} are not provided. */ - private static final String ITEM_KEY = ItemProviderProcessTasklet.class + ".ITEM"; + private static final String ITEM_KEY = ItemProviderProcessTasklet.class.getName() + ".ITEM"; private RetryPolicy retryPolicy = null; diff --git a/infrastructure/src/main/java/org/springframework/batch/io/cursor/HibernateCursorInputSource.java b/infrastructure/src/main/java/org/springframework/batch/io/cursor/HibernateCursorInputSource.java index 5bc4f6878..10400fd39 100644 --- a/infrastructure/src/main/java/org/springframework/batch/io/cursor/HibernateCursorInputSource.java +++ b/infrastructure/src/main/java/org/springframework/batch/io/cursor/HibernateCursorInputSource.java @@ -15,6 +15,8 @@ */ package org.springframework.batch.io.cursor; +import java.util.ArrayList; +import java.util.List; import java.util.Properties; import org.hibernate.ScrollableResults; @@ -22,6 +24,7 @@ import org.hibernate.Session; import org.hibernate.SessionFactory; import org.hibernate.StatelessSession; import org.springframework.batch.io.InputSource; +import org.springframework.batch.io.Skippable; import org.springframework.batch.item.ResourceLifecycle; import org.springframework.batch.repeat.synch.BatchTransactionSynchronizationManager; import org.springframework.batch.restart.GenericRestartData; @@ -38,47 +41,66 @@ import org.springframework.util.ClassUtils; * {@link InputSource} for reading database records built on top of Hibernate. * * It executes the HQL {@link #queryString} when initialized and iterates over - * the result set as {@link #read()} method is called, returning an object corresponding - * to current row. + * the result set as {@link #read()} method is called, returning an object + * corresponding to current row. * - * Input source can be configured to use either {@link StatelessSession} sufficient - * for simple mappings without the need to cascade to associated objects or standard hibernate - * {@link Session} for more advanced mappings or when caching is desired. + * Input source can be configured to use either {@link StatelessSession} + * sufficient for simple mappings without the need to cascade to associated + * objects or standard hibernate {@link Session} for more advanced mappings or + * when caching is desired. * - * When stateful session is used it will be cleared after successful commit *without* being flushed - * (no inserts or updates are expected). + * When stateful session is used it will be cleared after successful commit + * without being flushed (no inserts or updates are expected). * * @author Robert Kasanicky */ -public class HibernateCursorInputSource implements InputSource, Restartable, InitializingBean, DisposableBean, - ResourceLifecycle { - - private static final String RESTART_DATA_ROW_NUMBER_KEY = ClassUtils.getShortName(HibernateCursorInputSource.class)+".rowNumber"; +public class HibernateCursorInputSource implements InputSource, Restartable, + Skippable, InitializingBean, DisposableBean, ResourceLifecycle { + + private static final String RESTART_DATA_ROW_NUMBER_KEY = ClassUtils + .getShortName(HibernateCursorInputSource.class) + + ".rowNumber"; private SessionFactory sessionFactory; - + private StatelessSession statelessSession; - + private Session statefulSession; - + private ScrollableResults cursor; - + private String queryString; - + private boolean useStatelessSession = true; - + private int lastCommitRowNumber = 0; - + + private final List skippedRows = new ArrayList(); + + private int skipCount = 0; + + /* Current count of processed records. */ + private int currentProcessedRow = 0; + private boolean initialized = false; - + private TransactionSynchronization synchronization = new HibernateInputSourceTransactionSynchronization(); - - + public Object read() { if (!initialized) { open(); } if (cursor.next()) { + currentProcessedRow++; + if (!skippedRows.isEmpty()) { + // while is necessary to handle successive skips. + while (skippedRows.contains(new Integer(currentProcessedRow))) { + if (!cursor.next()) { + return null; + } + currentProcessedRow++; + } + } Object data = cursor.get(0); return data; } @@ -91,6 +113,9 @@ public class HibernateCursorInputSource implements InputSource, Restartable, Ini public void close() { initialized = false; cursor.close(); + currentProcessedRow = 0; + skippedRows.clear(); + skipCount = 0; if (useStatelessSession) { statelessSession.close(); } else { @@ -109,13 +134,15 @@ public class HibernateCursorInputSource implements InputSource, Restartable, Ini statefulSession = sessionFactory.openSession(); cursor = statefulSession.createQuery(queryString).scroll(); } - - BatchTransactionSynchronizationManager.registerSynchronization(synchronization ); + + BatchTransactionSynchronizationManager + .registerSynchronization(synchronization); initialized = true; } /** - * @param sessionFactory hibernate session factory + * @param sessionFactory + * hibernate session factory */ public void setSessionFactory(SessionFactory sessionFactory) { this.sessionFactory = sessionFactory; @@ -131,7 +158,8 @@ public class HibernateCursorInputSource implements InputSource, Restartable, Ini } /** - * @param queryString HQL query string + * @param queryString + * HQL query string */ public void setQueryString(String queryString) { this.queryString = queryString; @@ -139,9 +167,10 @@ public class HibernateCursorInputSource implements InputSource, Restartable, Ini /** * Can be set only in uninitialized state. - * @param useStatelessSession - * true to use {@link StatelessSession} - * false to use standard hibernate {@link Session} + * + * @param useStatelessSession + * true to use {@link StatelessSession} + * false to use standard hibernate {@link Session} */ public void setUseStatelessSession(boolean useStatelessSession) { Assert.state(!initialized); @@ -153,8 +182,9 @@ public class HibernateCursorInputSource implements InputSource, Restartable, Ini */ public RestartData getRestartData() { Properties props = new Properties(); - props.setProperty(RESTART_DATA_ROW_NUMBER_KEY, String.valueOf(cursor.getRowNumber())); - + props.setProperty(RESTART_DATA_ROW_NUMBER_KEY, String.valueOf(cursor + .getRowNumber())); + return new GenericRestartData(props); } @@ -162,28 +192,49 @@ public class HibernateCursorInputSource implements InputSource, Restartable, Ini * Sets the cursor to the received row number. */ public void restoreFrom(RestartData data) { - Assert.state(!initialized, - "Cannot restore when already intialized. Call close() first before restore()"); - + Assert + .state(!initialized, + "Cannot restore when already intialized. Call close() first before restore()"); + Properties props = data.getProperties(); if (props.getProperty(RESTART_DATA_ROW_NUMBER_KEY) == null) { return; } - int rowNumber = Integer.parseInt(props.getProperty(RESTART_DATA_ROW_NUMBER_KEY)); + int rowNumber = Integer.parseInt(props + .getProperty(RESTART_DATA_ROW_NUMBER_KEY)); open(); cursor.setRowNumber(rowNumber); } - + + /** + * Skip the current row. If the transaction is rolled back, this row will + * not be represented when read() is called. For example, if you read in row + * 2, find the data to be bad, and call skip(), then continue processing and + * find + */ + public void skip() { + skippedRows.add(new Integer(currentProcessedRow)); + skipCount++; + } + /** * Encapsulates transaction events handling. */ - private class HibernateInputSourceTransactionSynchronization extends TransactionSynchronizationAdapter { - + private class HibernateInputSourceTransactionSynchronization extends + TransactionSynchronizationAdapter { + public void afterCompletion(int status) { if (status == TransactionSynchronization.STATUS_ROLLED_BACK) { - cursor.setRowNumber(lastCommitRowNumber); + currentProcessedRow = lastCommitRowNumber; + if (lastCommitRowNumber == 0) { + cursor.beforeFirst(); + } else { + // Set the cursor so that next time it is advanced it will + // come back to the committed row. + cursor.setRowNumber(lastCommitRowNumber - 1); + } } else if (status == TransactionSynchronization.STATUS_COMMITTED) { - lastCommitRowNumber = cursor.getRowNumber(); + lastCommitRowNumber = currentProcessedRow; if (!useStatelessSession) { statefulSession.clear(); } diff --git a/infrastructure/src/main/java/org/springframework/batch/repeat/exception/handler/RethrowOnThresholdExceptionHandler.java b/infrastructure/src/main/java/org/springframework/batch/repeat/exception/handler/RethrowOnThresholdExceptionHandler.java index 06594e077..5c357b28e 100644 --- a/infrastructure/src/main/java/org/springframework/batch/repeat/exception/handler/RethrowOnThresholdExceptionHandler.java +++ b/infrastructure/src/main/java/org/springframework/batch/repeat/exception/handler/RethrowOnThresholdExceptionHandler.java @@ -129,7 +129,7 @@ public class RethrowOnThresholdExceptionHandler implements ExceptionHandler { } private RepeatContextCounter getCounter(RepeatContext context, Object key) { - String attribute = RethrowOnThresholdExceptionHandler.class + "." + String attribute = RethrowOnThresholdExceptionHandler.class.getName() + "." + key.toString(); // Creates a new counter and stores it in the correct context: return new RepeatContextCounter(context, attribute, useParent); diff --git a/infrastructure/src/main/java/org/springframework/batch/repeat/policy/CountingCompletionPolicy.java b/infrastructure/src/main/java/org/springframework/batch/repeat/policy/CountingCompletionPolicy.java index 9b7bf5975..feb4fa46a 100644 --- a/infrastructure/src/main/java/org/springframework/batch/repeat/policy/CountingCompletionPolicy.java +++ b/infrastructure/src/main/java/org/springframework/batch/repeat/policy/CountingCompletionPolicy.java @@ -35,7 +35,7 @@ public abstract class CountingCompletionPolicy extends DefaultResultCompletionPo /** * Session key for global counter. */ - public static final String COUNT = CountingCompletionPolicy.class + ".COUNT"; + public static final String COUNT = CountingCompletionPolicy.class.getName() + ".COUNT"; private boolean useParent = false; diff --git a/infrastructure/src/main/java/org/springframework/batch/repeat/synch/BatchTransactionSynchronizationManager.java b/infrastructure/src/main/java/org/springframework/batch/repeat/synch/BatchTransactionSynchronizationManager.java index d4e25f399..fe5d696e7 100644 --- a/infrastructure/src/main/java/org/springframework/batch/repeat/synch/BatchTransactionSynchronizationManager.java +++ b/infrastructure/src/main/java/org/springframework/batch/repeat/synch/BatchTransactionSynchronizationManager.java @@ -64,7 +64,7 @@ public class BatchTransactionSynchronizationManager { /** * The key in the context attributes for the list of synchronizations. */ - private static final String SYNCHS_ATTR_KEY = BatchTransactionSynchronizationManager.class + ".SYNCHRONIZATIONS"; + private static final String SYNCHS_ATTR_KEY = BatchTransactionSynchronizationManager.class.getName() + ".SYNCHRONIZATIONS"; /** * Static method to register synchronizations. A TransactionSyncrhonization diff --git a/infrastructure/src/main/java/org/springframework/batch/retry/callback/ItemProviderRetryCallback.java b/infrastructure/src/main/java/org/springframework/batch/retry/callback/ItemProviderRetryCallback.java index 638dbec77..3ef27a70c 100644 --- a/infrastructure/src/main/java/org/springframework/batch/retry/callback/ItemProviderRetryCallback.java +++ b/infrastructure/src/main/java/org/springframework/batch/retry/callback/ItemProviderRetryCallback.java @@ -42,7 +42,7 @@ public class ItemProviderRetryCallback implements RetryCallback { private final static Log logger = LogFactory.getLog(ItemProviderRetryCallback.class); - public static final String ITEM = ItemProviderRetryCallback.class + ".ITEM"; + public static final String ITEM = ItemProviderRetryCallback.class.getName() + ".ITEM"; private ItemProvider provider; diff --git a/infrastructure/src/main/java/org/springframework/batch/retry/policy/ItemProviderRetryPolicy.java b/infrastructure/src/main/java/org/springframework/batch/retry/policy/ItemProviderRetryPolicy.java index 643f53eb5..c2e8e487f 100644 --- a/infrastructure/src/main/java/org/springframework/batch/retry/policy/ItemProviderRetryPolicy.java +++ b/infrastructure/src/main/java/org/springframework/batch/retry/policy/ItemProviderRetryPolicy.java @@ -44,7 +44,7 @@ public class ItemProviderRetryPolicy extends AbstractStatefulRetryPolicy { protected Log logger = LogFactory.getLog(getClass()); - public static final String EXHAUSTED = ItemProviderRetryPolicy.class + ".EXHAUSTED"; + public static final String EXHAUSTED = ItemProviderRetryPolicy.class.getName() + ".EXHAUSTED"; private RetryPolicy delegate; diff --git a/infrastructure/src/test/java/org/springframework/batch/repeat/exception/handler/RethrowOnThresholdExceptionHandlerTests.java b/infrastructure/src/test/java/org/springframework/batch/repeat/exception/handler/RethrowOnThresholdExceptionHandlerTests.java index 82dd7c0c5..24459e79d 100644 --- a/infrastructure/src/test/java/org/springframework/batch/repeat/exception/handler/RethrowOnThresholdExceptionHandlerTests.java +++ b/infrastructure/src/test/java/org/springframework/batch/repeat/exception/handler/RethrowOnThresholdExceptionHandlerTests.java @@ -59,7 +59,7 @@ public class RethrowOnThresholdExceptionHandlerTests extends TestCase { handler.setThresholds(Collections.singletonMap("RuntimeException", new Integer(1))); // No exception... handler.handleException(context, new RuntimeException("Foo")); - RepeatContextCounter counter = new RepeatContextCounter(context, RethrowOnThresholdExceptionHandler.class + ".RuntimeException"); + RepeatContextCounter counter = new RepeatContextCounter(context, RethrowOnThresholdExceptionHandler.class.getName() + ".RuntimeException"); assertNotNull(counter); assertEquals(1, counter.getCount()); } diff --git a/samples/src/main/java/org/springframework/batch/sample/dao/HibernateCreditWriter.java b/samples/src/main/java/org/springframework/batch/sample/dao/HibernateCreditWriter.java index 4bb7359a1..46823d532 100644 --- a/samples/src/main/java/org/springframework/batch/sample/dao/HibernateCreditWriter.java +++ b/samples/src/main/java/org/springframework/batch/sample/dao/HibernateCreditWriter.java @@ -33,12 +33,14 @@ import org.springframework.orm.hibernate3.support.HibernateDaoSupport; public class HibernateCreditWriter extends HibernateDaoSupport implements CustomerCreditWriter, RepeatInterceptor { - private boolean failOnFlush = false; - private boolean first = true; + private int failOnFlush = -1; private List errors = new ArrayList(); - private Set processed = new HashSet(); private Set failed = new HashSet(); + // TODO: these need to be ThreadLocal (or a pure framework concern). + private RepeatContext context; + private Set processed = new HashSet(); + /** * Public accessor for the errors property. * @@ -54,16 +56,15 @@ public class HibernateCreditWriter extends HibernateDaoSupport implements * @see org.springframework.batch.sample.dao.CustomerCreditWriter#write(org.springframework.batch.sample.domain.CustomerCredit) */ public void writeCredit(CustomerCredit customerCredit) { - if (!failOnFlush || !first) { - getHibernateTemplate().update(customerCredit); - } else { + if (customerCredit.getId() == failOnFlush) { // try to insert one with a duplicate ID CustomerCredit newCredit = new CustomerCredit(); newCredit.setId(customerCredit.getId()); newCredit.setName(customerCredit.getName()); newCredit.setCredit(customerCredit.getCredit()); getHibernateTemplate().save(newCredit); - first = false; // fail on the first record only + } else { + getHibernateTemplate().update(customerCredit); } } @@ -75,23 +76,31 @@ public class HibernateCreditWriter extends HibernateDaoSupport implements public void write(Object output) { processed.add(output); writeCredit((CustomerCredit) output); + if (failed.contains(output)) { + // Force early completion to commit aggressively if we encounter a + // failed item (from a failed chunk but we don't know which one was + // the problem). + context.setCompleteOnly(); + // Flush now, so that if there is a failure this record will be + // skipped. + getHibernateTemplate().flush(); + } } /** - * Public setter for the {@link boolean} property. + * Public setter for the failOnFlush property. * * @param failOnFlush * true if you want to fail on flush (for testing) */ - public void setFailOnFlush(boolean failOnFlush) { + public void setFailOnFlush(int failOnFlush) { this.failOnFlush = failOnFlush; } - public void after(RepeatContext context, ExitStatus result) { - // + public void before(RepeatContext context) { } - public void before(RepeatContext context) { + public void after(RepeatContext context, ExitStatus result) { } /** @@ -117,9 +126,9 @@ public class HibernateCreditWriter extends HibernateDaoSupport implements } public void open(RepeatContext context) { + this.context = context; errors.clear(); processed.clear(); - System.err.println(failed); } } diff --git a/samples/src/main/java/org/springframework/batch/sample/dao/SqlNflPlayerSummaryDao.java b/samples/src/main/java/org/springframework/batch/sample/dao/SqlNflPlayerSummaryDao.java index 188d9c06f..dd3ca84a7 100644 --- a/samples/src/main/java/org/springframework/batch/sample/dao/SqlNflPlayerSummaryDao.java +++ b/samples/src/main/java/org/springframework/batch/sample/dao/SqlNflPlayerSummaryDao.java @@ -13,8 +13,8 @@ public class SqlNflPlayerSummaryDao extends JdbcDaoSupport implements ItemWriter public void write(Object output) { - Assert.isInstanceOf(NflPlayerSummary.class, output, SqlNflPlayerSummaryDao.class + " only " + - "supports outputing " + NflPlayerSummary.class + " instances."); + Assert.isInstanceOf(NflPlayerSummary.class, output, SqlNflPlayerSummaryDao.class.getName() + " only " + + "supports outputing " + NflPlayerSummary.class.getName() + " instances."); NflPlayerSummary summary = (NflPlayerSummary)output; diff --git a/samples/src/main/resources/jobs/hibernateJob.xml b/samples/src/main/resources/jobs/hibernateJob.xml index 216331a74..dc69b352f 100644 --- a/samples/src/main/resources/jobs/hibernateJob.xml +++ b/samples/src/main/resources/jobs/hibernateJob.xml @@ -1,9 +1,9 @@ - - + - - - - - - - - - - - - - - - - - - - - - - + + + + + + + - - - - - - + + + + - - - - - - - - - - - - - - - - - - - CustomerCredit.hbm.xml - - - - - hibernate.dialect=org.hibernate.dialect.HSQLDialect - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + CustomerCredit.hbm.xml + + + + + + + + + + + \ No newline at end of file diff --git a/samples/src/main/resources/simple-container-definition.xml b/samples/src/main/resources/simple-container-definition.xml index 55c0116f3..50e67c826 100644 --- a/samples/src/main/resources/simple-container-definition.xml +++ b/samples/src/main/resources/simple-container-definition.xml @@ -57,7 +57,7 @@ diff --git a/samples/src/test/java/org/springframework/batch/sample/AbstractCustomerCreditIncreaseTests.java b/samples/src/test/java/org/springframework/batch/sample/AbstractCustomerCreditIncreaseTests.java index 52cc74dbc..913d96e00 100644 --- a/samples/src/test/java/org/springframework/batch/sample/AbstractCustomerCreditIncreaseTests.java +++ b/samples/src/test/java/org/springframework/batch/sample/AbstractCustomerCreditIncreaseTests.java @@ -3,6 +3,7 @@ package org.springframework.batch.sample; import java.math.BigDecimal; import java.sql.ResultSet; import java.sql.SQLException; +import java.util.ArrayList; import java.util.List; import org.springframework.batch.sample.item.processor.CustomerCreditIncreaseProcessor; @@ -27,6 +28,8 @@ public abstract class AbstractCustomerCreditIncreaseTests extends private static final String CREDIT_COLUMN = "CREDIT"; + protected static final String ID_COLUMN = "ID"; + private List creditsBeforeUpdate; public void setJdbcTemplate(JdbcOperations jdbcTemplate) { @@ -49,6 +52,8 @@ public abstract class AbstractCustomerCreditIncreaseTests extends * Credit was increased by CREDIT_INCREASE */ protected void validatePostConditions() throws Exception { + + final List matches = new ArrayList(); jdbcTemplate.query(ALL_CUSTOMERS, new RowMapper() { @@ -56,12 +61,32 @@ public abstract class AbstractCustomerCreditIncreaseTests extends final BigDecimal creditBeforeUpdate = (BigDecimal) creditsBeforeUpdate.get(rowNum); final BigDecimal expectedCredit = creditBeforeUpdate .add(CREDIT_INCREASE); - assertEquals(expectedCredit, rs.getBigDecimal(CREDIT_COLUMN)); + if (expectedCredit.equals(rs.getBigDecimal(CREDIT_COLUMN))) { + matches.add(rs.getBigDecimal(ID_COLUMN)); + } return null; } }); + + + assertEquals(getExpectedMatches(), matches.size()); + checkMatches(matches); } + /** + * @param matches + */ + protected void checkMatches(List matches) { + // no-op... + } + + /** + * @return the expected number of matches in the updated credits. + */ + protected int getExpectedMatches() { + return creditsBeforeUpdate.size(); + } + } diff --git a/samples/src/test/java/org/springframework/batch/sample/HibernateFailureJobFunctionalTests.java b/samples/src/test/java/org/springframework/batch/sample/HibernateFailureJobFunctionalTests.java index c0f84423e..0f0ed7ff8 100644 --- a/samples/src/test/java/org/springframework/batch/sample/HibernateFailureJobFunctionalTests.java +++ b/samples/src/test/java/org/springframework/batch/sample/HibernateFailureJobFunctionalTests.java @@ -1,5 +1,8 @@ package org.springframework.batch.sample; +import java.math.BigDecimal; +import java.util.List; + import org.springframework.batch.sample.dao.HibernateCreditWriter; import org.springframework.jdbc.UncategorizedSQLException; import org.springframework.orm.hibernate3.HibernateJdbcException; @@ -32,7 +35,7 @@ public class HibernateFailureJobFunctionalTests extends */ protected void onTearDown() throws Exception { super.onTearDown(); - writer.setFailOnFlush(false); + writer.setFailOnFlush(-1); } protected String[] getConfigLocations() { @@ -45,7 +48,7 @@ public class HibernateFailureJobFunctionalTests extends * @see org.springframework.batch.sample.AbstractValidatingBatchLauncherTests#testLaunchJob() */ public void testLaunchJob() throws Exception { - writer.setFailOnFlush(true); + writer.setFailOnFlush(2); int before = jdbcTemplate.queryForInt("SELECT COUNT(*) from CUSTOMER"); assertTrue(before>0); @@ -57,8 +60,9 @@ public class HibernateFailureJobFunctionalTests extends } catch (UncategorizedSQLException e) { // Expected, but check that the exception was registered: assertEquals(1, writer.getErrors().size()); - throw e; + // throw e; } + validatePostConditions(); int after = jdbcTemplate.queryForInt("SELECT COUNT(*) from CUSTOMER"); assertEquals(before, after); } @@ -68,6 +72,21 @@ public class HibernateFailureJobFunctionalTests extends */ protected void validatePostConditions() throws Exception { // TODO: fix so that the postconditions in super class are true - // super.validatePostConditions(); + super.validatePostConditions(); + } + + /* (non-Javadoc) + * @see org.springframework.batch.sample.AbstractCustomerCreditIncreaseTests#checkMatches(java.util.List) + */ + protected void checkMatches(List matches) { + assertFalse(matches.contains(new BigDecimal(2))); + } + + /* (non-Javadoc) + * @see org.springframework.batch.sample.AbstractCustomerCreditIncreaseTests#getExpectedMatches() + */ + protected int getExpectedMatches() { + // One record was skipped, so it won't be processed in the final state. + return super.getExpectedMatches()-1; } }