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 秒实验
- 给
DBHelper加一个query_one(只取第一行),看能省掉多少rows[0]。 再想想:加第五个方法,会不会破坏「四个够用」? - 把
query改成返回「字典列表」而不是「列名 + 行」——想想哪种更好用? 字典列表好写页面,列名+行好做通用表格。 - 想一想:如果
DBHelper里加一行「记录每条 SQL 到日志」,要改几个地方? 一个。不分层的话要改七个页面。
常见误区
- 「getter/setter 是废话,字段公开就行」。
框架靠
get前缀找字段。 少一个,页面上就是空的,而且不报错。 - 「每个页面自己写连接更直接」。 改连接串要改十几处。 这不是直接,是埋雷。
- 「分页 SQL 各数据库都一样」。 四种写法互不通用。 换数据库一定要改分页代码。
自测三问(点开对答案)
1. Java 的 getter/setter 为什么不能省? —— **因为 Java 没有属性语法**,**框架靠「方法名以 get 开头」认字段。** **少一个,那个字段就是空的,而且不报错。** 2. `DBHelper` 四个方法分别干什么? —— **拿连接、增删改(返回行数)、查列表(返回结果集)、取单值。** **这四件事跟语言无关。** 3. `query` 返回 `ResultSet` 有什么隐患? —— **`ResultSet` 依赖连接,连接不关它就用不了**, **「谁负责关」就模糊了。** **返回 List 更稳。**下一篇 20.6,写登录:
三种角色三个入口、Session 里存什么、以及「密码对了却登不进去」是怎么发生的。
- ☐ (本篇完,下一篇 20.6 系统用户登录模块设计)