On September 4, 2011 wrote: Customising Data Access Exception Handling In Spring
Out-of-the box, Spring provides a well-conceived and fine-grained runtime data access exception hierarchy. As I am sure most of you are already aware, for most databases, this is achieved by mapping SQL error codes to exceptions via a file called sql-error-codes.xml (this lives in the org.springframework.jdbc.support package inside the org.springframework.jdbc.jar). However, what is less commonly know — because it is less commonly required — is how easy it is to customise this exception handling process. I came across a case where I needed to do this recently, so I thought I would share the process with you here.
I was working on an application that integrates with an Oracle database and I was operating on a table that had triggers associated with it. These triggers were performing complex data integrity checks. Now, the problem here of course comes when the trigger raises an error. In Oracle PL/SQL, this is done through the raise_application_error function. This takes 2 required arguments: a number in the range –20000 to –20999 (which will become a positive SQL exception code) and a varchar (which will be included in the exception message). So, for example, raise_application_error(-20999, 'something went wrong!') will result in a SQL exception with code 20999 and will include the message “something went wrong!”. So far, so obvious.
Now, because Oracle will not let you raise an application error using this technique that results in a standard, predefined SQL error code, Spring clearly will have no idea what the real nature of the problem is and your application will throw an UncategorizedSQLException, making it difficult to handle with any intelligence. So, for the sake of argument, let’s say we have a trigger that looks like this:
CREATE OR REPLACE TRIGGER check_data_integrity_trigger
BEFORE INSERT OR UPDATE ON some_table
REFERENCING OLD AS pre NEW AS post
FOR EACH ROW
DECLARE
lv_table_col some_table.table_col%type;
BEGIN
SELECT DISTINCT table_col INTO lv_table_col FROM some_table WHERE another_col = :post.another_col;
IF lv_some_col != :post.some_col THEN
raise_application_error(-20999, 'constraint violation: table_col must be unique for each another_col'); -- hey, Bob, why don't you just normalise your schema, rather than using nasty triggers? ;-)
END IF;
EXCEPTION
WHEN no_data_found THEN
NULL;
END check_data_integrity_trigger;
/
ALTER TRIGGER check_data_integrity_trigger ENABLE;
And we have a unit test that looks like this:
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:com/foo/bar/applicationContext.xml")
@Transactional
@TransactionConfiguration(defaultRollback = true)
public class SomeDAOIntegrationTest {
@Autowired private SomeDAO dao;
@Test(expected = DataIntegrityViolationException.class) public void shouldNotBeAbleToInsertTwoDifferentValuesForTableColWhenAnotherColHasTheSameValue() throws Exception {
// given
SomeTableBean b1 = instanceWhereTableColValueIs("foo").andAnotherColValueIs("baz");
dao.insert(b1);
SomeTableBean b2 = instanceWhereTableColValueIs("bar").andAnotherColValueIs("baz");
// when
dao.insert(b2);
}
}
Just so we get the full picture, let’s say the DAO implementation looks like like this:
@Repository @SuppressWarnings("synthetic-access") public class SomeJdbcDAO implements SomeDAO {
private final JdbcTemplate db;
@Autowired public SomeJdbcDAO(DataSource dataSource) {
db = new JdbcTemplate(dataSource);
}
@Override @Transactional public int insert(SomeTableBean bean) {
Long id = db.queryForLong("select some_table_seq.nextval from dual");
bean.setId(id);
return db.update("insert into some_table(table_id, table_col, another_col) values(?, ?, ?)", id, bean.getTableCol(), bean.getAnotherCol());
}
}
Now, when we run our test, it fails because, rather than getting the expected DataIntegrityViolationException, we get an UncategorizedSQLException. To make the test pass, all we need to do is add a simple Spring context file to the root of the classpath called sql-error-codes.xml, like so:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:util="http://www.springframework.org/schema/util"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util-3.0.xsd">
<bean id="dataIntegrityViolatonTriggerCodes" class="org.springframework.jdbc.support.CustomSQLErrorCodesTranslation">
<property name="errorCodes" value="20999" /><!-- comma-separated list of error code to translate into the given exception class -->
<property name="exceptionClass" value="org.springframework.dao.DataIntegrityViolationException" /><!-- we could also make this a custom exception if desired but why create our own when a perfectly suitable class already exists? -->
</bean>
<util:list id="customTranslations">
<ref bean="dataIntegrityViolatonTriggerCodes" />
</util:list>
<bean id="Oracle" class="org.springframework.jdbc.support.SQLErrorCodes">
<property name="customTranslations" ref="customTranslations" />
</bean>
</beans>
Now, whenever the trigger raises its custom error, the expected DataIntegrityViolationException will be thrown and our application will be able to interpret the meaning of the error and act accordingly if necessary.