欢迎光临
我们一直在努力

为什么查询接口一开始就要传入实体,而不是参数?

一个被严重低估的 Java 工程设计问题

在很多 Java 项目中,我们经常能看到这样的代码演进过程:

// 初版
getOrder(Long orderId);

// 第二版
getOrder(Long orderId, Integer status);

// 第三版
getOrder(Long orderId, Integer status, LocalDateTime startTime);

// 第四版(崩了)
getOrder(Long orderId, Integer status, LocalDateTime startTime, LocalDateTime endTime, Long userId);

直到有一天,有人站出来说了一句:

“我们直接传一个查询对象吧。”

很多人听完的第一反应是:

  • 查询而已,传这么重的对象是不是过度设计?
  • 直接参数不是更清晰吗?
  • DTO / VO / Query 到底有什么区别?

但在真正做过复杂系统、长期维护系统的人眼里,
查询传实体,往往不是设计洁癖,而是工程理性。

本文将从 工程演进、代码稳定性、可扩展性、分层解耦 四个角度,把这个问题一次性讲透。

一、从“参数爆炸”开始说起

先看一个真实的订单查询接口。

1️⃣ 参数式查询(初期)

public List<Order> queryOrder(
Long orderId,
Long userId,
Integer status,
LocalDateTime startTime,
LocalDateTime endTime) {
...
}

看起来没什么问题,但你很快会遇到:

  • 参数越来越多
  • 参数顺序极易写错
  • 新增条件 = 修改所有调用方
  • MyBatis / Controller / Service 层同时改

👉 接口一旦暴露,就很难再动

二、查询实体(Query Object)到底解决了什么?

1️⃣ 查询条件是“一个整体”,而不是一堆零散参数

在业务语义上:

“订单查询”本身就是一个概念

而不是:

“orderId + userId + status + timeRange 的组合体”

于是我们引入 查询实体(Query Object)。

三、标准做法:Query Object

1️⃣ 定义查询对象

/**
* 订单查询条件
*/

public class OrderQuery {

/** 订单号 */
private Long orderId;

/** 用户ID */
private Long userId;

/** 订单状态 */
private Integer status;

/** 创建时间开始 */
private LocalDateTime startTime;

/** 创建时间结束 */
private LocalDateTime endTime;

// getter / setter
}

👉 注意:这是 Query,不是 Entity,不是 DTO

2️⃣ Service 层接口立刻变得稳定

public List<Order> queryOrder(OrderQuery query) {
return orderMapper.selectByCondition(query);
}

此时你会发现一个关键变化:

接口签名稳定了

以后新增条件:

private Integer payType;
private Long merchantId;

  • 不改 Service 方法
  • 不改 Controller
  • 不影响历史调用方

四、MyBatis 中 Query Object 的真正威力

Mapper 接口

List<Order> selectByCondition(OrderQuery query);

Mapper XML(核心价值)

<select id="selectByCondition" resultType="Order">
SELECT *
FROM orders
<where>
<if test="orderId != null">
AND order_id = #{orderId}
</if>
<if test="userId != null">
AND user_id = #{userId}
</if>
<if test="status != null">
AND status = #{status}
</if>
<if test="startTime != null">
AND create_time &gt;= #{startTime}
</if>
<if test="endTime != null">
AND create_time &lt;= #{endTime}
</if>
</where>
</select>

💡 Query Object = 天然的动态 SQL 载体

五、为什么不推荐 Controller 直接传一堆参数?

❌ Controller 参数式写法(问题多)

@GetMapping("/list")
public List<Order> list(
@RequestParam(required = false) Long orderId,
@RequestParam(required = false) Integer status,
@RequestParam(required = false) Long userId) {
...
}

问题:

  • 参数多 → 方法臃肿
  • 参数校验零散
  • 不利于复用

✅ Controller 直接接收 Query Object(推荐)

@GetMapping("/list")
public List<Order> list(OrderQuery query) {
return orderService.queryOrder(query);
}

Spring MVC 会自动完成参数绑定:

/list?orderId=1&status=2

👉 自动注入到 OrderQuery

六、从架构角度看:这是“边界清晰”的体现

查询实体的本质是:

把“变化点”封装起来

维度参数式Query Object
扩展性 ❌ 差 ✅ 极强
接口稳定性 ❌ 易变 ✅ 稳定
代码可读性 ❌ 低 ✅ 高
动态 SQL ❌ 繁琐 ✅ 天然支持
复杂查询 ❌ 痛苦 ✅ 友好

七、进阶:Query + Page + Sort 的标准模型

1️⃣ 基础查询对象

public class BaseQuery {
private Integer pageNum = 1;
private Integer pageSize = 20;
}

2️⃣ 订单查询继承

public class OrderQuery extends BaseQuery {
private Long orderId;
private Integer status;
}

3️⃣ 接口永远不再变化

public PageResult<Order> queryOrder(OrderQuery query) {
...
}

👉 这就是大型系统为什么“一开始就传实体”

八、什么时候不该用 Query Object?

不是所有地方都必须用。

❌ 不适合的场景

  • 极简单查询(如:getById(id))
  • 底层工具方法
  • 极高频、对 GC 极度敏感的代码段(极少)

✅ 适合的场景

  • 列表查询
  • 条件组合查询
  • 后台管理系统

需要长期维护的业务接口

九、一句话总结

查询传实体,不是为了现在,而是为了三个月、半年、一年后的系统。

赞(0)
未经允许不得转载:171主机测评 » 为什么查询接口一开始就要传入实体,而不是参数?
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址