Java Web中DAO嵌套、实体跨引用架构问题及优化方案
我正在开发采用MVC架构(JSP+SERVLETS+MYSQL,未使用任何ORM框架)的Java Web应用,希望通过JSTL核心标签库在JSP视图层渲染如下结构的预约信息表格:
| ReceptionID | ServiceName | ReceptionCost | YourMaster | Status | DateTime |
|---|---|---|---|---|---|
| 1 | Haircut | 20 USD | Peter Anderson | New | 17.06.2022 10:00 |
基于该需求我设计了对应的Entity与DAO架构,以下为对应数据库Reception表的实体类定义:
public class Reception extends BaseEntity { private Service service; private User master; private User client; private int cost; private LocalDateTime receptionDateTime; private Status status; // 省略getter、setter方法 }
目前我存在架构层面的顾虑:Reception实体类中持有Service、User等其他实体类的引用,该设计导致我需要在一个DAO内部调用其他DAO的方法,相关实现代码如下:
public class ReceptionDAO implements GenericDAO<Reception> { public Reception mapReception(ResultSet resultSet) throws SQLException, DaoException { Reception reception = new Reception(); long receptionId = resultSet.getLong("id"); long serviceId = resultSet.getLong("service_id"); long masterId = resultSet.getLong("master_id"); long clientId = resultSet.getLong("client_id"); int cost = resultSet.getInt("cost"); LocalDateTime dateTime = resultSet.getTimestamp("reception_date_time").toLocalDateTime(); int statusId = resultSet.getInt("status_id"); UserDAO userDAO = new UserDAO(); ServiceDAO serviceDAO = new ServiceDAO(); reception.setId(receptionId); reception.setService(serviceDAO.findById(serviceId)); reception.setMaster(userDAO.findById(masterId)); reception.setClient(userDAO.findById(clientId)); reception.setCost(cost); reception.setReceptionDateTime(dateTime); reception.setStatus(Status.getStatus(statusId)); return reception; } }
- 在一个实体类中持有其他实体对象的引用是否属于良好开发实践?
- 在一个DAO内部嵌套调用其他DAO是否合理,是否属于不良架构实践?
- 如果上述实现存在较严重的架构问题,应当采用什么方案进行优化解决?
1. 实体持有其他实体引用的合理性
这是完全符合面向对象设计的常规实践,本质是领域模型的关联关系映射,不存在设计问题。页面需要同时展示预约关联的服务名、技师姓名,本来就需要获取关联实体的属性,在Reception里重复存储serviceName、masterName这类冗余字段反而违反数据库范式,后续数据更新很容易出现不一致问题。
唯一需要注意的是不要做无限制的深度嵌套,比如不要在Service里反向持有一堆关联的Reception、Reception里又嵌套其他关联对象形成循环引用,当前设计的引用层级完全没有问题。
2. DAO内部直接调用其他DAO的问题
这个实现属于典型的不良实践,核心问题有三个:
- 会触发N+1查询问题:查询1条Reception记录就要额外执行3条SQL查询关联的Service、Master、Client,如果列表页加载100条预约,就会多产生300条SQL,性能会随数据量上涨快速恶化。
- 职责越界耦合严重:DAO层的单一职责是负责对应单表的CRUD操作,在ReceptionDAO里直接实例化其他DAO,会让DAO之间形成强依赖,后续修改UserDAO、ServiceDAO的构造逻辑或者依赖规则时,所有嵌套调用的位置都需要同步修改,维护成本极高。
- 事务管控困难:多个DAO的调用散落在数据映射逻辑中,后续需要加事务控制时,很难统一管理数据库连接和回滚逻辑。
3. 可落地的优化方案
针对当前不使用ORM框架的技术栈,有两个成熟的优化方向,按推荐优先级排序:
方案一:DAO层通过JOIN查询完成关联映射,不跨DAO调用
直接在ReceptionDAO的查询SQL中用LEFT JOIN关联service、user表(user表需要关联两次分别获取技师和客户信息),单次SQL就把需要的所有字段查询出来,在mapReception方法中直接从ResultSet中取关联表字段构建Service、User对象,完全不需要调用其他DAO。
示例核心查询SQL如下:
SELECT r.*, s.name as service_name, s.price as service_price, mu.id as master_id, mu.name as master_name, cu.id as client_id, cu.name as client_name FROM reception r LEFT JOIN service s ON r.service_id = s.id LEFT JOIN user mu ON r.master_id = mu.id LEFT JOIN user cu ON r.client_id = cu.id -- 按需追加查询条件
这个方案不需要额外做架构分层改造,查询性能最好,DAO之间没有任何耦合,完全适配当前技术栈,也不会出现N+1问题。
方案二:拆分单表DAO职责,关联组装逻辑下沉到Service层
保持每个DAO只负责自身对应单表的查询:ReceptionDAO只查询reception表,返回的Reception对象初始只存储serviceId、masterId、clientId这类外键值,不直接持有关联实体对象。业务组装逻辑放到ReceptionService层:先调用ReceptionDAO查出预约列表,提取列表中所有不重复的serviceId、masterId、clientId,通过WHERE id IN (?,?,?)的批量查询方式一次性取出所有关联的Service、User数据,再根据ID匹配后set到对应的Reception实例中,最后将组装完成的数据返回给视图层。
这个方案的优势是DAO职责更纯粹,后续如果有其他业务场景只需要Reception本身的字段、不需要关联数据时,不需要执行多余的关联查询,DAO层代码复用性更高。
内容的提问来源于stack exchange,提问作者Vadym

