news 2026/9/30 2:03:33

用 Java 数据访问对象(DAO)模式隔离业务逻辑与数据库操作:从接口设计到 CRUD 落地实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
用 Java 数据访问对象(DAO)模式隔离业务逻辑与数据库操作:从接口设计到 CRUD 落地实战
  • 示例工程
  • 教程

【免费下载链接】java-design-patterns

Design patterns implemented in Java

项目地址:https://gitcode.com/GitHub_Trending/ja/java-design-patterns
点击查看免费下载

数据访问对象(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(测试文件)结构对称,均按三层场景组织:

  1. NonExistingCustomer(客户不存在时):新增成功(add返回 true);删除/更新失败(返回 false)且不影响已有数据;按 id 查询返回空 Optional。
  2. ExistingCustomer(客户已存在时):重复新增失败(add返回 false)且不影响已有数据;删除成功且客户不可再访问;更新成功且再次查询返回新信息。
  3. 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

项目地址:https://gitcode.com/GitHub_Trending/ja/java-design-patterns
点击查看免费下载

相关推荐

上一篇:在 Xinference 中部署 Qwen3.6:八种规格、五种引擎与量化方案全指南
下一篇:MSEC负载均衡与路由系统详解:构建高可用服务架构

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/30 2:01:41

BepInEx完整指南:零改动免费给Unity游戏装上Mod插件的框架

BepInEx完整指南&#xff1a;零改动免费给Unity游戏装上Mod插件的框架 【免费下载链接】BepInEx Unity / XNA game patcher and plugin framework 项目地址: https://gitcode.com/GitHub_Trending/be/BepInEx BepInEx 是一个免费的 Unity 游戏插件框架&#xff1a;把插件…

作者头像 李华
网站建设 2026/9/30 2:00:17

用手柄在电脑和主机上追B站:wiliwili 跨平台客户端完整指南

用手柄在电脑和主机上追B站&#xff1a;wiliwili 跨平台客户端完整指南 【免费下载链接】wiliwili 第三方B站客户端&#xff0c;目前可以运行在PC全平台、PSVita、PS4 、Xbox 和 Nintendo Switch上 项目地址: https://gitcode.com/GitHub_Trending/wi/wiliwili wiliwili…

作者头像 李华
网站建设 2026/9/30 1:59:54

Linux硬件时间戳实战:从网卡配置到纳秒级时间获取

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/30 1:57:42

ESPHome 接一个电磁阀:从接线到手机可控的 4 步

ESPHome 接一个电磁阀&#xff1a;从接线到手机可控的 4 步 【免费下载链接】esphome ESPHome is a system to control your ESP32, ESP8266, BK72xx, RP2040 by simple yet powerful configuration files and control them remotely through Home Automation systems. 项目地…

作者头像 李华