Adventure of H2 upgrade that lead to reveal Spring misconfiguration in my tests
Dependency upgrade sometimes may cause some fear of unknown … especially when release contains 100+ changes :smile: Recently I was updating H2 in tests sources to the latest version - 1.4.200. My project is using H2 in compatibility mode with MySQL
and this came out to be the root of all evil :wink:
Let’s start from the beginning. Everything was going nearly smooth till … one module, where tests were failing with werid exception:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
org.h2.jdbc.JdbcSQLIntegrityConstraintViolationException: NULL not allowed for column "ASSETREVISION"; SQL statement:
insert into table (id, version, col_1, col_2, asset_revision) values (null, ?, ?, ?, ?) [23502-200]
at org.h2.message.DbException.getJdbcSQLException(DbException.java:459)
at org.h2.message.DbException.getJdbcSQLException(DbException.java:429)
at org.h2.message.DbException.get(DbException.java:205)
at org.h2.message.DbException.get(DbException.java:181)
at org.h2.table.Column.validateConvertUpdateSequence(Column.java:374)
at org.h2.table.Table.validateConvertUpdateSequence(Table.java:845)
at org.h2.command.dml.Insert.insertRows(Insert.java:187)
at org.h2.command.dml.Insert.update(Insert.java:151)
at org.h2.command.CommandContainer.executeUpdateWithGeneratedKeys(CommandContainer.java:272)
at org.h2.command.CommandContainer.update(CommandContainer.java:191)
at org.h2.command.Command.executeUpdate(Command.java:251)
at org.h2.jdbc.JdbcPreparedStatement.executeUpdateInternal(JdbcPreparedStatement.java:191)
at org.h2.jdbc.JdbcPreparedStatement.executeUpdate(JdbcPreparedStatement.java:152)
...
at org.hibernate.BoringStuff.persist(BoringStuff.java:222)
...
at org.springframework.BoringStuff.invoke(BoringStuff.java:111)
at com.sun.proxy.$Proxy183.save(Unknown Source)
at my.project.TableManager.persist(TableManager.java:128)
What a heck is going on? Where this NULL
comes from? How is it related to version upgrade?
Entity class looks like this:
1
2
3
4
5
6
7
8
9
@javax.persistence.Entity
public class Table {
@Column(nullable = false)
private long assetRevision;
/* .. other fields **/
}
Field type is primitive long
, so default value will be 0L
. NULL
is not possible.
Let’s debug …
Initially I couldn’t reproduce bug, since single test class was green. I had to run all tests in module. Worth mention that not all tests were broken. First 50% of executed tests were green and only a few executed later were red.
I started debugging from org.h2.command.dml.Insert.insertRows
. And yes, the column value was indeed NULL
, but what intrigued me was the number of columns. Statement use 5 columns, but table in database had 8 columns! Why number of columns differs? Why some columns where duplicated? It looked like this:
1
["ID", "VERSION", "COL_1", "COL_2", "ASSET_REVISION", "COL1", "COL2", "ASSETREVISION"]
Ok, so it’s definitely not a fault of H2. Then why Hibernate is producing wrong statements?
I set breakpoint somewhere in org.hibernate.mapping.Table
and application stopped on … updating schema :smile: When Hibernate couldn’t find column with particular name, then he added one. Hibernate perfectly do his job! Why then column name was different? The answer is “hibernate naming strategy”. Came out that configurations of tests in my project were providing different naming strategies, however none of them was explicitly configured. You may ask - how is it possible? :wink:
The module causing such troubles contains mix of test based on SpringBoot and “classic” Spring configuration. Some test use autoconfiguration provided by @SpringBootTest
and other ones configuration provided by @ContextConfiguration
. The problem is that SpringBoot by default use his own hibernate naming strategy - org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy
. Here I must specify that I used Spring Boot 2 and Hibernate 5. Following test shows the difference:
1
2
3
4
5
6
Identifier identifier = new Identifier("assetRevision", false);
PhysicalNamingStrategy springBootStrategy = new org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy();
assertThat(springBootStrategy.toPhysicalColumnName(identifier, null)).hasToString("asset_revision");
PhysicalNamingStrategy hibernateStrategy = new org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl();
assertThat(hibernateStrategy.toPhysicalColumnName(identifier, null)).hasToString("assetRevision");
One of simplest solutions is to tell SpringBoot to use Hibernate’s default:
1
spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
Ok, but why it was working with previous version of H2?
I cloned H2 project from Github repo, then successfully reproduced my case and did git bisect
. Here is the culprit:
1
2
3
4
5
commit cedec8db5bd003d5566cb93cfc32f916e1ccd603
Author: Evgenij Ryazanov <katzyn@gmail.com>
Date: Thu May 23 18:56:56 2019 +0800
Remove Mode.convertInsertNullToZero because MySQL is now STRICT by default
Well, as I said, my project use H2 in compatibility mode with MySQL
. Fabulous Mode.convertInsertNullToZero
, removed in the latest version, was filling missing values in SQL statements with default ones. That’s why it was working before upgrade - misconfiguration was unluckily hidden.
Could it be avoided? Is sharing H2 database instance among tests a good idea? Is SpringBoot autoconfiguration feature worth it? I will leave it without answer. I’m not sure if such troubles can be avoided in the future :disappointed: