DAO模式概述

理解DAO设计模式的概念与价值

学习目标

掌握DAO模式的核心概念与解决的问题

1

什么是DAO模式?

DAO(Data Access Object)是J2EE核心模式之一,通过抽象数据访问接口,提供业务对象与数据源之间的解耦。

核心思想:分离业务逻辑与数据访问逻辑,提供统一的数据访问接口,隐藏底层数据源实现细节。

2

为什么需要DAO模式?

没有使用DAO模式的痛点:

  • 业务逻辑中混杂SQL/JDBC代码
  • 切换数据源需要修改多处代码
  • 数据访问逻辑无法复用
  • 难以进行单元测试
  • 无法统一处理数据访问异常

解耦性

分离业务逻辑与数据访问逻辑,使得业务层无需关心数据存储细节

可替换性

更换数据源(如MySQL→MongoDB)只需修改DAO实现,不影响业务层

可测试性

可以轻松模拟DAO进行业务逻辑单元测试,无需实际数据库

封装性

集中处理数据访问逻辑,避免SQL注入等安全问题

DAO应用场景

  • 任何需要数据持久化的应用系统
  • 分层架构的数据访问层
  • 多数据源切换场景
  • 需要数据访问抽象的项目
  • 需要单元测试保障的项目

DAO模式核心组件

构成DAO模式的关键元素及其职责

学习目标

理解DAO模式中的各类组件及其协作关系

数据模型(Model)

表示业务数据的Java对象(POJO)

  • 包含数据字段与getter/setter
  • 不包含业务逻辑
  • 通常与数据库表对应
  • 示例:User, Product, Order

DAO接口

定义数据访问操作的方法签名

  • 不包含具体实现细节
  • 定义CRUD操作方法
  • 定义查询操作方法
  • 示例:UserDAO, ProductDAO

DAO实现类

实现DAO接口的具体逻辑

  • 包含实际的数据库操作代码
  • 针对特定数据源实现(JDBC、Hibernate等)
  • 处理SQL异常与连接管理
  • 示例:UserDAOImpl, ProductDAOImpl

DAO模式类图结构

+-------------------+ +-----------------+ +------------------------+ | BusinessObject | | < > | | Data Source | |-------------------| | DAO | |------------------------| | + execute() |-------> |-----------------| | (MySQL, Oracle, etc.) | +-------------------+ | + create() |-------> +------------------------+ | + read() | | + update() | | + delete() | +-----------------+ ^ | +---------------+---------------+ | | +----------------------------+ +----------------------------+ | DAOImplForDB1 | | DAOImplForDB2 | |----------------------------| |----------------------------| | + create() {DB1 specific} | | + create() {DB2 specific} | | + read() {DB1 specific} | | + read() {DB2 specific} | | + update() {DB1 specific} | | + update() {DB2 specific} | | + delete() {DB1 specific} | | + delete() {DB2 specific} | +----------------------------+ +----------------------------+
1

业务层调用

业务对象(BusinessObject)通过DAO接口发起数据访问请求

2

DAO接口转发

DAO接口将请求路由到具体的DAO实现类

3

数据访问执行

DAO实现类执行具体的数据源操作(SQL/JDBC等)

4

结果返回

DAO实现将结果集转换为模型对象返回给业务层

DAO模式实现步骤

手把手实现一个完整的DAO模式

学习目标

掌握从创建数据模型到实现DAO的完整过程

1

定义数据模型(User)

创建简单的POJO类表示用户数据:

