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'])

适用于 executeexecuteUpdaterowseachRow

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 项目,可直接导入运行)。


原始标题:JDBC with Groovy

« 上一篇: Java Weekly, 第218期
» 下一篇: Java 方法句柄详解