实体类切断「界面」和「SQL」的直接联系:界面只认 Room,不认 SELECT。
数据库公共类的价值不在「封装」,在「统一」:连接串、参数化、日志各只有一个出口。
只写 query 和 execute 就够起步;scalar、transaction 等用到第三次再抽。
18.3 建好了表,这一节写「所有模块都要用的那两个类」——它们是后面五个业务模块的地基。
教材 18.4 分两小节:各种实体类的编写、操作数据库公共类的编写。
两个小节各对应一个经典模式:实体类是「值对象」,公共类是「数据访问层」。
C++ 的 CDB 类原码照贴不执行,
等价的 Python 版是真跑的——连接、参数化、返回列名这些行为一模一样。
一、实体类:把一行数据变成一个对象
| 层 | 叫什么 | 装什么 |
|---|---|---|
| 数据层 | Room 实体类 |
一行客房记录的字段(房号、类型、价格、状态) |
| 界面层 | 窗体控件 | 把实体类的字段填进文本框、表格 |
| 业务层 | 函数 | 接收实体类、返回实体类,不碰控件也不碰 SQL |
实体类的作用是「切断界面和 SQL 的直接联系」。
界面只认 Room,不认 SELECT;SQL 只认参数,不认文本框。
没有实体类会怎样? 每个窗体自己写 SQL、自己拼字符串、
自己处理空值——同一个查询在五个地方有五份不同实现,改一处要改五处。
实体类还会顺手解决一个烦人问题: 数据库里 price 是 NULL 的时候怎么显示?
在实体类里做一次转换(None → 0),所有界面都受益;
不做的话,每个窗体各写一遍,早晚有一处崩在空值上。
二、数据库公共类:所有连接和参数化都收在这里
class CDB {
public:
_ConnectionPtr m_pConn;
bool Open(); // 打开连接
_RecordsetPtr Query(LPCTSTR sql); // 查询
bool Execute(LPCTSTR sql); // 增删改
};
这个类的价值不在「封装」,在「统一」: 连接串只写一处、日志只打一处、异常只处理一处。
class Db(object):
def __init__(self, conn):
self.c = conn
def query(self, sql, args=()):
cur = self.c.execute(sql, args)
cols = [d[0] for d in (cur.description or [])]
return cols, cur.fetchall()
def execute(self, sql, args=()):
return self.c.execute(sql, args).rowcount
| rno | rtype | price | state |
|---|---|---|---|
| 101 | 标准间 | 288.0 | 空闲 |
| 102 | 标准间 | 288.0 | 空闲 |
| 201 | 大床房 | 388.0 | 空闲 |
| 202 | 大床房 | 388.0 | 维修 |
| 301 | 商务套 | 688.0 | 空闲 |
注意 query 里那句 self.c.execute(sql, args)——
参数是单独传的,不是拼进 SQL 的(17.1 讲过为什么)。
公共类最大的安全价值就在这里:想拼都拼不了。
第二句 cur.description 也值得说: 它把「有哪些列」一起返回,
界面层就不用硬编码列名了——改个 SELECT 的列顺序,界面自动跟着变。
三、只暴露两个方法,够不够
| 方法 | 够不够用 | 缺什么 |
|---|---|---|
query(多行) |
够 | —— |
execute(增删改) |
够 | 要再包一层事务 |
scalar(单值) |
不够,得加 | SELECT COUNT(*) 这种很常用 |
transaction(成批) |
不够,得加 | 开房、结账都要 |
公共类不要一上来就写全。 先写 query 和 execute,
用到第三个功能时再抽 scalar 和 transaction——
预先设计一堆「以后可能用」的方法,最后基本都没用上。
这条经验对初学者不太友好,但它是真的: 你现在还不知道业务会怎么变,
所以「先能跑、再抽象」比「一开始就设计完美」靠谱。
抽象的前提是「重复出现了三次」——不到三次的重复,容忍它。
四、真正该放进去的三件事
| 放什么 | 为什么 |
|---|---|
| 连接串 | 只在编译时定一次,不散落在代码里 |
| 参数化查询 | 写在公共类里,业务代码想拼也没机会 |
| 异常与日志 | 所有 SQL 报错都从一个出口走,排查有迹可循 |
日志这一条最实用。 出问题时第一句话永远是「刚才那条 SQL 报了什么」——
如果每条 SQL 都从公共类走,日志里就一定有。
如果是各处自己 connect,那就只能靠猜。
还有一件小事,但很值: 在公共类里给每条 SQL 打一个耗时。
不用上 APM、不用改架构,日志里多一个毫秒数——慢查询自己就会浮出来(14.5 讲的那些排查,
靠的就是这个)。
最后一句:公共类不要「顺手加业务判断」。
比如「价格不能为负」——那是约束或业务层的事。
数据层一旦掺业务,就没法给第二个业务用了。
五、动手 Demo:实体类 + 公共类:把 SQL 关进一个盒子里
这一段写 Db 公共类(query / execute)并真跑一次查询,
看点两个:参数化怎么传、列名怎么来的。
C++ 的 CDB 版原码也贴了——结构一模一样,只是语言不同。
class Db(object):
def __init__(self, conn):
self.c = conn
def query(self, sql, args=()):
cur = self.c.execute(sql, args) # 参数单独传
cols = [d[0] for d in (cur.description or [])]
return cols, cur.fetchall()
def execute(self, sql, args=()):
return self.c.execute(sql, args).rowcount
db = Db(c)
cols, rows = db.query(u"SELECT rno, rtype, price, state FROM tb_room ORDER BY rno")
print(len(rows), u"、".join(cols)) # 5 行,列名也拿到了
print(rows[0]) # (101, 标准间, 288.0, 空闲)
实跑输出:
--------------------------------------------------------------------
公共模块:实体类与数据库操作类
--------------------------------------------------------------------
① 实体类:把一行数据变成一个对象
--------------------------------------------------------------------
| 层 | 叫什么 | 装什么 |
|---|---|---|
| 数据层 | Room 实体类 | 一行客房记录的字段(房号、类型、价格、状态) |
| 界面层 | 窗体控件 | 把实体类的字段填进文本框、表格 |
| 业务层 | 函数 | 接收实体类、返回实体类,不碰控件也不碰 SQL |
实体类的作用是「切断界面和 SQL 的直接联系」。界面只认 Room,不认 SELECT;
SQL 只认参数,不认文本框。
没有实体类会怎样?
每个窗体自己写 SQL、自己拼字符串、自己处理空值——同一个查询在五个地方有五份不同实现,
改一处要改五处。
② 数据库公共类:所有连接和参数化都收在这里
--------------------------------------------------------------------
[教材原码 · 本沙箱不执行]
| // C++ / ADO 的写法(教材原码,本沙箱不执行)
| class CDB {
| public:
| _ConnectionPtr m_pConn;
| bool Open(); // 打开连接
| _RecordsetPtr Query(LPCTSTR sql); // 查询
| bool Execute(LPCTSTR sql); // 增删改
| };
这个类的价值不在「封装」,在「统一」:连接串只写一处、日志只打一处、异常只处理一处。
Python 里的等价实现(真跑):
查询返回 5 行,列:rno、rtype、price、state
| rno | rtype | price | state |
|---|---|---:|---|
| 101 | 标准间 | 288.0 | 空闲 |
| 102 | 标准间 | 288.0 | 空闲 |
| 201 | 大床房 | 388.0 | 空闲 |
| 202 | 大床房 | 388.0 | 维修 |
| 301 | 商务套 | 688.0 | 空闲 |
注意 query 里那句 self.c.execute(sql, args)——参数是单独传的,
不是拼进 SQL 的(17.1 讲过为什么)。公共类最大的安全价值就在这里:想拼都拼不了。
③ 只暴露两个方法,够不够
--------------------------------------------------------------------
| 方法 | 够不够用 | 缺什么 |
|---|---|---|
| query(多行) | 够 | —— |
| execute(增删改) | 够 | 要再包一层事务 |
| scalar(单值) | 不够,得加 | SELECT COUNT(*) 这种很常用 |
| transaction(成批) | 不够,得加 | 开房、结账都要 |
公共类不要一上来就写全。
先写 query 和 execute,
用到第三个功能时再抽 scalar 和 transaction——预先设计一堆「以后可能用」的方法,最后基本都没用上。
④ 真正该放进去的三件事
--------------------------------------------------------------------
| 放什么 | 为什么 |
|---|---|
| 连接串 | 只在编译时定一次,不散落在代码里 |
| 参数化查询 | 写在公共类里,业务代码想拼也没机会 |
| 异常与日志 | 所有 SQL 报错都从一个出口走,排查有迹可循 |
日志这一条最实用。
出问题时第一句话永远是「刚才那条 SQL 报了什么」——如果每条 SQL 都从公共类走,日志里就一定有。
如果是各处自己 connect,那就只能靠猜。
这段只有十四行,但它把「安全」和「统一」两件事都做了。
安全:业务代码只能调 query(sql, args),没有地方可以拼字符串;
统一:所有查询都经过同一个出口,日志、计时、异常处理都只写一处。
cur.description 拿列名这一点,用的人不多但很有用。
界面层可以直接按列名填表格,不用再写一遍 [r[0], r[1], r[2]]。
execute 返回 rowcount 也有讲究: 「改了 0 行」和「改成功」是两件事。
很多 bug 是「更新了 0 行,但代码以为成功了」——
判断 rowcount == 0 就该报「记录不存在」,而不是继续往下走。
最后提醒: 这个类现在缺 transaction。
别急着补——等 18.7 写到开房时,你会清楚地知道它该长什么样。
30 秒实验
- 给
query加一个参数limit,让它自动在 SQL 末尾拼LIMIT ?。 注意:这才是唯一可以「拼」的地方(拼的是关键字,不是用户输入)。 - 给
execute加一行日志(打印 SQL 和耗时),然后跑一次更新。 你会看到「改了 1 行、3 毫秒」——这就是以后排查问题最常用的信息。 - 想一想:如果要在公共类里做「连接池」,该改哪里?
改
__init__和连接获取那一层,业务代码一行不用动——这就是「统一出口」带来的好处。
常见误区
- 「实体类只是把字段抄一遍,麻烦」。 抄这一遍换来的是「一处改、处处生效」(空值处理、格式化、校验)。 不抄的话,每个界面各写一遍。
- 「公共类方法越多越好」。 用不上的方法就是负担——没人知道它该返回什么、也没人测它。 用到第三次再抽。
- 「SQL 写在窗体里方便调试」。 方便一时,后面每改一次都要找五个地方。 真需要调试,就把 SQL 打进日志——一样方便,而且只在一个地方。
自测三问(点开对答案)
1. 实体类解决了什么问题? —— **切断界面和 SQL 的直接联系。** 界面只认对象、不认 `SELECT`; **空值处理、格式化只写一次,所有界面共用。** 2. 公共类最大的安全价值是什么? —— **参数化写在里面,业务代码没有机会拼字符串。** **「想拼也拼不了」,比「记得别拼」可靠得多。** 3. 为什么不要一开始就把公共类写全? —— **因为还不知道业务会怎么变。** 预先设计的「以后可能用」方法,**最后基本都没用上**; **抽象的前提是「重复出现三次」。**下一篇 18.5 讲登录模块: 三步(查空、参数化查人、验密),以及为什么「登录成功」要带回角色——它决定了后面能看到什么。
- ☐ (本篇完,下一篇 18.5 系统用户登录模块设计)