Skip to content

Spring DAO Support:数据访问的统一之道 🎯

引言:为什么需要 DAO Support?

想象一下,你正在开发一个电商系统,需要操作用户数据、订单数据、商品数据等。你可能会使用不同的数据访问技术:

  • 用户服务使用 JDBC 直接操作数据库
  • 订单服务使用 JPA 进行对象关系映射
  • 商品服务使用 Hibernate 作为 ORM 框架

传统方式的痛点

没有 Spring DAO Support 时,你会遇到这些问题:

  • 每种技术都有自己的异常体系(SQLExceptionHibernateExceptionPersistenceException
  • 异常处理代码重复且复杂
  • 切换数据访问技术时需要大量代码修改
  • 缺乏统一的编程模型

Spring DAO Support 就是为了解决这些痛点而生的!它提供了一套统一的数据访问抽象层,让你可以用一致的方式处理不同的数据访问技术。

核心概念:一致性异常层次结构 📊

设计哲学:统一异常处理

Spring 的核心思想是:将各种数据访问技术的特定异常转换为统一的异常层次结构

异常层次结构

Spring 提供了以 DataAccessException 为根的异常层次结构:

异常层次结构的优势

  • 技术无关性:无论底层使用什么技术,上层代码都能以统一方式处理异常
  • 信息保留:包装原始异常,不丢失任何错误信息
  • 运行时异常:所有异常都是非检查异常,简化代码编写

@Repository 注解:DAO 的身份标识 🏷️

核心作用

@Repository 注解不仅仅是一个标记,它还承担着重要的功能:

  1. 组件扫描识别:让 Spring 自动发现和注册 DAO 组件
  2. 异常转换:自动将技术特定异常转换为 Spring 的统一异常
  3. 语义化标识:明确标识这是一个数据访问层组件

实战示例:不同技术栈的 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 的核心价值

  1. 技术无关性 🔧

    • 提供统一的编程模型
    • 支持无缝技术切换
    • 降低学习和维护成本
  2. 异常处理统一化 🛡️

    • 统一的异常层次结构
    • 自动异常转换
    • 简化错误处理逻辑
  3. 依赖注入集成 💉

    • 与 Spring IoC 完美集成
    • 支持多种注入方式
    • 简化资源管理

最佳实践建议

开发建议

  1. 始终使用 @Repository 注解:确保异常转换和组件扫描
  2. 定义清晰的接口:便于技术切换和单元测试
  3. 分层处理异常:DAO 层专注数据访问,服务层处理业务异常
  4. 合理选择注入方式:根据技术特点选择合适的注入注解
  5. 编写集成测试:验证 DAO 层与数据库的交互

记住这个核心理念

Spring DAO Support 不是要替代具体的数据访问技术,而是要统一它们的使用方式,让开发者能够以一致的方式处理不同的数据访问场景,从而提高代码的可维护性和可扩展性。

通过 Spring DAO Support,你可以专注于业务逻辑的实现,而不用担心底层数据访问技术的差异。这就是 Spring 框架"简化企业级 Java 开发"理念的完美体现! 🎉