Skip to content

Spring JDBC SimpleJdbc 类详解:让数据库操作变得简单优雅 🚀

概述

在传统的 JDBC 开发中,我们经常需要编写大量的样板代码来处理数据库操作。Spring Framework 提供了 SimpleJdbcInsertSimpleJdbcCall 类,通过利用数据库元数据信息,大大简化了 JDBC 操作的配置和使用。

TIP

SimpleJdbc 类的核心优势在于自动化配置:它们能够通过 JDBC 驱动程序获取数据库元数据,自动推断表结构、参数类型等信息,让开发者专注于业务逻辑而非繁琐的配置。

为什么需要 SimpleJdbc 类?

传统 JDBC 的痛点

kotlin
// 传统 JDBC 插入数据的繁琐代码
fun addActor(actor: Actor) {
    val sql = "INSERT INTO t_actor (first_name, last_name) VALUES (?, ?)"
    val connection = dataSource.connection 
    val statement = connection.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS) 
    
    try {
        statement.setString(1, actor.firstName) 
        statement.setString(2, actor.lastName) 
        statement.executeUpdate() 
        
        val keys = statement.generatedKeys 
        if (keys.next()) { 
            actor.id = keys.getLong(1) 
        }
    } finally {
        statement.close() 
        connection.close() 
    }
}
kotlin
// 使用 SimpleJdbcInsert 的简洁代码
private val insertActor = SimpleJdbcInsert(dataSource) 
    .withTableName("t_actor") 
    .usingGeneratedKeyColumns("id") 

fun addActor(actor: Actor): Actor {
    val parameters = mapOf( 
        "first_name" to actor.firstName, 
        "last_name" to actor.lastName 
    )
    val newId = insertActor.executeAndReturnKey(parameters) 
    return actor.copy(id = newId.toLong()) 
}

IMPORTANT

对比可以看出,SimpleJdbc 类消除了:

  • 手动编写 SQL 语句
  • 复杂的连接和语句管理
  • 繁琐的参数设置
  • 手动处理生成的键值

SimpleJdbcInsert:简化数据插入操作

基本用法

SimpleJdbcInsert 是专门用于简化数据插入操作的工具类。它通过数据库元数据自动构建 INSERT 语句。

kotlin
@Repository
class ActorRepository(dataSource: DataSource) {
    
    // 在初始化时配置 SimpleJdbcInsert
    private val insertActor = SimpleJdbcInsert(dataSource)
        .withTableName("t_actor") 
    
    fun addActor(actor: Actor) {
        // 创建参数映射,键名必须与数据库列名匹配
        val parameters = mapOf(
            "id" to actor.id,
            "first_name" to actor.firstName, 
            "last_name" to actor.lastName
        )
        
        // 执行插入操作
        insertActor.execute(parameters) 
    }
}

NOTE

参数映射中的键名必须与数据库表的列名完全匹配,因为 SimpleJdbcInsert 会读取元数据来构建实际的 INSERT 语句。

自动获取生成的主键

在实际开发中,我们经常需要获取数据库自动生成的主键值:

kotlin
@Repository
class ActorRepository(dataSource: DataSource) {
    
    private val insertActor = SimpleJdbcInsert(dataSource)
        .withTableName("t_actor")
        .usingGeneratedKeyColumns("id") 
    
    fun addActor(actor: Actor): Actor {
        val parameters = mapOf(
            "first_name" to actor.firstName,
            "last_name" to actor.lastName
            // 注意:这里不需要包含 id,因为它是自动生成的
        )
        
        // 执行插入并返回生成的主键
        val newId = insertActor.executeAndReturnKey(parameters) 
        return actor.copy(id = newId.toLong())
    }
}

指定插入列

有时我们只想插入特定的列,可以使用 usingColumns 方法:

kotlin
private val insertActor = SimpleJdbcInsert(dataSource)
    .withTableName("t_actor")
    .usingColumns("first_name", "last_name") 
    .usingGeneratedKeyColumns("id")

TIP

使用 usingColumns 可以:

  • 提高性能(只插入必要的列)
  • 避免触发不必要的触发器
  • 确保数据一致性

参数源:让参数传递更灵活

Spring 提供了多种 SqlParameterSource 实现,让参数传递更加灵活和便捷。

BeanPropertySqlParameterSource

当你的参数来自一个 JavaBean 对象时,这是最方便的选择:

kotlin
@Repository
class ActorRepository(dataSource: DataSource) {
    
