🪟 第18章 Visual C++与SQL Server实现酒店管理系统
📄 一句话概括

实体类切断「界面」和「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 秒实验

  1. 给 query 加一个参数 limit,让它自动在 SQL 末尾拼 LIMIT ?。 注意:这才是唯一可以「拼」的地方(拼的是关键字,不是用户输入)。
  2. 给 execute 加一行日志(打印 SQL 和耗时),然后跑一次更新。 你会看到「改了 1 行、3 毫秒」——这就是以后排查问题最常用的信息。
  3. 想一想:如果要在公共类里做「连接池」,该改哪里? 改 __init__ 和连接获取那一层,业务代码一行不用动——这就是「统一出口」带来的好处。

常见误区

  1. 「实体类只是把字段抄一遍,麻烦」。 抄这一遍换来的是「一处改、处处生效」(空值处理、格式化、校验)。 不抄的话,每个界面各写一遍。
  2. 「公共类方法越多越好」。 用不上的方法就是负担——没人知道它该返回什么、也没人测它。 用到第三次再抽。
  3. 「SQL 写在窗体里方便调试」。 方便一时,后面每改一次都要找五个地方。 真需要调试,就把 SQL 打进日志——一样方便,而且只在一个地方。
自测三问(点开对答案) 1. 实体类解决了什么问题? —— **切断界面和 SQL 的直接联系。** 界面只认对象、不认 `SELECT`; **空值处理、格式化只写一次,所有界面共用。** 2. 公共类最大的安全价值是什么? —— **参数化写在里面,业务代码没有机会拼字符串。** **「想拼也拼不了」,比「记得别拼」可靠得多。** 3. 为什么不要一开始就把公共类写全? —— **因为还不知道业务会怎么变。** 预先设计的「以后可能用」方法,**最后基本都没用上**; **抽象的前提是「重复出现三次」。**

下一篇 18.5 讲登录模块: 三步(查空、参数化查人、验密),以及为什么「登录成功」要带回角色——它决定了后面能看到什么。

相关笔记