前言

H2数据库 是一个开源的嵌入式数据库引擎,采用 java 语言编写,不受平台的限制,
能够支持标准的 SQL 语法,支持存储过程等。因此采用 H2 数据库作为开发期、测试期和演示的数据库非常方便,不过不太适合作为大规模生产数据库。

H2 主要有如下三个用途:

  • 第一个用途,也是最常使用的用途就在于可以同应用程序打包在一起发布,这样可以非常方便地存储少量结构化数据。
  • 第二个用途是用于单元测试。启动速度快,而且可以关闭持久化功能,每一个用例执行完随即还原到初始状态。
  • 第三个用途是作为缓存,即当做内存数据库,作为 NoSQL 的一个补充。当某些场景下数据模型必须为关系型,可以拿它当 Memcached 使,作为后端 MySQL/Oracle 的一个缓冲层,缓存一些不经常变化但需要频繁访问的数据,比如字典表、权限表。

借助 H2 做持久层的单元测试是一个不错的方案

1 简单 SQL 的测试

我们提供了 H2DBUtil 工具类和 @H2DB 注解来直接执行 SQL 语句,只需要在 @H2DB 注解中定义好需要执行的 SQL 文件即可。

1
2
3
4
5
6
7
8
9
10
public class FunctionsTest {

@H2DB(value = "/sql/testing.sql")
H2DBUtil h2DBUtil;

@Test
public void test() {
h2DBUtil.executeQuery();
}
}

2 Mybatis 持久层单测

由于我们的工具 SDK 没有引入 MybatisSpring 依赖,所以这个场景的工具还没有怎么封装。
对于持久层的单测,我们需要做一些配置,也就是将基于 MySQL 的 DataSource 转换成基于 H2 数据库的,以此来解耦对于 MySQL 的依赖。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
@Configuration
@MapperScan(basePackages = "com.lucky.example.infrastructure.*.mapper", sqlSessionFactoryRef = "sqlSessionFactory")
public class MockDatabaseConfig {


@Bean(name = "sampleDataSource")
public DataSource sampleDatasource(){
return MockDatabase.context.dataSource();
}


@Resource(name = "sampleDataSource")
private DataSource sampleDataSource;


@Bean(name = "transactionManager")
public DataSourceTransactionManager transactionManager() {
return new DataSourceTransactionManager(sampleDataSource);
}

@Bean(name = "sqlSessionFactory")
@Primary
public SqlSessionFactory sqlSessionFactory()
throws Exception {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
bean.setDataSource(sampleDataSource);
bean.setMapperLocations(
// 设置mybatis的xml所在位置
new PathMatchingResourcePatternResolver().getResources("classpath*:mapper/*.xml"));
return bean.getObject();
}
}
```
接着,我们把持久层的接口注入进来,就能进行持久层的单元测试了。
```java
@Import({MockDatabaseConfig.class})
@ExtendWith(SpringExtension.class)
@Component
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
@Rollback(false)
public class GoodsMapperTest {

@Resource
GoodsMapper goodsMapper;

@Test
@Order(1)
@DisplayName("数据库单测用例 - 查询")
public void select() {
GoodsPo goodsPo = new GoodsPo("1", "张三", "sku-1", 10);
GoodsPo result = goodsMapper.selectOne(goodsPo.getId());
Assertions.assertThat(result)
.hasFieldOrPropertyWithValue("id", goodsPo.getId())
.hasFieldOrPropertyWithValue("name", goodsPo.getName())
.hasFieldOrPropertyWithValue("sku", goodsPo.getSku())
.hasFieldOrPropertyWithValue("inventory", goodsPo.getInventory());
}
}

3. H2 数据库对 MySQL 的函数扩展

虽然 H2 数据库能够支持标准的 SQL 语法,但对 Oracle/MySQL 中的内置函数却显得无能为力,比如 MD5()、TIMEDIFF() 等等。实际开发中,
我们无可避免会使用到一些内置函数,因此,我们扩展了一些常用的 MySQL 函数(Oracle/SQLServer 的数据库函数需自定义扩展),也支持用户
自定义扩展。

3.1 自定义扩展

首先,用户需要定义一个 H2Function 实例, function 属性表示扩展的函数别称,比如 “MD5”;fullClassName 表示实现该函数的全类名路径 + 方法,比如 “com.lucky.ut.effective.h2.mysql.EncryptionFunctions.md5”。

1
2
3
4
5
6
7
8
9
10
11
public class H2Function {
/**
* Mysql function alias,etc:version
*/
private String function;

/**
* The full class name for this function implementation
*/
private String fullClassName;
}

然后再把定义好的实例添加到 com.lucky.ut.effective.h2.H2FunctionsLoader 的 H2_FUNCTIONS 集合内。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class H2FunctionsLoader {

protected static final Set<H2Function> H2_FUNCTIONS = new CopyOnWriteArraySet<>();

/**
* @param h2Function
*/
protected static void add(H2Function h2Function) {
H2_FUNCTIONS.add(h2Function);
}

/**
* @param collection
*/
protected static void addAll(Collection<H2Function> collection) {
H2_FUNCTIONS.addAll(collection);
}
}

3.2 目前已经扩展的 MySQL 函数列表

