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

抠实体的办法只有一个:找名词里「能独立存在、有自己的生命周期」的那些。 「姓名」不是实体,「入住」是——它有开始、有结束、能单独查、能被翻出来。 把「入住」当成客人的字段,是老手也会犯的错:客人再住一次就覆盖上一回了。

📝 本章和前面几章的关系

前面 17 章讲的是「零件」:SQL、查询、视图、存储过程、触发器、游标、索引、事务、安全。 从这一章开始拼装——三个完整的项目,每个都从需求走到代码。 教材 18.1 分三小节:业务背景、功能需求、性能与约束。 「性能与约束」那一节最好看——它告诉你「什么时候该纠结性能」这个判断本身。 沙箱没有 VC++ 也没有 SQL Server,所以这一章的做法是: 数据库层用 SQLite 真跑、界面层用「菜单树 + 表格输出」复刻数据流, C++ 原码照贴标不执行——复刻的是「数据怎么流」,不是「界面怎么画」。

一、先听业务怎么说,别急着建表

业务原话 抠出的实体 为什么
客人来了要开房,登记姓名身份证 客人(guest) 一个人可以住很多次,得单独存
房间分标准间、大床房、商务套 客房(room) 房号唯一,价格跟类型走
入住要记哪天来、几天、哪天走 入住单(checkin) 「住」是一个过程,不是客人的属性
退房要结账,房费加其他消费 账单(bill) 一次入住一张账,金额要能追溯
前台、管理员、保洁看到的菜单不一样 用户 + 角色 权限是独立的一维

抠实体的办法只有一个:找「名词」里能独立存在、有自己生命周期的那些。「姓名」不是实体, 是客人的属性;「入住」是实体——它有开始、有结束、能单独查、能被翻出来。 这一步做错,后面全得返工。 最常见的错是把「入住」当成客人的字段(给客人表加 room_no、 in_time),结果老客人再住一次就得覆盖上一回的记录——上一回住了几天、花了多少,全没了。 一个简单的检验办法: 这件事会不会「重复发生」? 会重复的,就该是独立实体(入住、账单、考勤);不会重复的,才是属性(生日、身份证)。

二、实体变表:先只挑必要的属性

表 关键属性 哪些先不放
tb_room 房号、类型、价格、状态 朝向、面积、图片
tb_guest 姓名、身份证、电话 会员等级、积分
tb_checkin 房、客、入住时间、天数、状态 押金、备注
tb_bill 关联入住单、房费、其他、总额、是否结清 发票号、折扣明细

「先不放」那一列是需求分析里最容易被忽略的部分。 新人容易把能想到的字段全加上——结果一半的列永远空着,还不敢删。 判断标准:这一个字段,现在有哪个功能要用它? 答不上来就先不加——加一列便宜,删一列要改代码、改报表、改导出。 这一条也有反例: 有些字段看着没用, 其实是审计或合规要求(比如「录入人」「最后修改时间」)。 区别在于:能说出「谁要用」,就加;说不出,就不加。

三、建出来看看:五张表都插上样例数据

表 行数
tb_room 5
tb_guest 3
tb_checkin 0
tb_bill 0
tb_user 3

tb_checkin 和 tb_bill 是 0 行,这是对的——还没有人入住过。 建库阶段就要能接受「空表」, 别为了好看硬塞假数据进业务表(测试数据可以塞,但要跟真数据分开,别混到一个库里)。 样例数据的作用是「验证设计」: 拿三个客人、五间房试一遍, 能立刻发现「一个客人住两间房怎么记」「换房怎么处理」这种设计漏洞。 光看表结构看不出来。

四、性能与约束:现在就该想的三件事

要问的问题 这一版的决定
一天大概多少单入住 小酒店几十单,SQLite 都够;百万级才谈分库
房号能重复吗 不能,UNIQUE
价格能是负数吗 不能,CHECK (price > 0)
能不能删掉还有入住记录的房 不能,外键拦住

