面试里被问“怎么复用公共 SQL”,其实挺能看出一个人代码风格和工程思维的。 我前段时间面了个同学,简历上写着“精通 MyBatis、熟悉 SQL 优化”,我就顺嘴问了句:你们项目里相同条件的 SQL 是怎么复用的?结果他愣了三秒,说“就复制粘贴嘛”。这就有点危险了。
我用一个简单的用户查询场景,慢慢把这个事讲清楚,你回头也可以照着这套思路在面试里回答。
先看“惨案版”:满项目到处复制的 SQL
假设有张用户表 user:
CREATETABLEuser (
idBIGINT PRIMARY KEY,
username VARCHAR(64),
mobile VARCHAR(20),
statusTINYINT, -- 1=启用 0=禁用
deleted TINYINT, -- 0=正常 1=删除
created_at DATETIME
);
常见几个需求:
很多项目里,会变成下面这种写法,在不同地方写三个几乎一样的 SQL。
// A 模块
String sql1 = "select id, username, mobile, status, created_at " +
"from user " +
"where deleted = 0 " +
"and status = ? " +
"and created_at between ? and ? " +
"order by created_at desc limit ?, ?";
// B 模块
String sql2 = "select id, username, mobile, status, created_at " +
"from user " +
"where deleted = 0 " +
"and status = ? " +
"and created_at between ? and ? " +
"order by created_at desc";
// C 模块
String sql3 = "select count(1) " +
"from user " +
"where deleted = 0 " +
"and status = ? " +
"and created_at between ? and ?";
一旦业务改了个需求,比如“deleted 字段逻辑变更成 2 代表删除”,你要在项目里全局搜 deleted = 0,挨个改,极容易漏。 这个时候面试官问“怎么复用公共 SQL”,如果你还能说自己到处复制粘贴,就说不过去了。
第一层:先把 SQL 收拢到一处,再谈复用
最基础的一步,其实就是“分层”:别在 service、controller 里乱写 SQL,统一塞到 Repository / DAO 里,让 SQL 至少只在一处出现。
比如用 Spring 的 JdbcTemplate,可以先收拢成这样:
@Repository
publicclassUserRepository{
@Autowired
private JdbcTemplate jdbcTemplate;
// 公共的 SELECT 片段
privatestaticfinal String BASE_SELECT =
"select id, username, mobile, status, created_at " +
"from user " +
"where deleted = 0 ";
// 公共的 WHERE 业务逻辑
privatestaticfinal String BASE_WHERE =
"and status = ? " +
"and created_at between ? and ? ";
public List<User> queryPage(int status, LocalDateTime start, LocalDateTime end,
int offset, int pageSize){
String sql = BASE_SELECT + BASE_WHERE + "order by created_at desc limit ?, ?";
return jdbcTemplate.query(sql,
new Object[]{status, start, end, offset, pageSize},
new UserRowMapper());
}
public List<User> queryForExport(int status, LocalDateTime start, LocalDateTime end){
String sql = BASE_SELECT + BASE_WHERE + "order by created_at desc";
return jdbcTemplate.query(sql,
new Object[]{status, start, end},
new UserRowMapper());
}
publicintcount(int status, LocalDateTime start, LocalDateTime end){
String sql = "select count(1) from user where deleted = 0 " + BASE_WHERE;
return jdbcTemplate.queryForObject(sql,
new Object[]{status, start, end},
Integer.class);
}
}
这一步其实就已经是“SQL 复用”的雏形:公共条件抽成一个常量或方法,分页、导出、计数这些场景共用一套 where 条件。 以后改业务逻辑,只动 BASE_WHERE 就行。
第二层:公共 SQL + 条件对象,做到既复用又好扩展
上面这种写法还有两个小问题:
比较舒服的做法:定义一个查询条件对象,再写个“SQL 构造方法”,统一拼公共 SQL。
publicclassUserQuery{
private String username;
private Integer status;
private LocalDateTime startTime;
private LocalDateTime endTime;
private Integer pageNo;
private Integer pageSize;
// getter/setter 省略
}
Repository 里这样写:
@Repository
publicclassUserRepository{
@Autowired
private JdbcTemplate jdbcTemplate;
privatestaticfinal String BASE_SELECT =
"select id, username, mobile, status, created_at " +
"from user where deleted = 0 ";
privatestaticfinal String BASE_COUNT =
"select count(1) from user where deleted = 0 ";
privatestaticclassSqlAndArgs{
privatefinal String sql;
privatefinal List<Object> args;
SqlAndArgs(String sql, List<Object> args) {
this.sql = sql;
this.args = args;
}
}
// 核心:公共 SQL 拼装逻辑
private SqlAndArgs buildWhereSql(String baseSql, UserQuery query, boolean needPage){
StringBuilder sb = new StringBuilder(baseSql);
List<Object> args = new ArrayList<>();
if (query.getUsername() != null && !query.getUsername().isEmpty()) {
sb.append(" and username like ? ");
args.add("%" + query.getUsername() + "%");
}
if (query.getStatus() != null) {
sb.append(" and status = ? ");
args.add(query.getStatus());
}
if (query.getStartTime() != null) {
sb.append(" and created_at >= ? ");
args.add(query.getStartTime());
}
if (query.getEndTime() != null) {
sb.append(" and created_at <= ? ");
args.add(query.getEndTime());
}
sb.append(" order by created_at desc ");
if (needPage && query.getPageNo() != null && query.getPageSize() != null) {
sb.append(" limit ?, ? ");
int offset = (query.getPageNo() - 1) * query.getPageSize();
args.add(offset);
args.add(query.getPageSize());
}
returnnew SqlAndArgs(sb.toString(), args);
}
public List<User> queryList(UserQuery query){
SqlAndArgs sqlAndArgs = buildWhereSql(BASE_SELECT, query, true);
return jdbcTemplate.query(sqlAndArgs.sql,
sqlAndArgs.args.toArray(),
new UserRowMapper());
}
publicintcount(UserQuery query){
// 计数不需要分页
SqlAndArgs sqlAndArgs = buildWhereSql(BASE_COUNT, query, false);
return jdbcTemplate.queryForObject(sqlAndArgs.sql,
sqlAndArgs.args.toArray(),
Integer.class);
}
public List<User> export(UserQuery query){
// 导出不分页
SqlAndArgs sqlAndArgs = buildWhereSql(BASE_SELECT, query, false);
return jdbcTemplate.query(sqlAndArgs.sql,
sqlAndArgs.args.toArray(),
new UserRowMapper());
}
}
这里有几个点,面试时可以顺嘴提一下:
BASE_SELECT、BASE_COUNT 和 buildWhereSqlUserQuery 传递,调用方扩展条件不用改一堆参数签名? 占位符,避免 SQL 注入问题这种“条件对象 + SQL 构造器”的写法,在不用 ORM、只用 JDBC 的项目里非常常见,用来做 SQL 复用是很合理的。
第三层:如果用 MyBatis,就好好用 <sql> 片段
面试官一听你说 MyBatis,一般会追问一句:“那你公共 SQL 怎么抽?” 很典型的就是 <sql> + <include>。
比如还是用户表的例子,可以这样:
<!-- 公共字段列表 -->
<sqlid="Base_Column_List">
id, username, mobile, status, created_at
</sql>
<!-- 公共查询条件 -->
<sqlid="User_Base_Where">
<where>
deleted = 0
<iftest="username != null and username != ''">
and username like concat('%', #{username}, '%')
</if>
<iftest="status != null">
and status = #{status}
</if>
<iftest="startTime != null">
and created_at >= #{startTime}
</if>
<iftest="endTime != null">
and created_at <= #{endTime}
</if>
</where>
</sql>
<selectid="listUser"resultType="com.demo.User">
select
<includerefid="Base_Column_List" />
from user
<includerefid="User_Base_Where" />
order by created_at desc
<iftest="pageSize != null and pageSize > 0">
limit #{offset}, #{pageSize}
</if>
</select>
<selectid="countUser"resultType="int">
select count(1)
from user
<includerefid="User_Base_Where" />
</select>
这里“公共 SQL 复用”的点就很清晰了:
Base_Column_List 一改,全改User_Base_Where 被列表、统计、导出等多个语句引用你可以顺便提一句: 如果有“订单公共条件”“有效商品公共条件”,也可以抽成不同 <sql id="Order_Base_Where"> 这类片段,避免 XML 写成“复制地狱”。
第四层:ORM 的“查询构造器”其实也是在复用 SQL
如果项目用的是 MyBatis-Plus、JPA Specification、Querydsl 这些库,公共 SQL 很多时候就不再体现为“文本片段”,而是公共的“条件构造方法”。
还是用 MyBatis-Plus 举个例子:
publicclassUserQuery{
private String username;
private Integer status;
private LocalDateTime startTime;
private LocalDateTime endTime;
// 分页字段省略
}
@Service
publicclassUserService{
@Autowired
private UserMapper userMapper;
// 构造公共查询条件
private QueryWrapper<User> buildWrapper(UserQuery query){
QueryWrapper<User> wrapper = new QueryWrapper<>();
wrapper.eq("deleted", 0);
if (query.getUsername() != null && !query.getUsername().isEmpty()) {
wrapper.like("username", query.getUsername());
}
if (query.getStatus() != null) {
wrapper.eq("status", query.getStatus());
}
if (query.getStartTime() != null) {
wrapper.ge("created_at", query.getStartTime());
}
if (query.getEndTime() != null) {
wrapper.le("created_at", query.getEndTime());
}
wrapper.orderByDesc("created_at");
return wrapper;
}
public IPage<User> pageUser(UserQuery query){
Page<User> page = new Page<>(query.getPageNo(), query.getPageSize());
return userMapper.selectPage(page, buildWrapper(query));
}
public List<User> exportUser(UserQuery query){
return userMapper.selectList(buildWrapper(query));
}
publicintcountUser(UserQuery query){
return userMapper.selectCount(buildWrapper(query)).intValue();
}
}
看着虽然没有一句“裸 SQL”,但本质上,你还是在复用“同一套 where 条件”,只是换成了对象的形式。 面试时可以直接说:“如果项目用 ORM,我的做法是统一封装条件构造逻辑,比如 MyBatis-Plus 的 buildWrapper,让分页、导出、统计共用同一套条件。”
第五层:数据库层面的复用——视图、存储过程、公共 CTE
有些面试官会再追问一句:“那有没有考虑在数据库层面复用,比如视图、存储过程?” 这里可以选一些你熟悉的说,不需要全覆盖。
比如业务上有个“活跃用户”的概念,条件很复杂,Java 里写起来一大坨,你可以直接在数据库里建视图:
CREATEVIEW v_active_user AS
SELECTid, username, mobile, status, created_at
FROMuser
WHERE deleted = 0
ANDstatus = 1
AND created_at >= DATE_SUB(NOW(), INTERVAL30DAY);
Java 里就变成:
privatestaticfinal String BASE_ACTIVE_SQL =
"select id, username, mobile, status, created_at " +
"from v_active_user ";
public List<User> listActive(UserQuery query){
// 在这个基础上再叠加一些简单条件
}
优点是业务含义很清晰:看见 v_active_user,就知道是“活跃用户”的那套条件。 缺点也要顺便提一下:视图、存储过程改起来要走 DBA 流程,不如应用层改代码灵活,所以适合相对稳定的公共逻辑。
如果数据库版本支持 CTE(with 语句),有时候也能写出更好复用的“公共子查询”,不过一般面试里点到为止就够了。
第六层:回答面试官时,可以这么组织话术
把上面的东西揉一揉,你可以用一种比较自然的方式说出来,比如:
“我们线上项目里公共 SQL 复用,我一般分两层做。 应用层会先把 SQL 收拢到 Repository/Mapper 这一层,然后把相同业务含义的 where 条件抽成公共片段,比如 MyBatis 用
<sql>+<include>,JDBC 就用一个统一的buildWhereSql方法,所有列表、导出、统计都走这一套逻辑,避免到处复制粘贴。 另外如果用 MyBatis-Plus 或者 JPA,我会再封一层查询构造器方法,比如buildWrapper(UserQuery query)或者 Specification,这样既复用条件,也方便扩展。 如果某些查询特别复杂、比较稳定,比如‘活跃用户’、‘有效订单’,我会考虑在数据库里建视图,让 Java 里直接查视图,做到跨系统的 SQL 复用。”
基本上你能把这几层意思讲清楚,再穿插一两个简单代码例子,面试官一般就知道你不是只会写“select * from table where 1=1”的那种人。
顺带提一个容易被忽略的小点
有些同学会说:“那我把 SQL 公共片段抽成静态字符串常量不就行了?” 比如:
publicstaticfinal String USER_BASE_WHERE =
" where deleted = 0 and status = ? ";
这一看像是复用了,但有两个坑:
" and a = ? and b = ? ",只要参数位置搞错就出事更稳妥的做法,还是像前面那样:封装成“构造 SQL 的方法”,统一用 StringBuilder 追加,让条件逻辑和参数顺序都在一个地方看得清楚,而不是一堆散落的魔法字符串。
最后随口说一句
面试里问“公共 SQL 怎么复用”,表面看是在问 SQL,其实是在看你有没有工程化思维: 你是只顾眼前“能跑就行”,还是会想半年之后这段代码好不好维护、好不好改。
上面的这些写法,你在自己项目里试着改一两个查询,很快就有感觉了。行了,我也得去把自己老项目里那堆 copy 过来的 where 条件收拾一下,不然迟早被自己坑。
-END-
我为大家打造了一份RPA教程,完全免费:songshuhezi.com/rpa.html