函数 扩展实现路径
ADDDATE com.lucky.ut.effective.h2.mysql.DateTimeFunctions.addDate
ADDTIME com.lucky.ut.effective.h2.mysql.DateTimeFunctions.addTime
TIME com.lucky.ut.effective.h2.mysql.DateTimeFunctions.time
UTC_TIMESTAMP com.lucky.ut.effective.h2.mysql.DateTimeFunctions.utcTimestamp
UTC_DATE com.lucky.ut.effective.h2.mysql.DateTimeFunctions.utcDate
UTC_TIME com.lucky.ut.effective.h2.mysql.DateTimeFunctions.utcTime
FROM_DAYS com.lucky.ut.effective.h2.mysql.DateTimeFunctions.fromDays
TO_DAYS com.lucky.ut.effective.h2.mysql.DateTimeFunctions.toDays
TO_SECONDS com.lucky.ut.effective.h2.mysql.DateTimeFunctions.toSeconds
TIME_TO_SEC com.lucky.ut.effective.h2.mysql.DateTimeFunctions.timeToSeconds
DATE_FORMAT com.lucky.ut.effective.h2.mysql.DateTimeFunctions.dateFormat
TIME_FORMAT com.lucky.ut.effective.h2.mysql.DateTimeFunctions.timeFormat
LAST_DAY com.lucky.ut.effective.h2.mysql.DateTimeFunctions.lastDay
MAKEDATE com.lucky.ut.effective.h2.mysql.DateTimeFunctions.makeDate
MAKETIME com.lucky.ut.effective.h2.mysql.DateTimeFunctions.makeTime
SEC_TO_TIME com.lucky.ut.effective.h2.mysql.DateTimeFunctions.secondsToTime
SLEEP com.lucky.ut.effective.h2.mysql.DateTimeFunctions.sleep
STR_TO_DATE com.lucky.ut.effective.h2.mysql.DateTimeFunctions.strToDate
SUBDATE com.lucky.ut.effective.h2.mysql.DateTimeFunctions.subDate
SUBTIME com.lucky.ut.effective.h2.mysql.DateTimeFunctions.subTime
YEARWEEK com.lucky.ut.effective.h2.mysql.DateTimeFunctions.yearWeek
WEEKOFYEAR com.lucky.ut.effective.h2.mysql.DateTimeFunctions.weekOfYear
WEEKDAY com.lucky.ut.effective.h2.mysql.DateTimeFunctions.weekDay
MICROSECOND com.lucky.ut.effective.h2.mysql.DateTimeFunctions.microSecond
CONVERT_TZ com.lucky.ut.effective.h2.mysql.DateTimeFunctions.convertTZ
PERIOD_ADD com.lucky.ut.effective.h2.mysql.DateTimeFunctions.periodAdd
PERIOD_DIFF com.lucky.ut.effective.h2.mysql.DateTimeFunctions.periodDiff
TIMEDIFF com.lucky.ut.effective.h2.mysql.DateTimeFunctions.timeDiff
MD5 com.lucky.ut.effective.h2.mysql.EncryptionFunctions.md5
SHA1 com.lucky.ut.effective.h2.mysql.EncryptionFunctions.sha1
SHA com.lucky.ut.effective.h2.mysql.EncryptionFunctions.sha
HEX com.lucky.ut.effective.h2.mysql.EncryptionFunctions.hex
UNHEX com.lucky.ut.effective.h2.mysql.EncryptionFunctions.unhex
PASSWORD com.lucky.ut.effective.h2.mysql.EncryptionFunctions.password
TO_BASE64 com.lucky.ut.effective.h2.mysql.EncryptionFunctions.base64
FROM_BASE64 com.lucky.ut.effective.h2.mysql.EncryptionFunctions.unbase64
RANDOM_BYTES com.lucky.ut.effective.h2.mysql.EncryptionFunctions.randomBytes
AES_ENCRYPT com.lucky.ut.effective.h2.mysql.EncryptionFunctions.aesEncrypt
AES_DECRYPT com.lucky.ut.effective.h2.mysql.EncryptionFunctions.aesDecrypt
CRC32 com.lucky.ut.effective.h2.mysql.EncryptionFunctions.crc32
CREATE_DIGEST com.lucky.ut.effective.h2.mysql.EncryptionFunctions.createDigest
BIN com.lucky.ut.effective.h2.mysql.StringFunctions.bin
UUID_SHORT com.lucky.ut.effective.h2.mysql.StringFunctions.uuidShort
FIND_IN_SET com.lucky.ut.effective.h2.mysql.StringFunctions.findInSet
IS_IPV4 com.lucky.ut.effective.h2.mysql.StringFunctions.isIpv4
IS_IPV6 com.lucky.ut.effective.h2.mysql.StringFunctions.isIpv6
IS_UUID com.lucky.ut.effective.h2.mysql.StringFunctions.isUUID
REVERSE com.lucky.ut.effective.h2.mysql.StringFunctions.reverse
SUBSTRING_INDEX com.lucky.ut.effective.h2.mysql.StringFunctions.subStringIndex
STRCMP com.lucky.ut.effective.h2.mysql.StringFunctions.strCmp
CHARSET com.lucky.ut.effective.h2.mysql.StringFunctions.charset
FIELD com.lucky.ut.effective.h2.mysql.StringFunctions.field
MID com.lucky.ut.effective.h2.mysql.StringFunctions.mid
ORD com.lucky.ut.effective.h2.mysql.StringFunctions.ord
QUOTE com.lucky.ut.effective.h2.mysql.StringFunctions.quote
POW com.lucky.ut.effective.h2.mysql.MathFunctions.pow
CONV com.lucky.ut.effective.h2.mysql.MathFunctions.conv
FORMAT com.lucky.ut.effective.h2.mysql.MiscFunctions.format
VERSION com.lucky.ut.effective.h2.mysql.MiscFunctions.version
CONNECTION_ID com.lucky.ut.effective.h2.mysql.MiscFunctions.connectId
SESSION_USER com.lucky.ut.effective.h2.mysql.MiscFunctions.sessionUser
SYSTEM_USER com.lucky.ut.effective.h2.mysql.MiscFunctions.sessionUser
CURRENT_ROLE com.lucky.ut.effective.h2.mysql.MiscFunctions.currentRole