Closed
Description
Here's what happened to me:
- During application startup, connecting to database fails
- SqlSessionFactoryBean configures mappers and tries to determine the databaseId to parse mappers xml https://github.com/mybatis/spring/blob/d081a644e12eb036652e010037fd1bf84e398d7c/src/main/java/org/mybatis/spring/SqlSessionFactoryBean.java#L664
- Finding databaseId fails: in case of failure, VendorDatabaseIdProvider returns null, not a SqlException
- VendorDatabaseIdProvider logs the error, then SqlSessionFactoryBean continues and does not bind all statements with a databaseId="xxx" attribute
- The application starts up
- Database connection is resumed
- Several hours, or days, later, the application runs the rarely-used database-specific statement that could not be bound (ok this is oddly specific, but very important: this specific fault happened several times in production, but it was very difficult debugging it because the revealing log from VendorDatabaseIdProvider was much earlier than the runtime bug)
- The statement cannot be executed due to
org.apache.ibatis.binding.BindingException: Invalid bound statement (not found):
Proposed solutions:
- When databaseId is set on a statement, resolve the correct statement when the query is executed instead of during application startup, using the currently live connection instead of the possibly different one used during startup. This would defer binding and avoid situations where the application starts up despite the database being down but then is unable to execute queries.
- If VendorDatabaseIdProvider returns null, fail (or fix VendorDatabaseIdProvider to not return null on exception)
- Probably the safest and easiest: if all candidates for binding a statement declared in a mapper interface require a specific databaseId, but no databaseId is available, fail immediately instead of silently not binding the statement
Current workaround: instead of the default VendorDatabaseIdProvider, we decided to inject a subclass where we override getDatabaseId; if super.getDatabaseId() returns null, we throw an exception. This way the application fails and our application orchestrator restarts the application (until the database becomes available again).
Steps to reproduce in attached demo project:
- Run the application with the spring boot "h2" profile -> "X" is logged by the DemoCommand
- Run the application with the spring boot "oracle" profile -> Invalid bound statement is thrown (the jdbc url for oracle is willingly bogus, to simulate a connection failure)