🪟 第20章 Java与SQL Server实现学生成绩管理系统
📄 一句话概括

四个条件「选或不选」一共十六种组合,写十六条 SQL 是死路——要「拼」。 动态 SQL 唯一的难点是「条件和参数必须同步增减」,一个 append 漏了结果就错。 必选条件写在 SQL 里,可选条件才进 if——混在一起写,等于给自己挖坑。

📝 本章和前面几章的关系

前面几节的查询条件都是固定的(这个班、这一科), 这一节的查询条件是「用户填的」——填几个、填哪几个,运行时才知道。 教材 20.11 分三小节:概述、技术分析、实现过程。 「技术分析」里讲的是「动态拼 SQL」,这是全章唯一会写错且不容易发现的地方。 沙箱没有 JSP,条件用函数参数模拟表单, 拼出来的 SQL 原样打出来——这样能一眼看出拼得对不对,比在浏览器里点半天强。

一、四个条件,十六种组合

条件 可能的值
班级 四个班之一,或不选
性别 男/女/不选
姓名关键字 任意字符串,或空
分数区间 下限、上限,或都不填

四个条件「选或不选」,一共 2⁴ = 16 种组合。 如果每种组合写一条 SQL,就是 16 条——而且加一个条件变 32 条。 正确做法是「拼」:条件选了才 AND 上去,参数跟着一起加。 关键在「条件和参数必须同步增减」——这是动态 SQL 唯一的难点。

二、WHERE 1=1 这个技巧

写法 每个分支长什么样
不加 1=1 第一个条件不能带 AND,得判断「是不是第一个」
加 1=1 每个分支都是 AND xxx,长得一模一样

WHERE 1=1 这个写法被很多人嫌弃「丑」, 但它换来的是「五个 if 长得一模一样」。 代码整齐比好看重要,因为整齐的代码不容易漏。 另一种做法是用 List<String> 攒条件、最后 join(" AND "), 也能避免「第一个条件」的问题,而且更干净。 两种都行,别用的是「每个分支各写一套判断」。

三、参数个数是自检的抓手

现象 说明什么
参数个数比预期少 某个 if 里 append 漏了
参数个数对但查不到 顺序错了,或者条件写反了
报「参数个数不匹配」 SQL 里 ? 和参数数量不一致

「参数个数」这一列是自检用的。 如果某次运行参数个数跟预期对不上,一定是某个 if 里 append 漏了或者多写了。 第三种错反而是好事——它会报错,能被发现。 真正麻烦的是第二种:个数对、顺序错、结果错,一声不响。

四、拼接错法:看着能跑,其实会炸

输入 拼接的结果 参数化的结果
张 查到 3 行 查到 3 行
O'Brien SQL 语法错误 正常执行,0 行

这里一个坏人都没有,只是一个带撇的输入。 'O'Brien' 里那个单引号把 SQL 提前闭合了。 参数化之后「带撇」和「不带撇」走的是同一条路径—— 这才是「正常」的样子。 拼接之所以危险,是因为它把「数据」当成了「代码」。 还有一个 LIKE 特有的坑: % 和 _ 在 LIKE 里是通配符。 用户搜「_」会匹配任意一个字符,搜「%」会匹配一切。 要按字面搜,得用 ESCAPE 转义——教材没讲,但线上一定有人会踩。

五、动手 Demo:动态 WHERE:两个列表一起长

这一段把「条件列表」和「参数列表」一起构建,然后跑五种组合,每种都打印「行数 + 参数个数」。 最后对比拼接与参数化在「带单引号」时的表现。

def q(cname=None, sex=None, kw=None, lo=None, hi=None):
    sql  = 'SELECT sno, sname, cname, score FROM v_score'
    sql += ' WHERE subject = ?'        # 必选条件直接写
    args = ['数学']
    if cname:
        sql += ' AND cname = ?';  args.append(cname)
    if sex:
        sql += ' AND sno IN (SELECT sno FROM tb_student WHERE sex = ?)'
        args.append(sex)
    if kw:
        sql += ' AND sname LIKE ?';  args.append('%' + kw + '%')
    if lo is not None:
        sql += ' AND score >= ?';  args.append(lo)
    if hi is not None:
        sql += ' AND score <= ?';  args.append(hi)
    sql += ' ORDER BY score DESC'
    return sql, args

for label, kw in CASES:
    sql, args = q(**kw)
    rows = c.execute(sql, args).fetchall()
    print('%-18s %2d 行  参数 %d 个' % (label, len(rows), len(args)))

实跑输出:

--------------------------------------------------------------------
多条件查询:动态 WHERE 的安全拼法
--------------------------------------------------------------------


  ① 四个条件,十六种组合
  --------------------------------------------------------------------