public class User { private Long id; private String username; private String email; private Date createdAt; // 构造器 public User() {} public User(String username, String email) { this.username = username; this.email = email; this.createdAt = new Date(); } // Getter和Setter public Long getId() { return id; } public void setId(Long id) { this.id = id; } public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } // 其他getter/setter省略... @Override public String toString() { return "User [id=" + id + ", username=" + username + ", email=" + email + "]"; } }
2

创建DAO接口

定义用户数据访问操作:

import java.util.List; import java.util.Optional; public interface UserDao { // 创建用户 void save(User user); // 根据ID查找用户 Optional findById(Long id); // 查找所有用户 List findAll(); // 更新用户 void update(User user); // 删除用户 void delete(Long id); // 根据用户名查找用户 Optional findByUsername(String username); // 其他业务相关查询方法... }
3

实现DAO接口(JDBC版)

使用原生JDBC实现数据访问:

import java.sql.*; import java.util.ArrayList; import java.util.List; import java.util.Optional; public class UserJdbcDao implements UserDao { private static final String URL = "jdbc:mysql://localhost:3306/mydb"; private static final String USER = "root"; private static final String PASSWORD = "password"; @Override public void save(User user) { String sql = "INSERT INTO users (username, email, created_at) VALUES (?, ?, ?)"; try (Connection conn = DriverManager.getConnection(URL, USER, PASSWORD); PreparedStatement stmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS)) { stmt.setString(1, user.getUsername()); stmt.setString(2, user.getEmail()); stmt.setTimestamp(3, new Timestamp(user.getCreatedAt().getTime())); int affectedRows = stmt.executeUpdate(); if (affectedRows == 0) { throw new SQLException("创建用户失败"); } try (ResultSet rs = stmt.getGeneratedKeys()) { if (rs.next()) { user.setId(rs.getLong(1)); } } } catch (SQLException e) { // 转换为应用自定义异常 throw new DataAccessException("保存用户失败", e); } } @Override public Optional findById(Long id) { String sql = "SELECT * FROM users WHERE id = ?"; try (Connection conn = DriverManager.getConnection(URL, USER, PASSWORD); PreparedStatement stmt = conn.prepareStatement(sql)) { stmt.setLong(1, id); try (ResultSet rs = stmt.executeQuery()) { if (rs.next()) { return Optional.of(mapRowToUser(rs)); } } } catch (SQLException e) { throw new DataAccessException("按ID查询用户失败", e); } return Optional.empty(); } // 实现其他接口方法... private User mapRowToUser(ResultSet rs) throws SQLException { User user = new User(); user.setId(rs.getLong("id")); user.setUsername(rs.getString("username")); user.setEmail(rs.getString("email")); user.setCreatedAt(rs.getTimestamp("created_at")); return user; } }
4

业务层使用DAO

在业务逻辑中使用DAO进行数据访问:

public class UserService { private UserDao userDao; // 通过构造函数注入DAO public UserService(UserDao userDao) { this.userDao = userDao; } // 注册新用户 public void registerUser(String username, String email) { // 业务验证 if (userDao.findByUsername(username).isPresent()) { throw new BusinessException("用户名已存在"); } User newUser = new User(username, email); userDao.save(newUser); // 发送欢迎邮件等业务逻辑... } // 其他业务方法... }

相关模式对比

DAO模式与其他数据访问模式的比较

学习目标

理解不同数据访问模式的使用场景和优劣

DAO模式

特点:每个模型对应一个DAO,提供CRUD操作

优点:

  • 简单直接
  • 完全控制SQL
  • 技术无关实现

缺点:

  • 存在代码重复
  • 需要手写大量样板代码

Repository模式

特点:以聚合根为中心的数据访问

优点:

  • 更面向领域驱动
  • 隐藏数据访问细节
  • 更好地处理复杂查询

缺点:

  • 概念复杂
  • 学习曲线陡峭

Active Record模式

特点:模型自身包含数据访问逻辑

优点:

  • 简单易用
  • 快速开发
  • 适合小型应用

缺点:

  • 违反单一职责
  • 模型与持久化耦合
  • 难以测试

Data Mapper模式

特点:对象与数据库完全解耦

优点:

  • 完全解耦
  • 支持复杂映射
  • 高度灵活

缺点:

  • 实现复杂
  • 性能开销较大
  • 需要额外配置

模式选择建议

  • DAO:中小型项目,需要直接控制SQL
  • Repository:复杂领域模型,遵循DDD
  • Active Record:简单CRUD应用,快速原型
  • Data Mapper:大型企业应用,需要完全解耦

Spring集成DAO

在现代Spring应用中实现DAO模式

学习目标

掌握在Spring框架中实现DAO的最佳方式

1

Spring JDBC集成

使用JdbcTemplate简化JDBC操作:

@Repository public class UserDaoSpringJdbcImpl implements UserDao { private final JdbcTemplate jdbcTemplate; @Autowired public UserDaoSpringJdbcImpl(DataSource dataSource) { this.jdbcTemplate = new JdbcTemplate(dataSource); } @Override public void save(User user) { String sql = "INSERT INTO users (username, email, created_at) VALUES (?, ?, ?)"; GeneratedKeyHolder keyHolder = new GeneratedKeyHolder(); jdbcTemplate.update(connection -> { PreparedStatement ps = connection.prepareStatement(sql, new String[]{"id"}); ps.setString(1, user.getUsername()); ps.setString(2, user.getEmail()); ps.setTimestamp(3, Timestamp.valueOf(LocalDateTime.now())); return ps; }, keyHolder); user.setId(keyHolder.getKey().longValue()); } @Override public Optional findById(Long id) { String sql = "SELECT * FROM users WHERE id = ?"; try { User user = jdbcTemplate.queryForObject(sql, (rs, rowNum) -> mapRowToUser(rs), id); return Optional.ofNullable(user); } catch (EmptyResultDataAccessException e) { return Optional.empty(); } } // 其他方法实现... private User mapRowToUser(ResultSet rs) throws SQLException { // 映射逻辑 } }
2

Spring Data JPA

使用Repository接口自动实现DAO:

// 定义数据模型(使用JPA注解) @Entity @Table(name = "users") public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @Column(nullable = false, unique = true) private String username; @Column(nullable = false) private String email; @Column(name = "created_at", nullable = false) private LocalDateTime createdAt; // 构造器、getter/setter... } // 定义Repository接口(DAO) public interface UserRepository extends JpaRepository { // 自动实现基本CRUD方法 // 按用户名查询 Optional findByUsername(String username); // 自定义查询方法 @Query("SELECT u FROM User u WHERE u.email LIKE %:domain") List findByEmailDomain(@Param("domain") String domain); } // 在服务类中使用 @Service public class UserService { private final UserRepository userRepository; @Autowired public UserService(UserRepository userRepository) { this.userRepository = userRepository; } public void registerUser(User user) { if (userRepository.findByUsername(user.getUsername()).isPresent()) { throw new UsernameExistsException(); } user.setCreatedAt(LocalDateTime.now()); userRepository.save(user); } // 其他业务方法... }

Spring DAO最佳实践

  • 使用Spring的@Repository注解标识DAO类
  • 利用依赖注入提供DataSource
  • 使用JdbcTemplate处理JDBC样板代码
  • 对于新项目,优先考虑Spring Data JPA
  • 将数据源配置外部化(application.properties)
  • 使用Spring事务管理

常见错误与解决方案

使用DAO模式时的陷阱与防范

过度复杂的DAO接口

问题:DAO接口中定义了过多的业务逻辑方法

解决:

  • DAO只负责基本CRUD和简单查询
  • 复杂业务逻辑放在服务层
  • 使用Specification模式处理复杂查询

DAO层异常处理不当

问题:将SQLException直接抛出到业务层

解决:

  • 使用Spring的DataAccessException转换机制
  • 定义统一的数据访问异常体系
  • 异常信息合理封装,避免暴露底层细节

对象关系映射(N+1)问题

问题:访问关联对象时触发大量查询

解决:

  • 使用JOIN FETCH加载关联对象
  • 利用二级缓存
  • 采用DTO投影代替加载完整对象

资源未正确释放

问题:忘记关闭Connection, Statement, ResultSet

解决:

  • 使用try-with-resources语法
  • 使用JdbcTemplate等工具类
  • 采用连接池管理数据库连接

DAO模式最佳实践

构建高效可靠的DAO层

接口导向设计

始终面向接口编程,依赖抽象而非具体实现

封装数据访问细节

将JDBC/Hibernate等实现细节封装在DAO内部

使用连接池

通过连接池管理数据库连接,提高性能

使用模板方法模式

抽象通用操作,减少重复代码

1

抽象基类DAO

创建AbstractDao处理通用逻辑:

public abstract class AbstractJdbcDao { protected final JdbcTemplate jdbcTemplate; private final RowMapper rowMapper; public AbstractJdbcDao(JdbcTemplate jdbcTemplate, RowMapper rowMapper) { this.jdbcTemplate = jdbcTemplate; this.rowMapper = rowMapper; } protected Optional findOne(String sql, Object... args) { try { T entity = jdbcTemplate.queryForObject(sql, rowMapper, args); return Optional.ofNullable(entity); } catch (EmptyResultDataAccessException e) { return Optional.empty(); } } protected List findAll(String sql, Object... args) { return jdbcTemplate.query(sql, rowMapper, args); } protected int executeUpdate(String sql, Object... args) { return jdbcTemplate.update(sql, args); } // 其他通用方法... } // 具体DAO实现 @Repository public class UserJdbcDao extends AbstractJdbcDao implements UserDao { private static final String FIND_BY_ID_SQL = "SELECT * FROM users WHERE id = ?"; @Autowired public UserJdbcDao(JdbcTemplate jdbcTemplate) { super(jdbcTemplate, (rs, rowNum) -> { User user = new User(); // 映射字段 return user; }); } @Override public Optional findById(Long id) { return findOne(FIND_BY_ID_SQL, id); } // 其他方法实现... }
2

使用DTO优化性能

避免返回完整领域对象:

// 用户DTO类 public class UserDto { private String username; private String email; // 构造器、getter... } // DAO接口中添加方法 public interface UserDao { // 获取用户列表DTO List findUserDtos(); } // DAO实现中使用BeanPropertyRowMapper public class UserJdbcDao implements UserDao { @Override public List findUserDtos() { String sql = "SELECT username, email FROM users"; return jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(UserDto.class)); } }

动手练习

通过实践巩固DAO模式知识

练习目标

实现一个完整的DAO层,包括多种实现方式

练习1:原生JDBC实现

  • 创建User模型类
  • 定义UserDAO接口
  • 实现JDBC版UserDAOImpl
  • 处理数据库连接、SQL执行和结果映射

练习2:Spring JdbcTemplate

  • 使用Spring Boot创建项目
  • 配置DataSource
  • 使用JdbcTemplate重写DAO实现
  • 比较与原生JDBC实现差异

练习3:单元测试

  • 使用Mockito模拟DAO
  • 测试UserService逻辑
  • 使用内存数据库测试DAO实现
  • 测试异常处理逻辑

练习4:多数据源支持

  • 设计支持MySQL和PostgreSQL的DAO
  • 使用工厂模式创建不同DAO实现
  • 通过配置文件切换数据源
  • 验证相同接口不同实现的行为

扩展挑战

  • 实现分页查询DAO方法
  • 添加事务管理支持
  • 实现批处理操作
  • 添加二级缓存支持
  • 实现软删除模式
  • 添加审计字段支持