MySQL WHERE 条件查询:3种 NULL 值处理方案与 MyBatis 参数安全实践
·
MySQL WHERE 条件查询:3种 NULL 值处理方案与 MyBatis 参数安全实践
在数据库查询中,WHERE 子句是最常用的过滤条件之一。然而,当涉及到 NULL 值的处理时,许多开发者常常会遇到各种问题。本文将深入探讨 MySQL 中 NULL 值的特殊处理方式,并分享在 MyBatis 框架中如何安全地处理参数,避免常见的陷阱。
1. NULL 值的特殊性及其查询处理
NULL 在数据库中表示"未知"或"不存在"的值,它与空字符串、0 或其他任何值都不同。这种特殊性使得 NULL 值的处理需要特别注意。
1.1 为什么不能用 = NULL?
在 SQL 中,NULL 与任何值的比较(包括 NULL 本身)都会返回 UNKNOWN,而不是 TRUE 或 FALSE。这是因为 NULL 代表未知,无法确定它是否等于其他值。
-- 错误的 NULL 值查询方式
SELECT * FROM users WHERE name = NULL; -- 不会返回任何结果
1.2 正确的 NULL 值查询方式
MySQL 提供了专门的运算符来处理 NULL 值:
-- 查询为 NULL 的记录
SELECT * FROM users WHERE name IS NULL;
-- 查询不为 NULL 的记录
SELECT * FROM users WHERE name IS NOT NULL;
1.3 NULL 值处理的三种实用方案
方案一:使用 IFNULL 函数转换
-- 将 NULL 转换为默认值后再比较
SELECT * FROM users WHERE IFNULL(name, '') = '';
方案二:使用 COALESCE 函数
-- 返回参数列表中第一个非 NULL 的值
SELECT * FROM users WHERE COALESCE(name, email, '') LIKE '%search%';
方案三:使用 <=> 安全等于运算符
-- 可以正确处理 NULL 值的等于比较
SELECT * FROM users WHERE name <=> NULL;
注意:<=> 运算符在 MySQL 中可用,但不是标准 SQL 的一部分,可能在其他数据库中不支持。
2. MyBatis 中的参数安全处理
MyBatis 作为流行的 ORM 框架,提供了两种参数占位符方式:#{} 和 ${},它们在处理 NULL 值和防止 SQL 注入方面有显著差异。
2.1 #{} 与 ${} 的本质区别
| 特性 | #{} | ${} |
|---|---|---|
| 处理方式 | 预编译,参数化查询 | 字符串替换 |
| SQL 注入风险 | 安全 | 存在风险 |
| NULL 值处理 | 自动转换为 JDBC 的 NULL | 直接拼接为 NULL 字符串 |
| 适用场景 | 值参数 | 动态表名、列名等 |
2.2 安全处理 NULL 值的 MyBatis 实践
场景一:基本 NULL 值处理
<select id="findUsers" resultType="User">
SELECT * FROM users
<where>
<if test="name != null">
AND name = #{name}
</if>
</where>
</select>
场景二:动态 NULL 值查询
<select id="findUsers" resultType="User">
SELECT * FROM users
<where>
<choose>
<when test="includeNull == true">
AND (name IS NULL OR name = #{name})
</when>
<otherwise>
AND name = #{name}
</otherwise>
</choose>
</where>
</select>
场景三:LIKE 查询中的 NULL 处理
<select id="searchUsers" resultType="User">
SELECT * FROM users
<where>
<if test="keyword != null">
AND name LIKE CONCAT('%', #{keyword}, '%')
</if>
</where>
</select>
2.3 连续 % 通配符的处理案例
在模糊查询中,有时查询条件本身包含 % 字符,这时需要特殊处理:
// Java 代码中处理包含 % 的查询条件
public List<User> searchUsers(String keyword) {
if (keyword != null) {
keyword = keyword.replace("%", "\\%");
}
return userMapper.searchUsers(keyword);
}
<!-- MyBatis 映射文件 -->
<select id="searchUsers" resultType="User">
SELECT * FROM users
<where>
<if test="keyword != null">
AND name LIKE CONCAT('%', #{keyword}, '%') ESCAPE '\\'
</if>
</where>
</select>
3. 高级 NULL 值处理技巧
3.1 NULL 值与索引的关系
当字段允许为 NULL 时,索引的行为会有一些特殊之处:
- 对于 IS NULL 条件,只有当索引列包含 NULL 值时才会使用索引
- 复合索引中,如果所有列都为 NULL,则该行不会被索引
- 使用
WHERE col IS NOT NULL可以利用索引
3.2 使用索引优化 NULL 值查询
-- 创建包含 NULL 值的索引
CREATE INDEX idx_name ON users(name);
-- 优化 IS NULL 查询
EXPLAIN SELECT * FROM users WHERE name IS NULL;
3.3 NULL 值与聚合函数
聚合函数对 NULL 值的处理方式各不相同:
| 函数 | NULL 处理方式 |
|---|---|
| COUNT() | 不统计 NULL 值 |
| SUM() | 忽略 NULL 值 |
| AVG() | 忽略 NULL 值 |
| MAX() | 忽略 NULL 值 |
| MIN() | 忽略 NULL 值 |
| GROUP_CONCAT() | 忽略 NULL 值 |
4. MyBatis 参数安全的最佳实践
4.1 始终优先使用 #{}
<!-- 安全的方式 -->
<select id="findUserById" resultType="User">
SELECT * FROM users WHERE id = #{id}
</select>
<!-- 不安全的方式 -->
<select id="findUserByIdUnsafe" resultType="User">
SELECT * FROM users WHERE id = ${id}
</select>
4.2 必须使用 ${} 时的安全措施
当必须使用 ${} 时(如动态表名),应采取以下防护措施:
- 使用白名单验证输入
- 对输入进行严格的转义处理
- 限制数据库用户的权限
// 表名白名单验证
private static final Set<String> ALLOWED_TABLES =
Set.of("users", "products", "orders");
public List<Map<String, Object>> queryTable(String tableName) {
if (!ALLOWED_TABLES.contains(tableName)) {
throw new IllegalArgumentException("Invalid table name");
}
return mapper.queryTable(tableName);
}
4.3 使用 SQL 注入检测工具
- 在开发阶段使用 SQL 注入检测工具扫描代码
- 定期进行安全审计
- 使用 MyBatis 插件进行运行时检测
// 简单的 MyBatis 插件示例,检测 ${} 使用
@Intercepts({
@Signature(type= StatementHandler.class,
method="prepare",
args={Connection.class, Integer.class})
})
public class SqlInjectionInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
StatementHandler handler = (StatementHandler) invocation.getTarget();
BoundSql boundSql = handler.getBoundSql();
String sql = boundSql.getSql();
if (sql.contains("${")) {
log.warn("Potential SQL injection risk: " + sql);
}
return invocation.proceed();
}
}
5. 实际案例分析与性能优化
5.1 NULL 值对查询性能的影响
案例:用户表有 100 万条记录,其中 10% 的 email 字段为 NULL
-- 查询1: 使用 IS NULL
SELECT * FROM users WHERE email IS NULL;
-- 执行时间: 120ms
-- 查询2: 使用 = ''
SELECT * FROM users WHERE email = '';
-- 执行时间: 80ms
-- 查询3: 使用 IFNULL 转换
SELECT * FROM users WHERE IFNULL(email, '') = '';
-- 执行时间: 350ms
优化建议:
- 尽量避免在 WHERE 子句中对列使用函数
- 考虑使用默认值代替 NULL
- 为 NULL 值查询创建专门的索引
5.2 MyBatis 批量操作中的 NULL 处理
<!-- 批量插入时处理 NULL 值 -->
<insert id="batchInsert" parameterType="list">
INSERT INTO users (name, email, age)
VALUES
<foreach collection="list" item="user" separator=",">
(#{user.name}, #{user.email,jdbcType=VARCHAR},
#{user.age,jdbcType=INTEGER})
</foreach>
</insert>
提示:指定 jdbcType 可以更精确地控制 NULL 值的处理方式
5.3 动态 SQL 中的复杂 NULL 处理
<!-- 复杂的 NULL 值条件组合 -->
<select id="findUsers" resultType="User">
SELECT * FROM users
<where>
<if test="name != null">
AND name = #{name}
</if>
<if test="email != null">
<choose>
<when test="email == ''">
AND (email IS NULL OR email = '')
</when>
<otherwise>
AND email = #{email}
</otherwise>
</choose>
</if>
<if test="minAge != null or maxAge != null">
AND age BETWEEN
<choose>
<when test="minAge != null">#{minAge}</when>
<otherwise>0</otherwise>
</choose>
AND
<choose>
<when test="maxAge != null">#{maxAge}</when>
<otherwise>200</otherwise>
</choose>
</if>
</where>
</select>
6. 总结与最佳实践
在实际开发中,处理 NULL 值和保证 SQL 安全是每个后端开发者的必备技能。以下是一些关键的最佳实践:
-
NULL 值处理 :
- 始终使用 IS NULL/IS NOT NULL 来检查 NULL 值
- 考虑使用 COALESCE 或 IFNULL 函数简化逻辑
- 在设计表结构时,慎重考虑是否真的需要允许 NULL
-
MyBatis 安全 :
- 99% 的情况下使用 #{} 预编译参数
- 限制 ${} 的使用,仅在必要时使用并严格验证输入
- 为可能为 NULL 的参数指定 jdbcType
-
性能优化 :
- 避免在 WHERE 子句中对列使用函数
- 为包含 NULL 值的列创建合适的索引
- 考虑使用默认值代替 NULL 以提高查询性能
-
代码可维护性 :
- 在 MyBatis 映射文件中使用清晰的注释
- 将复杂的条件逻辑封装到方法或标签中
- 保持 SQL 的可读性和一致性
更多推荐


所有评论(0)