目录
-
- [1. 引言:为什么需要关注数据流转?](#1. 引言:为什么需要关注数据流转?)
- [2. 整体流程概览](#2. 整体流程概览)
- [3. 逐节点详细流转分析](#3. 逐节点详细流转分析)
-
- [3.1 起点:Domain 层发起调用](#3.1 起点:Domain 层发起调用)
- [3.2 进入Infrastructure层下的 Repository 实现:`AgentRepository.queryAvailableAgents()`](#3.2 进入Infrastructure层下的 Repository 实现:
AgentRepository.queryAvailableAgents()) - [3.3 进入 Infrastructure 层:MyBatis 代理执行](#3.3 进入 Infrastructure 层:MyBatis 代理执行)
- [3.4 回到 Repository 层:进行数据转换与聚合](#3.4 回到 Repository 层:进行数据转换与聚合)
- [3.5 终点:Domain 层接收并消费数据](#3.5 终点:Domain 层接收并消费数据)
- [4. 数据形态演变全览](#4. 数据形态演变全览)
- [5. 核心设计思想与最佳实践](#5. 核心设计思想与最佳实践)
- [6. 总结](#6. 总结)
1. 引言:为什么需要关注数据流转?
在分层架构(如 DDD、Clean Architecture)中,Repository 是连接领域层(Domain)与数据层(Infrastructure)的核心枢纽。理解一次查询请求中,数据如何从 Domain 层出发,经过层层转换,最终又以 Domain 层可理解的形态返回,是掌握系统架构与性能优化的关键。
本文将以一个典型的 AgentRepository.queryAvailableAgents() 方法为例,逐行、逐节点剖析数据从 Domain 层出发,到返回 Domain 层的完整流转过程。
2. 整体流程概览
bash
graph TD
A[Domain Layer: 调用 repository.queryAvailableAgents()] --> B[Repository Layer: AgentRepository]
B --> C[① 调用 DAO 接口]
C --> D[Infrastructure Layer: MyBatis 代理介入]
D --> E[② 执行 SQL 查询]
E --> F[③ JDBC 返回 ResultSet]
F --> G[④ ResultSet 映射为 PO 列表]
G --> H[⑤ 接收 List<AiAgent> PO 列表]
H --> I[⑥ 遍历 PO,转换为 VO]
I --> J[⑦ 返回 List<AiAgentVO>]
J --> K[Domain Layer: 接收 VO 列表,继续业务逻辑]
核心路径:Domain → Repository → DAO/MyBatis → Database → ResultSet → PO → VO → Domain。
3. 逐节点详细流转分析
流程对应
bash
① domain层Service需要查询数据
│ 例如:ArmoryService.acceptArmoryAgent() 需要查询智能体配置
│
↓
② Service调用仓储接口
│ IAgentRepository repository;
│ repository.queryAvailableAgents();
│
↓
③ 进入AgentRepository实现
│ aiAgentDao.queryEnabledAgents();
│
↓
④ MyBatis动态代理(Mybatis)
│ 找到XML中对应的SQL
│
↓
⑤ 执行SQL(Mybatis)
│ SELECT * FROM ai_agent WHERE status = 1
│
↓
⑥ JDBC返回ResultSet(Mybatis)
│ 表格形式的原始数据
│
↓
⑦ resultMap映射(Mybatis)
│ column="agent_id" → property="agentId"
│ column="agent_name" → property="agentName"
│ ...
│
↓
⑧ 填充PO对象(Repository里实现的接口)
│ List<AiAgent> aiAgents
│
↓
⑨ PO转VO(Repository里实现的接口)
│ aiAgent.getAgentId() → vo.agentId
│ aiAgent.getAgentName() → vo.agentName
│ ...
│
↓
⑩ 返回List<AiAgentVO>给domain层Service(Repository里实现的接口)
代码流程详解
bash
// ① domain层Service
@Service
public class ArmoryService implements IArmoryService {
@Resource
private IAgentRepository repository;
@Override
public void acceptArmoryAgent(String agentId) {
// ② 调用仓储接口查询数据
List<AiAgentClientFlowConfigVO> configs = repository.queryAiAgentClientsByAgentId(agentId);
}
}
// ③ 仓储实现
@Repository
public class AgentRepository implements IAgentRepository {
@Resource
private IAiAgentDao aiAgentDao;
@Override
public List<AiAgentVO> queryAvailableAgents() {
// ④ 调用DAO接口,触发MyBatis动态代理
List<AiAgent> aiAgents = aiAgentDao.queryEnabledAgents();
// ↑
// │ ⑤ MyBatis找到XML中的SQL并执行
// │ SELECT * FROM ai_agent WHERE status = 1
// │
// │ ⑥ JDBC返回ResultSet(表格数据)
// │
// │ ⑦ 根据resultMap映射 column → property
// │ column="agent_id" → property="agentId"
// │ column="agent_name" → property="agentName"
// │
// │ ⑧ 反射创建AiAgent对象,调用setter赋值
// │ aiAgent.setAgentId("45855306")
// │ aiAgent.setAgentName("测试智能体")
// │
// ↓ 返回 List<AiAgent>(PO列表)
// ⑨ PO转VO
List<AiAgentVO> aiAgentVOS = new ArrayList<>();
for (AiAgent aiAgent : aiAgents) {
aiAgentVOS.add(AiAgentVO.builder()
.agentId(aiAgent.getAgentId())
.agentName(aiAgent.getAgentName())
.build());
}
// ⑩ 返回VO
return aiAgentVOS;
}
}
<!-- ⑤⑥⑦⑧ 这些步骤发生在MyBatis内部 -->
<select id="queryEnabledAgents" resultMap="AiAgentMap">
SELECT id, agent_id, agent_name, description, channel, strategy, status, create_time, update_time
FROM ai_agent
WHERE status = 1
ORDER BY create_time DESC
</select>
<!-- ⑦ resultMap定义映射关系 -->
<resultMap id="AiAgentMap" type="cn.moonice.infrastructure.dao.po.AiAgent">
<id column="id" property="id"/>
<result column="agent_id" property="agentId"/>
<result column="agent_name" property="agentName"/>
<result column="description" property="description"/>
<result column="channel" property="channel"/>
<result column="strategy" property="strategy"/>
<result column="status" property="status"/>
<result column="create_time" property="createTime"/>
<result column="update_time" property="updateTime"/>
</resultMap>
3.1 起点:Domain 层发起调用
位置 :领域模型的仓储服务
代码示意:
java
@Resource
private IAgentRepository repository;
// 在某个 Domain Re或 Application Service 中
List<AiAgentVO> aiAgentVOS = repository.queryAvailableAgents();
// 接下来基于 VO 进行领域逻辑计算,如:过滤、排序、组合业务规则等
java
# repository可以调用infrastructure层实现的IAgentRepository方法原因
Spring启动时扫描所有模块
│
↓
发现 IAgentRepository 接口(domain层)
│
↓
发现 AgentRepository 实现了 IAgentRepository(infrastructure层)
│
↓
AgentRepository 有 @Repository 注解,注册为Spring Bean
│
↓
ArmoryService 中 @Resource IAgentRepository repository
│
↓
Spring自动把 AgentRepository 注入到 repository 字段
│
↓
调用 repository.queryAvailableAgents() 实际执行的是 AgentRepository 的实现
数据形态 :此时尚未有具体数据,只是一个方法调用契约。Domain 层期望得到一个 List<AiAgentVO>(值对象列表),用于后续的纯领域逻辑。
3.2 进入Infrastructure层下的 Repository 实现:AgentRepository.queryAvailableAgents()
节点 ①:调用 DAO 接口
java
List<AiAgent> aiAgents = aiAgentDao.queryEnabledAgents();
- 作用:Repository 作为防腐层,调用基础设施层的具体数据访问对象(DAO)。
- 数据形态转换 :方法签名从返回
List<AiAgentVO>(Domain 所需)转换为操作List<AiAgent>(持久化对象,与数据库表结构对齐)。 - 关键点 :这里开始了 Domain Model 与 Persistence Model 的分离。
3.3 进入 Infrastructure 层:MyBatis 代理执行
节点 ②:MyBatis 动态代理介入
aiAgentDao是一个 MyBatis Mapper 接口。- MyBatis 在运行时通过动态代理,将接口方法调用
queryEnabledAgents()绑定到对应的 SQL 映射语句。
节点 ③:执行 SQL 查询
sql
-- 对应的 SQL 映射(通常在 XML 或注解中)
SELECT agent_id, agent_name, description, channel, strategy, status
FROM ai_agent
WHERE status = 1;
- SQL 经由连接池发送到数据库。
- 数据形态:从数据库的二进制存储格式,转换为网络传输的字节流。
节点 ④:JDBC 返回 ResultSet
- 数据库执行查询,通过 JDBC 驱动将结果以
ResultSet对象的形式返回给 MyBatis。 ResultSet是一个游标,指向查询结果集的第一行之前。- 数据形态:此时数据是原始的、按列组织的二维表结构,带有数据库元数据(如列名、类型)。
节点 ⑤:ResultSet 映射,填充 PO(关键映射步骤)
- MyBatis 根据
<resultMap>配置,将ResultSet的每一行映射为一个AiAgentPOJO 实例。
xml
<!-- 示例 resultMap -->
<resultMap id="AiAgentResult" type="com.example.infrastructure.po.AiAgent">
<id column="agent_id" property="agentId"/>
<result column="agent_name" property="agentName"/>
<result column="description" property="description"/>
<result column="channel" property="channel"/>
<result column="strategy" property="strategy"/>
<result column="status" property="status"/>
</resultMap>
- 映射过程(以第一行为例) :
ResultSet游标移动到第一行。- 读取
column="agent_id"的值,例如"45855306"。 - 调用
aiAgent.setAgentId("45855306")。 - 循环处理所有列,完成一个
AiAgent对象的属性填充。 - 将该对象加入
List<AiAgent>。 - 游标下移,重复直到
ResultSet结束。
- 数据形态转换 :
ResultSet(原始表数据) →AiAgentPO(带有业务语义的 Java 对象)。
3.4 回到 Repository 层:进行数据转换与聚合
节点 ⑥:Repository 接收 PO 列表
java
// aiAgents 现在是一个包含所有查询结果的 List<AiAgent>
List<AiAgent> aiAgents = aiAgentDao.queryEnabledAgents(); // 承接上一步的结果
- 数据形态 :
List<AiAgent>,此时数据已脱离 JDBC 原始形态,是完整的 Java 对象集合。
节点 ⑦:PO 转换为 VO(领域值对象)
java
List<AiAgentVO> aiAgentVOS = new ArrayList<>();
for (AiAgent aiAgent : aiAgents) {
aiAgentVOS.add(AiAgentVO.builder()
.agentId(aiAgent.getAgentId())
.agentName(aiAgent.getAgentName())
.description(aiAgent.getDescription())
.channel(aiAgent.getChannel())
.strategy(aiAgent.getStrategy())
.status(aiAgent.getStatus())
.build());
}
- 转换目的 :
- 隔离变化:数据库表结构(PO)变化不影响 Domain 层。
- 语义丰富:VO 可以包含衍生字段、计算属性,更贴合领域语言。
- 序列化友好:VO 通常设计为用于网络传输或前端展示。
- 数据形态转换 :
AiAgentPO(持久化对象) →AiAgentVO(领域值对象)。字段值被复制,但对象语义和用途发生变化。
节点 ⑧:返回 VO 列表给 Domain 层
java
return aiAgentVOS;
- 至此,Repository 完成了其核心职责:为 Domain 层提供透明的、领域语义的数据访问。
3.5 终点:Domain 层接收并消费数据
- Domain 层拿到
List<AiAgentVO>,完全不知道这些数据来自 MySQL、Redis 还是任何其他数据源。 - Domain 层可以基于这些 VO 进行纯业务逻辑处理,例如:
java
// 领域逻辑示例:过滤出特定渠道的智能体
List<AiAgentVO> availableAgents = agentRepository.queryAvailableAgents();
List<AiAgentVO> webChannelAgents = availableAgents.stream()
.filter(vo -> "WEB".equals(vo.getChannel()))
.collect(Collectors.toList());
// 或者进行业务规则校验、组合聚合等复杂操作
- 数据形态 :
AiAgentVO作为 Domain 层内部流通的"货币",其生命周期仅限于本次业务操作,通常不持久化。
4. 数据形态演变全览
| 阶段 | 位置 | 数据形态 | 代表对象 | 特点与职责 |
|---|---|---|---|---|
| 存储 | 数据库磁盘 | 二进制行记录 | ai_agent 表 |
持久化存储,遵循数据库范式 |
| 传输 | JDBC 网络流 | 字节流 / ResultSet | ResultSet |
原始查询结果,带元数据 |
| 持久化对象 | Infrastructure 层 | Java POJO | AiAgent |
与表结构严格映射,包含数据库注解 |
| 领域值对象 | Repository/Domain 层 | 不可变值对象 | AiAgentVO |
承载领域语义,用于业务逻辑流转 |
| 消费 | Domain 层 | 业务对象集合 | List<AiAgentVO> |
领域逻辑操作的输入/输出 |
关键转换点:
- ResultSet → PO :由 MyBatis
resultMap完成,是 数据库世界到 Java 对象世界 的桥梁。 - PO → VO :由 Repository 手动或使用工具(如 MapStruct)完成,是 持久化模型到领域模型 的转换。
5. 核心设计思想与最佳实践
- 单一职责:Repository 只负责数据访问与初步聚合,不包含业务逻辑。
- 依赖倒置:Domain 层依赖 Repository 接口,而非具体实现(如 MyBatis)。
- 模型隔离:PO(持久化对象)与 VO/DTO(领域/传输对象)严格分离,避免数据库 schema 污染领域模型。
- 性能考量 :
- N+1 查询问题 :在
resultMap中使用<collection>或<association>实现关联查询,避免在循环中查询数据库。 - 字段冗余:VO 仅包含 Domain 层需要的字段,避免大对象网络传输。
- 转换开销:大量数据转换时,考虑使用 MapStruct 等编译期代码生成工具提升性能。
- N+1 查询问题 :在
6. 总结
一次 domain层service的queryAvailableAgents() 调用,看似简单,实则完成了一次跨越 Domain层、Repository层、Infrastructure层、数据库 的完整数据旅程。
- 起点与终点都是 Domain 层,体现了"领域驱动"的核心。
- Repository 是关键的协调者,它屏蔽了底层数据获取的复杂性,并为 Domain 层提供了整洁的、富含领域语义的接口。
- 理解每一步的数据形态转换,有助于我们定位性能瓶颈(如过度转换)、设计更清晰的模型(如 PO/VO 分离),并构建更健壮、可维护的系统架构。
作者基于代码数据流程拆解,梳理ddd架构下的流转过程,也希望对读者有帮助。