查询对象模式:让数据库查询像搭积木一样优雅
说实话,我做了十几年后端,最头疼的就是那些又臭又长的SQL拼接。尤其是业务复杂起来,条件判断、排序、分页混在一起,代码简直没法看。后来接触到查询对象模式(Query Object),才算是找到了解药。
这个模式说白了就是:把查询条件封装成对象。你不需要关心SQL怎么拼,只需要告诉它「我要查什么条件」,它帮你搞定一切。
为什么需要查询对象模式?
我举个例子。你写过这样的代码吗?
String sql = "SELECT * FROM users WHERE 1=1";
if (name != null) {
sql += " AND name = '" + name + "'";
}
if (age > 0) {
sql += " AND age > " + age;
}
if (orderBy != null) {
sql += " ORDER BY " + orderBy;
}
嗯,我年轻时也这么干。直到有一次,用户输入了一个特殊字符,直接把表给删了……那叫一个惨。
查询对象模式要解决的就是三个核心问题:
- SQL注入——参数绑定帮你挡掉
- 代码可读性——条件封装成对象,一眼看懂
- 复用性——同一个查询条件,换个数据源照样用
核心三要素:查询构建器、条件封装、参数绑定
我个人习惯把查询对象模式拆成三个部分来理解。你想想看,就像盖房子:
- 查询构建器(Builder)——负责搭框架,SELECT、FROM、WHERE这些骨架
- 条件封装(Criteria)——负责填砖块,等于、大于、模糊匹配这些具体条件
- 参数绑定(Parameter Binding)——负责安全加固,防止有人往砖缝里塞炸药
先看一个简单的Java实现
// 查询构建器
public class QueryBuilder {
private String table;
private List<Criterion> criteria = new ArrayList<>();
private List<Object> params = new ArrayList<>();
public QueryBuilder from(String table) {
this.table = table;
return this;
}
public QueryBuilder where(Criterion criterion) {
this.criteria.add(criterion);
return this;
}
public String build() {
StringBuilder sql = new StringBuilder("SELECT * FROM ");
sql.append(table);
if (!criteria.isEmpty()) {
sql.append(" WHERE ");
for (int i = 0; i < criteria.size(); i++) {
if (i > 0) sql.append(" AND ");
sql.append(criteria.get(i).toSql());
params.addAll(criteria.get(i).getParams());
}
}
return sql.toString();
}
public List<Object> getParams() {
return params;
}
}
// 条件封装
public interface Criterion {
String toSql();
List<Object> getParams();
}
public class SimpleCriterion implements Criterion {
private String field;
private String operator;
private Object value;
public SimpleCriterion(String field, String operator, Object value) {
this.field = field;
this.operator = operator;
this.value = value;
}
@Override
public String toSql() {
return field + " " + operator + " ?";
}
@Override
public List<Object> getParams() {
return Collections.singletonList(value);
}
}
你看,这样写查询就清爽多了:
QueryBuilder qb = new QueryBuilder()
.from("users")
.where(new SimpleCriterion("name", "=", "张三"))
.where(new SimpleCriterion("age", ">", 18));
String sql = qb.build();
List<Object> params = qb.getParams();
// sql = "SELECT * FROM users WHERE name = ? AND age > ?"
// params = ["张三", 18]
参数绑定自动处理了,SQL注入?不存在的。
C++版本:模板与多态的较量
做C++的朋友可能会问:Java有反射,C++怎么办?
嗯,我在一个C++项目中用过类似方案。C++没有反射,但我们可以用模板和虚函数来模拟:
#include <vector>
#include <string>
#include <sstream>
class Criterion {
public:
virtual std::string toSql() const = 0;
virtual std::vector<std::string> getParams() const = 0;
virtual ~Criterion() = default;
};
class SimpleCriterion : public Criterion {
private:
std::string field;
std::string op;
std::string value;
public:
SimpleCriterion(const std::string& f, const std::string& o, const std::string& v)
: field(f), op(o), value(v) {}
std::string toSql() const override {
return field + " " + op + " ?";
}
std::vector<std::string> getParams() const override {
return {value};
}
};
class QueryBuilder {
private:
std::string table;
std::vector<std::unique_ptr<Criterion>> criteria;
public:
QueryBuilder& from(const std::string& t) {
table = t;
return *this;
}
QueryBuilder& where(std::unique_ptr<Criterion> c) {
criteria.push_back(std::move(c));
return *this;
}
std::string build() {
std::ostringstream sql;
sql << "SELECT * FROM " << table;
if (!criteria.empty()) {
sql << " WHERE ";
for (size_t i = 0; i < criteria.size(); ++i) {
if (i > 0) sql << " AND ";
sql << criteria[i]->toSql();
}
}
return sql.str();
}
};
说实话,C++写起来比Java繁琐一些,但核心思想完全一样。把条件抽象成对象,构建器负责组装,参数绑定保证安全。
JPA Criteria API:工业级的查询对象模式
如果你用过JPA,那你其实已经在用查询对象模式了。JPA Criteria API就是官方提供的查询构建器。
我记得刚接触JPA Criteria时,觉得这玩意儿写起来真啰嗦。但用久了才发现,它解决了一个大问题:类型安全。
// JPA Criteria API 示例
CriteriaBuilder cb = entityManager.getCriteriaBuilder();
CriteriaQuery<User> query = cb.createQuery(User.class);
Root<User> root = query.from(User.class);
// 构建条件
Predicate namePredicate = cb.equal(root.get("name"), "张三");
Predicate agePredicate = cb.greaterThan(root.get("age"), 18);
query.where(cb.and(namePredicate, agePredicate));
List<User> users = entityManager.createQuery(query).getResultList();
你看,这里没有字符串拼接,没有SQL注入风险。所有的字段名、操作符都是通过API调用的,编译期就能发现错误。
核心优势:JPA Criteria API把查询对象模式做到了极致——类型安全、动态组合、与ORM深度集成。
避坑指南:我曾经踩过的坑
我曾经在一个项目中,把查询对象模式用过了头。每个查询条件都封装成独立的类,结果搞出了几十个类,维护起来比直接写SQL还痛苦。
后来我总结了几条经验:
- 不要过度抽象——简单的等于、大于条件,用SimpleCriterion就够了,没必要每个字段都建一个类
- 注意性能——查询对象模式会带来一定的对象创建开销。高并发场景下,可以考虑对象池或缓存
- 复杂查询要谨慎——多表关联、子查询、聚合函数,这些用查询对象模式写起来反而更复杂。我一般会退回到原生SQL
特别注意:查询对象模式不是银弹。如果你的查询全是简单的单表CRUD,直接用MyBatis Plus或Spring Data JPA的Query Methods就够了,没必要自己造轮子。
知识体系总览
下面这张图是我自己整理的查询对象模式知识结构,你可以对照着看:
实际项目中的取舍
我在一个报表系统中用过查询对象模式。报表的查询条件特别多——时间范围、地区、品类、渠道……每个条件都可能组合。如果用传统方式,光if-else就得写上百行。
用了查询对象模式后,每个条件都是一个独立的对象,可以自由组合。而且因为参数绑定是自动的,再也不用担心SQL注入了。
但我也要提醒你:查询对象模式不是万能的。如果你的项目只有三五个查询,直接写SQL反而更快。这个模式的价值在于「动态组合」和「类型安全」,场景对了才值得用。
我的建议:先从简单的条件封装开始,不要一上来就搞复杂的构建器。等你的查询条件真的需要动态组合了,再逐步引入查询对象模式。步子迈大了,容易扯着蛋。
好了,关于查询对象模式就聊这么多。记住它的核心:把查询条件变成对象,让代码更安全、更可读、更可复用。下次写动态查询时,不妨试试这个思路。