| 条件 | 可能的值 |
|---|---|
| 班级 | 四个班之一,或不选 |
| 性别 | 男/女/不选 |
| 姓名关键字 | 任意字符串,或空 |
| 分数区间 | 下限、上限,或都不填 |

  四个条件「选或不选」,一共 2⁴ = 16 种组合。
  如果每种组合写一条 SQL,就是 16 条——而且加一个条件变 32 条。
  正确做法是「拼」:条件选了才 AND 上去,参数跟着一起加。
  关键在「条件和参数必须同步增减」——这是动态 SQL 唯一的难点。

  ② 正确的拼法:两个列表一起长
  --------------------------------------------------------------------

  sql  = "SELECT ... FROM v_score WHERE 1=1"
  args = []
  if 班级:  sql += " AND cname = ?";        args.append(班级)
  if 性别:  sql += " AND sno IN (子查询)";  args.append(性别)
  if 关键字: sql += " AND sname LIKE ?";     args.append("%" + 关键字 + "%")
  if 下限:  sql += " AND score >= ?";       args.append(下限)
  if 上限:  sql += " AND score <= ?";       args.append(上限)

  WHERE 1=1 是个技巧:后面每条都能以 AND 开头,不用判断「是不是第一条」。
  WHERE 1=1 这个写法被很多人嫌弃「丑」,但它换来的是「五个 if 长得一模一样」——代码整齐比好看重要,
  因为整齐的代码不容易漏。

  ③ 真跑四种组合
  --------------------------------------------------------------------

  只选科目               → 12 行   参数 1 个
      20260201 | 陈子轩 | 2026级计算机2班 | 数学 | 96.0
      20260101 | 张一鸣 | 2026级计算机1班 | 数学 | 92.0
      20260301 | 黄梓涵 | 2026级软件1班 | 数学 | 90.0
      …… 共 12 行
  科目 + 班级            →  4 行   参数 2 个
      20260101 | 张一鸣 | 2026级计算机1班 | 数学 | 92.0
      20260102 | 李思彤 | 2026级计算机1班 | 数学 | 88.0
      20260104 | 赵雨欣 | 2026级计算机1班 | 数学 | 78.0
      …… 共 4 行
  科目 + 姓名含「张」        →  1 行   参数 2 个
      20260101 | 张一鸣 | 2026级计算机1班 | 数学 | 92.0
  科目 + 80~90 分       →  5 行   参数 3 个
      20260301 | 黄梓涵 | 2026级软件1班 | 数学 | 90.0
      20260102 | 李思彤 | 2026级计算机1班 | 数学 | 88.0
      20250101 | 孙志强 | 2025级计算机1班 | 数学 | 88.0
      …… 共 5 行
  全不选(等于全查)          → 12 行   参数 1 个
      20260201 | 陈子轩 | 2026级计算机2班 | 数学 | 96.0
      20260101 | 张一鸣 | 2026级计算机1班 | 数学 | 92.0
      20260301 | 黄梓涵 | 2026级软件1班 | 数学 | 90.0
      …… 共 12 行
  第 1 条和第 5 条一模一样(都是 12 行),因为「科目」是必选条件。
  这也说明:必选条件和可选条件要分开写,必选的直接写在 SQL 里,可选的才进 if。
  「参数个数」那一列是自检用的。
  如果某次运行参数个数跟预期对不上,一定是某个 if 里 append 漏了或者多写了。

  ④ 拼接错法:看着能跑,其实会炸
  --------------------------------------------------------------------

  拼接·正常输入 → 3 行
  拼接·带单引号 → 直接报错:near "Brien": syntax error
  参数化·带单引号 → 正常执行,0 行
  LIKE 里的 % 也要注意。
  参数化的时候,两个百分号要拼在参数值上(Python 里一个百分号、加号、关键字、加号、一个百分号),
  不能拼进 SQL 文本——拼进 SQL 就等于又回到字符串拼接了。
  还有个小坑:% 和 _ 在 LIKE 里是通配符。用户搜「_」会匹配任意一个字符,搜「%」会匹配一切。
  要按字面搜,得用 ESCAPE 转义——教材没讲,但线上一定有人会踩。

「参数个数」那一列是这段代码的自检表。 第一个用例参数 1 个(只有必选的科目),第四个用例参数 3 个(科目 + 上下限)。 数一数就知道有没有漏 append。 必选条件「科目」直接写在初始 SQL 里,没有进 if。 这一条很重要:必选和可选要分开写。 如果必选条件也进 if,某次忘了传参数,就会变成「查全表」。 最后一个用例是「全不选」,结果跟「只选科目」一样。 这不是 bug,是「没有可选条件」的自然结果—— 但如果业务上「全不选」应该报错(不能查全表),就要单独判断。

30 秒实验

  1. 给 q() 加上一个「班级下拉框默认选第一个」的行为,看第一个用例变成几行。 这就是「默认值」对结果的影响。
  2. 把 kw 分支改成拼接(不用参数),传 O'Brien,看报什么错。
  3. 想一想:如果用户搜「%」,会查出什么? 全部。 要按字面搜得用 ESCAPE。

常见误区

  1. 「十六种组合写十六条 SQL」。 加一个条件变三十二条。 要拼。
  2. 「WHERE 1=1 太丑,不用」。 不用它就得处理「第一个条件」,那个判断更容易错。
  3. 「LIKE 的 % 拼在 SQL 里和拼在参数里一样」。 拼在 SQL 里就等于回到字符串拼接。 必须拼在参数值上。
自测三问(点开对答案) 1. 动态 SQL 唯一的难点是什么? —— **条件和参数必须同步增减。** **一个 `append` 漏了,SQL 和参数就对不上。** 2. `WHERE 1=1` 解决了什么问题? —— **让每个分支都能以 `AND` 开头**,**不用判断「是不是第一个条件」。** 3. 为什么必选条件不要进 `if`? —— **忘传参数时会变成「查全表」**,**而全表查询往往就是数据泄露。**

下一篇 20.12 收尾: 按班级汇总和明细怎么在一个页面里共存、窗口函数算排名——最后把三个项目共用的那套骨架收一次口。

相关笔记