- 示例工程
- 教程
【免费下载链接】java-design-patterns
Design patterns implemented in Java
数据访问对象(Data Access Object,简称 DAO)是 Java 企业级应用中最经典的结构型模式之一,其核心思想是把业务逻辑与持久化层彻底隔离,让应用通过一个抽象接口读写数据,而不必关心底层究竟是关系型数据库、XML 文件还是内存 Map。本文以 java-design-patterns 仓库中>@Setter @Getter @ToString @EqualsAndHashCode(onlyExplicitlyIncluded = true) @AllArgsConstructor public class Customer { @EqualsAndHashCode.Include private int id; private String firstName; private String lastName; }
第二步:定义数据访问接口 CustomerDao
接口是整个 DAO 模式的灵魂。它把"应用需要什么数据操作"与"如何用具体 DBMS、数据库 schema 满足这些需求"彻底分开。仓库中的接口定义非常规范,每个方法都有完整的 Javadoc,见 CustomerDao.java:
public interface CustomerDao { // 返回所有客户。流可能是惰性或即时求值,取决于实现;使用后必须关闭。 Stream<Customer> getAll() throws Exception; // 按 id 查询客户,返回 Optional;不存在时返回空 Optional。 Optional<Customer> getById(int id) throws Exception; // 新增客户;成功返回 true,若客户已存在返回 false。 boolean add(Customer customer) throws Exception; // 更新客户;客户存在且更新成功返回 true,否则返回 false。 boolean update(Customer customer) throws Exception; // 删除客户;客户存在且删除成功返回 true,否则返回 false。 boolean delete(Customer customer) throws Exception; }有几个设计要点值得注意:
- 返回
Optional<Customer>而非裸对象或 null,明确表达"可能查不到"的语义,强制调用方处理空值; - 返回
Stream<Customer>而非List,允许实现惰性求值、按需拉取,配合 try-with-resources 管理资源; - 方法签名统一抛出
Exception,把异常策略交给调用方(后续 DbCustomerDao 实际用CustomException包装 SQLException)。
第三步:两种实现——内存版与 RDBMS 版
仓库为同一个接口提供了两个实现,展示 DAO 模式"换存储不改业务"的核心价值:
InMemoryCustomerDao(内存实现):用HashMap<Integer, Customer>存数据,适合临时数据库、原型验证和测试场景。数据随 JVM 退出而丢失。完整实现见 InMemoryCustomerDao.java:
public class InMemoryCustomerDao implements CustomerDao { private final Map<Integer, Customer> idToCustomer = new HashMap<>(); @Override public Stream<Customer> getAll() { return idToCustomer.values().stream(); // 即时求值的流 } @Override public Optional<Customer> getById(final int id) { return Optional.ofNullable(idToCustomer.get(id)); } @Override public boolean add(final Customer customer) { if (getById(customer.getId()).isPresent()) { return false; // 主键已存在,拒绝插入 } idToCustomer.put(customer.getId(), customer); return true; } @Override public boolean update(final Customer customer) { return idToCustomer.replace(customer.getId(), customer) != null; } @Override public boolean delete(final Customer customer) { return idToCustomer.remove(customer.getId()) != null; } }DbCustomerDao(JDBC 实现):通过注入的DataSource与关系型数据库交互,是"真实生产形态"的实现。构造函数由@RequiredArgsConstructor生成,强制要求传入DataSource,见 DbCustomerDao.java。
getAll()的实现尤其精彩——它把一个ResultSet包装成惰性 Stream,并在流关闭时通过onClose回调释放连接、语句和结果集:
@Override public Stream<Customer> getAll() throws Exception { try { var connection = getConnection(); var statement = connection.prepareStatement("SELECT * FROM CUSTOMERS"); var resultSet = statement.executeQuery(); return StreamSupport.stream( new Spliterators.AbstractSpliterator<Customer>(Long.MAX_VALUE, Spliterator.ORDERED) { @Override public boolean tryAdvance(Consumer<? super Customer> action) { try { if (!resultSet.next()) { return false; } action.accept(createCustomer(resultSet)); return true; } catch (SQLException e) { throw new RuntimeException(e); } } }, false) .onClose(() -> mutedClose(connection, statement, resultSet)); } catch (SQLException e) { throw new CustomException(e.getMessage(), e); } }其余四个 CRUD 方法全部基于PreparedStatement的占位符参数(?)防 SQL 注入,使用 try-with-resources 自动关闭连接,并将底层SQLException统一包装为CustomException抛出。核心 SQL 语句如下:
| 操作 | SQL | 说明 |
|---|---|---|
| 查询全部 | SELECT * FROM CUSTOMERS | 包装为惰性流,需关闭 |
| 按 id 查询 | SELECT * FROM CUSTOMERS WHERE ID = ? | 用setInt(1, id)绑定参数 |
| 新增 | INSERT INTO CUSTOMERS VALUES (?,?,?) | id 已存在时返回 false |
| 更新 | UPDATE CUSTOMERS SET FNAME = ?, LNAME = ? WHERE ID = ? | executeUpdate() > 0判断成功 |
| 删除 | DELETE FROM CUSTOMERS WHERE ID = ? | executeUpdate() > 0判断成功 |
表结构定义在独立的CustomerSchemaSql常量类中(CustomerSchemaSql.java):
public static final String CREATE_SCHEMA_SQL = "CREATE TABLE CUSTOMERS (ID NUMBER, FNAME VARCHAR(100), " + "LNAME VARCHAR(100))"; public static final String DELETE_SCHEMA_SQL = "DROP TABLE CUSTOMERS";第四步:组装并运行 App
App.java 演示了同一套业务操作函数如何先后作用于两个 DAO 实现——这正是 DAO 模式价值的最直观体现:业务代码零改动,存储实现随意切换。
@Slf4j public class App { private static final String DB_URL = "jdbc:h2:mem:dao;DB_CLOSE_DELAY=-1"; public static void main(final String[] args) throws Exception { final var inMemoryDao = new InMemoryCustomerDao(); performOperationsUsing(inMemoryDao); final var dataSource = createDataSource(); createSchema(dataSource); final var dbDao = new DbCustomerDao(dataSource); performOperationsUsing(dbDao); deleteSchema(dataSource); } private static DataSource createDataSource() { var dataSource = new JdbcDataSource(); dataSource.setURL(DB_URL); // H2 内存数据库 return dataSource; } private static void performOperationsUsing(final CustomerDao customerDao) throws Exception { addCustomers(customerDao); // 批量新增 3 个客户 LOGGER.info(ALL_CUSTOMERS); try (var customerStream = customerDao.getAll()) { // 关闭流以释放资源 customerStream.forEach(customer -> LOGGER.info(customer.toString())); } LOGGER.info("customerDao.getCustomerById(2): " + customerDao.getById(2)); final var customer = new Customer(4, "Dan", "Danson"); customerDao.add(customer); // 新增 customer.setFirstName("Daniel"); customer.setLastName("Danielson"); customerDao.update(customer); // 更新 customerDao.delete(customer); // 删除 LOGGER.info(ALL_CUSTOMERS + customerDao.getAll()); } public static List<Customer> generateSampleCustomers() { final var customer1 = new Customer(1, "Adam", "Adamson"); final var customer2 = new Customer(2, "Bob", "Bobson"); final var customer3 = new Customer(3, "Carl", "Carlson"); return List.of(customer1, customer2, customer3); } }示例运行输出(H2 内存数据库,jdbc:h2:mem:dao;DB_CLOSE_DELAY=-1表示 JVM 退出前保留内存库):
INFO com.iluwatar.dao.App -- customerDao.getAllCustomers(): INFO com.iluwatar.dao.App -- Customer(id=1, firstName=Adam, lastName=Adamson) INFO com.iluwatar.dao.App -- Customer(id=2, firstName=Bob, lastName=Bobson) INFO com.iluwatar.dao.App -- Customer(id=3, firstName=Carl, lastName=Carlson) INFO com.iluwatar.dao.App -- customerDao.getCustomerById(2): Optional[Customer(id=2, firstName=Bob, lastName=Bobson)] INFO com.iluwatar.dao.App -- customerDao.getAllCustomers(): java.util.stream.ReferencePipeline$Head@4c3e4790 INFO com.iluwatar.dao.App -- Customer(id=4, firstName=Daniel, lastName=Danielson)可以看到,内存版与 DB 版输出完全一致,业务调用方对底层存储毫无感知。
依赖与运行方式
模块依赖(见 />
从上图可以看出典型的 DAO 调用链:业务客户端(Client/App)→ CustomerDao 接口 → 具体 DAO 实现 → DataSource/数据库。业务层永远不直接触碰Connection、Statement、ResultSet等 JDBC 细节,这些全部封装在 DAO 实现内部。
源码级验证:测试用例如何保障 DAO 行为
模块的测试代码与接口 Javadoc 中的契约一一对应,是最佳的行为规格说明书。
InMemoryCustomerDaoTest(测试文件)与DbCustomerDaoTest(测试文件)结构对称,均按三层场景组织:
- NonExistingCustomer(客户不存在时):新增成功(
add返回 true);删除/更新失败(返回 false)且不影响已有数据;按 id 查询返回空 Optional。 - ExistingCustomer(客户已存在时):重复新增失败(
add返回 false)且不影响已有数据;删除成功且客户不可再访问;更新成功且再次查询返回新信息。 - ConnectivityIssue(仅 DbCustomerDaoTest,数据库不可达):用 Mockito mock 一个会抛
SQLException的DataSource,断言增删改查全部以异常形式反馈给客户端,验证了 DAO 对故障的显式暴露策略。
测试前后通过DriverManager.getConnection(DB_URL)执行CustomerSchemaSql.CREATE_SCHEMA_SQL/DELETE_SCHEMA_SQL,保证每个用例从干净的CUSTOMERS表开始。这套测试直接证明了 DAO 接口的"行为契约"与实现细节无关——换实现、换数据库,测试逻辑完全复用。
何时使用 DAO 模式
出现以下任何一种情况,都适合引入 DAO 模式:
- 需要对数据源访问进行抽象和封装:所有数据访问都收敛到 DAO 层,业务层不散落 SQL;
- 应用需要支持多种数据库或存储机制且不希望大改代码:例如同一套业务同时跑 H2 与生产 MySQL,或从内存 Map 平滑切换到 JDBC;
- 希望数据访问保持干净简单、与业务逻辑分离:SQL、连接管理、事务边界都收在 DAO 内部。
典型真实应用场景包括:需要数据库交互的企业应用;数据访问需要适配多种存储类型(关系数据库、XML 文件、纯文本文件等)的应用;提供通用数据访问能力的框架层。
收益与权衡
收益:
- 解耦(Decoupling):数据访问逻辑与业务逻辑分离,提升模块化程度与可读性;
- 复用(Reusability):DAO 可在应用的不同部分甚至不同项目间复用;
- 可测试性(Testability):业务逻辑可与数据访问逻辑分开测试,配合内存实现或 Mock 数据源即可完成单元测试;
- 灵活性(Flexibility):切换底层存储机制时对应用代码影响极小。
代价:
- 层数复杂度(Layer Complexity):多出的一层抽象会增加整体复杂度和开发时间;
- 额外开销(Overhead):对简单应用而言,DAO 模式可能引入超出必要的间接层;
- 学习曲线(Learning Curve):尤其在大项目中,开发者需要时间理解并正确实现该模式。
与相关设计模式的关系
DAO 模式在仓库中往往与以下模式搭配使用,形成完整的分层架构:
- Abstract Factory(抽象工厂):抽象 DAO 的创建过程,尤其适合需要支持多种数据库或存储机制的场合;
- Factory(工厂):动态实例化 DAO,在选择实现时提供灵活性;
- Service Layer(服务层):常与 DAO 联用,定义应用边界及其可用操作集合——典型的三层结构是 Controller → Service → DAO;
- Strategy(策略):可在运行时根据上下文切换数据访问策略。
小结
DAO 模式的价值不在于代码量,而在于边界:它用接口划清了"业务要什么数据"与"存储怎么给数据"之间的界线。java-design-patterns 仓库的>赞
- 示例工程
- 教程
【免费下载链接】java-design-patterns
Design patterns implemented in Java
相关推荐
Yii 2 DAO 数据库访问对象实战:从连接管理到读写分离的完整指南
Yii 2 DAO 数据库访问对象实战:从连接管理到读写分离的完整指南 Yii 2 内置的数据访问层(DAO,Database Access Objects)构
后端Web框架Yii 2 DAO 数据库访问对象完全指南:连接、查询、事务与读写分离实战
Yii 2 DAO 数据库访问对象完全指南:连接、查询、事务与读写分离实战 Yii 2 的 DAO(Database Access Objects,数据库访问对
后端Web框架告别SQL繁琐操作:Exposed DAO模式让数据库访问像操作对象一样简单
告别SQL繁琐操作:Exposed DAO模式让数据库访问像操作对象一样简单 你还在为编写复杂SQL语句烦恼吗?还在手动映射数据库表和Java对象吗?Expos
ORM后端数据存储