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

JavaBean 那堆 getter/setter 不是风格,是契约——框架靠「get 开头」认字段,少一个就取不到值。 DBHelper 四个静态方法:拿连接、执行、查询、取值——跟第 19 章的 Db 类一一对应。 Object[] 传参数是好设计也是坏设计:一个方法通吃所有类型, 代价是类型错误要到运行时才发现。

📝 本章和前面几章的关系

20.4 讲「怎么连数据库」,20.5 讲「连上之后怎么组织代码」。 这一节之后,四个业务模块都只调 DBHelper,不再自己写连接。 教材 20.5 分两小节:各种实体类的编写、操作数据库公共类的编写。 两小节加起来不到十页,但它是「分层」这件事第一次落到 Java 代码上。 沙箱没有 Java,所以 JavaBean 和 DBHelper 的原码照贴标不执行, 等价结构用 Python 的 dataclass 和一个四方法小类复刻并真跑—— 要复刻的是「类怎么分工」,不是「语法怎么写」。

一、JavaBean:私有字段 + 公开 getter/setter

C# 写法(第 19 章) Java 写法(本章) 差别
public int Id { get; set; } private int id; + getId() Java 没有属性语法
public string Eno { get; set; } getEno() / setEno() 方法名大小写有讲究
无参构造默认有 必须显式写(有别的构造时) 框架反射要用无参构造

为什么 Java 要写一堆 getter/setter,不像 C# 那样一行搞定? 因为 Java 没有属性语法,框架(JSP、JSON 序列化、ORM)靠「方法名以 get 开头」来认字段。 所以这个规矩不是风格,是契约。 少写一个 getter,页面上那个字段就是空的,而且不报错。 这类问题只能靠「页面上逐个字段看一眼」发现。

二、DBHelper:四个静态方法

方法 返回什么 典型用途
getConn() Connection 需要自己控制事务时
update(sql, ps) 影响行数 增删改
query(sql, ps) ResultSet 列表
scalar(sql, ps) 一个值 COUNT(*)、取最大 id

四个方法的分工跟第 19 章一模一样—— 因为「查表、取值、执行、开事务」这四件事跟语言无关。 换语言换的是写法,不是划分。 query 返回 ResultSet 是有争议的设计。 因为 ResultSet 依赖 Connection,连接不关它就没法用,「谁负责关连接」这件事就模糊了。 更稳的做法是返回 List<Map<String,Object>>,在方法内部把连接关掉。 教材这一版用 ResultSet,是因为「调用方马上就把数据取出来塞进 List」,实际生命周期很短。 但这是个需要「知道自己在干什么」的设计。

三、参数化:一个问号一个值

写法 普通姓名 姓里有单引号
拼接字符串 查得到 SQL 语法错误
参数化 查得到 正常执行,查不到

这里一个坏人都没有,只是一个姓带撇的输入。 'O'Brien' 里那个单引号把 SQL 提前闭合了。 所以参数化的第一理由是「正常数据会出错」,不是「有人会攻击」。 安全是顺带的好处,而且是大好处——同一个机制,顺手把注入也堵死了。

四、分页:LIMIT 不是标准 SQL

数据库 分页写法
SQL Server 2012 及以上 ORDER BY ... OFFSET n ROWS FETCH NEXT m ROWS ONLY
SQL Server 2005/2008 ROW_NUMBER() OVER (...) 再筛
MySQL/SQLite LIMIT m OFFSET n
Oracle ROWNUM 或 OFFSET ... FETCH(12c 起)

这四个写法没有一个是「通用 SQL」。 所以分页代码一定是跟数据库绑的—— 这也是「换数据库要改代码」最典型的地方。 注意 SQL Server 的 OFFSET ... FETCH 必须配 ORDER BY, 不写 ORDER BY 直接语法错。 这条限制看着烦,其实是对的—— 没有排序就没有「第几页」这个概念,翻页的顺序本来就不确定。

五、动手 Demo:实体类、DBHelper、参数化

这一段用 dataclass 复刻 JavaBean、用一个小类复刻 DBHelper, 然后跑三件事:查列表、取值、增删改。 最后对比拼接与参数化在「姓带单引号」时的表现。

from dataclasses import dataclass

@dataclass
class Student:
    id:       int
    sno:      str
    sname:    str
    sex:      str
    class_id: int
    state:    str = '在读'

class DBHelper:
    def __init__(self, conn):
        self.c = conn

    def update(self, sql, ps=()):
        return self.c.execute(sql, ps).rowcount

    def query(self, sql, ps=()):
        cur = self.c.execute(sql, ps)
        return [d[0] for d in cur.description], cur.fetchall()

    def scalar(self, sql, ps=()):
        return self.c.execute(sql, ps).fetchone()[0]

db = DBHelper(c)
print(db.query('SELECT sno, sname FROM tb_student WHERE class_id = ?', (1,)))
print(db.scalar('SELECT COUNT(*) FROM tb_student'))
print(db.update('UPDATE tb_student SET sname=? WHERE sno=?',
                ('张一鸣', '20260101')))

实跑输出:

