前言

H2 是一个用 Java 编写的开源关系数据库,特别适合作为嵌入式数据库和内存数据库使用。

针对其特性,我不会太深入讲解什么其他跟原理和很细节方面的东西,本文旨在让大家快速理解什么是内存数据库以及如何使用内存数据库快速进行开发。

什么是内存数据库

内存数据库(In-Memory Database,IMDB)是一种将数据完全存储在计算机内存中的数据库系统。与传统磁盘数据库不同,它跳过了磁盘 I/O 操作,直接通过内存读写数据,因此具备毫秒级响应速度高并发处理能力。其核心优势在于:

  • 速度优势:内存访问速度(纳秒级)比磁盘(毫秒级)快 3-4 个数量级,适合实时分析、高频交易等场景。
  • 架构简化:无需复杂的磁盘缓存机制,数据结构设计更贴近内存特性(如哈希表、索引树)。
  • 内存计算融合:数据与计算逻辑在内存中直接交互,减少数据搬运开销。
  • 数据存储与持久化
    • 主要存储在内存:数据加载到内存中运行,减少 I/O 操作。
    • 持久化机制:通过日志(Redo Log)、定期快照(Snapshot)或异步写入磁盘等方式,避免断电等故障导致数据丢失(例如 Redis 的 RDB 和 AOF 机制)。

但内存数据库也存在数据易失性(断电数据丢失)、内存容量限制等挑战,因此常与持久化机制结合使用。

维度 内存数据库 传统磁盘数据库(如 MySQL)
数据存储位置 主要在内存 主要在磁盘
性能瓶颈 受内存容量限制 受磁盘 I/O 速度限制
适用场景 实时性、高并发、低延迟场景 海量数据存储、事务强一致性场景
成本 硬件成本高(需大量内存) 硬件成本较低
数据一致性 部分支持最终一致性,事务能力较弱 强一致性(ACID)

H2 是一个用 Java 开发的嵌入式关系型数据库,它既支持传统磁盘存储,也提供纯内存模式

H2 数据库介绍

H2是一个采用java语言编写的嵌入式数据库引擎,只是一个类库(即只有一个 jar 文件),可以直接嵌入到应用项目中,不受平台的限制,而且比较重要的是,Spring Boot、Hibernate 等框架默认集成 H2,用于快速搭建测试数据库,这样就是 spring 框架不依赖真实数据库进行测试的场景。

H2 的独特之处在于完全兼容 SQL 标准,支持 JOIN、子查询等复杂关系操作,这是 Redis 等 NoSQL 内存数据库无法替代的。例如,在需要实时分析多张关联表数据时,H2 内存模式可直接执行 SQL,而且H2 通过轻量级架构灵活的内存 - 磁盘混合模式,展现了内存数据库的核心优势:既满足了 “极速响应” 的性能需求,又通过持久化机制弥补了数据易失性的缺陷。

应用场景:

  • 可以同应用程序打包在一起发布,可以非常方便地存储少量结构化数据
  • 可用于单元测试
  • 可以用作缓存,即当做内存数据库

H2的产品优势:

  • 纯Java编写,不受平台的限制;
  • 只有一个jar文件,适合作为嵌入式数据库使用;
  • h2提供了一个十分方便的web控制台用于操作和管理数据库内容;
  • 功能完整,支持标准SQL和JDBC。麻雀虽小五脏俱全;
  • 支持内嵌模式、服务器模式和集群。

H2数据库安装,这是它的官网,可以看到 H2 数据库非常小

H2 数据库也是和 Mysql 一样要配环境的。

H2 数据库的安装和简易部署

下载安装

如果你去官网解压下载包,内容就是这样

image-20250614201758813

下载安装程序后也是一样,只不过没有 Uninstall.exe

目录说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
h2-1.4.200.jar   H2数据库的jar包

h2.bat Windows控制台启动脚本

h2.sh Linux控制台启动脚本

h2w.bat Windows控制台启动脚本

build.bat windows构建脚本

build.sh linux构建脚本

docs            H2数据库的帮助文档(内有H2数据库的使用手册)

service          通过wrapper包装成服务。

src            H2数据库的源代码

数据库启动——Windows环境下

那么如何启动

下载H2后,你会看到几个关键的启动文件:

1
2
3
4
h2-1.4.200.jar   # 核心jar包,可直接运行
h2.bat # Windows图形界面启动脚本
h2w.bat # Windows后台静默启动脚本
h2.sh # Linux启动脚本

启动方式对比:

  • h2.bat:前台运行,会显示控制台窗口,关闭窗口就停止服务
  • h2w.bat:后台运行,没有控制台窗口,适合生产环境
  • 直接运行jar:java -jar h2-1.4.200.jar,等同于h2.bat

进入bin目录下,执行h2.bath2w.bath2-1.4.200.jar都可以。启动后会自动打开浏览器控制台,可以选择语言为中文

启动命令说明如下

1
2
3
4
5
6
7
8
9
# Windows环境
cd h2安装目录/bin
h2.bat

# 或者后台启动
h2w.bat

# Linux环境
./h2.sh

系统会进入H2 数据库的Web Consolehttp://<你的ip>:8082,注意占用的是 8082 端口,你 Spring 或者 Servlet 改过prot的就非常容易重复。

默认会创建一个数据库Generic H2,用户名为sa,空密码

image-20250614201932713

其中的字段的含义如下

  • 保存的连接设置

    这是一个下拉选项,作用是快速选择之前保存过的一组完整连接配置 。比如图里选的 “Generic H2 (Embedded)”,选中后,驱动类、JDBC URL、用户名等相关连接参数会自动填充,不用每次手动输入,方便下次直接用相同配置连数据库,避免重复设置参数。是快捷选择历史配置的入口

  • 连接设置名称

    是给当前这组连接配置起的标识名 ,像 “Generic H2 (Embedded)” 。主要用途:是配置的标识,用来存、取、管理连接参数,让连数据库更高效

    • 保存配置:填好驱动类、JDBC URL 等信息后,点 “保存”,H2 控制台会把这组配置以该名称存起来,之后就能在 “保存的连接设置” 下拉里选,直接复用。
    • 管理配置:方便区分不同连接配置(比如有多个 H2 库,或同库不同模式、用户的连接),看到名称就知道对应哪套参数,也能通过 “删除” 按钮,移除不用的配置。
  • 驱动类(Driver Class)

    指定用于连接 H2 数据库的 JDBC 驱动程序类。

    org.h2.Driver 是 H2 数据库官方提供的、实现了 JDBC 规范的驱动类,Java 程序通过加载这个类,才能建立和 H2 数据库的连接,让 Java 代码借助 JDBC API 操作 H2 数据库(执行 SQL 语句、管理数据等 )。在 Java 代码里,通常会用 Class.forName("org.h2.Driver") 加载该驱动(不过现在部分框架能自动加载 ),然后基于此驱动去获取数据库连接。

  • JDBC URL

    定义连接 H2 数据库的具体路径、模式及相关配置参数,告诉程序该如何找到并连接到 H2 数据库

    它是符合 JDBC 规范的连接字符串,H2 支持多种连接模式(嵌入式、服务器模式等 ),不同模式对应不同格式这个后面细嗦

  • 用户名和密码和mysql的都一样,H2 里用户名不区分大小写,但密码区分,创建数据库的用户默认成为该数据库管理员,拥有对应管理权限,而且 H2 可以把可把密码字段留空

