Skip to content

Spring JDBC 对象建模:让数据库操作更面向对象 🎯

引言:为什么需要对象化的数据库操作?

在传统的 JDBC 开发中,我们经常需要编写大量重复的样板代码:创建连接、准备语句、处理结果集、异常处理等。Spring Framework 的 org.springframework.jdbc.object 包为我们提供了一种更优雅的解决方案——将 JDBC 操作建模为 Java 对象

核心理念

将数据库操作封装成可重用的 Java 对象,让每个查询、更新或存储过程调用都成为一个独立的、线程安全的组件。

技术背景与设计哲学

解决的核心痛点

kotlin
// 传统方式:大量样板代码,难以复用
fun getActor(id: Long): Actor? {
    val sql = "SELECT id, first_name, last_name FROM t_actor WHERE id = ?"
    var connection: Connection? = null
    var statement: PreparedStatement? = null
    var resultSet: ResultSet? = null
    
    try {
        connection = dataSource.connection
        statement = connection.prepareStatement(sql)
        statement.setLong(1, id)
        resultSet = statement.executeQuery()
        
        if (resultSet.next()) {
            return Actor(
                resultSet.getLong("id"),
                resultSet.getString("first_name"),
                resultSet.getString("last_name")
            )
        }
    } catch (e: SQLException) {
        // 异常处理...
    } finally {
        // 资源清理...
        resultSet?.close()
        statement?.close()
        connection?.close()
    }
    return null
}
kotlin
// Spring 对象化方式:简洁、可重用、线程安全
class ActorMappingQuery(ds: DataSource) : MappingSqlQuery<Actor>(
    ds, "SELECT id, first_name, last_name FROM t_actor WHERE id = ?"
) {
    init {
        declareParameter(SqlParameter("id", Types.BIGINT)) 
        compile() 
    }

    override fun mapRow(rs: ResultSet, rowNumber: Int) = Actor(
        rs.getLong("id"),
        rs.getString("first_name"),
        rs.getString("last_name")
    )
}

// 使用时只需一行代码
fun getActor(id: Long): Actor? = actorMappingQuery.findObject(id)

设计哲学

核心设计原则

  1. 封装性:将 SQL 操作封装为独立的 Java 对象
  2. 可重用性:一次定义,多处使用
  3. 线程安全:编译后的对象可以在多线程环境中安全使用
  4. 类型安全:强类型的参数和返回值

核心组件详解

1. SqlQuery - 查询操作的抽象基类

SqlQuery 是所有查询操作的基础,它将 SQL 查询封装为可重用的对象。

2. MappingSqlQuery - 最常用的查询类

MappingSqlQuery 是实际开发中最常用的查询类,它简化了行映射的过程。

基础用法示例

kotlin
/**
 * 演员查询类 - 展示基本的查询封装
 */
class ActorMappingQuery(ds: DataSource) : MappingSqlQuery<Actor>(
    ds, "SELECT id, first_name, last_name FROM t_actor WHERE id = ?"
) {
    init {
        // 声明参数:参数名和SQL类型
        declareParameter(SqlParameter("id", Types.BIGINT)) 
        // 编译查询,使其线程安全
        compile() 
    }

    /**
     * 将数据库行映射为业务对象
     */
    override fun mapRow(rs: ResultSet, rowNumber: Int) = Actor(
        id = rs.getLong("id"),
        firstName = rs.getString("first_name"),
        lastName = rs.getString("last_name")
    )
}

在 Service 中的使用

kotlin
@Service
class ActorService(private val dataSource: DataSource) {
    
    // 查询对象作为实例变量,线程安全且可重用
    private val actorMappingQuery = ActorMappingQuery(dataSource) 
    private val actorSearchQuery = ActorSearchQuery(dataSource) 
    
    /**
     * 根据ID查询单个演员
     */
    fun getActor(id: Long): Actor? {
        return actorMappingQuery.findObject(id) 
    }
    
    /**
     * 根据条件搜索演员列表
     */
    fun searchActors(age: Int, namePattern: String): List<Actor> {
        return actorSearchQuery.execute(age, namePattern) 
    }
}

复杂查询示例

多参数查询实现
kotlin
/**
 * 复杂的演员搜索查询
 */
class ActorSearchQuery(ds: DataSource) : MappingSqlQuery<Actor>(
    ds, """
        SELECT id, first_name, last_name, birth_date 
        FROM t_actor 
        WHERE age > ? AND (first_name LIKE ? OR last_name LIKE ?)
        ORDER BY last_name, first_name
    """
) {
    init {
        // 声明多个参数
        declareParameter(SqlParameter("age", Types.INTEGER))
        declareParameter(SqlParameter("firstNamePattern", Types.VARCHAR))
        declareParameter(SqlParameter("lastNamePattern", Types.VARCHAR))
        compile()
    }

    override fun mapRow(rs: ResultSet, rowNumber: Int) = Actor(
        id = rs.getLong("id"),
        firstName = rs.getString("first_name"),
        lastName = rs.getString("last_name"),
        birthDate = rs.getDate("birth_date")?.toLocalDate()
    )
}

3. SqlUpdate - 更新操作的封装

SqlUpdate 类专门用于封装 INSERT、UPDATE、DELETE 操作。

kotlin
/**
 * 信用评级更新操作
 */
class UpdateCreditRating(ds: DataSource) : SqlUpdate() {
    
    init {
        dataSource = ds
        sql = "UPDATE customer SET credit_rating = ? WHERE id = ?"
        
        // 声明参数(注意参数顺序)
        declareParameter(SqlParameter("creditRating", Types.NUMERIC))
        declareParameter(SqlParameter("id", Types.NUMERIC))
        compile()
    }