    private val insertActor = SimpleJdbcInsert(dataSource)
        .withTableName("t_actor")
        .usingGeneratedKeyColumns("id")
    
    fun addActor(actor: Actor): Actor {
        // 直接使用对象作为参数源
        val parameters = BeanPropertySqlParameterSource(actor) 
        val newId = insertActor.executeAndReturnKey(parameters)
        return actor.copy(id = newId.toLong())
    }
}

MapSqlParameterSource

提供了链式调用的便利方法:

kotlin
fun addActor(actor: Actor): Actor {
    val parameters = MapSqlParameterSource() 
        .addValue("first_name", actor.firstName) 
        .addValue("last_name", actor.lastName) 
    
    val newId = insertActor.executeAndReturnKey(parameters)
    return actor.copy(id = newId.toLong())
}

SimpleJdbcCall:简化存储过程调用

基本存储过程调用

假设我们有以下存储过程:

sql
CREATE PROCEDURE read_actor (
    IN in_id INTEGER,
    OUT out_first_name VARCHAR(100),
    OUT out_last_name VARCHAR(100),
    OUT out_birth_date DATE
)
BEGIN
    SELECT first_name, last_name, birth_date
    INTO out_first_name, out_last_name, out_birth_date
    FROM t_actor WHERE id = in_id;
END;

使用 SimpleJdbcCall 调用:

kotlin
@Repository
class ActorRepository(dataSource: DataSource) {
    
    private val procReadActor = SimpleJdbcCall(dataSource)
        .withProcedureName("read_actor") 
    
    fun readActor(id: Long): Actor {
        val input = MapSqlParameterSource().addValue("in_id", id)
        val output = procReadActor.execute(input) 
        
        return Actor(
            id = id,
            firstName = output["out_first_name"] as String, 
            lastName = output["out_last_name"] as String,
            birthDate = output["out_birth_date"] as Date
        )
    }
}

处理大小写敏感问题

不同数据库对参数名的大小写处理不同,为了提高代码的可移植性:

kotlin
@Repository
class ActorRepository(dataSource: DataSource) {
    
    private val procReadActor: SimpleJdbcCall
    
    init {
        val jdbcTemplate = JdbcTemplate(dataSource).apply {
            isResultsMapCaseInsensitive = true
        }
        procReadActor = SimpleJdbcCall(jdbcTemplate)
            .withProcedureName("read_actor")
    }
}

调用存储函数

存储函数与存储过程类似,但有返回值:

sql
CREATE FUNCTION get_actor_name (in_id INTEGER)
RETURNS VARCHAR(200) READS SQL DATA
BEGIN
    DECLARE out_name VARCHAR(200);
    SELECT CONCAT(first_name, ' ', last_name)
        INTO out_name
        FROM t_actor WHERE id = in_id;
    RETURN out_name;
END;
kotlin
@Repository
class ActorRepository(dataSource: DataSource) {
    
    private val funcGetActorName = SimpleJdbcCall(dataSource)
        .withFunctionName("get_actor_name") 
    
    fun getActorName(id: Long): String {
        val input = MapSqlParameterSource().addValue("in_id", id)
        return funcGetActorName.executeFunction(String::class.java, input) 
    }
}

处理结果集返回

当存储过程返回结果集时:

sql
CREATE PROCEDURE read_all_actors()
BEGIN
    SELECT a.id, a.first_name, a.last_name, a.birth_date 
    FROM t_actor a;
END;
kotlin
@Repository
class ActorRepository(dataSource: DataSource) {
    
    private val procReadAllActors = SimpleJdbcCall(dataSource)
        .withProcedureName("read_all_actors")
        .returningResultSet("actors", 
            BeanPropertyRowMapper.newInstance(Actor::class.java)) 
    
    fun getAllActors(): List<Actor> {
        val result = procReadAllActors.execute(emptyMap<String, Any>())
        return result["actors"] as List<Actor>
    }
}

显式参数声明

当需要更精确的控制或数据库不支持元数据查询时,可以显式声明参数:

kotlin
private val procReadActor = SimpleJdbcCall(dataSource)
    .withProcedureName("read_actor")
    .withoutProcedureColumnMetaDataAccess() 
    .useInParameterNames("in_id") 
    .declareParameters( 
        SqlParameter("in_id", Types.NUMERIC),
        SqlOutParameter("out_first_name", Types.VARCHAR),
        SqlOutParameter("out_last_name", Types.VARCHAR),
        SqlOutParameter("out_birth_date", Types.DATE)
    )

