Spring JDBC实战:参数处理与嵌入式数据库
以下内容是 Spring Framework 官方文档中关于 JDBC 支持的两个重要章节(3.8 和 3.9),主要讲解了在使用 Spring 的 JdbcTemplate 和相关工具时,如何处理一些常见的、复杂的数据库操作问题,以及如何使用嵌入式数据库(Embedded Database)进行开发和测试。
下面我将用通俗易懂的方式,帮你系统地理解这段内容的核心思想和关键知识点,并说明它们在实际开发中的意义。
🧩 一、整体结构概览
| 章节 | 主题 | 用途 |
|---|---|---|
| 3.8 | 参数与数据值处理常见问题 | 解决 JDBC 操作中的特殊场景 |
| 3.9 | 嵌入式数据库支持 | 快速搭建轻量级数据库用于开发/测试 |
🔍 二、深入理解 3.8 节:参数与数据值处理的常见问题
✅ 3.8.1 提供 SQL 类型信息(SQL Type for Parameters)
❓ 问题背景:
Java 中的 null 值传给数据库时,JDBC 不知道它对应的是 VARCHAR 还是 INTEGER,所以无法正确设置类型。这会导致插入 NULL 失败。
✅ 解决方案:
Spring 允许你在设置参数时显式指定 SQL 类型(来自 java.sql.Types 的常量)。
三种方式:
-
int 数组方式(适用于位置参数)
jdbcTemplate.update("INSERT INTO users(name, age) VALUES(?, ?)","Tom", 25,new int[]{Types.VARCHAR, Types.INTEGER} // 显式指定类型 ); -
使用
SqlParameterValue包装参数new SqlParameterValue(Types.VARCHAR, "Tom")可以更精细控制,比如设置
scale(小数位数)。 -
命名参数 +
SqlParameterSource
使用MapSqlParameterSource或BeanPropertySqlParameterSource并注册类型:MapSqlParameterSource params = new MapSqlParameterSource(); params.addValue("name", "Tom", Types.VARCHAR); params.addValue("age", 25, Types.INTEGER);
💡 关键点:主要用于处理
NULL插入或类型模糊的情况。
✅ 3.8.2 处理 BLOB 和 CLOB(大对象)
❓ 什么是 BLOB / CLOB?
- BLOB:Binary Large Object → 图片、音频、PDF 等二进制文件。
- CLOB:Character Large Object → 大段文本(如文章、日志)。
❓ 为什么需要特殊处理?
普通 String 或 byte[] 在读写大文件时会占用大量内存。理想做法是流式处理。
✅ Spring 的解决方案:LobHandler + LobCreator
| 功能 | 接口 | 方法 |
|---|---|---|
| 写入 LOB | LobCreator | setBlobAsBinaryStream, setClobAsCharacterStream |
| 读取 LOB | LobHandler | getBlobAsBytes, getClobAsString |
示例:插入图片和文本文件
jdbcTemplate.execute("INSERT INTO docs(id, content, image) VALUES (?, ?, ?)",new AbstractLobCreatingPreparedStatementCallback(lobHandler) {protected void setValues(PreparedStatement ps, LobCreator lc) throws SQLException {ps.setLong(1, 1L);lc.setClobAsCharacterStream(ps, 2, reader, (int)file.length()); // CLOBlc.setBlobAsBinaryStream(ps, 3, inputStream, (int)image.length()); // BLOB}}
);
⚠️ 注意:
lobHandler通常是DefaultLobHandler,但注意它不支持流式读取超过Integer.MAX_VALUE的数据。
✅ 优势:避免一次性加载整个大文件到内存。
✅ 3.8.3 为 IN 子句传递列表(List in IN Clause)
❓ 问题:
SQL 不允许预编译语句动态占位符数量,比如:
SELECT * FROM users WHERE id IN (?)
但如果要传 (1,2,3),就需要三个 ?。
✅ Spring 的解决方案:
自动拼接 SQL,根据 List 长度生成对应数量的 ?。
List<Long> ids = Arrays.asList(1L, 2L, 3L);
List<User> users = jdbcTemplate.query("SELECT * FROM users WHERE id IN (:ids)",new MapSqlParameterSource("ids", ids),userRowMapper
);
✅ 使用
NamedParameterJdbcTemplate自动处理。
⚠️ 注意限制:
- 大多数数据库对
IN列表有上限(如 Oracle 是 1000)。 - 如果超过,应分批查询。
✅ 3.8.4 存储过程调用中的复杂类型(Complex Types)
❓ 问题:
某些数据库(如 Oracle)支持自定义对象类型(如 STRUCT, ARRAY),Java 如何传递和接收?
✅ 解决方案:
-
输出参数(返回复杂类型)→
SqlReturnTypedeclareParameter(new SqlOutParameter("item", OracleTypes.STRUCT, "ITEM_TYPE",(cs, idx, sqlType, typeName) -> {STRUCT struct = (STRUCT) cs.getObject(idx);// 转换为 Java 对象return new TestItem(...);})); -
输入参数(传入复杂类型)→
SqlTypeValueSqlTypeValue value = new AbstractSqlTypeValue() {protected Object createTypeValue(Connection conn, ...) {StructDescriptor desc = new StructDescriptor("ITEM_TYPE", conn);return new STRUCT(desc, conn, new Object[]{id, name, date});} };然后作为参数传入:
Map<String, Object> in = new HashMap<>(); in.put("item", value); storedProc.execute(in);
✅ 适用于 Oracle、PostgreSQL 等支持复杂类型的数据库。
🛠️ 三、深入理解 3.9 节:嵌入式数据库(Embedded Database)
✅ 3.9.1 为什么要用嵌入式数据库?
- 轻量快速:无需安装 MySQL/PostgreSQL。
- 启动快:内存中运行,适合单元测试。
- 隔离性好:每个测试独立数据库,不污染真实数据。
- 便于自动化测试:CI/CD 中无需外部依赖。
常见用途:单元测试、集成测试、原型开发
✅ 3.9.2 通过 XML 创建嵌入式数据库
<jdbc:embedded-database id="dataSource" generate-name="true"><jdbc:script location="classpath:schema.sql"/><jdbc:script location="classpath:test-data.sql"/>
</jdbc:embedded-database>
- 自动创建 HSQL 内存数据库。
- 执行建表脚本和测试数据脚本。
- 生成一个
DataSourceBean,可注入 DAO。
✅ 3.9.3 编程方式创建(推荐用于测试)
EmbeddedDatabase db = new EmbeddedDatabaseBuilder().generateUniqueName(true).setType(H2).addScript("schema.sql").addScripts("data.sql").build();
✅
EmbeddedDatabase实现了DataSource,可以直接传给JdbcTemplate。
✅ 测试结束后记得
db.shutdown()。
✅ 3.9.4 支持的嵌入式数据库类型
| 类型 | 说明 |
|---|---|
| HSQL | 默认,老牌嵌入式数据库 |
| H2 | 功能强大,支持 MySQL 模式、Web 控制台 |
| Derby | Apache 开源,Java 编写 |
推荐使用 H2,功能最全,调试方便。
✅ 3.9.5 测试数据访问逻辑(最佳实践模板)
class DataAccessTest {private EmbeddedDatabase db;@BeforeEachvoid setUp() {db = new EmbeddedDatabaseBuilder().generateUniqueName(true).addDefaultScripts() // 自动加载 schema.sql + data.sql.build();}@Testvoid shouldFindUserById() {JdbcTemplate template = new JdbcTemplate(db);List<User> users = template.query("SELECT * FROM users WHERE id = ?", new Object[]{1}, userRowMapper);assertThat(users).hasSize(1);}@AfterEachvoid tearDown() {db.shutdown();}
}
✅ 这是典型的 集成测试模板。
✅ 3.9.6 为什么需要“唯一数据库名”?
❓ 问题:
多个测试类都用相同的配置创建嵌入式数据库,Spring 会复用同一个实例(因为默认名字是 testdb),导致数据污染。
✅ 解决方案:
启用唯一名称生成:
.generateUniqueName(true)
或 XML 中:
<jdbc:embedded-database generate-name="true">
✅ 每次创建都是独立数据库,避免冲突。
✅ 3.9.7 扩展支持(高级)
你可以自定义:
- 新的嵌入式数据库类型(实现
EmbeddedDatabaseConfigurer) - 新的
DataSource工厂(如连接池)
一般开发者不需要做,但框架开发者可以贡献扩展。
🎯 四、总结:这些知识能用来做什么?
| 技术点 | 实际应用场景 |
|---|---|
SqlParameterValue | 插入 NULL 字段,确保类型正确 |
LobHandler | 存储图片、文档、大文本(如博客内容) |
IN 子句传 List | 批量查询用户、订单等 |
SqlTypeValue | 调用 Oracle 存储过程传对象 |
| 嵌入式数据库 | 单元测试、快速原型、CI/CD 自动化 |
📌 五、学习建议
- 先掌握
JdbcTemplate基础用法(查询、更新) - 再学命名参数和
IN子句(日常开发高频) - 测试时使用嵌入式数据库(提升测试效率)
- 只有对接 Oracle 存储过程时才深入
SqlTypeValue
📘 补充:现代 Spring 推荐替代方案
虽然这些技术仍然有效,但在现代 Spring Boot 项目中:
| 传统技术 | 现代替代 |
|---|---|
JdbcTemplate + LobHandler | 使用文件存储服务(如 MinIO、S3)+ 数据库存路径 |
| 手动创建嵌入式数据库 | 使用 @DataJpaTest + H2(Spring Boot 自动配置) |
SqlTypeValue | 使用 JPA + Hibernate 映射复杂类型,或 MyBatis |
如果你告诉我你目前在做什么项目(比如:Web 后端?单元测试?还是对接 Oracle?),我可以给出更具体的建议 😊