在Web控制台中,你需要配置连接参数

1
2
3
4
驱动类: org.h2.Driver
JDBC URL: 根据使用模式选择
用户名: sa (默认)
密码: (默认为空)

例如,jdbc:h2:D:/software/h2/data/test,会在指定路径下创建一个名为 test.mv.db 的数据库,你可能还会看到一个 test.trace.db 的文件,这个是h2的错误信息,可以直接打开看。有没有跟我一样好奇为啥名称里面有个mv,这是因为高版本的H2存储引擎默默认为mvStore,文档最后面有讲这个存储引擎的。

设置好配置好,点击”连接”就可以连接上服务器,页面就是这样的

image-20250614202120911

与外部数据库工具连接

这里我以 DataGrip 进行演示,其实这些工具都一样

连接数据源时候选择 H2

image-20250614204419214

填写基本信息

image-20250614204613619

创建表进行测试

正如上述所说,H2数据库支持原生的sql,这个也支持别的,包括非关系型,跟你在创建数据库时候的配置有关

以常见的 “用户信息表” 和 “订单表” 为例,演示关系型数据的创建:

1
2
3
4
5
6
7
8
CREATE TABLE users (
user_id INT PRIMARY KEY AUTO_INCREMENT, -- 自增主键
username VARCHAR(50) NOT NULL UNIQUE, -- 用户名(非空且唯一)
email VARCHAR(100) NOT NULL, -- 邮箱
password VARCHAR(100) NOT NULL, -- 密码
create_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- 创建时间
last_login TIMESTAMP -- 最后登录时间
);

执行后看到左边出现了USERS表

冷知识,H2数据库的控制台UI界面是用纯 Swing 写的

image-20250614204141685
1
2
3
4
5
6
7
8
CREATE TABLE orders (
order_id INT PRIMARY KEY AUTO_INCREMENT,
user_id INT,
order_amount DECIMAL(10, 2) NOT NULL, -- 订单金额(最多10位,2位小数)
order_status VARCHAR(20) DEFAULT 'pending', -- 订单状态(默认待处理)
order_time TIMESTAMP NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(user_id) -- 关联用户表外键
);

执行方式:将 SQL 语句复制到控制台输入框,点击 执行 按钮(或按 F9),成功后会显示 更新: 0(创建表不涉及数据行更新)

注意 H2 不支持 InnoDB 索引

插入数据测试

向用户表插入数据

1
2
3
4
INSERT INTO users (username, email, password) VALUES
('alice', 'alice@example.com', 'pass123'),
('bob', 'bob@example.com', 'pass456'),
('charlie', 'charlie@example.com', 'pass789');
image-20250614204254411

向订单表插入数据

1
2
3
4
5
INSERT INTO orders (user_id, order_amount, order_status, order_time) VALUES
(1, 199.99, 'completed', '2025-06-10 10:30:00'),
(1, 59.50, 'pending', '2025-06-12 14:20:00'),
(2, 399.00, 'completed', '2025-06-11 09:15:00'),
(3, 89.99, 'cancelled', '2025-06-09 16:45:00');
image-20250619194032621

查找测试

简单查询

1
2
3
4
5
-- 通过 user_id 关联 users 表和 orders 表,查询 alice(user_id=1)的所有订单
SELECT orders.order_id, orders.order_amount, orders.order_status, orders.order_time
FROM users
JOIN orders ON users.user_id = orders.user_id
WHERE users.username = 'alice';
image-20250619194112767

复杂查询

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
-- 先通过子查询算出每个用户的平均订单金额,再关联查询出金额大于自身平均的订单
WITH UserAvgAmount AS (
SELECT
user_id,
AVG(order_amount) AS avg_amount -- 计算每个用户的平均订单金额
FROM orders
GROUP BY user_id
)
SELECT
orders.order_id,
orders.user_id,
orders.order_amount,
UserAvgAmount.avg_amount
FROM orders
JOIN UserAvgAmount ON orders.user_id = UserAvgAmount.user_id
WHERE orders.order_amount > UserAvgAmount.avg_amount;
image-20250619194052838

关闭数据库

点击 [Disconnect]:

image-20250619211225660

来断开连接

在JAVA应用内部,也可以通过代码来实现TCP服务的启动和停止,例子代码如下

1
2
3
4
5
6
7
import org.h2.tools.Server;
...
// 启动 TCP Server
Server server = Server.createTcpServer(args).start();
...
// 关闭 TCP Server
server.stop();

可以从另外的程序关闭 TCP 服务器,使用下面命令行:

1
java org.h2.tools.Server -tcpShutdown tcp://localhost:9092

在用户应用中关闭服务器,使用

1
org.h2.tools.Server.shutdownTcpServer("tcp://localhost:9094");

这个功能将仅仅关闭 TCP 服务器。如果相同的程序有其他服务器,他们不会被关闭,而是继续执行。为了避免覆盖在数据库下次打开时,在调用这个方法时,所有的到数据库的连接将会关闭。要实现远程关闭服务器,需启用远程连接。关闭一个 TCP 服务器可以通过选项 -tcpPassword 来保护 (启动和关闭 TCP 服务器也要用这个密码)

H2数据库的运行模式与运行方式介绍

运行模式