「性能」在需求阶段只需要回答一个问题:这个量级,用什么方案都行吗? 小酒店的答案显然是「都行」——这时候纠结分库分表就是浪费。 约束倒是现在就该定,而且要比想象中严。 因为约束是「一次写对、永久生效」,而代码里的检查会漏。 这句话在 14.4 讲过一次,这里是它在项目里的样子。 还有一个「性能」问题容易被忽略: 报表。 「月度营收」这种查询会扫全年的单子, 表大了之后要建索引(14.2)——但那是第二期的事,现在先记住「有这么个东西」。

五、动手 Demo:从五句业务原话,抠出五张表

这一段把「业务原话 → 实体 → 表 → 约束」走一遍,最后把五张表建出来、塞上样例数据。 注意「哪些先不放」那一列——它是需求分析里最省钱的一个动作。

import sqlite3

c = sqlite3.connect(u"hotel.db", isolation_level=None)
c.execute(u"PRAGMA foreign_keys = ON")

c.executescript(u"""
CREATE TABLE tb_room (
  id    INTEGER PRIMARY KEY AUTOINCREMENT,
  rno   TEXT NOT NULL UNIQUE,
  rtype TEXT NOT NULL,
  price REAL NOT NULL CHECK (price > 0),
  state TEXT NOT NULL DEFAULT '空闲'
        CHECK (state IN ('空闲', '已住', '维修'))
);
CREATE TABLE tb_guest (
  id     INTEGER PRIMARY KEY AUTOINCREMENT,
  gname  TEXT NOT NULL,
  idcard TEXT NOT NULL UNIQUE,
  phone  TEXT
);
""")
# ... 另外三张:tb_checkin / tb_bill / tb_user ...

实跑输出:

--------------------------------------------------------------------
需求分析:从业务话里抠出实体
--------------------------------------------------------------------


  ① 先听业务怎么说,别急着建表
  --------------------------------------------------------------------
| 业务原话 | 抠出的实体 | 为什么 |
|---|---|---|
| 客人来了要开房,登记姓名身份证 | 客人(guest) | 一个人可以住很多次,得单独存 |
| 房间分标准间、大床房、商务套 | 客房(room) | 房号唯一,价格跟类型走 |
| 入住要记哪天来、几天、哪天走 | 入住单(checkin) | 「住」是一个过程,不是客人的属性 |
| 退房要结账,房费加其他消费 | 账单(bill) | 一次入住一张账,金额要能追溯 |
| 前台、管理员、保洁看到的菜单不一样 | 用户(user)+ 角色 | 权限是独立的一维 |

  抠实体的办法只有一个:找「名词」里能独立存在、有自己生命周期的那些。
  「姓名」不是实体,是客人的属性;「入住」是实体——它有开始、有结束、能单独查。
  这一步做错,后面全得返工。
  最常见的错是把「入住」当成客人的字段(给客人表加 room_no、in_time),
  结果老客人再住一次就得覆盖上一回的记录。

  ② 实体变表:先只挑必要的属性
  --------------------------------------------------------------------
| 表 | 关键属性 | 哪些先不放 |
|---|---|---|
| tb_room | 房号、类型、价格、状态 | 朝向、面积、图片(先不建,用得上再加) |
| tb_guest | 姓名、身份证、电话 | 会员等级、积分(第二期再说) |
| tb_checkin | 房、客、入住时间、天数、状态 | 押金、备注 |
| tb_bill | 关联入住单、房费、其他、总额、是否结清 | 发票号、折扣明细 |

  「先不放」那一列是需求分析里最容易被忽略的部分。
  新人容易把能想到的字段全加上——结果一半的列永远空着,还不敢删。
  判断标准:这一个字段,现在有哪个功能要用它?
  答不上来就先不加——加一列便宜,删一列要改代码、改报表、改导出。

  ③ 建出来看看:五张表都插上样例数据
  --------------------------------------------------------------------

