1. 引言
本文将探讨如何使用 Groovy 的惯用方式通过 JDBC 操作关系型数据库。JDBC 作为 JVM 上大多数 ORM 和高级数据访问库的基础,虽然相对底层,但 API 相当繁琐。幸运的是,Groovy 标准库在 JDBC 之上构建了一个简洁、强大且易用的接口——Groovy SQL 模块。
我们将专注于纯 Groovy 环境下的 JDBC 使用,不涉及 Spring 等框架(相关内容可参考其他指南)。
2. JDBC 和 Groovy 环境搭建
首先需要添加 groovy-sql 模块依赖:
<dependency>
<groupId>org.apache.groovy</groupId>
<artifactId>groovy</artifactId>
<version>4.0.21</version>
</dependency>
<dependency>
<groupId>org.apache.groovy</groupId>
<artifactId>groovy-sql</artifactId>
<version>4.0.21</version>
</dependency>
若使用 groovy-all 则无需显式添加:
<dependency>
<groupId>org.apache.groovy</groupId>
<artifactId>groovy-all</artifactId>
<version>4.0.21</version>
</dependency>
最新版本可在 Maven Central 查找:
3. 连接数据库
3.1. 指定连接参数
本文使用轻量级测试数据库 HSQLDB。连接需要 URL、驱动和凭据:
Map dbConnParams = [
url: 'jdbc:hsqldb:mem:testDB',
user: 'sa',
password: '',
driver: 'org.hsqldb.jdbc.JDBCDriver']
通过 Sql 类获取连接:
def sql = Sql.newInstance(dbConnParams)
操作完成后务必释放资源:
sql.close()
3.2. 使用 DataSource
在应用服务器或需要连接池/JNDI 的场景,使用 DataSource 更自然:
def sql = Sql.newInstance(datasource)
3.3. 自动资源管理
手动调用 close() 容易遗漏,Groovy 提供自动管理方案:
Sql.withInstance(dbConnParams) {
Sql sql -> haveFunWith(sql)
}
即使发生异常,资源也会自动释放。
4. 执行数据库操作
4.1. 插入数据
简单插入可用 execute,但需获取自增主键时推荐 executeInsert:
sql.execute "CREATE TABLE PROJECT (ID IDENTITY, NAME VARCHAR (50), URL VARCHAR (100))"
插入并获取自增 ID:
def ids = sql.executeInsert """
INSERT INTO PROJECT (NAME, URL) VALUES ('tutorials', 'github.com/eugenp/tutorials')
"""
返回值是二维数组:行×列。单行单列场景直接取值:
assertEquals(0, ids[0][0]) // 首次插入返回 0
后续插入返回递增值:
ids = sql.executeInsert """
INSERT INTO PROJECT (NAME, URL)
VALUES ('REST with Spring', 'github.com/eugenp/REST-With-Spring')
"""
assertEquals(1, ids[0][0])
4.2. 更新和删除数据
使用 executeUpdate 获取受影响行数:
def count = sql.executeUpdate("UPDATE PROJECT SET URL = 'https://' + URL")
assertEquals(2, count)
5. 查询数据库
5.1. 遍历查询结果
告别传统 while 循环,用闭包优雅遍历:
sql.eachRow("SELECT * FROM PROJECT") { GroovyResultSet rs ->
haveFunWith(rs)
}
GroovyResultSet 是标准 ResultSet 的增强版。
5.2. 访问结果集
支持列名(不区分大小写)和索引访问:
sql.eachRow("SELECT * FROM PROJECT") { rs ->
assertNotNull(rs.name) // 列名访问
assertNotNull(rs.URL) // 列名访问
assertNotNull(rs[0]) // 索引访问
assertNotNull(rs[1])
assertNotNull(rs[2])
}
5.3. 分页
通过偏移量和最大行数实现分页:
def offset = 1
def maxResults = 1
def rows = sql.rows('SELECT * FROM PROJECT ORDER BY NAME', offset, maxResults)
assertEquals(1, rows.size())
assertEquals('REST with Spring', rows[0].name)
rows() 返回列表而非迭代器。
6. 参数化查询和语句
⚠️ 严禁字符串拼接! 使用参数化查询防止 SQL 注入。
6.1. 带占位符的字符串
支持位置参数和命名参数:
// 位置参数
sql.execute(
'INSERT INTO PROJECT (NAME, URL) VALUES (?, ?)',
'tutorials', 'github.com/eugenp/tutorials')
// 命名参数
sql.execute(
'INSERT INTO PROJECT (NAME, URL) VALUES (:name, :url)',
[name: 'REST with Spring', url: 'github.com/eugenp/REST-With-Spring'])
适用于 execute、executeUpdate、rows 和 eachRow。
6.2. Groovy 字符串
使用 GString 更 Groovy,自动转义防注入:
def name = 'REST with Spring'
def url = 'github.com/eugenp/REST-With-Spring'
sql.execute "INSERT INTO PROJECT (NAME, URL) VALUES (${name}, ${url})"
7. 事务和连接
7.1. 短生命周期连接
默认情况下,每个操作使用独立连接,操作后立即关闭。连接池可缓解性能问题,但事务需要跨操作共享连接。
7.2. 使用缓存连接的事务
通过 withTransaction 管理事务:
sql.withTransaction {
sql.execute """
INSERT INTO PROJECT (NAME, URL)
VALUES ('tutorials', 'github.com/eugenp/tutorials')
"""
sql.execute """
INSERT INTO PROJECT (NAME, URL)
VALUES ('REST with Spring', 'github.com/eugenp/REST-With-Spring')
"""
}
✅ 自动提交(异常时回滚),也可手动控制:
sql.withTransaction {
sql.execute "INSERT INTO PROJECT (NAME, URL) VALUES ('tutorials', 'github.com/eugenp/tutorials')"
sql.commit()
sql.execute "INSERT INTO PROJECT (NAME, URL) VALUES ('REST with Spring', 'github.com/eugenp/REST-With-Spring')"
sql.rollback()
}
7.3. 无事务的缓存连接
仅需复用连接时用 cacheConnection:
sql.cacheConnection {
sql.execute "INSERT INTO PROJECT (NAME, URL) VALUES ('tutorials', 'github.com/eugenp/tutorials')"
throw new Exception('此异常不会回滚')
}
8. 总结和延伸阅读
Groovy SQL 通过闭包和 GString 让 JDBC 焕发新生,简洁而不失强大。本文未覆盖批处理、存储过程等特性,更多细节参考官方文档。
完整示例代码见 GitHub 项目(Maven 项目,可直接导入运行)。