H2有三种运行模式。

  • 内嵌模式(Embedded Mode):内嵌模式下,应用和数据库同在一个JVM中,通过JDBC进行连接。可持久化,但同时只能一个客户端连接。内嵌模式性能会比较好。
  • 服务器模式(Server Mode):使用服务器模式和内嵌模式一样,只不过它可以跑在另一个进程里。
  • 混合模式:第一个应用以内嵌模式启动它,对于后面的应用来说它是服务器模式跑着的。混合模式是内嵌模式和服务器模式的组合。第一个应用通过内嵌模式与数据库建立连接,同时也作为一个服务器启动,于是另外的应用(运行在不同的进程或是虚拟机上)可以同时访问同样的数据。第一个应用的本地连接与嵌入式模式的连接性能一样的快,而其它连接理论上会略慢。

连接方式

  1. 以嵌入式模式(本地文件)连接方式连接H2数据库

    这种连接方式默认情况下只允许有一个客户端连接到H2数据库,有客户端连接到H2数据库之后,此时数据库文件就会被锁定,那么其他客户端就无法再连接了。

    连接语法:jdbc:h2:[file:][]<databaseName>

    1
    2
    3
    JDBC URL: jdbc:h2:~/test
    # 或指定完整路径
    JDBC URL: jdbc:h2:file:D:/data/mydb

    例如:

    • jdbc:h2:~/test    // 连接位于用户目录下的test数据库
    • jdbc:h2:file:/data/sample
    • jdbc:h2:file:E:/H2/gacl // Windows only

    特点:

    • 数据持久化到磁盘文件
    • 只允许一个连接
    • 性能较好
    • 会生成.mv.db文件
  2. 使用TCP/IP的服务器模式(远程连接)方式连接H2数据库(推荐)

    这种连接方式就和其他数据库类似了,是基于Service的形式进行连接的,因此允许多个客户端同时连接到H2数据库。

    连接语法:jdbc:h2:tcp://<server>[:<port>]/[<path>]<databaseName>

    举例:jdbc:h2:tcp://localhost/~/test

    特点:

    • 支持多客户端同时连接
    • 需要先启动H2服务器
    • 通过TCP/IP通信
    • 适合多用户环境
  3. H2数据库的内存模式,纯内存数据库

    连接语法:jdbc:h2:mem:<databaseName>

    • H2数据库被称为内存数据库,因为它支持在内存中创建数据库和表。
    • 注意:如果使用H2数据库的内存模式,那么我们创建的数据库和表都只是保存在内存中,一旦服务器重启,那么内存中的数据库和表就不存在了。所以 H2 数据库的持久化是个很关键的问题

可选配置

在用户目录下新建 .h2.server.properties,支持如下属性配置:

  • webAllowOthers: 是否允许远程连接,默认 false。
  • webPort: h2 端口,默认为 8082。
  • webSSL: 是否启用 SSL 加密连接,默认 false。
  • webAdminPassword: 超级管理员密码。

如果没有手动配置此文件,以 web-server方式首次启动 H2 后,点击打开的浏览器页面的 Save 按钮后就会自动创建一个。

端口配置

如果8082端口冲突,可以修改启动参数:

1
java -jar h2-1.4.200.jar -webPort 8083

允许远程访问

默认只允许本地访问,开启远程访问:

1
java -jar h2-1.4.200.jar -webAllowOthers

配置文件方式

在用户目录创建.h2.server.properties

1
2
3
4
webAllowOthers=true
webPort=8083
webSSL=false
webAdminPassword=admin123

在你的 Spring 项目中使用 H2 数据库

这一次经过 H2 的基本了解之后,我们就需要在实际项目中使用 H2 这种小巧的数据库方便我们的开发测试了

在 Spring Boot 项目里使用 H2 数据库,能让我们便捷地进行开发、测试,特别是在需要轻量级数据库支持,或者想快速验证数据操作逻辑时,H2 是个很不错的选择。使用一个项目来了解如何使用

而且,H2 数据库支持 Hibernate 3.1及以上的版本。 你能够使用 HSQLDB 方言,或是 H2 自己的方言。注意的是,在 Hibernate 中包含的 H2 方言有BUG,针对这些 BUG 的补丁已经被发布和修复,见https://hibernate.atlassian.net/browse/HHH-3401。你能够将它改名为H2Dialect.java,直接把它包含在你的应用中即可使用,或者使用 已经修复了这个问题的 Hibernate 的版本。

当使用 Hibernate,尝试使用 H2Dialect 如果可能的话。当使用H2Dialect,兼容性模式比如MODE=MySQL是不支持的。当使用兼容模式时,使用 Hibernate 相应的数据库的方言,而不是 H2Dialect;但请注意 H2 不支持所有数据库的所有功能。

我们在 Spring Boot 项目中(Servlet的Web应用也大差不差)使用 H2 数据库的思路如下,最简单(目前)的方法就是将数据库内嵌到应用中,这样就意味着应用启动的时候就打开了一个连接,数据库能被多个 session 和应用访问,他们跟应用运行在一个进程内,大部分的 Servlet 容器只适用一个进程(如Tomcat),这些容器都是没有问题的,Tomcat 使用多线程和多类加载器。如果多个应用同时访问同一个数据库,你需要将数据库的 jar 文件放在shared/lib或是server/lib目录。好的方案是 Web 应用启动时打开数据库,Web 应用停止时关闭数据库。如果是多个应用,只需要一个应用来处理启动和关闭。好的方案是一个 Session 一个连接,或者是一个请求(action)一个连接,连接使用完后尽可能的关闭它,当然不关闭并不会引起可怕的后果。服务器模式是差不多的,但是它可以运行在其他的进程中。

添加依赖

pom.xml 文件中添加 H2 数据库和 Spring Boot 数据访问相关依赖

1
2
3
4
5
6
7
8
9
10
11
<!-- Spring Boot 数据访问基础依赖,用于简化数据库操作 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- H2 数据库依赖 -->
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>

spring-boot-starter-data-jpa 是 Spring Boot 提供的用于简化 JPA(Java Persistence API,Java 持久化 API )操作的 starter 依赖,它能帮我们快速实现对数据库的各种操作;h2 依赖则引入了 H2 数据库的相关类和功能。

