当前位置:首页>java>面试官:写代码如何复用公共 SQL ?

面试官:写代码如何复用公共 SQL ?

  • 2026-10-22 00:26:14
面试官:写代码如何复用公共 SQL ?

面试里被问“怎么复用公共 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
);

常见几个需求:

  • 页面列表查询用户
  • 导出 Excel 查询用户
  • 后台风控系统按同样条件查用户

很多项目里,会变成下面这种写法,在不同地方写三个几乎一样的 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());
    }
}

这里有几个点,面试时可以顺嘴提一下:

  • 公共 SQL 片段体现在 BASE_SELECT、BASE_COUNT 和 buildWhereSql
  • 业务条件统一通过 UserQuery 传递,调用方扩展条件不用改一堆参数签名
  • 条件拼接统一走 ? 占位符,避免 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 &gt;= #{startTime}
</if>
<iftest="endTime != null">
            and created_at &lt;= #{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 &gt; 0">
        limit #{offset}, #{pageSize}
</if>
</select>

<selectid="countUser"resultType="int">
    select count(1)
    from user
<includerefid="User_Base_Where" />
</select>

这里“公共 SQL 复用”的点就很清晰了:

  • 字段列表复用:Base_Column_List 一改,全改
  • 业务 where 复用: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 = ? ";

这一看像是复用了,但有两个坑:

  • 这些常量如果放在某个 util 里到处被引用,很容易写成“看不懂语义的模板字符串”,比如 " and a = ? and b = ? ",只要参数位置搞错就出事
  • where 前面有没有空格、and 前面有没有空格,稍微不注意就拼出语法错误

更稳妥的做法,还是像前面那样:封装成“构造 SQL 的方法”,统一用 StringBuilder 追加,让条件逻辑和参数顺序都在一个地方看得清楚,而不是一堆散落的魔法字符串。

最后随口说一句

面试里问“公共 SQL 怎么复用”,表面看是在问 SQL,其实是在看你有没有工程化思维: 你是只顾眼前“能跑就行”,还是会想半年之后这段代码好不好维护、好不好改。

上面的这些写法,你在自己项目里试着改一两个查询,很快就有感觉了。行了,我也得去把自己老项目里那堆 copy 过来的 where 条件收拾一下,不然迟早被自己坑。

-END-

我为大家打造了一份RPA教程,完全免费:songshuhezi.com/rpa.html

🔥东哥私藏精品 🔥
东哥作为一名超级老码农,整理了全网最全《Java高级架构师资料合集》。总量高达650GB。

最新文章

随机文章