架构之美:为小程序打造一个“零读取”的JPA创建接口
在后端开发中,我们经常需要为不同的客户端(如PC管理后台、小程序App)提供功能相似但上下文完全不同的API (Application Programming Interface)。一个常见的错误是将所有逻辑都堆砌在同一个Service类中,导致代码臃肿、职责混乱。
一个更优雅、更具可维护性的架构,是为不同的“端”创建专属的Service层。
今天,我将带你完整地设计并实现一个POST /api/app/demands(小程序创建需求)接口。我们将看到,如何通过创建专属的AppSolutionDemandService,并利用 existsById 和 getReference 这两个JPA (Java Persistence API) “神器”,构建一个几乎“零读取”、性能极致的创建接口。
业务场景:一个简洁的小程序表单提交 📱
我们的需求非常明确:小程序用户填写一个简单的表单(预算、数量等),提交后为自己创建一个新的“福利需求单”(SolutionDemand)。
核心挑战:
- 架构清晰:如何将小程序端的业务逻辑与PC后台的管理逻辑清晰地分离开来?
- 安全:如何确保创建的需求单准确地关联到当前登录的用户,而不是任何其他人?
- 性能:如何以最少的数据库交互,最高效地完成创建操作?
解决方案:专属Service + 轻量级校验 + 实体引用
我们通过一套组合拳,完美地解决了所有挑战。
1. 架构决策:创建AppSolutionDemandService
我们做的第一个、也是最重要的决策,就是拒绝在已有的SolutionDemandService(服务于PC端)中增加if-else逻辑。我们为小程序端创建了一个全新的、职责单一的Service:AppSolutionDemandService。
为什么这么做?
- 职责分离:
SolutionDemandService的上下文是管理员,而AppSolutionDemandService的上下文是当前登录用户。将它们分开,使得每个Service的逻辑都更纯粹。 - 高内聚,低耦合:小程序端的所有需求相关逻辑,都内聚在
AppSolutionDemandService中,与PC端完全解耦。 - 易于维护:未来修改小程序逻辑,只需关心
AppSolutionDemandService,完全不用担心会影响到PC后台的功能。
2. Service层实现:追求极致性能
在专属的AppSolutionDemandService中,我们的每一步都以性能和安全为最高优先级。
// AppSolutionDemandService.java
@Service
@RequiredArgsConstructor
public class AppSolutionDemandService {
private final EntityManager entityManager;
private final SolutionDemandRepository demandRepository;
private final SolutionUserRepository solutionUserRepository;
@Transactional
public AppDemandCreateVO createDemandForApp(Integer currentUserId, AppDemandCreatePayload payload) {
// 1. 轻量级用户存在性校验
if (!solutionUserRepository.existsById(currentUserId)) {
throw new NotFoundException("当前登录用户不存在");
}
// 2. 构建实体并复制前端数据
SolutionDemand newDemand = new SolutionDemand();
BeanUtils.copyProperties(payload, newDemand);
// 3. 使用 getReference() 高效获取用户实体引用
SolutionUser userReference = entityManager.getReference(SolutionUser.class, currentUserId);
newDemand.setSolutionUser(userReference);
// 4. 后端填充业务控制字段
newDemand.setStatus(1);
newDemand.setDemandCode(generateDemandCode());
// 5. 保存实体
SolutionDemand savedDemand = demandRepository.save(newDemand);
// 6. 转换为精确的VO并返回
return convertToVO(savedDemand);
}
// ...
}
技术解读:
existsById(currentUserId): 我们没有用findById去加载完整的SolutionUser实体。existsById只会生成一条极其高效的SELECT COUNT(*)SQL (Structured Query Language),用于确认用户ID的合法性。这是我们的第一重性能优化。entityManager.getReference(...): 在确认用户存在后,我们使用getReference()来获取一个SolutionUser的“代理”对象。这个操作完全不查询数据库,但足以让JPA在生成INSERT语句时正确填充solution_user_id外键。这是我们的第二重性能优化。
成果验证:一份“零读取”的SQL日志 ✨
执行这个精心设计的接口,我们得到的SQL日志堪称完美:
【最终日志】
-- 1. 极轻量级的存在性检查
Hibernate:
select count(*) as col_0_0_ from solution_user solutionus0_ where solutionus0_.id=?
-- 2. 唯一的写入操作
Hibernate:
insert into solution_demand (...) values (...)
日志解读:
- 无过度查询:我们没有
SELECT任何完整的实体对象,彻底避免了过度查询。 - 最低交互:整个业务逻辑只产生了一次极轻量级的读(用于校验)和一次写。这是在需要校验用户存在性的前提下,能达到的最低数据库交互次数。
结论:好的架构,让高性能成为必然 💡
这次小程序创建接口的实现,为我们揭示了构建高性能API的几个关键原则:
- 架构先行,职责分离:为不同端(PC/App)或不同领域创建专属的Service,是保证代码长期可维护性的基石。
- 校验用
exists,关联用reference:在“写”操作中,这是一个黄金组合。用existsBy...进行轻量级校验,用getReference()进行零开销的关联设置。 - 后端控制业务规则:将状态、编码等核心业务字段的生成逻辑保留在后端,可以保证数据的一致性和安全性。
通过遵循这些原则,我们构建的不再是一个简单的save接口,而是一个在架构、安全和性能上都达到“工业级”标准的、真正专业的API。
附录:图表化总结与深度解析 📊✨
接口设计原则总结表 📋
| 设计原则 | 核心问题 | 解决方案 | 关键技术/代码 |
|---|---|---|---|
| 架构清晰 🏛️ | PC/App逻辑混杂,职责不清 | 创建专属Service | AppSolutionDemandService |
| 数据安全 🛡️ | 信任前端传入的用户ID | 使用Token解析出的currentUserId | @RequestAttribute |
| 高性能 🚀 | 为INSERT而SELECT,过度查询 | 轻量级校验 + 实体引用 | existsById, getReference() |
| 业务健壮 💪 | 关键业务字段由前端控制 | 后端填充 | newDemand.setStatus(1) |
接口处理流程图 (Flowchart) 💡
关键交互时序图 (Sequence Diagram) 🔄
实体状态图 (State Diagram) 🚦
以SolutionDemand实体在创建接口中的状态流转为例。
核心类图 (Class Diagram) 🏗️
展示了AppSolutionDemandService与各组件的依赖关系。
实体关系图 (Entity Relationship Diagram) 🔗
用ER图的形式更直观地展示SolutionDemand与SolutionUser的关系。
思维导图 (Markdown Format) 🧠

155

被折叠的 条评论
为什么被折叠?