    /**
     * 执行更新操作
     * @param id 客户ID
     * @param rating 新的信用评级
     * @return 受影响的行数
     */
    fun execute(id: Int, rating: Int): Int {
        return update(rating, id) 
    }
}

在业务代码中的应用

kotlin
@Service
class CustomerService(dataSource: DataSource) {
    
    private val updateCreditRating = UpdateCreditRating(dataSource)
    
    /**
     * 批量更新客户信用评级
     */
    @Transactional
    fun batchUpdateCreditRatings(updates: List<CreditRatingUpdate>): Int {
        var totalUpdated = 0
        
        updates.forEach { update ->
            val affected = updateCreditRating.execute(update.customerId, update.newRating)
            if (affected == 0) {
                throw BusinessException("客户 ${update.customerId} 不存在")
            }
            totalUpdated += affected
        }
        
        return totalUpdated
    }
}

4. StoredProcedure - 存储过程调用

StoredProcedure 类用于封装存储过程的调用,支持输入参数、输出参数和返回值。

简单存储过程示例

kotlin
/**
 * 获取系统日期的存储过程
 */
class GetSysdateProcedure(dataSource: DataSource) : StoredProcedure() {
    
    companion object {
        private const val SQL = "sysdate"
    }
    
    init {
        setDataSource(dataSource)
        isFunction = true // [!code highlight] // 标记为函数而非过程
        sql = SQL
        // 声明输出参数
        declareParameter(SqlOutParameter("date", Types.DATE)) 
        compile()
    }

    /**
     * 执行存储过程并返回日期
     */
    fun execute(): Date {
        // 无输入参数,传入空Map
        val results = execute(emptyMap<String, Any>())
        return results["date"] as Date 
    }
}

复杂存储过程示例

带输入输出参数的存储过程
kotlin
/**
 * 根据日期获取标题的存储过程
 */
class TitlesAfterDateStoredProcedure(dataSource: DataSource) : StoredProcedure(
    dataSource, SPROC_NAME
) {
    
    companion object {
        private const val SPROC_NAME = "TitlesAfterDate"
        private const val CUTOFF_DATE_PARAM = "cutoffDate"
    }

    init {
        // 输入参数
        declareParameter(SqlParameter(CUTOFF_DATE_PARAM, Types.DATE))
        // 输出参数(REF CURSOR)
        declareParameter(SqlOutParameter("titles", OracleTypes.CURSOR, TitleMapper()))
        compile()
    }

    /**
     * 执行存储过程
     */
    fun execute(cutoffDate: Date): Map<String, Any> {
        return super.execute(mapOf(CUTOFF_DATE_PARAM to cutoffDate))
    }
}

/**
 * 标题映射器
 */
class TitleMapper : RowMapper<Title> {
    override fun mapRow(rs: ResultSet, rowNum: Int) = Title(
        id = rs.getLong("id"),
        name = rs.getString("name")
    )
}

最佳实践与使用建议

1. 何时使用对象化 JDBC

使用场景建议

  • 复杂查询:需要复用的复杂 SQL 查询
  • 存储过程:调用数据库存储过程时
  • 批量操作:需要高性能的批量数据处理
  • 简单查询:简单的 CRUD 操作建议直接使用 JdbcTemplate

2. 性能优化技巧

kotlin
@Configuration
class JdbcConfig {
    
    /**
     * 将查询对象配置为 Bean,实现单例复用
     */
    @Bean
    fun actorMappingQuery(dataSource: DataSource) = ActorMappingQuery(dataSource)
    
    @Bean
    fun updateCreditRating(dataSource: DataSource) = UpdateCreditRating(dataSource)
}

@Service
class ActorService(
    private val actorMappingQuery: ActorMappingQuery, 
    private val updateCreditRating: UpdateCreditRating
) {
    // 直接注入预编译的查询对象,避免重复创建
}

3. 错误处理最佳实践

kotlin
class ActorMappingQuery(ds: DataSource) : MappingSqlQuery<Actor>(
    ds, "SELECT id, first_name, last_name FROM t_actor WHERE id = ?"
) {
    
    init {
        declareParameter(SqlParameter("id", Types.BIGINT))
        compile()
    }

    override fun mapRow(rs: ResultSet, rowNumber: Int): Actor {
        return try {
            Actor(
                id = rs.getLong("id"),
                firstName = rs.getString("first_name") ?: "", 
                lastName = rs.getString("last_name") ?: ""
            )
        } catch (e: SQLException) {
            throw DataAccessException("映射演员数据失败: ${e.message}", e) 
        }
    }
}

与现代 Spring Data 的对比

技术演进提醒

虽然 Spring JDBC 对象化方式在某些场景下仍然有用,但现代 Spring 应用更推荐使用:

  • Spring Data JPA:用于 ORM 场景
  • Spring Data JDBC:用于轻量级数据访问
  • 直接使用 JdbcTemplate:用于简单的 SQL 操作

总结

Spring JDBC 对象化操作为我们提供了一种介于原生 JDBC 和 ORM 之间的解决方案:

  • 🎯 封装性好:将 SQL 操作封装为可重用的对象
  • 🚀 性能优秀:预编译 SQL,线程安全
  • 🔧 灵活控制:完全控制 SQL 和映射逻辑
  • 📦 易于测试:独立的查询对象便于单元测试

选择建议

在现代 Spring Boot 应用中,建议优先考虑 Spring Data JPA 或直接使用 JdbcTemplate。只有在需要复杂 SQL 控制或调用存储过程时,才考虑使用对象化 JDBC 方式。

通过合理使用这些工具,我们可以在保持代码简洁性的同时,获得更好的性能和可维护性! ✨