抠实体的办法只有一个:找名词里「能独立存在、有自己的生命周期」的那些。 「姓名」不是实体,「入住」是——它有开始、有结束、能单独查、能被翻出来。 把「入住」当成客人的字段,是老手也会犯的错:客人再住一次就覆盖上一回了。
前面 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 秒实验
- 给
tb_guest加上「会员等级」字段,然后想一下:等级会不会随时间变?变了要不要留历史? 要留历史就该另建一张「等级变更记录」表——这就是「属性 vs 实体」的又一次判断。 - 把
state改成存数字(0/1/2),再写一个字典表。 能跑,但多了一次连表——小系统里用中文枚举更省事,大系统里有统一编码规范时才用数字。 - 想一想:客人「换房」怎么记?重开一张单(旧单标已退、新单开新房价),还是改原单的房号? 取决于「账单要不要分开」——这件事需求阶段就必须问清,写代码时再想就晚了。
常见误区
- 「先把表建全,以后再说」。 建错的表改起来最贵——数据已经在里面、代码已经在用它。 宁可在纸上多画半小时,也别建一张要拆的表。
- 「字段多没关系,反正不用」。
空列会误导人(新人以为它有用)、会让导出变宽、会让
SELECT *变慢,而且没人敢删。 - 「约束写在代码里更灵活」。 灵活的另一面是「会漏」——换个入口、写个脚本、直接改库,约束全绕过了。 能放表上的就放表上。
自测三问(点开对答案)
1. 怎么判断一个名词是「实体」还是「属性」? —— **看它会不会重复发生。** 会重复的(入住、账单、考勤)是实体,要单独建表; **不会重复的(生日、身份证)是属性,跟着主实体走。** 2. 把「入住」做成客人的字段,会有什么后果? —— **老客人再住一次就覆盖上一回**——上次住了几天、花了多少全没了。 **而且「一个人住过几次」这个问题根本查不出来。** 3. 为什么需求阶段就要定约束? —— **因为约束一次写对、永久生效,而代码里的检查会漏。** **而且约束能挡住「所有入口」,代码只能挡住「你想到的那个入口」。**下一篇 18.2 讲系统设计: 功能结构图怎么画、它怎么直接决定模块划分,以及「哪些功能一期根本不该做」这个判断。
- ☐ (本篇完,下一篇 18.2 系统设计)