配置 application.properties

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 配置 H2 数据库的 JDBC 连接 URL,这里用嵌入式模式,数据存储在项目根目录下的 test.mv.db 文件中
spring.datasource.url=jdbc:h2:~/test
# 数据库驱动类,H2 数据库对应的驱动
spring.datasource.driver-class-name=org.h2.Driver
# 数据库用户名和密码
spring.datasource.username=ccb
spring.datasource.password=
# 配置 Hibernate 相关属性,Hibernate 是实现 JPA 规范的框架
spring.jpa.hibernate.ddl-auto=update
# 配置数据库方言,让 Hibernate 能正确生成适合 H2 数据库的 SQL 语句
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.show-sql=true
spring.h2.console.enabled=true
# 控制台访问路径,通过这个路径可以在浏览器中访问 H2 控制台,比如 http://localhost:8080/h2-console
spring.h2.console.path=/h2-console
spring.h2.console.settings.web-allow-others=true

如果是yml,如下

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
spring:
datasource:
# 配置 H2 数据库的 JDBC 连接 URL,这里用嵌入式模式,数据存储在项目根目录下的 test.mv.db 文件中
url: jdbc:h2:~/test
# 数据库驱动类,H2 数据库对应的驱动
driver-class-name: org.h2.Driver
# 数据库用户名
username: ccb
# 数据库密码
password:
jpa:
# 配置 Hibernate 相关属性,Hibernate 是实现 JPA 规范的框架
hibernate:
# 自动根据实体类创建、更新数据库表结构,创建表时会依据实体类字段和注解生成对应的表和列
ddl-auto: update
# 配置数据库方言,让 Hibernate 能正确生成适合 H2 数据库的 SQL 语句
database-platform: org.hibernate.dialect.H2Dialect
# 显示执行的 SQL 语句,方便开发调试时查看具体执行的 SQL 内容
show-sql: true
# 配置 H2 数据库控制台相关参数
h2:
console:
# 启用 H2 数据库控制台
enabled: true
# 控制台访问路径,通过这个路径可以在浏览器中访问 H2 控制台,比如 http://localhost:8080/h2-console
path: /h2-console
# 允许从远程访问控制台,开发时若需要在本地以外的机器访问控制台可开启(注意生产环境谨慎开启)
settings:
web-allow-others: true

创建实体类

比如针对前面的 users 表和 orders 表,创建对应的实体类

User 实体类

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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
package edu.software.ergoutree.h2andspringboottest.entity;

import java.sql.Timestamp;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;

@Entity
public class Users {
// 主键,自增
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "user_id")
private Integer userId;
// 用户名,非空且唯一
private String username;
// 邮箱,非空
private String email;
// 密码,非空
private String password;
// 创建时间,默认当前时间戳
@Column(name = "create_time")
private Timestamp createTime;
// 最后登录时间
@Column(name = "last_login")
private Timestamp lastLogin;

// 省略 getter 和 setter 方法
public Integer getUserId() {
return userId;
}

public void setUserId(Integer userId) {
this.userId = userId;
}

public String getUsername() {
return username;
}

public void setUsername(String username) {
this.username = username;
}

public String getEmail() {
return email;
}

public void setEmail(String email) {
this.email = email;
}

public String getPassword() {
return password;
}

public void setPassword(String password) {
this.password = password;
}

public Timestamp getCreateTime() {
return createTime;
}

public void setCreateTime(Timestamp createTime) {
this.createTime = createTime;
}

public Timestamp getLastLogin() {
return lastLogin;
}

public void setLastLogin(Timestamp lastLogin) {
this.lastLogin = lastLogin;
}
}

Order 实体类

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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
package edu.software.ergoutree.h2andspringboottest.entity;

import java.math.BigDecimal;
import java.sql.Timestamp;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.ManyToOne;

@Entity
public class Orders {
// 主键,自增
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "order_id")
private Integer orderId;
// 关联用户表,多对一关系,一个用户可以有多个订单
@ManyToOne
@JoinColumn(name = "user_id")
private Users user;
// 订单金额,精度 10 位,小数 2 位
@Column(name = "order_amount")
private BigDecimal orderAmount;
// 订单状态,默认 pending
@Column(name = "order_status")
private String orderStatus;
// 订单时间,非空
@Column(name = "order_time")
private Timestamp orderTime;

// 省略 getter 和 setter 方法
public Integer getOrderId() {
return orderId;
}

public void setOrderId(Integer orderId) {
this.orderId = orderId;
}

public Users getUser() {
return user;
}

public void setUser(Users user) {
this.user = user;
}

public BigDecimal getOrderAmount() {
return orderAmount;
}

public void setOrderAmount(BigDecimal orderAmount) {
this.orderAmount = orderAmount;
}

public String getOrderStatus() {
return orderStatus;
}

public void setOrderStatus(String orderStatus) {
this.orderStatus = orderStatus;
}

public Timestamp getOrderTime() {
return orderTime;
}

public void setOrderTime(Timestamp orderTime) {
this.orderTime = orderTime;
}
}

创建 Repository 接口(JPA Repository)

通过继承 JpaRepository 来实现对数据库的基本操作,无需手动编写 SQL 语句(当然也支持自定义 SQL )。

UsersRepository

1
2
3
4
5
6
7
8
9
package edu.software.ergoutree.h2andspringboottest.repository;

import edu.software.ergoutree.h2andspringboottest.entity.Users;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UsersRepository extends JpaRepository<Users, Integer> {
// 可以自定义查询方法,比如根据用户名查询用户,遵循 JPA 方法命名规则
Users findByUsername(String username);
}

OrdersRepository

1
2
3
4
5
6
7
8
9
10
import edu.software.ergoutree.h2andspringboottest.entity.Orders;
import edu.software.ergoutree.h2andspringboottest.entity.Users;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface OrdersRepository extends JpaRepository<Orders, Integer> {
// 自定义查询方法,比如根据用户和订单状态查询订单
List<Orders> findByUserAndOrderStatus(Users user, String status);
}

JpaRepository 提供了诸如 save(保存实体 )、findAll(查询所有实体 )、delete(删除实体 )等基础方法;自定义方法像 findByUsername 是按照 JPA 的方法命名规则来定义的,框架会自动生成对应的 SQL 语句执行查询操作。

编写业务逻辑和控制器(Service 和 Controller)

UsersService

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
package edu.software.ergoutree.h2andspringboottest.service;

import java.util.List;
import java.util.Optional;

import org.springframework.stereotype.Service;

import edu.software.ergoutree.h2andspringboottest.entity.Users;
import edu.software.ergoutree.h2andspringboottest.repository.UsersRepository;

