Appearance
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)
设计哲学
核心设计原则
- 封装性:将 SQL 操作封装为独立的 Java 对象
- 可重用性:一次定义,多处使用
- 线程安全:编译后的对象可以在多线程环境中安全使用
- 类型安全:强类型的参数和返回值
核心组件详解
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 方式。
通过合理使用这些工具,我们可以在保持代码简洁性的同时,获得更好的性能和可维护性! ✨