Affects: 6.0.0


Hi, I just upgraded from 5.3.24 to 6.0.0 and noticed a different behaviour of Spring JDBC/TX. I have a class containing simple JDBC statements executed with JdbcTemplate (no JPA, no ORMs), and I wrote some test cases based on H2.

In case of duplicate key errors, I expect DuplicateKeyException, but after the upgrade I'm getting a more generic DataIntegrityViolationException (that obviously breaks the test):

org.springframework.dao.DataIntegrityViolationException: PreparedStatementCallback; SQL [INSERT INTO "STORAGE" (filename, last_modified, compressed, file_contents) VALUES (?, ?, ?, ?)]; Unique index or primary key violation: "PUBLIC.PRIMARY_KEY_B ON PUBLIC.STORAGE(FILENAME) VALUES ( /* 1 */ 'myfile.txt' )"; SQL statement:
INSERT INTO "STORAGE" (filename, last_modified, compressed, file_contents) VALUES (?, ?, ?, ?) [23505-214]

Looking into the sources, I noticed that v5 instantiates a DuplicateKeyException in SQLErrorCodeSQLExceptionTranslator while v6 instantiates a DataIntegrityViolationException in SQLExceptionSubclassTranslator.

  • My code: https://github.com/albertus82/simple-jdbc-filestore/blob/spring6/src/main/java/io/github/albertus82/filestore/jdbc/SimpleJdbcFileStore.java#L273
  • Test: https://github.com/albertus82/simple-jdbc-filestore/blob/spring6/src/test/java/io/github/albertus82/filestore/jdbc/SimpleJdbcFileStoreTest.java#L188
  • Failure log: https://github.com/albertus82/simple-jdbc-filestore/actions/runs/3492195047/jobs/5845701283#step:4:13616

Thank you.

Comment From: sbrannen

This is to be expected due to the switch to using SQLExceptionSubclassTranslator by default.

See:

  • commit 083113d8a4ce1220ee4ee7e21395830e34ea8450
  • 28216

If you absolutely need to differentiate between DataIntegrityViolationException and DuplicateKeyException (which is a subclass of DataIntegrityViolationException), you can configure your JdbcTemplate to use SQLErrorCodeSQLExceptionTranslator instead.

For example:

DataSource dataSource = ...
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
jdbcTemplate.setExceptionTranslator(new SQLErrorCodeSQLExceptionTranslator(dataSource));
jdbcTemplate.afterPropertiesSet();

In light of the above, I am closing this issue as "works as designed"; however, I have also created #29518 to update the Upgrading to Spring Framework 6.x wiki page.

Comment From: albertus82

Hi @sbrannen,

I cannot set anything on JdbcTemplate because my class is part of a library that depends on the JdbcOperations provided by the client. However this new behaviour seems to me a slight regression: I could catch DataIntegrityViolationException but how to differentiate a duplicate key violation from other substantially different violations without binding my library to a specific RDBMS?

Thanks a lot.

Comment From: sbrannen

I cannot set anything on JdbcTemplate because my class is part of a library that depends on the JdbcOperations provided by the client.

Understood.

However this new behaviour seems to me a slight regression:

Technically speaking it is indeed a regression in the default behavior, but there are ways to get the old behavior back (the one I mentioned previously and one that I'll mention below).

I could catch DataIntegrityViolationException but how to differentiate a duplicate key violation from other substantially different violations without binding my library to a specific RDBMS?

I believe you should be able to introduce an empty sql-error-codes.xml file in the root of the classpath to achieve the same result.

If neither of these options works for you, please post back here, and we will continue the discussion.

Comment From: albertus82

Fixed adding the following "empty" sql-error-codes.xml file in the root of the classpath:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN 2.0//EN" "https://www.springframework.org/dtd/spring-beans-2.0.dtd">
<beans />

Thank you again.

Comment From: lukas-krecan

As a library author, I can't use any of the mentioned workarounds

Comment From: sbrannen

Reopening to improve documentation.

See https://github.com/spring-projects/spring-framework/issues/29673#issuecomment-1345383721

Comment From: jhoeller

As a documentation issue for the default exception translator change in 6.0, we should highlight DuplicateKeyException specifically indeed since that is a common exception subclass not covered by standard JDBC 4 exception subclasses (SQLExceptionSubclassTranslator), and not by SQL state analysis (in our fallback SQLStateSQLExceptionTranslator) either. Further such non-covered sub-subclasses are CannotAcquireLockException and DeadlockLoserDataAccessException.

That said, if these turn out to be common enough scenarios, we can try to make DuplicateKeyException and also CannotAcquireLockException etc work for common databases in our new default arrangement, e.g. through discovering database-specific exception variants in SQLExceptionSubclassTranslator or even by some hard-coded error code checks. However, all of this would have to be covered through programmatic checks rather than an XML file to be parsed by default, and also without having to discover the database product name through a live JDBC connection check first.

Comment From: jhoeller

It turns out that all databases that I checked support 23505 as a common SQL state value for duplicate keys, within the general 23xxx range that indicates data integrity violations. I'm therefore adding a specific 23505 check to SQLExceptionSubclassTranslator and also to SQLStateSQLExceptionTranslator, within the corresponding subclass/state range. While this may not cover every potential scenario, I expect it to cover the common duplicate key cases out there. If this is not sufficient, let me know; 6.0.3 snapshot builds including this change will be available for early testing soon.

Of course, re-enabling the SQLErrorCodeSQLExceptionTranslator with its legacy mappings file or providing a custom sql-error-codes.xml file still remains as a workaround in any case, providing database-specific error codes which require database product name detection at runtime.

From the perspective above, I'm turning this ticket into a regression issue that we will address in the 6.0.3 release, also documenting the general default change and its potential impact a bit better in the upgrade notes.

Comment From: lukas-krecan

Hi, the fix did not help with confilct on insert. In MariaDb, MySQL, MS SQL and Oracle the SQL state is 23000.

Comment From: sbrannen

@lukas-krecan, this issue is closed, and the changes have already been released in Spring Framework 6.0.3.

Please create a new issue to discuss the "conflict on insert".

Thanks