@Service
public class UsersService {
private final UsersRepository usersRepository;

public UsersService(UsersRepository usersRepository) {
this.usersRepository = usersRepository;
}

// 保存用户信息
public Users saveUser(Users user) {
return usersRepository.save(user);
}

// 根据用户ID查询用户
public Optional<Users> findUserById(Integer id) {
return usersRepository.findById(id);
}

// 根据用户名查询用户
public Users findUserByUsername(String username) {
return usersRepository.findByUsername(username);
}

// 查询所有用户
public List<Users> findAllUsers() {
return usersRepository.findAll();
}
}

OrdersService

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
package edu.software.ergoutree.h2andspringboottest.service;

import java.util.List;
import java.util.Optional;

import org.springframework.stereotype.Service;

import edu.software.ergoutree.h2andspringboottest.entity.Orders;
import edu.software.ergoutree.h2andspringboottest.entity.Users;
import edu.software.ergoutree.h2andspringboottest.repository.OrdersRepository;

@Service
public class OrdersService {
private final OrdersRepository ordersRepository;

public OrdersService(OrdersRepository ordersRepository) {
this.ordersRepository = ordersRepository;
}

// 保存订单信息
public Orders saveOrder(Orders order) {
return ordersRepository.save(order);
}

// 根据订单 ID 查询订单
public Optional<Orders> findOrderById(Integer id) {
return ordersRepository.findById(id);
}

// 根据用户和订单状态查询订单列表
public List<Orders> findOrdersByUserAndStatus(Users user, String status) {
return ordersRepository.findByUserAndOrderStatus(user, status);
}

// 查询所有订单
public List<Orders> findAllOrders() {
return ordersRepository.findAll();
}
}

UsersController

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
package edu.software.ergoutree.h2andspringboottest.controller;

import java.util.List;
import java.util.Optional;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import edu.software.ergoutree.h2andspringboottest.entity.Users;
import edu.software.ergoutree.h2andspringboottest.service.UsersService;

@RestController
@RequestMapping("/api/users")
public class UsersController {
private final UsersService usersService;

public UsersController(UsersService usersService) {
this.usersService = usersService;
}

// 创建用户
@PostMapping
public ResponseEntity<Users> createUser(@RequestBody Users user) {
Users savedUser = usersService.saveUser(user);
return ResponseEntity.ok(savedUser);
}

// 根据用户ID查询用户
@GetMapping("/{id}")
public ResponseEntity<Users> getUserById(@PathVariable Integer id) {
Optional<Users> user = usersService.findUserById(id);
return user.map(ResponseEntity::ok)
.orElseGet(() -> ResponseEntity.notFound().build());
}

// 根据用户名查询用户
@GetMapping("/username/{username}")
public ResponseEntity<Users> getUserByUsername(@PathVariable String username) {
Users user = usersService.findUserByUsername(username);
if (user == null) {
return ResponseEntity.notFound().build();
}
return ResponseEntity.ok(user);
}

// 获取所有用户
@GetMapping
public ResponseEntity<List<Users>> getAllUsers() {
List<Users> users = usersService.findAllUsers();
return ResponseEntity.ok(users);
}
}

OrdersController

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
59
60
61
package edu.software.ergoutree.h2andspringboottest.controller;

import edu.software.ergoutree.h2andspringboottest.entity.Orders;
import edu.software.ergoutree.h2andspringboottest.entity.Users;
import edu.software.ergoutree.h2andspringboottest.service.OrdersService;
import edu.software.ergoutree.h2andspringboottest.service.UsersService;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Optional;

@RestController
@RequestMapping("/api/orders")
public class OrdersController {
private final OrdersService ordersService;
private final UsersService usersService;

public OrdersController(OrdersService ordersService, UsersService usersService) {
this.ordersService = ordersService;
this.usersService = usersService;
}

// 创建订单
@PostMapping
public ResponseEntity<Orders> createOrder(@RequestBody Orders order) {
Orders savedOrder = ordersService.saveOrder(order);
return ResponseEntity.ok(savedOrder);
}

// 根据订单ID查询订单
@GetMapping("/{id}")
public ResponseEntity<Orders> getOrderById(@PathVariable Integer id) {
Optional<Orders> order = ordersService.findOrderById(id);
return order.map(ResponseEntity::ok)
.orElseGet(() -> ResponseEntity.notFound().build());
}

// 根据用户ID和订单状态查询订单
@GetMapping("/user/{userId}/status/{status}")
public ResponseEntity<List<Orders>> getOrdersByUserAndStatus(
@PathVariable Integer userId,
@PathVariable String status) {

Optional<Users> user = usersService.findUserById(userId);

if (user.isEmpty()) {
return ResponseEntity.notFound().build();
}

List<Orders> orders = ordersService.findOrdersByUserAndStatus(user.get(), status);
return ResponseEntity.ok(orders);
}

// 获取所有订单
@GetMapping
public ResponseEntity<List<Orders>> getAllOrders() {
List<Orders> orders = ordersService.findAllOrders();
return ResponseEntity.ok(orders);
}
}

H2 数据库控制台的使用

H2 控制台是一个包含在 web 服务中的独立的应用

前面在配置里启用了 H2 控制台,启动 Spring Boot 项目后,在浏览器中访问 http://localhost:8080/h2-console(具体路径根据配置的 spring.h2.console.path 而定 ),会出现登录页面,填写对应的 JDBC URL、用户名、密码(就是配置文件里 spring.datasource 相关的配置 ),登录后就能看到数据库的表结构、执行 SQL 语句、查看数据等。比如可以在控制台里执行 SELECT * FROM users; 来查询用户表数据,方便开发过程中调试和检查数据。

冷知识,H2支持原生的CSV

CSV(逗号分隔文件)文件在数据库系统中支持CSVREADCSVWRITE方法,也可以把它作为数据库之外的一个工具来使用。 将数据库查询结果写成CSV文件

image-20250619211909855

可以看到访问网址就来到了如上控制台页面,和上面的都是一样的,输入你配置文件中的密码,就可以进入了,进入后发现 Hibernate 为我们的实体类已经自动创建表

重新写一个Home定向页面,来看看各个该Spring程序的各个功能进行测试

可以看到是没有任何问题的

image-20250619212554001
image-20250619212624133

在 Spring Boot 与 H2 的搭配中使用连接池

H2 数据库的连接池机制通过复用数据库连接,避免了频繁创建和关闭连接的开销。H2 内置连接池基于Mini Connection Pool Manager,相比直接使用DriverManager.getConnection()有以下优势:

  • 性能提升:连接池管理的连接获取速度比原生方式快 2 倍以上
  • 资源复用:维护一定数量的活跃连接,避免重复握手开销
  • 连接管理:自动处理连接超时、异常回收等场景

