Appearance
Spring DAO Support:数据访问的统一之道 🎯
引言:为什么需要 DAO Support?
想象一下,你正在开发一个电商系统,需要操作用户数据、订单数据、商品数据等。你可能会使用不同的数据访问技术:
- 用户服务使用 JDBC 直接操作数据库
- 订单服务使用 JPA 进行对象关系映射
- 商品服务使用 Hibernate 作为 ORM 框架
传统方式的痛点
没有 Spring DAO Support 时,你会遇到这些问题:
- 每种技术都有自己的异常体系(
SQLException
、HibernateException
、PersistenceException
) - 异常处理代码重复且复杂
- 切换数据访问技术时需要大量代码修改
- 缺乏统一的编程模型
Spring DAO Support 就是为了解决这些痛点而生的!它提供了一套统一的数据访问抽象层,让你可以用一致的方式处理不同的数据访问技术。
核心概念:一致性异常层次结构 📊
设计哲学:统一异常处理
Spring 的核心思想是:将各种数据访问技术的特定异常转换为统一的异常层次结构。
异常层次结构
Spring 提供了以 DataAccessException
为根的异常层次结构:
异常层次结构的优势
- 技术无关性:无论底层使用什么技术,上层代码都能以统一方式处理异常
- 信息保留:包装原始异常,不丢失任何错误信息
- 运行时异常:所有异常都是非检查异常,简化代码编写
@Repository 注解:DAO 的身份标识 🏷️
核心作用
@Repository
注解不仅仅是一个标记,它还承担着重要的功能:
- 组件扫描识别:让 Spring 自动发现和注册 DAO 组件
- 异常转换:自动将技术特定异常转换为 Spring 的统一异常
- 语义化标识:明确标识这是一个数据访问层组件
实战示例:不同技术栈的 DAO 实现
kotlin
@Repository
class JpaMovieFinder : MovieFinder {
@PersistenceContext
private lateinit var entityManager: EntityManager
override fun findByTitle(title: String): List<Movie> {
return entityManager.createQuery(
"SELECT m FROM Movie m WHERE m.title LIKE :title",
Movie::class.java
).setParameter("title", "%$title%")
.resultList
}
override fun save(movie: Movie): Movie {
return entityManager.merge(movie)
}
}
kotlin
@Repository
class JdbcMovieFinder(dataSource: DataSource) : MovieFinder {
private val jdbcTemplate = JdbcTemplate(dataSource)
override fun findByTitle(title: String): List<Movie> {
return jdbcTemplate.query(
"SELECT id, title, director, year FROM movies WHERE title LIKE ?",
{ rs, _ ->
Movie(
id = rs.getLong("id"),
title = rs.getString("title"),
director = rs.getString("director"),
year = rs.getInt("year")
)
},
"%$title%"
)
}
override fun save(movie: Movie): Movie {
val keyHolder = GeneratedKeyHolder()
jdbcTemplate.update({ connection ->
val ps = connection.prepareStatement(
"INSERT INTO movies (title, director, year) VALUES (?, ?, ?)",
Statement.RETURN_GENERATED_KEYS
)
ps.setString(1, movie.title)
ps.setString(2, movie.director)
ps.setInt(3, movie.year)
ps
}, keyHolder)
return movie.copy(id = keyHolder.key?.toLong() ?: 0L)
}
}
kotlin
@Repository
class HibernateMovieFinder(private val sessionFactory: SessionFactory) : MovieFinder {
override fun findByTitle(title: String): List<Movie> {
return sessionFactory.currentSession
.createQuery("FROM Movie WHERE title LIKE :title", Movie::class.java)
.setParameter("title", "%$title%")
.list()
}
override fun save(movie: Movie): Movie {
sessionFactory.currentSession.saveOrUpdate(movie)
return movie
}
}
统一的服务层调用
无论底层使用哪种技术,服务层的代码都保持一致:
kotlin
@Service
class MovieService(private val movieFinder: MovieFinder) {
fun searchMovies(title: String): List<Movie> {
return try {
movieFinder.findByTitle(title)
} catch (ex: DataAccessException) {
// 统一的异常处理,无需关心底层技术
logger.error("搜索电影失败: ${ex.message}", ex)
emptyList()
}
}
fun addMovie(movie: Movie): Movie {
return try {
movieFinder.save(movie)
} catch (ex: DataIntegrityViolationException) {
// 处理数据完整性违反(如重复键)
throw BusinessException("电影已存在")
} catch (ex: DataAccessException) {
// 处理其他数据访问异常
throw BusinessException("保存电影失败")
}
}
}
依赖注入:资源的优雅管理 💉
不同技术的资源注入方式
每种数据访问技术都需要特定的资源,Spring 提供了多种注入方式:
注入注解选择指南
@PersistenceContext
:用于注入 JPA 的 EntityManager@Autowired
:通用的依赖注入,适用于大多数场景@Resource
:按名称注入,适用于有多个同类型 Bean 的场景@Inject
:JSR-330 标准注解,与 @Autowired 功能类似
完整的配置示例
kotlin
// 数据源配置
@Configuration
@EnableJpaRepositories
class DatabaseConfig {
@Bean
@Primary
fun dataSource(): DataSource {
return HikariDataSource().apply {
jdbcUrl = "jdbc:mysql://localhost:3306/moviedb"
username = "root"
password = "password"
maximumPoolSize = 20
}
}
@Bean
fun entityManagerFactory(dataSource: DataSource): LocalContainerEntityManagerFactoryBean {
return LocalContainerEntityManagerFactoryBean().apply {
setDataSource(dataSource)
setPackagesToScan("com.example.movie.entity")
jpaVendorAdapter = HibernateJpaVendorAdapter()
}
}
@Bean
fun transactionManager(entityManagerFactory: EntityManagerFactory): PlatformTransactionManager {
return JpaTransactionManager(entityManagerFactory)
}
}
实际业务场景:电影管理系统 🎬
让我们通过一个完整的电影管理系统来展示 DAO Support 的威力:
1. 领域模型
kotlin
data class Movie(
val id: Long = 0,
val title: String,
val director: String,
val year: Int,
val genre: String,
val rating: Double = 0.0
)
data class Review(
val id: Long = 0,
val movieId: Long,
val reviewer: String,
val rating: Int,
val comment: String,
val createdAt: LocalDateTime = LocalDateTime.now()
)
2. DAO 接口定义
kotlin
interface MovieRepository {
fun findById(id: Long): Movie?
fun findByGenre(genre: String): List<Movie>
fun findTopRated(limit: Int): List<Movie>
fun save(movie: Movie): Movie
fun delete(id: Long): Boolean
}
interface ReviewRepository {
fun findByMovieId(movieId: Long): List<Review>
fun save(review: Review): Review
fun calculateAverageRating(movieId: Long): Double
}
3. JPA 实现
JPA Repository 完整实现
kotlin
@Repository
class JpaMovieRepository : MovieRepository {
@PersistenceContext
private lateinit var entityManager: EntityManager
override fun findById(id: Long): Movie? {
return try {
entityManager.find(Movie::class.java, id)
} catch (ex: Exception) {
null // Spring 会自动转换异常
}
}
override fun findByGenre(genre: String): List<Movie> {
return entityManager.createQuery(
"SELECT m FROM Movie m WHERE m.genre = :genre ORDER BY m.rating DESC",
Movie::class.java
).setParameter("genre", genre)
.resultList
}
override fun findTopRated(limit: Int): List<Movie> {
return entityManager.createQuery(
"SELECT m FROM Movie m ORDER BY m.rating DESC",
Movie::class.java
).setMaxResults(limit)
.resultList
}
override fun save(movie: Movie): Movie {
return if (movie.id == 0L) {
entityManager.persist(movie)
movie
} else {
entityManager.merge(movie)
}
}
override fun delete(id: Long): Boolean {
return try {
val movie = entityManager.find(Movie::class.java, id)
if (movie != null) {
entityManager.remove(movie)
true
} else {
false
}
} catch (ex: Exception) {
false
}
}
}
@Repository
class JpaReviewRepository : ReviewRepository {
@PersistenceContext
private lateinit var entityManager: EntityManager
override fun findByMovieId(movieId: Long): List<Review> {
return entityManager.createQuery(
"SELECT r FROM Review r WHERE r.movieId = :movieId ORDER BY r.createdAt DESC",
Review::class.java
).setParameter("movieId", movieId)
.resultList
}
override fun save(review: Review): Review {
entityManager.persist(review)
return review
}
override fun calculateAverageRating(movieId: Long): Double {
return entityManager.createQuery(
"SELECT AVG(r.rating) FROM Review r WHERE r.movieId = :movieId",
Double::class.java
).setParameter("movieId", movieId)
.singleResult ?: 0.0
}
}
4. 业务服务层
kotlin
@Service
@Transactional
class MovieService(
private val movieRepository: MovieRepository,
private val reviewRepository: ReviewRepository
) {
fun getMovieDetails(id: Long): MovieDetails? {
return try {
val movie = movieRepository.findById(id) ?: return null
val reviews = reviewRepository.findByMovieId(id)
val averageRating = reviewRepository.calculateAverageRating(id)
MovieDetails(
movie = movie.copy(rating = averageRating),
reviews = reviews,
reviewCount = reviews.size
)
} catch (ex: DataAccessException) {
logger.error("获取电影详情失败: movieId=$id", ex)
null
}
}
fun addReview(movieId: Long, review: Review): Review {
return try {
// 验证电影是否存在
movieRepository.findById(movieId)
?: throw BusinessException("电影不存在")
// 保存评论
val savedReview = reviewRepository.save(review.copy(movieId = movieId))
// 更新电影平均评分
val newRating = reviewRepository.calculateAverageRating(movieId)
val movie = movieRepository.findById(movieId)!!
movieRepository.save(movie.copy(rating = newRating))
savedReview
} catch (ex: DataIntegrityViolationException) {
throw BusinessException("评论数据不完整")
} catch (ex: DataAccessException) {
logger.error("添加评论失败: movieId=$movieId", ex)
throw BusinessException("添加评论失败")
}
}
fun getRecommendations(genre: String, limit: Int = 10): List<Movie> {
return try {
movieRepository.findByGenre(genre)
.take(limit)
} catch (ex: DataAccessException) {
logger.warn("获取推荐电影失败: genre=$genre", ex)
emptyList()
}
}
}
异常处理最佳实践 ⚠️
分层异常处理策略
kotlin
// 1. DAO 层:让 Spring 自动转换异常
@Repository
class MovieRepository {
// 不需要手动捕获技术特定异常
// Spring 会自动转换为 DataAccessException
}
// 2. 服务层:处理业务逻辑相关的数据异常
@Service
class MovieService {
fun createMovie(movie: Movie): Movie {
return try {
movieRepository.save(movie)
} catch (ex: DataIntegrityViolationException) {
// 处理数据完整性问题(如唯一约束违反)
when {
ex.message?.contains("title") == true ->
throw BusinessException("电影标题已存在")
ex.message?.contains("isbn") == true ->
throw BusinessException("ISBN 已存在")
else -> throw BusinessException("数据完整性错误")
}
} catch (ex: DataAccessResourceFailureException) {
// 处理资源访问失败(如数据库连接问题)
logger.error("数据库连接失败", ex)
throw SystemException("系统暂时不可用,请稍后重试")
} catch (ex: DataAccessException) {
// 处理其他数据访问异常
logger.error("数据访问异常", ex)
throw SystemException("操作失败")
}
}
}
// 3. 控制器层:转换为 HTTP 响应
@RestController
class MovieController {
@ExceptionHandler(BusinessException::class)
fun handleBusinessException(ex: BusinessException): ResponseEntity<ErrorResponse> {
return ResponseEntity.badRequest()
.body(ErrorResponse(ex.message ?: "业务异常"))
}
@ExceptionHandler(SystemException::class)
fun handleSystemException(ex: SystemException): ResponseEntity<ErrorResponse> {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(ErrorResponse(ex.message ?: "系统异常"))
}
}
技术切换的无缝体验 🔄
场景:从 JDBC 迁移到 JPA
假设你的项目最初使用 JDBC,后来决定迁移到 JPA:
kotlin
@Repository
class JdbcMovieRepository(dataSource: DataSource) : MovieRepository {
private val jdbcTemplate = JdbcTemplate(dataSource)
override fun findById(id: Long): Movie? {
return try {
jdbcTemplate.queryForObject(
"SELECT * FROM movies WHERE id = ?",
{ rs, _ -> mapRowToMovie(rs) },
id
)
} catch (ex: EmptyResultDataAccessException) {
null
}
}
private fun mapRowToMovie(rs: ResultSet): Movie {
return Movie(
id = rs.getLong("id"),
title = rs.getString("title"),
director = rs.getString("director"),
year = rs.getInt("year"),
genre = rs.getString("genre"),
rating = rs.getDouble("rating")
)
}
}
kotlin
@Repository
class JpaMovieRepository : MovieRepository {
@PersistenceContext
private lateinit var entityManager: EntityManager
override fun findById(id: Long): Movie? {
return entityManager.find(Movie::class.java, id)
}
// 其他方法实现...
}
迁移的关键优势
由于使用了统一的 MovieRepository
接口和 @Repository
注解:
- 服务层代码无需修改
- 异常处理逻辑保持一致
- 只需要修改配置和 DAO 实现
- 测试用例可以复用
总结与最佳实践 ✨
Spring DAO Support 的核心价值
技术无关性 🔧
- 提供统一的编程模型
- 支持无缝技术切换
- 降低学习和维护成本
异常处理统一化 🛡️
- 统一的异常层次结构
- 自动异常转换
- 简化错误处理逻辑
依赖注入集成 💉
- 与 Spring IoC 完美集成
- 支持多种注入方式
- 简化资源管理
最佳实践建议
开发建议
- 始终使用 @Repository 注解:确保异常转换和组件扫描
- 定义清晰的接口:便于技术切换和单元测试
- 分层处理异常:DAO 层专注数据访问,服务层处理业务异常
- 合理选择注入方式:根据技术特点选择合适的注入注解
- 编写集成测试:验证 DAO 层与数据库的交互
记住这个核心理念
Spring DAO Support 不是要替代具体的数据访问技术,而是要统一它们的使用方式,让开发者能够以一致的方式处理不同的数据访问场景,从而提高代码的可维护性和可扩展性。
通过 Spring DAO Support,你可以专注于业务逻辑的实现,而不用担心底层数据访问技术的差异。这就是 Spring 框架"简化企业级 Java 开发"理念的完美体现! 🎉