| 表 | 行数 |
|---|---:|
| tb_room | 5 |
| tb_guest | 3 |
| tb_checkin | 0 |
| tb_bill | 0 |
| tb_user | 3 |

  tb_checkin 和 tb_bill 是 0 行,这是对的——还没有人入住过。
  建库阶段就要能接受「空表」,别为了好看硬塞假数据进业务表。

  ④ 性能与约束:现在就该想的三件事
  --------------------------------------------------------------------
| 要问的问题 | 这一版的决定 |
|---|---|
| 一天大概多少单入住 | 小酒店几十单,SQLite 都够;百万级才谈分库 |
| 房号能重复吗 | 不能,UNIQUE |
| 价格能是负数吗 | 不能,CHECK (price > 0) |
| 能不能删掉还有入住记录的房 | 不能,外键拦住 |

  「性能」在需求阶段只需要回答一个问题:这个量级,用什么方案都行吗?
  小酒店的答案显然是「都行」——这时候纠结分库分表就是浪费。
  约束倒是现在就该定,而且要定得比想象中严。因为约束是「一次写对、永久生效」,而代码里的检查会漏。

注意 tb_room 上压了三个约束: rno 唯一、price > 0、state 三选一。 这三条都不是「技术炫技」,而是「业务上一旦破就会出现麻烦」的地方。 CHECK (state IN (...)) 这一个特别值: 房态只有三种, 用约束锁死之后,代码里就不可能出现「已入住」「入住中」「在住」三种写法混用—— 这种脏数据一旦产生,统计永远对不上。 tb_checkin 单独存的理由是这一节的核心: 「住」这件事会重复发生,所以它必须是实体。 如果把它做成客人的字段,一个客人第二次来住,上次的记录就被覆盖了。 这一段没写界面、没写登录,只建表。 这是有意的顺序: 表定不下来,界面写多少都是白写。

30 秒实验

  1. 给 tb_guest 加上「会员等级」字段,然后想一下:等级会不会随时间变?变了要不要留历史? 要留历史就该另建一张「等级变更记录」表——这就是「属性 vs 实体」的又一次判断。
  2. 把 state 改成存数字(0/1/2),再写一个字典表。 能跑,但多了一次连表——小系统里用中文枚举更省事,大系统里有统一编码规范时才用数字。
  3. 想一想:客人「换房」怎么记?重开一张单(旧单标已退、新单开新房价),还是改原单的房号? 取决于「账单要不要分开」——这件事需求阶段就必须问清,写代码时再想就晚了。

常见误区

  1. 「先把表建全,以后再说」。 建错的表改起来最贵——数据已经在里面、代码已经在用它。 宁可在纸上多画半小时,也别建一张要拆的表。
  2. 「字段多没关系,反正不用」。 空列会误导人(新人以为它有用)、会让导出变宽、会让 SELECT * 变慢,而且没人敢删。
  3. 「约束写在代码里更灵活」。 灵活的另一面是「会漏」——换个入口、写个脚本、直接改库,约束全绕过了。 能放表上的就放表上。
自测三问(点开对答案) 1. 怎么判断一个名词是「实体」还是「属性」? —— **看它会不会重复发生。** 会重复的(入住、账单、考勤)是实体,要单独建表; **不会重复的(生日、身份证)是属性,跟着主实体走。** 2. 把「入住」做成客人的字段,会有什么后果? —— **老客人再住一次就覆盖上一回**——上次住了几天、花了多少全没了。 **而且「一个人住过几次」这个问题根本查不出来。** 3. 为什么需求阶段就要定约束? —— **因为约束一次写对、永久生效,而代码里的检查会漏。** **而且约束能挡住「所有入口」,代码只能挡住「你想到的那个入口」。**

下一篇 18.2 讲系统设计: 功能结构图怎么画、它怎么直接决定模块划分,以及「哪些功能一期根本不该做」这个判断。

相关笔记