在 Spring Boot 中配置 H2 内置连接池只需在数据源配置中添加连接池参数:

1
2
3
4
5
6
7
8
9
spring:
datasource:
url: jdbc:h2:file:~/test;DB_CLOSE_DELAY=-1;POOL_SIZE=10;AUTO_COMMIT=true
driver-class-name: org.h2.Driver
username: sa
password:
h2:
console:
enabled: true

关键参数说明:

  • POOL_SIZE:连接池最大连接数(默认 10)
  • AUTO_COMMIT:是否自动提交事务(默认 true)
  • MAX_IDLE_TIME:连接最大空闲时间(毫秒,默认 60000)
  • CONNECTION_TIMEOUT:获取连接超时时间(毫秒,默认 30000)

若需更强大的连接池功能,可集成 Apache Commons DBCP

1
2
3
4
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-dbcp2</artifactId>
</dependency>

我使用 Spring Boot 默认的 Hikari 数据池添加如下控制器和服务层

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
package edu.software.ergoutree.h2andspringboottest.controller;

import java.util.Map;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import edu.software.ergoutree.h2andspringboottest.service.DataSourceMonitorService;

/**
* 数据源监控控制器
* 提供API接口用于监控和测试数据库连接池
*/
@RestController
@RequestMapping("/api/datasource")
public class DataSourceMonitorController {

private final DataSourceMonitorService monitorService;

public DataSourceMonitorController(DataSourceMonitorService monitorService) {
this.monitorService = monitorService;
}

/**
* 获取连接池状态
*
* @return 连接池统计信息
*/
@GetMapping("/stats")
public ResponseEntity<Map<String, Object>> getPoolStats() {
return ResponseEntity.ok(monitorService.getPoolStats());
}

/**
* 测试数据库连接
*
* @return 连接测试结果
*/
@GetMapping("/test-connection")
public ResponseEntity<Map<String, Object>> testConnection() {
return ResponseEntity.ok(monitorService.testConnection());
}

/**
* 测试连接池性能
*
* @param count 要创建的连接数量
* @return 性能测试结果
*/
@GetMapping("/test-performance")
public ResponseEntity<Map<String, Object>> testPerformance(
@RequestParam(defaultValue = "10") int count) {
return ResponseEntity.ok(monitorService.createMultipleConnections(count));
}
}
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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
package edu.software.ergoutree.h2andspringboottest.service;

import java.sql.Connection;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.Map;

import javax.sql.DataSource;

import org.springframework.stereotype.Service;

import com.zaxxer.hikari.HikariDataSource;
import com.zaxxer.hikari.HikariPoolMXBean;

/**
* 数据源监控服务
* 用于监控和测试数据库连接池的状态
*/
@Service
public class DataSourceMonitorService {

private final DataSource dataSource;

public DataSourceMonitorService(DataSource dataSource) {
this.dataSource = dataSource;
}

/**
* 获取连接池状态信息
*
* @return 包含连接池统计信息的Map
*/
public Map<String, Object> getPoolStats() {
Map<String, Object> stats = new HashMap<>();

if (dataSource instanceof HikariDataSource) {
HikariDataSource hikariDataSource = (HikariDataSource) dataSource;
HikariPoolMXBean poolMXBean = hikariDataSource.getHikariPoolMXBean();

stats.put("poolName", hikariDataSource.getPoolName());
stats.put("activeConnections", poolMXBean.getActiveConnections());
stats.put("idleConnections", poolMXBean.getIdleConnections());
stats.put("totalConnections", poolMXBean.getTotalConnections());
stats.put("threadsAwaitingConnection", poolMXBean.getThreadsAwaitingConnection());
stats.put("maxPoolSize", hikariDataSource.getMaximumPoolSize());
} else {
stats.put("message", "Not a HikariCP datasource");
stats.put("dataSourceClass", dataSource.getClass().getName());
}

return stats;
}

/**
* 测试数据库连接
*
* @return 测试结果
*/
public Map<String, Object> testConnection() {
Map<String, Object> result = new HashMap<>();

long startTime = System.currentTimeMillis();
try (Connection connection = dataSource.getConnection()) {
long endTime = System.currentTimeMillis();

result.put("status", "success");
result.put("connectionValid", connection.isValid(5));
result.put("connectionTime", (endTime - startTime) + "ms");
result.put("connectionClass", connection.getClass().getName());
result.put("autoCommit", connection.getAutoCommit());

} catch (SQLException e) {
result.put("status", "error");
result.put("errorMessage", e.getMessage());
result.put("sqlState", e.getSQLState());
result.put("errorCode", e.getErrorCode());
}

return result;
}

/**
* 创建多个连接以测试连接池性能
*
* @param count 要创建的连接数量
* @return 测试结果
*/
public Map<String, Object> createMultipleConnections(int count) {
Map<String, Object> result = new HashMap<>();
long startTime = System.currentTimeMillis();
int successCount = 0;
int failureCount = 0;

for (int i = 0; i < count; i++) {
try (Connection connection = dataSource.getConnection()) {
if (connection.isValid(1)) {
successCount++;
} else {
failureCount++;
}
// 执行一个简单的查询以模拟真实使用
connection.createStatement().execute("SELECT 1");
} catch (SQLException e) {
failureCount++;
}
}

long endTime = System.currentTimeMillis();
result.put("totalConnections", count);
result.put("successCount", successCount);
result.put("failureCount", failureCount);
result.put("totalTime", (endTime - startTime) + "ms");
result.put("averageTime", (double)(endTime - startTime) / count + "ms");
result.put("poolStats", getPoolStats());

return result;
}
}

可以发现 H2 数据库成功连接到连接池,并且进行了数据访问

image-20250620113120874
image-20250620113133393

测试

可以编写单元测试或集成测试来验证数据库操作是否正确。比如使用 SpringBootTest 注解进行集成测试

测试才是在 Spring Boot 中使用内存数据库的精髓之处,因为H2 数据库作为轻量级内存数据库,在 Spring Boot 测试体系中具备不可替代的优势:

  • 隔离性测试环境:每次测试启动时创建独立数据库实例,测试结束后自动销毁,避免真实数据库的数据污染
  • 提升测试效率:内存中运行速度极快,相比传统数据库可减少 70% 以上的测试启动时间
  • 简化测试配置:无需部署独立数据库服务,通过纯 Java 驱动即可完成测试环境搭建
  • 精准测试验证:支持 SQL 标准语法,能准确模拟生产环境的数据库操作逻辑

