简介:面向数据库课程设计与毕业设计人群,提供一份基于MySQL的图书管理系统数据库设计文档,系统解决图书信息、读者信息与借阅记录三大核心数据的高效存储、查询及维护问题。资源为单文件docx文档,压缩包约852KB,内容从需求分析、数据流图、ER图到逻辑结构设计与函数依赖集逐层展开,明确给出图书表、读者表、借阅记录表等关键表结构,并配有清晰目录便于按章节检索。文档后半部分包含可直接使用的数据库源代码,涉及建表、数据初始化、单表查询、借书操作、超期处理、还书操作、书籍状态与读者状态管理等,同时涵盖系统登录、图书管理、读者管理、借阅服务、统计分析等模块的功能描述,便于读者对照实现或二次开发。已有5891人浏览学习,适合需要快速完成课程设计、毕业设计或系统学习MySQL数据库设计流程的读者参考。
1. 基于MYSQL的图书管理系统数据库设计:一张docx怎么变成一套能跑的库表
一个反直觉的事实:图书管理系统在业务上几乎是最朴素的一类管理系统,不外乎借书、还书、查在架、管逾期。但把它作为MySQL数据库设计题来做,你等于把关系建模、外键约束、事务、锁、索引优化这一串核心知识点从头到尾碾了一遍。这也是它常年霸榜课程设计和毕业设计选题的原因。这篇文章要解决的,就是把一份《基于MYSQL的图书管理系统数据库设计.docx》从纸面变成真正能跑的库表:先讲怎么从借书还书场景倒推出表结构,再给出建库建表SQL、借还书事务与查询模板,最后用一整章讲我见过最容易被新手踩进去的五个坑。适合手里只有设计稿、下一步要动手建库建表的人;也适合想快速搭一套能演示的库表后端、回去交作业或者作为简历项目打磨的人。全文所有SQL都基于MySQL 8.0验证过,5.7版本也能直接照抄。
2. 图书管理系统需要哪些表:从借书还书场景倒推数据模型
做基于MySQL的数据库设计,起点不是打开Navicat新建表,而是先把业务用例列清楚。我给图书管理系统列的核心业务用例其实只有四个:读者注册、图书上架、借书、还书。别小看这四个用例,它们决定了你最终需要几张表、表之间是什么关系。很多设计文档写得花团锦簇,但导出的ER图连"读者和图书是什么关系"都讲不清,这种文档落到建表阶段必然反复返工。
2.1 核心实体识别:读者、图书、借阅、管理员的最小集合
从业务名词里提取"必须被记住"的实体,是数据库设计的第一步。图书管理系统里真正绕不开的只有三类实体:人(读者和管理员)、书(图书)、行为(借阅)。其中借阅是读者和图书之间的多对多关系,必须拆成独立记录。所以最小表集合就是四张:reader(读者表)、book(图书表)、borrow_record(借阅记录表)、admin(管理员表)。
来看每个实体的核心属性。读者表至少要存读者编号、姓名、手机号、注册日期、状态;图书表要存书名、作者、出版社、ISBN、分类、馆藏位置、状态;借阅记录表要存借出时间、应还时间、实际归还时间、状态、罚款;管理员表则更简单,账号、密码哈希、角色就够了。值得摆在台面上讨论的是"图书类别要不要单独拆表"。我的建议是:分类有限且固定时,直接在book表里放category字段,不要拆;只有分类有层级、或者需要按分类维度做统计报表时,才考虑单独的category表。拆表意味着每次查询都多一次JOIN,在业务复杂度撑不起这个拆分的收益时,它只是给查询添堵。
2.2 借阅关系建模:为什么必须拆出一张独立的借阅记录表
很多第一次做设计的人,会把"借阅信息"直接怼进读者表或图书表。比如在reader表里加一个"当前借了哪几本书"的字段——这是典型误用关系型数据库的姿势。原因有两条:
第一,一本书可以被不同读者在不同时间借走,一个读者也可以同时借多本,这种多对多关系如果压成单表字段,只能在列里存多个值,后续想统计"谁还没还书"就只能靠字符串切割拼接,SQL根本没法写。第二,借阅行为本身有生命周期——借出、归还、可能续借——这些状态变化需要每一行独立存在,才能在某个读者头上叠加多条历史记录。
因此正确的做法是单独拆一张 borrow_record 表。它是 reader 和 book 之间的中间表,同时携带每次借阅行为的状态属性。这里给新手一个建议:表名别叫 borrow,那是一个动词,在SQL里容易和业务概念搞混,我习惯用 borrow_record 或 loan_record。表名起清楚,后面写联表查询时能少看无数行注释。
2.3 字段类型选型:int、varchar、datetime 的一次性决策
MySQL建表时字段类型选错,后面改结构的代价在数据量大时极其痛苦。我自己在做数据库设计时,会对照一张固定的选型表来定字段,这也是从多个项目里总结出来的:
| 字段含义 | 推荐类型 | 理由 |
|---|---|---|
| 主键ID | INT UNSIGNED | 单表上限42亿,图书管理系统绰绰有余 |
| 书名 | VARCHAR(200) | 丛书名加副标题经常超过50字符 |
| ISBN | VARCHAR(20) | 10位/13位并存,可能带连字符 |
| 状态 | TINYINT | 用数字枚举,比字符串省空间且可控 |
| 时间 | DATETIME | 没有时区换算需求,避免TIMESTAMP的2038问题 |
| 罚款金额 | DECIMAL(6,2) | 精确小数,绝不用FLOAT/DOUBLE |
特别强调ISBN这一行。现实中的ISBN既有10位老版本,也有13位新版本,还可能带连字符排版,比如978-7-115-12345-6。如果建表时用了CHAR(10)或CHAR(13),带连字符的数据写入时会被截断,或者直接报错。这个坑我在后面避坑章节还会展开。字段类型这一层还把存储引擎定死了——选InnoDB,它是MySQL 8.0的默认引擎,也是唯一同时支持事务和外键的引擎,图书管理系统二选一的时候没有任何悬念。
3. 建库建表SQL实战:字符集、存储引擎与索引一次到位
设计文档描述的是"业务世界",但MySQL要的是"可执行的DDL"。这一章把上一章的四张表全部落到CREATE TABLE语句,同时把字符集、存储引擎、索引、注释这些容易被文档忽略的细节一次性补齐。你以后回看这份设计,能不能一眼看懂每张表要干什么,取决于今天写COMMENT时有没有耐心。
3.1 建库与字符集:utf8mb4 是唯一正解,安装默认配置要自查
写建表SQL之前,先把库建好。字符集选择这件事,看起来不起眼,却决定了这个库能不能存中文、存生僻字和emoji。MySQL有一个历史包袱:老版本的utf8并不是真正的全量UTF-8,最多只能存3字节,像一些生僻汉字和emoji需要4字节编码,写入时要么报错,要么静默变成问号。
所以我建库时永远用下面这段:
CREATE DATABASE IF NOT EXISTS library_system DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci;逻辑说明:utf8mb4是MySQL里唯一能完整覆盖Unicode字符集的编码;utf8mb4_unicode_ci是配套的排序规则,对中文、英文的排序表现稳定,适合图书管理系统这种以文本检索为主的场景。如果你在极致性能场景下,可以换成utf8mb4_general_ci,但图书管理系统的数据量完全感知不到二者差别,直接选unicode_ci更省心。
这里要提醒你自查一件事:如果你用的是Windows下按安装教程默认配置装出来的MySQL,尤其5.7版本,默认字符集很可能是latin1或utf8mb3,不是utf8mb4。所以创建数据库时必须显式写DEFAULT CHARACTER SET utf8mb4,不要默认安装完就能直接存中文,那是我见过第一批翻车现场里最普遍的。另外,如果你准备用Docker部署MySQL,建容器时也要挂载配置文件,把character-set-server显式写成utf8mb4,否则容器内默认字符集照样把你顶在乱码墙上。
3.2 读者表与图书表:完整的CREATE TABLE与索引规划
下面把读者表 reader 和图书表 book 一次性建出来。建表语句里我顺手把约束、默认值、注释、索引全部写进去,这是数据库设计落地的标准姿势:能写进DDL的规则绝不留给应用层。
USE library_system; CREATE TABLE reader ( reader_id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, reader_name VARCHAR(50) NOT NULL, phone VARCHAR(20) NOT NULL, email VARCHAR(100) DEFAULT NULL, reg_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, status TINYINT NOT NULL DEFAULT 1 COMMENT '1正常 0停借', UNIQUE KEY uk_phone (phone) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='读者表'; CREATE TABLE book ( book_id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, title VARCHAR(200) NOT NULL, author VARCHAR(100) NOT NULL, publisher VARCHAR(100) DEFAULT NULL, isbn VARCHAR(20) NOT NULL, category VARCHAR(50) DEFAULT NULL, location VARCHAR(20) DEFAULT NULL COMMENT '馆藏位置,如A-03-2', status TINYINT NOT NULL DEFAULT 0 COMMENT '0在架 1借出 2下架 3丢失', UNIQUE KEY uk_isbn (isbn), KEY idx_category (category) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='图书表';参数说明:reader表的自增主键用INT UNSIGNED,单表可容纳42亿条记录,图书管理系统是足够用了;phone字段加唯一键,保证一个手机号只能注册一次,这个约束必须在数据库层做,不要指望应用层先查再插,并发场景下两次查询同时通过就破防了。
book表有三个细节值得单独说。第一,title用VARCHAR(200)而不是VARCHAR(50),因为真实书名带丛书名、副标题的情况很多,50字符经常不够用。第二,status用TINYINT配合COMMENT说明取值,这是我个人做数据库设计时非常看重的小习惯——把业务语义写死在表结构里,后来者不需要翻文档就能明白0和1分别代表什么。第三,location字段提前预留,图书管理系统几乎都要展示"这本书在哪个架子",宁可建表时多一个字段,也别等上线了再ALTER TABLE去加列。
关于索引,MySQL创建索引的最佳时机就是建表阶段,而不是等查询慢了再补。book表常见的检索是"按分类查"和"按ISBN精确查",所以idx_category普通索引和uk_isbn唯一索引一起建好。但要注意,不要给每一列都建索引,索引越多写入越慢,尤其是大量INSERT的场景,会让页分裂和索引维护成为新瓶颈。
3.3 借阅记录表:外键、联合索引与Delete/Update行为
borrow_record 是整个设计的灵魂。它要同时关联读者和图书,还要承载每一次借还的状态流转,所以外键和联合索引要一起设计到位:
CREATE TABLE borrow_record ( borrow_id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, reader_id INT UNSIGNED NOT NULL, book_id INT UNSIGNED NOT NULL, borrow_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, due_time DATETIME NOT NULL COMMENT '应还时间', return_time DATETIME DEFAULT NULL COMMENT '实际归还时间,NULL表示未还', status TINYINT NOT NULL DEFAULT 0 COMMENT '0借出中 1已归还', fine_amount DECIMAL(6,2) NOT NULL DEFAULT 0.00 COMMENT '逾期罚款', CONSTRAINT fk_borrow_reader FOREIGN KEY (reader_id) REFERENCES reader(reader_id), CONSTRAINT fk_borrow_book FOREIGN KEY (book_id) REFERENCES book(book_id), KEY idx_reader_status (reader_id, status), KEY idx_book_status (book_id, status) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='借阅记录表';逻辑说明:两个外键约束保证了每条借阅记录必须关联一个真实存在的读者和一本真实存在的图书,从数据库层面杜绝孤儿数据。有不少从MyISAM时代过来的老开发会劝你生产环境不要用外键,让应用层自己保证一致性——那套观点在分库分表、高并发写入的场景下有道理,但图书管理系统是典型的单库单表应用,外键带来的数据完整性收益远大于那点性能损耗,这是我坚定保留外键的原因。
索引方面,idx_reader_status 是给"查某位读者当前借了什么书"这种高频查询准备的,idx_book_status 是给"查某本书现在在不在架"准备的。两个都是联合索引,因为这类查询几乎总是带着status条件。如果只建单列索引,MySQL在回表时效率会差不少。
这里还要特别说一下DECIMAL(6,2)。罚款金额用DECIMAL而不是FLOAT或DOUBLE,因为浮点数的二进制表示决定了它在累加、比较时会产生精度异常,这属于MySQL事务处理里很经典的翻车点。DECIMAL(6,2)最大表示9999.99元,对学生借阅场景绰绰有余。还有一点:return_time默认NULL、status默认0,这两个字段存在一定冗余——理论上可以用return_time IS NULL判断是否未还,但在MySQL里对NULL建索引和写WHERE条件都不直观,所以我宁可多一个status字段来明确表达业务状态。
4. 借还书核心SQL:事务边界、行锁与高频查询模板
表结构建好只是地基,真正的业务逻辑集中在借书和还书这两个动作里。图书管理系统能不能经得起并发折腾,就看这两个动作有没有被正确的事务边界和锁保护起来。这一章给的是可以直接抄进项目的SQL模板,附带每个动作背后的原因。
4.1 借书为什么必须开事务:三个写操作不能拆开
如果只是做演示,借书可以写成三条独立的SQL,但真实系统里这三条SQL绝不能各自为政。借一本书这个动作,实际上要完成三件事:
- 检查这本书的status是否为0(在架)
- 在borrow_record插入一条借出记录
- 将book表的status从0改成1
这三步中间任何一步失败,都会留下脏数据。比如book已经改成借出但borrow_record没插进去,这本书就在系统里凭空消失了。所以必须用事务把它们包起来,并且利用行锁防止并发超借:
START TRANSACTION; -- 锁定这本书的行,防止并发借阅 SELECT status FROM book WHERE book_id = 3 FOR UPDATE; -- 业务判断:如果status != 0,ROLLBACK并提示“已被借出” INSERT INTO borrow_record (reader_id, book_id, due_time) VALUES (1001, 3, DATE_ADD(NOW(), INTERVAL 30 DAY)); UPDATE book SET status = 1 WHERE book_id = 3; COMMIT;逻辑说明:START TRANSACTION 开启事务;SELECT ... FOR UPDATE 是关键,它把book_id=3这一行锁住,直到当前事务提交或回滚。这样当两个读者同时借同一本书的最后一本时,第二个事务的SELECT会阻塞,等第一个事务COMMIT后,它读到的status已经是1,业务判断直接让它回滚并返回"已被借出"。这就是MySQL锁的分类里记录锁(Record Lock)最典型的应用场景。
这里要提醒一个新手常犯的错误:SELECT FOR UPDATE 读出来的status必须在应用层或存储过程里做显式判断,然后再决定是INSERT还是ROLLBACK。如果不做判断直接无脑INSERT和UPDATE,事务虽然不会报错,但业务上早就超借了。借书成功后,返回数据里最好带着本次的borrow_id和due_time,方便前端显示"您已借出,应还日期是XXXX-XX-XX"。
4.2 还书SQL与逾期罚款:DATEDIFF和时间函数的正确用法
还书是借数的逆操作,同样要包事务。它要完成三件事:把borrow_record的return_time设为当前时间、status改为1,把book的status改回0,必要时计算逾期罚款。罚款计算的正确姿势是直接在SQL里用MySQL的时间函数算完,而不是查出数据拿回Java或Python里算好再传回来,后者多一次网络往返,还容易丢事务边界:
START TRANSACTION; UPDATE borrow_record SET return_time = NOW(), status = 1, fine_amount = IF( NOW() > due_time, DATEDIFF(NOW(), due_time) * 1.50, 0.00 ) WHERE borrow_id = 5001 AND status = 0; UPDATE book SET status = 0 WHERE book_id = (SELECT book_id FROM borrow_record WHERE borrow_id = 5001); COMMIT;逻辑说明:DATEDIFF返回两个日期之间的天数差,这里用它算出逾期天数,再乘每天1.50元的单价,结果直接写进fine_amount。IF函数判断当前时间是否超过应还时间,没超过罚款就是0。把计算放在UPDATE语句里一次性完成的好处是减少一次应用层与MySQL之间的来回,也让事务提交后数据一定处于一致状态。
这里还要留意一个细节:UPDATE ... SET 后面的子查询 SELECT book_id FROM borrow_record WHERE borrow_id = 5001,必须保证结果只有一行。borrow_id是主键,天然满足这个条件。如果你用读者ID和书ID去查,就要小心子查询返回多行导致MySQL报错。另外,如果业务允许续借,还书SQL稍加修改就能变成续借逻辑:把due_time加30天、status改为2,再写一条延期操作记录,这是从核心功能往外扩展的最常见路径。
4.3 三个高频查询:在架、读者借阅、逾期清单
表结构设计得再好,最终都要落到查询上。下面三个SQL是图书管理系统里出现频率最高的查询模板,也是你docx里"系统功能实现"部分可以直接拿来当附录的素材:
-- 查询当前在架图书,支持书名/作者模糊搜索 SELECT book_id, title, author, publisher, category, location FROM book WHERE status = 0 AND (title LIKE CONCAT('%', '算法', '%') OR author LIKE CONCAT('%', '算法', '%')) ORDER BY book_id DESC LIMIT 20; -- 查询某读者当前所有未还借阅 SELECT b.title, br.book_id, br.borrow_time, br.due_time FROM borrow_record br JOIN book b ON br.book_id = b.book_id WHERE br.reader_id = 1001 AND br.status = 0 ORDER BY br.due_time ASC; -- 逾期未还图书清单,前端按overdue_days标红催还 SELECT r.reader_name, r.phone, b.title, br.due_time, DATEDIFF(NOW(), br.due_time) AS overdue_days FROM borrow_record br JOIN reader r ON br.reader_id = r.reader_id JOIN book b ON br.book_id = b.book_id WHERE br.status = 0 AND br.due_time < NOW() ORDER BY overdue_days DESC;参数说明:第一条查询里的LIKE CONCAT('%', ?, '%')是中文模糊匹配的标准写法。搜索词来自程序变量时用CONCAT拼接占位符,而不是直接把%写进参数,是为了配合预处理语句、避免SQL注入。ORDER BY book_id DESC让新入库的书排在最前面,这是图书管理系统默认的排序习惯。第三条查询里的overdue_days是通过MySQL的DATEDIFF算出来的,在前端列表里大于0就显示红色催还提醒。三个查询都尽量走索引——第一条走idx_category或主键,第二条走idx_reader_status,第三条走联合索引加主键,这也是我建表时那个索引规划的直接收益。
5. 图书管理系统数据库设计的避坑清单:5个让你半夜改表的真实故障
这一章写的都是我见过最多、也最容易在课程设计和项目演示前一刻爆发的故障。每一条都按"现象→原因→解决"展开,你可以一条条对着自己的设计检查。这些坑不是某个人的独有经历,而是几乎每个MySQL初学者都会踩进去的同一条沟。
5.1 中文乱码和"锟斤拷":字符集不一致的症状与修复
现象:表建好后用Navicat打开,中文全部显示为问号,导出数据看到成片的"锟斤拷"。
原因:库表字符集不是utf8mb4。可能是安装MySQL时的默认配置,也可能是连接工具或JDBC驱动字符集与数据库不一致。中文在写入时就已经被转成错误编码,存进去的内容本身就是脏的,事后靠改显示设置救不回来。
解决:分两步走。第一步,建库建表时显式统一为utf8mb4,这一步在建表SQL里已经做掉;如果已经建好了,执行ALTER TABLE book CONVERT TO CHARACTER SET utf8mb4,注意这是转换表,不是修改单个列类型。第二步,检查连接参数,JDBC连接串里追加characterEncoding=utf8,SQLyog或Navicat则在连接配置里选utf8mb4。我自己排查这类问题时,会用一条SQL直接确认现状:
SELECT table_name, table_collation FROM information_schema.tables WHERE table_schema = 'library_system';看到table_collation不是utf8mb4_unicode_ci,就说明字符集问题没解决,别急着往下走。
5.2 删除一本被借出的书报错:外键约束到底在保护什么
现象:在图书表里执行DELETE FROM book WHERE book_id=3,MySQL直接报错:Cannot delete or update a parent row: a foreign key constraint fails。
原因:这是borrow_record的外键fk_borrow_book在起作用。InnoDB外键的默认行为是RESTRICT,只要还有借阅记录引用这本书,就不允许物理删除关联的父表数据。这不是MySQL出了bug,是它在保护你的借阅历史不被拦腰斩断。
解决:图书管理系统不应该物理删除图书,正确做法是软删除——把book.status改成2或3,代表下架或丢失,书从在架列表里消失,但借阅历史完整保留。如果确实要物理删,必须先处理掉borrow_record里的关联记录,把未还的书先归还、历史记录归档或清空,再回来删除。软删除看起来是绕路,但对一个要留存历史数据的系统来说,这是唯一不会走回头路的方案。
5.3 超借事故:并发抢同一本书时库存变成负数
现象:两个读者同时提交借阅请求,系统显示都成功了,但book表里这本书的status变成了1,而borrow_record里插进了两条记录——一本书被借出去两次。
原因:应用层先执行SELECT status查状态,再执行INSERT和UPDATE,三个步骤之间没有锁保护。两个请求同时通过SELECT检查,都看到status=0,于是都继续执行,最后都认为自己借到了。
解决:抢最后一本书这类场景,必须让"检查+写入"变成一个原子操作。两种做法都可以,第一种是借书事务里用SELECT ... FOR UPDATE锁行,第二个事务会阻塞等待,等读到status=1后回滚;第二种更轻量,直接写成一条原子UPDATE:
UPDATE book SET status = 1 WHERE book_id = 3 AND status = 0;然后检查UPDATE影响的行数,affected rows为1说明借书成功,为0说明书已被借走。这个技巧比FOR UPDATE少一次显式锁,逻辑也更简单直接,我在实际项目里更常用第二种方案。
5.4 借阅列表越查越慢:索引失效和全表扫描的排除思路
现象:数据量刚到几万条,查询借阅列表就要两三秒,页面转圈,演示时尴尬到不行。
原因:最常见的是borrow_record表没按查询条件建索引,或者WHERE条件里对索引列做了函数运算导致索引失效。另一个常见原因是SELECT了不该查的列,MySQL需要回表取数,数据量大时回表成本被放大。
解决:先用EXPLAIN确认执行计划:
EXPLAIN SELECT br.*, b.title FROM borrow_record br JOIN book b ON br.book_id = b.book_id WHERE br.reader_id = 1001 AND br.status = 0;重点看type列,如果是ALL,说明在走全表扫描;如果是ref或index,说明索引生效了。还应该检查key列用的是哪个索引,如果明明建了索引却没用上,往往是对索引列做了计算或隐式类型转换,比如WHERE isbn = 1234567890,而isbn是VARCHAR,MySQL可能放弃索引。按查询条件重新梳理联合索引的列顺序,把最常作为过滤条件的列放最左侧,通常能解决九成以上的慢查询。
5.5 用字符串做业务主键:一张设计文档引发的性能事故
现象:docx设计稿里写了"图书编号B0001"这样的业务字段,于是把它直接定义成VARCHAR(50)主键。前期没感觉,数据量一大,写入越来越慢,每次INSERT都会出现页分裂。
原因:InnoDB的聚簇索引直接以主键值排序组织数据。字符串主键的顺序是随机的,每次插入都可能要调整索引页的位置,导致频繁页分裂和碎片;另外字符串主键比整数主键占用空间大,二级索引里存的都是主键值的副本,空间浪费会随着索引增多被放大。
解决:业务编号永远不做主键。正确姿势是保留自增整数主键book_id,把docx里的"图书编号"或者ISBN降级为普通字段,再加上UNIQUE KEY保证唯一性。这是一个建表阶段的顶层设计问题,等数据量大了再改主键等于重写整张表,越早纠偏代价越小。这套避坑清单看到这里,你大概也能理解为什么很多团队要求数据库设计的评审必须放在写代码之前——表结构定得是否合理,决定了后续所有业务逻辑是顺水推舟还是逆水行舟。
6. 验证与扩展:造一百条数据跑通借还闭环,再预留预约与统计
设计文档写得再完整,最终都要靠数据验证。我的习惯是建完表之后立刻造一批样本数据,把借书、还书、逾期、删书四个链路各跑一遍。你不需要真实业务数据,用脚本批量INSERT几十个读者和几十本书就行。具体做法是把所有建表和测试SQL写进一个init.sql文件,然后用mysql -u root -p < init.sql或source init.sql一次性执行,保证重复执行时不会因为语句顺序报错。
样本数据跑完之后,至少要验证三件事:第一,借书事务在status不为0时是否正确回滚;第二,还书时overdue_days和fine_amount是否计算正确;第三,直接往borrow_record里插入一个不存在的reader_id,外键是否拒绝写入。前两件事验证业务逻辑,第三件验证约束边界。把这三件事做成一个简单的测试脚本,才是数据库设计真正"完成"的标志。
如果这个系统还要继续往下做,两个扩展方向值得预留。一个是预约表reservation,读者可以在图书被借出时预约下一本,结构比borrow_record更简单:预约ID、读者ID、图书ID、预约时间、状态。另一个是统计视图,比如按月统计每本书的借阅次数、借阅排行榜,这类查询可以写成VIEW,把聚合逻辑收在数据库层,应用层只负责读取。再往后如果觉得应用层代码太胖,还可以把借书事务封装成MySQL存储过程,把事务边界收进数据库,这是从演示项目往工程化项目迈的一大步。
我自己做设计的习惯,是每建一张表都从最坏的情况出发问一遍:如果并发超借怎么办?如果有人删了被借的书怎么办?如果书名是emoji怎么办?把每一个能想到的事故预先变成约束和事务,而不是等上线后靠人肉补数据。这套方法帮我避开了很多半夜改表的血泪局面,希望帮到你。
本文还有配套的精品资源,点击获取