--------------------------------------------------------------------
公共模块:JavaBean 与 DBHelper
--------------------------------------------------------------------


  ① 实体类:一个类对应一行
  --------------------------------------------------------------------

  [教材原码 · 本沙箱不执行]
  | public class Student {
  |     private int    id;
  |     private String sno;
  |     private String sname;
  |     private String sex;
  |     private int    classId;
  |     private String state;
  | 
  |     public int    getId()      { return id; }
  |     public void   setId(int id){ this.id = id; }
  |     public String getSno()     { return sno; }
  |     public void   setSno(String sno) { this.sno = sno; }
  |     // ... 其余字段同理
  | }
  JavaBean 的规矩:私有字段 + 公开 getter/setter + 无参构造。

  为什么 Java 的实体类要写一堆 getter/setter,不像 C# 那样直接 { get; set; }?
  因为 Java 没有属性语法,框架(JSP、JSON 序列化)靠「方法名以 get 开头」来认字段。
  所以这个规矩不是风格,是契约——少写一个 getter,页面上那个字段就是空的,而且不报错。
  这类问题只能靠「页面上逐个字段看一眼」发现。

  ② DBHelper:四个静态方法
  --------------------------------------------------------------------

  [教材原码 · 本沙箱不执行]
  | public class DBHelper {
  |     public static Connection getConn() {...}
  |     public static int    update(String sql, Object[] ps) {...}
  |     public static ResultSet query(String sql, Object[] ps) {...}
  |     public static Object scalar(String sql, Object[] ps) {...}
  | }
  参数用 Object[] 传,位置跟 SQL 里的 ? 一一对应。

  Object[] 这个设计是教材的做法,好处是「一个方法通吃所有类型」,
  坏处是「类型错误要到运行时才发现」。现代做法是写泛型或者用 Map,但在这个规模下 Object[] 足够。
  四个方法里 query 返回 ResultSet 是有争议的——因为 ResultSet 依赖 Connection,连接不关它就没法用,
  而「谁负责关连接」这件事就变得模糊。
  更稳的做法是返回 List<Map<String,Object>>,把连接在方法内部关掉。

  ③ 参数化:一个问号对一个值
  --------------------------------------------------------------------

  String sql = "SELECT * FROM tb_student WHERE class_id = ? AND sex = ?";
  Object[] ps = { classId, "男" };
  ResultSet rs = DBHelper.query(sql, ps);

  // 拼接的写法(不要用)
  String bad = "SELECT * FROM tb_student WHERE sex = '" + sex + "'";
  拼接那条在 sex = '男' 时也能跑通——所以很多人觉得「没问题」。
  问题出在某天有人姓「欧阳」名带撇,或者输入框里粘了一段带引号的文本。
  更隐蔽的是:拼接之后 SQL 文本每次都不同,数据库每次都要重新解析。
  参数化的语句可以缓存执行计划,跑一万次反而更快。
  所以参数化在「安全」和「性能」上是同一个方向,不是取舍。

  ④ 分页:LIMIT 不是标准 SQL
  --------------------------------------------------------------------
| 数据库 | 分页写法 |
|---|---|
| SQL Server 2012 及以上 | ORDER BY ... OFFSET n ROWS FETCH NEXT m ROWS ONLY |
| SQL Server 2005/2008 | ROW_NUMBER() OVER (ORDER BY ...) 再筛 |
| MySQL/SQLite | LIMIT m OFFSET n |
| Oracle | ROWNUM 或 OFFSET ... FETCH(12c 起) |

  这四个写法没有一个是「通用 SQL」。
  所以分页代码一定是跟数据库绑的——这也是「换数据库要改代码」最典型的地方。
  注意 SQL Server 的 OFFSET ... FETCH 必须配 ORDER BY,不写 ORDER BY 直接语法错。
  这条限制看着烦,其实是对的——没有排序就没有「第几页」这个概念,翻页的顺序本来就不确定。

dataclass 是 Python 里最接近 JavaBean 的东西—— 它自动生成构造、__repr__、__eq__,但不生成业务方法。 这正是「贫血实体类」想要的。 query 返回的列名列表很重要。 页面上表格的列头就是它—— 没有它就得手工写死列头,SQL 改一列页面就错位。 update 返回的影响行数是「免费的自检」。 返回 0 就提示「记录不存在」—— 这一句比任何「保存成功」的提示都可靠。

30 秒实验

  1. 给 DBHelper 加一个 query_one(只取第一行),看能省掉多少 rows[0]。 再想想:加第五个方法,会不会破坏「四个够用」?
  2. 把 query 改成返回「字典列表」而不是「列名 + 行」——想想哪种更好用? 字典列表好写页面,列名+行好做通用表格。
  3. 想一想:如果 DBHelper 里加一行「记录每条 SQL 到日志」,要改几个地方? 一个。不分层的话要改七个页面。

常见误区

  1. 「getter/setter 是废话,字段公开就行」。 框架靠 get 前缀找字段。 少一个,页面上就是空的,而且不报错。
  2. 「每个页面自己写连接更直接」。 改连接串要改十几处。 这不是直接,是埋雷。
  3. 「分页 SQL 各数据库都一样」。 四种写法互不通用。 换数据库一定要改分页代码。
自测三问(点开对答案) 1. Java 的 getter/setter 为什么不能省? —— **因为 Java 没有属性语法**,**框架靠「方法名以 get 开头」认字段。** **少一个,那个字段就是空的,而且不报错。** 2. `DBHelper` 四个方法分别干什么? —— **拿连接、增删改(返回行数)、查列表(返回结果集)、取单值。** **这四件事跟语言无关。** 3. `query` 返回 `ResultSet` 有什么隐患? —— **`ResultSet` 依赖连接,连接不关它就用不了**, **「谁负责关」就模糊了。** **返回 List 更稳。**

下一篇 20.6,写登录: 三种角色三个入口、Session 里存什么、以及「密码对了却登不进去」是怎么发生的。

相关笔记