在 Spring Boot 测试体系中,H2 内存数据库的应用不仅仅是 “替代真实数据库”,其核心价值体现在内存数据库的轻量级特性大幅缩短 CI/CD 流水线时间,而且无需维护独立数据库实例,测试脚本可随代码一同版本管理。

这时候,可以在测试专用的配置文件下写一个如下配置项来保持测试时候数据库表的隔离性

1
spring.jpa.hibernate.ddl-auto=create-drop  # 测试时自动创建/删除表结构

可以进行的测试如下,当然,数据库的CURD测试可以做到,那么控制器层,接口测试和集成测试也就相对应的可以完美实现。

基于 @DataJpaTest 的单元测试(Repository 层)

  • UsersRepositoryTest - 测试用户仓库的CRUD操作

    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
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    package edu.software.ergoutree.h2andspringboottest.repository;

    import java.sql.Timestamp;
    import java.time.Instant;
    import java.util.List;
    import java.util.Optional;

    import static org.assertj.core.api.Assertions.assertThat;

    import org.junit.jupiter.api.BeforeEach;
    import org.junit.jupiter.api.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
    import org.springframework.test.annotation.DirtiesContext;

    import edu.software.ergoutree.h2andspringboottest.entity.Users;

    @DataJpaTest
    @DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
    public class UsersRepositoryTest {

    @Autowired
    private UsersRepository usersRepository;

    @BeforeEach
    public void setup() {
    // 清空用户表
    usersRepository.deleteAll();
    }

    @Test
    public void testSaveUser() {
    // 准备测试数据
    Users user = new Users();
    user.setUsername("testuser");
    user.setEmail("test@example.com");
    user.setPassword("password123");
    user.setCreateTime(Timestamp.from(Instant.now()));

    // 保存用户
    Users savedUser = usersRepository.save(user);

    // 验证结果
    assertThat(savedUser.getUserId()).isNotNull();
    assertThat(savedUser.getUsername()).isEqualTo("testuser");
    assertThat(savedUser.getEmail()).isEqualTo("test@example.com");
    }

    @Test
    public void testFindByUsername() {
    // 准备测试数据
    Users user1 = new Users();
    user1.setUsername("user1");
    user1.setEmail("user1@example.com");
    user1.setPassword("password1");
    user1.setCreateTime(Timestamp.from(Instant.now()));
    usersRepository.save(user1);

    Users user2 = new Users();
    user2.setUsername("user2");
    user2.setEmail("user2@example.com");
    user2.setPassword("password2");
    user2.setCreateTime(Timestamp.from(Instant.now()));
    usersRepository.save(user2);

    // 测试根据用户名查找
    Users foundUser = usersRepository.findByUsername("user1");

    // 验证结果
    assertThat(foundUser).isNotNull();
    assertThat(foundUser.getEmail()).isEqualTo("user1@example.com");
    }

    @Test
    public void testFindAll() {
    // 准备测试数据
    Users user1 = new Users();
    user1.setUsername("user1");
    user1.setEmail("user1@example.com");
    user1.setPassword("password1");
    user1.setCreateTime(Timestamp.from(Instant.now()));
    usersRepository.save(user1);

    Users user2 = new Users();
    user2.setUsername("user2");
    user2.setEmail("user2@example.com");
    user2.setPassword("password2");
    user2.setCreateTime(Timestamp.from(Instant.now()));
    usersRepository.save(user2);

    // 测试查找所有用户
    List<Users> allUsers = usersRepository.findAll();

    // 验证结果
    assertThat(allUsers).hasSize(2);
    assertThat(allUsers).extracting(Users::getUsername).containsExactlyInAnyOrder("user1", "user2");
    }

    @Test
    public void testDeleteUser() {
    // 准备测试数据
    Users user = new Users();
    user.setUsername("userToDelete");
    user.setEmail("delete@example.com");
    user.setPassword("password");
    user.setCreateTime(Timestamp.from(Instant.now()));
    Users savedUser = usersRepository.save(user);

    // 删除用户
    usersRepository.deleteById(savedUser.getUserId());

    // 验证结果
    Optional<Users> deletedUser = usersRepository.findById(savedUser.getUserId());
    assertThat(deletedUser).isEmpty();
    }
    }
  • OrdersRepositoryTest - 测试订单仓库的api可用性

    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
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    package edu.software.ergoutree.h2andspringboottest.controller;

    import java.math.BigDecimal;
    import java.sql.Timestamp;
    import java.time.Instant;

    import static org.hamcrest.Matchers.containsInAnyOrder;
    import static org.hamcrest.Matchers.everyItem;
    import static org.hamcrest.Matchers.hasSize;
    import static org.hamcrest.Matchers.is;
    import org.junit.jupiter.api.BeforeEach;
    import org.junit.jupiter.api.Test;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
    import org.springframework.boot.test.context.SpringBootTest;
    import org.springframework.http.MediaType;
    import org.springframework.test.annotation.DirtiesContext;
    import org.springframework.test.web.servlet.MockMvc;
    import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
    import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
    import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
    import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
    import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

    import com.fasterxml.jackson.databind.ObjectMapper;

    import edu.software.ergoutree.h2andspringboottest.entity.Orders;
    import edu.software.ergoutree.h2andspringboottest.entity.Users;
    import edu.software.ergoutree.h2andspringboottest.service.OrdersService;
    import edu.software.ergoutree.h2andspringboottest.service.UsersService;

    @SpringBootTest
    @AutoConfigureMockMvc
    @DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
    public class OrdersControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private OrdersService ordersService;

    @Autowired
    private UsersService usersService;

    @Autowired
    private ObjectMapper objectMapper;

    private Users testUser;

    @BeforeEach
    public void setup() {
    // 创建测试用户
    testUser = new Users();
    testUser.setUsername("orderControllerTestUser");
    testUser.setEmail("ordercontroller@example.com");
    testUser.setPassword("password");
    testUser.setCreateTime(Timestamp.from(Instant.now()));
    testUser = usersService.saveUser(testUser);
    }

    @Test
    public void testGetOrderById() throws Exception {
    // 准备测试数据
    Orders order = new Orders();
    order.setUser(testUser);
    order.setOrderAmount(new BigDecimal("123.45"));
    order.setOrderStatus("processing");
    order.setOrderTime(Timestamp.from(Instant.now()));
    Orders savedOrder = ordersService.saveOrder(order);

    // 执行GET请求并验证结果
    mockMvc.perform(get("/api/orders/{id}", savedOrder.getOrderId()))
    .andExpect(status().isOk())
    .andExpect(content().contentType(MediaType.APPLICATION_JSON))
    .andExpect(jsonPath("$.orderAmount", is(123.45)))
    .andExpect(jsonPath("$.orderStatus", is("processing")));
    }

    @Test
    public void testGetOrdersByUserIdAndStatus() throws Exception {
    // 准备测试数据
    Orders order1 = new Orders();
    order1.setUser(testUser);
    order1.setOrderAmount(new BigDecimal("100.00"));
    order1.setOrderStatus("pending");
    order1.setOrderTime(Timestamp.from(Instant.now()));
    ordersService.saveOrder(order1);

    Orders order2 = new Orders();
    order2.setUser(testUser);
    order2.setOrderAmount(new BigDecimal("200.00"));
    order2.setOrderStatus("pending");
    order2.setOrderTime(Timestamp.from(Instant.now()));
    ordersService.saveOrder(order2);

    Orders order3 = new Orders();
    order3.setUser(testUser);
    order3.setOrderAmount(new BigDecimal("300.00"));
    order3.setOrderStatus("completed");
    order3.setOrderTime(Timestamp.from(Instant.now()));
    ordersService.saveOrder(order3);

    // 执行GET请求并验证结果
    mockMvc.perform(get("/api/orders/user/{userId}/status/{status}", testUser.getUserId(), "pending"))
    .andExpect(status().isOk())
    .andExpect(content().contentType(MediaType.APPLICATION_JSON))
    .andExpect(jsonPath("$", hasSize(2)))
    .andExpect(jsonPath("$[*].orderAmount", containsInAnyOrder(100.00, 200.00)))
    .andExpect(jsonPath("$[*].orderStatus", everyItem(is("pending"))));
    }

    @Test
    public void testCreateOrder() throws Exception {
    // 准备测试数据
    Orders order = new Orders();
    order.setUser(testUser);
    order.setOrderAmount(new BigDecimal("999.99"));
    order.setOrderStatus("new");
    order.setOrderTime(Timestamp.from(Instant.now()));

    // 执行POST请求并验证结果
    mockMvc.perform(post("/api/orders")
    .contentType(MediaType.APPLICATION_JSON)
    .content(objectMapper.writeValueAsString(order)))
    .andExpect(status().isOk())
    .andExpect(jsonPath("$.orderAmount", is(999.99)))
    .andExpect(jsonPath("$.orderStatus", is("new")));
    }

    @Test
    public void testGetNonExistentOrder() throws Exception {
    // 执行GET请求并验证结果 - 应该返回404
    mockMvc.perform(get("/api/orders/999"))
    .andExpect(status().isNotFound());
    }
    }
    image-20250620114801513