完整的实战示例

让我们通过一个完整的 Actor 管理服务来展示 SimpleJdbc 的实际应用:

完整的 ActorService 实现
kotlin
@Service
@Transactional
class ActorService(dataSource: DataSource) {
    
    // 插入操作
    private val insertActor = SimpleJdbcInsert(dataSource)
        .withTableName("t_actor")
        .usingGeneratedKeyColumns("id")
    
    // 存储过程调用
    private val procReadActor = SimpleJdbcCall(dataSource)
        .withProcedureName("read_actor")
    
    // 存储函数调用
    private val funcGetActorName = SimpleJdbcCall(dataSource)
        .withFunctionName("get_actor_name")
    
    // 批量查询
    private val procReadAllActors = SimpleJdbcCall(dataSource)
        .withProcedureName("read_all_actors")
        .returningResultSet("actors",
            BeanPropertyRowMapper.newInstance(Actor::class.java))
    
    /**
     * 添加新演员
     */
    fun addActor(actor: Actor): Actor {
        val parameters = BeanPropertySqlParameterSource(actor)
        val newId = insertActor.executeAndReturnKey(parameters)
        return actor.copy(id = newId.toLong())
    }
    
    /**
     * 根据ID查询演员详情
     */
    fun getActorById(id: Long): Actor? {
        return try {
            val input = MapSqlParameterSource().addValue("in_id", id)
            val output = procReadActor.execute(input)
            
            Actor(
                id = id,
                firstName = output["out_first_name"] as String,
                lastName = output["out_last_name"] as String,
                birthDate = output["out_birth_date"] as Date
            )
        } catch (e: Exception) {
            null
        }
    }
    
    /**
     * 获取演员全名
     */
    fun getActorFullName(id: Long): String {
        val input = MapSqlParameterSource().addValue("in_id", id)
        return funcGetActorName.executeFunction(String::class.java, input)
    }
    
    /**
     * 获取所有演员列表
     */
    fun getAllActors(): List<Actor> {
        val result = procReadAllActors.execute(emptyMap<String, Any>())
        return result["actors"] as List<Actor>
    }
}

// 数据类定义
data class Actor(
    val id: Long = 0,
    val firstName: String,
    val lastName: String,
    val birthDate: Date? = null
)

数据流程图

让我们通过时序图来理解 SimpleJdbc 的工作流程:

最佳实践与注意事项

1. 性能优化

性能建议

  • 复用 SimpleJdbc 实例:在类初始化时创建,避免重复创建
  • 使用连接池:确保 DataSource 配置了合适的连接池
  • 批量操作:对于大量数据插入,考虑使用 JdbcTemplate 的批量操作

2. 错误处理

kotlin
fun addActorSafely(actor: Actor): Result<Actor> {
    return try {
        val parameters = BeanPropertySqlParameterSource(actor)
        val newId = insertActor.executeAndReturnKey(parameters)
        Result.success(actor.copy(id = newId.toLong()))
    } catch (e: DataAccessException) { 
        logger.error("Failed to insert actor: ${actor.firstName} ${actor.lastName}", e)
        Result.failure(e)
    }
}

3. 事务管理

kotlin
@Service
@Transactional
class ActorService(dataSource: DataSource) {
    
    @Transactional(readOnly = true) 
    fun getActorById(id: Long): Actor? {
        // 只读事务,提高性能
    }
    
    @Transactional(rollbackFor = [Exception::class]) 
    fun addActor(actor: Actor): Actor {
        // 任何异常都会回滚
    }
}

总结

SimpleJdbc 类通过以下方式简化了数据库操作:

自动化配置:利用数据库元数据自动推断表结构和参数类型
流畅的 API:提供链式调用的配置方法
类型安全:减少手动类型转换的错误
减少样板代码:消除大量重复的 JDBC 代码
灵活的参数传递:支持多种参数源类型

IMPORTANT

SimpleJdbc 类特别适合:

  • 简单的 CRUD 操作
  • 存储过程和函数调用
  • 需要获取自动生成主键的场景
  • 希望减少 SQL 编写的项目

通过合理使用 SimpleJdbc 类,我们可以在保持代码简洁性的同时,享受 Spring JDBC 带来的便利和安全性。这正是 Spring Framework "约定优于配置" 哲学的完美体现! 🎉