​ 可以发现 H2 数据库做测试是很方便而且环境高度隔离的

基于 @SpringBootTest 的集成测试(全流程验证)

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
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
package edu.software.ergoutree.h2andspringboottest.integration;

@SpringBootTest
@DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
@Transactional
public class UserOrderIntegrationTest {

@Autowired
private UsersService usersService;

@Autowired
private OrdersService ordersService;

@Autowired
private JdbcTemplate jdbcTemplate;

@Test
public void testCreateUserAndOrder() {
// 创建用户
Users user = new Users();
user.setUsername("integrationTestUser");
user.setEmail("integration@example.com");
user.setPassword("password");
user.setCreateTime(Timestamp.from(Instant.now()));
Users savedUser = usersService.saveUser(user);

// 创建订单
Orders order = new Orders();
order.setUser(savedUser);
order.setOrderAmount(new BigDecimal("999.99"));
order.setOrderStatus("processing");
order.setOrderTime(Timestamp.from(Instant.now()));
Orders savedOrder = ordersService.saveOrder(order);

// 验证用户和订单关联关系
List<Orders> userOrders = ordersService.findOrdersByUserAndStatus(savedUser, "processing");
assertThat(userOrders).hasSize(1);
assertThat(userOrders.get(0).getOrderId()).isEqualTo(savedOrder.getOrderId());
assertThat(userOrders.get(0).getUser().getUserId()).isEqualTo(savedUser.getUserId());
}

@Test
@Sql("/test-data.sql")
public void testWithSqlScript() {
// 这个测试方法会在执行前运行 test-data.sql 脚本,预先填充测试数据

// 验证脚本创建的用户数据
Users user = usersService.findUserByUsername("scriptUser");
assertThat(user).isNotNull();
assertThat(user.getEmail()).isEqualTo("script@example.com");

// 验证脚本创建的订单数据
List<Orders> orders = ordersService.findOrdersByUserAndStatus(user, "completed");
assertThat(orders).hasSize(1);
assertThat(orders.get(0).getOrderAmount()).isEqualTo(new BigDecimal("888.88"));
}

@Test
public void testDirectJdbcAccess() {
// 创建用户
Users user = new Users();
user.setUsername("jdbcTestUser");
user.setEmail("jdbc@example.com");
user.setPassword("password");
user.setCreateTime(Timestamp.from(Instant.now()));
Users savedUser = usersService.saveUser(user);

// 创建订单
Orders order = new Orders();
order.setUser(savedUser);
order.setOrderAmount(new BigDecimal("123.45"));
order.setOrderStatus("pending");
order.setOrderTime(Timestamp.from(Instant.now()));
ordersService.saveOrder(order);

// 使用原生SQL查询验证数据
String sql = "SELECT u.username, u.email, o.order_amount, o.order_status " +
"FROM users u JOIN orders o ON u.user_id = o.user_id " +
"WHERE u.username = ?";

List<Map<String, Object>> results = jdbcTemplate.queryForList(sql, "jdbcTestUser");

assertThat(results).hasSize(1);
assertThat(results.get(0).get("USERNAME").toString()).isEqualTo("jdbcTestUser");
assertThat(results.get(0).get("EMAIL").toString()).isEqualTo("jdbc@example.com");
assertThat(results.get(0).get("ORDER_STATUS").toString()).isEqualTo("pending");
}
}
image-20250620114851241

可以从上述测试过程中看出,从仓库到控制器到项目集成测试的各个层次,充分展示了H2数据库在Spring Boot项目中的测试优势。