资讯详情

资讯详情

图书管理系统SQL实战:六表建模、借还罚事务与课设代码避坑

简介这份SQL数据库图书管理系统资料面向数据库课程设计、课程实践与入门项目开发者围绕图书馆读者、图书与操作员三类基础数据覆盖借阅、还书、超期罚款等业务流程可用于理解关系型数据库从需求分析到上机实现的完整过程。包内仅含1个doc文档约709KB以课程设计报告书形式组织包含设计目标、数据需求、E-R图、数据字典、关系模式、关系实例、查询描述及SQL实现等内容便于对照梳理实体关系与建表查询思路。已有2319人学习或下载适合需要完成图书管理系统作业、复盘数据库设计步骤或补充SQL练习的读者参考也能为借还书记录、罚款登记与借阅统计等模块提供较完整的实现框架。1. 图书管理系统这套 SQL 到底能拿来干什么手上这份《SQL数据库图书管理系统(完整代码).doc》本质是一份带完整 SQL 源码的数据库课程设计报告六个CREATE TABLE、一批INSERT初始化数据、若干单表和多表查询、外加 E-R 图、数据字典和关系模式说明。它不是一套带界面的可执行程序而是把「图书馆借还业务怎么建模」这件事从图纸一路写到 SQL Server 里能跑的语句。适合两类人一类是要交数据库课设、需要照着抄建表和查询的在校开发者另一类是工作几年、想补一补关系型建模基本功看看一个真实的借还流程怎么被拆成表、外键和状态位的人。把这批语句跑通你能得到一个能借书、还书、算超期罚款、出借阅统计的最小数据库骨架后面接 C#、Java 还是 Python 界面都只是往上贴皮。2. 六张表的关系骨架从 E-R 图落到建表语句2.1 实体到表的映射为什么最后是六个表报告里的 E-R 图给了五个实体图书类别、读者、书籍、借阅记录、归还记录另外单挂了一张罚款单。落到关系模式就是六张表第一步先看它们怎么被拆的。类别和书籍是一对多一本书只能属于一个类别所以把类别编号放在书籍表里做外键而不是给类别表加一个书籍列表字段——关系型数据库没有「列表字段」这种东西只能靠外键反向指。读者和书籍是多对多一个人可以借多本书一本书也能被不同人先后借走。多对多没法直接靠两个表表达必须引入一张中间表这就是借阅记录的由来。归还记录同理它的存在不是为了存新信息而是为了让「这本书什么时候还的」有一个独立的、可以按时间检索的落点。至于罚款单本来是能由借阅记录和归还记录算出来的派生数据报告中把它固化成一张reader_fee表好处是罚款一旦登记就有据可查、不会被后来的计算规则变化改掉历史金额坏处是多了一份需要人工同步的数据副本。这个取舍在课程设计里无所谓在真实系统里要谨慎——我一般会把罚款表保留为「罚款流水」把计算规则放在存储过程里而不是靠人手INSERT。原始关系模式写得很直白关系模式字段书籍类别种类编号、种类名称读者借书证编号、姓名、性别、读者种类、登记日期书籍书籍编号、名称、类别、作者、出版社、出版日期、登记日期、是否借出借阅借书证编号、书籍编号、借书时间还书借书证编号、书籍编号、还书时间罚款借书证编号、姓名、书籍编号、书名、罚金、借阅时间看这张对照表就能发现一个隐患罚款表里同时存了读者姓名和书名属于典型的冗余字段。读者改了名字、书改名了罚款历史就会和主表对不上。这在课设里没人管但你要把这套东西往生产环境挪第一刀就该砍掉这些冗余列只保留readerid、bookid这种外键姓名书名全部靠 JOIN 现取。2.2 建库建表语句逐段拆从 CREATE DATABASE 到外键先看建库那段。报告里的原文是USE master go CREATE DATABASE tangzhangsentsg ON ( NAME librarysystem, FILENAME c:\, SIZE 10, MAXSIZE 50, FILEGROWTH 5 ) LOG ON ( NAME library, FILENAME c:\, SIZE 5MB, MAXSIZE 25MB, FILEGROWTH 5MB ) go这段直接粘到 SQL Server 里跑大概率报错。FILENAME要的是一个带文件名的完整路径比如D:\data\librarysystem.mdf只给一个c:\目录 SQL Server 会认为你要建一个叫目录的文件直接给你 5123 号错误。另一个问题是SIZE 10没写单位默认按 MB 处理能跑但读起来含糊写清楚10MB更稳。LOG ON里日志文件名写的是library同样缺路径和扩展名正确写法应该是library_log.ldf。go是 SSMS 的批处理分隔符不是 T-SQL 语句用命令行sqlcmd跑也认但如果你把这段塞进 JDBC 或者 Python 的pymssql里执行go会直接报语法错误必须切掉或者是拆成两次 execute。修正后的版本我一般写成这样CREATE DATABASE LibrarySystem ON ( NAME librarysystem_data, -- 逻辑文件名后续备份/还原要用 FILENAME D:\data\librarysystem.mdf, -- 必须带文件名和 .mdf 扩展名 SIZE 10MB, -- 初始大小别贴着 0 写 MAXSIZE 50MB, -- 上限超出会报 1105 空间不足 FILEGROWTH 5MB -- 每次扩 5MB避免频繁小步扩 ) LOG ON ( NAME librarysystem_log, FILENAME D:\data\librarysystem_log.ldf, SIZE 5MB, MAXSIZE 25MB, FILEGROWTH 5MB );参数含义一目了然SIZE是初始分配FILEGROWTH是每次不够时扩多少MAXSIZE是天花板。课程设计数据量小10MB 完全够但如果你后面要压几十万条借阅记录做性能测试MAXSIZE 50MB会让你在半路撞墙那是血泪经验别问怎么知道的。建库之后记得USE LibrarySystem;切过去否则后面的表全建在 master 库里。接着看六张表按依赖顺序建——被引用的表先建引用的表后建否则外键找不到目标直接报错。-- 1. 类别表被 books 引用最先建 CREATE TABLE book_style ( bookstyleno varchar(30) PRIMARY KEY, -- 类别编号主键 bookstyle varchar(30) -- 类别名称 ); -- 2. 书籍表引用类别表 CREATE TABLE system_books ( bookid varchar(20) PRIMARY KEY, -- 书籍编号 bookname varchar(30) NOT NULL, bookstyleno varchar(30) NOT NULL, -- 外键指向 book_style bookauthor varchar(30), bookpub varchar(30), bookpubdate datetime, bookindate datetime, isborrowed varchar(2), -- 借出标记1/0 FOREIGN KEY (bookstyleno) REFERENCES book_style(bookstyleno) ); -- 3. 读者表独立实体 CREATE TABLE system_readers ( readerid varchar(9) PRIMARY KEY, -- 借书证号 readername varchar(9) NOT NULL, readersex varchar(2) NOT NULL, readertype varchar(10), regdate datetime ); -- 4. 借阅表同时引用书籍和读者 CREATE TABLE borrow_record ( bookid varchar(20) PRIMARY KEY, -- 这里用 bookid 做主键是个坑 readerid varchar(9), borrowdate datetime, FOREIGN KEY (bookid) REFERENCES system_books(bookid), FOREIGN KEY (readerid) REFERENCES system_readers(readerid) ); -- 5. 还书表结构同借阅表 CREATE TABLE return_record ( bookid varchar(20) PRIMARY KEY, readerid varchar(9), returndate datetime, FOREIGN KEY (bookid) REFERENCES system_books(bookid), FOREIGN KEY (readerid) REFERENCES system_readers(readerid) ); -- 6. 罚款表冗余了姓名和书名 CREATE TABLE reader_fee ( readerid varchar(9) NOT NULL, readername varchar(9) NOT NULL, bookid varchar(20) PRIMARY KEY, bookname varchar(30) NOT NULL, bookfee varchar(30), borrowdate datetime, FOREIGN KEY (bookid) REFERENCES system_books(bookid), FOREIGN KEY (readerid) REFERENCES system_readers(readerid) );重点看第 4、5、6 三张表的主键全部拿bookid当主键。这意味着什么同一本书在整个借阅表里只能存在一条记录。第一次借还行第二次借同一本书INSERT直接撞主键冲突。还书表更荒谬——一本书历史上被还过多次你却只留得下最后一次。这不是小瑕疵是设计级的硬伤后面第 5 章我会单独展开怎么改。罚款表同样一本书只能罚一次款学生把书弄丢赔了钱、后来又找到再赔一次系统就装不下了。2.3 字段类型与约束varchar 的坑在哪原始代码通篇用varchar没有一个nvarchar。varchar是变长非 Unicode 字符中文能不能存取决于数据库排序规则默认的Chinese_PRC_CI_AS下一般能存但一旦系统要迁到英文环境或者被别的库 JOIN中文就成问号。稳妥做法是所有承载中文的列——bookname、readername、bookstyle、bookauthor——改成nvarchar长度按字符算。readername varchar(9)意味着最多 9 个字符的名字三个字的中文名够用但遇到少数民族姓名或者外籍读者就会截断我一般直接给到nvarchar(50)多占那点空间不值得为它冒风险。时间字段混用datetime和varchar也很别扭。borrowdate在borrow_record里是datetime可报告后面初始化数据插的是2013-01-18 12:20这种字符串SQL Server 会隐式转换运气好能过但return_record.returndate是datetime、reader_fee.borrowdate又是datetime格式只要写成2013/1/18或者中文日期隐式转换当场翻车。规范做法是全部统一datetime或datetime2插入时用2013-01-18T12:20:00这种 ISO 8601 格式任何语言环境都不歧义。isborrowed varchar(2)这个设计也值得一提。它只有两种状态用varchar(2)存1和0等于用一个 2 字节的字符串表达一个比特的信息而且没约束理论上能插9、abc进去。我会改成bit或者char(1) CHECK (isborrowed IN (0,1))。更麻烦的是语义——报告里初始化数据先插书、isborrowed填1然后借出去时UPDATE ... SET isborrowed 0也就是说1代表「在架未借」0代表「已借出」。这个语义和字段名isborrowed是否被借出正好相反isborrowed 1读起来像「被借出了」实际含义却是「没被借出」。这种反直觉命名是后期 bug 的温床我在第 5 章会给出改法。3. 初始化数据那批 INSERT 里的三个暗雷3.1 类别字典与书目台账的插入报告的初始化部分先灌类别七条INSERT全是小说分类写法是标准的值列表插入INSERT INTO book_style(bookstyleno, bookstyle) VALUES (1, 玄幻小说); INSERT INTO book_style(bookstyleno, bookstyle) VALUES (2, 穿越小说); INSERT INTO book_style(bookstyleno, bookstyle) VALUES (3, 悬疑小说);这种一条一条写的方式跑七次就是七次事务提交慢倒是不慢就是脚本长、不好维护。真要写初始化脚本我会合成一条多值插入-- 合并为单条语句一次提交失败可整体回滚 INSERT INTO book_style(bookstyleno, bookstyle) VALUES (1, 玄幻小说), (2, 穿越小说), (3, 悬疑小说), (4, 都市小说), (5, 科幻小说), (6, 仙侠小说), (7, 言情小说);注意类别编号用17这种字符串数字当主键后面如果要做「按类别排序」字符串排序会变成1,10,2这种字典序不是数值序。数据量小看不出来超过 9 个类别就露馅。能用自增int就用自增int非要用业务编码至少保证定长或统一宽度。书目插入是八条每条都带bookid、书名、类别、作者、出版社、出版日期、登记日期、isborrowed八个字段。这里有个细节所有书的bookindate登记入库日期都比bookpubdate出版日期晚好几年逻辑上是对的——书出版几年后才被图书馆采购入库。这个字段顺序在建模时容易搞反我见过有人把登记日期填成出版日期统计「年度新增藏书」时数据全错。3.2 借阅台账与 isborrowed 的联动真正要命的是读者和借阅这两段初始化。原文读者插入是这样的insert into system_readers(readerid,readername,readersex,readertype,regdate) values(Q,李雷,男,学生,2013-01-18 12:20) insert into system_readers(readerid,readername,readersex,readertype,regdate) values(Q,唐张森,男,学生,2013-01-19 13:15)readerid是主键八条数据里前五条的readerid全填了Q。第一条能进第二条开始全部主键冲突报 2627。这明显是原作者抄数据时占位符没替换——本来应该是Q001、Q002这种借书证号。你照着跑SSMS 会刷一屏红字然后你以为是自己环境问题开始怀疑人生。修正方式很简单给它编号-- 借书证号按 Q序号 补齐保证主键唯一 INSERT INTO system_readers(readerid, readername, readersex, readertype, regdate) VALUES (Q001, 读者甲, 男, 学生, 2013-01-18 12:20), (Q002, 读者乙, 男, 学生, 2013-01-19 13:15), (Q003, 读者丙, 女, 学生, 2013-01-20 13:33), (Q004, 读者丁, 女, 学生, 2013-01-21 12:01), (Q005, 读者戊, 男, 学生, 2013-01-22 15:23), (201005, 读者己, 男, 教师, 2013-01-23 18:50), (201006, 读者庚, 男, 教师, 2013-01-24 18:25), (GL001, 读者辛, 女, 管理, 2013-01-01 16:20);接着是借阅记录 状态位联动的经典两步操作原文写法insert into borrow_record(bookid,readerid,borrowdate) values(901,Q,2013-01-18 12:20) update system_books set isborrowed0 where bookid901 insert into borrow_record(bookid,readerid,borrowdate) values(902,Q,2013-01-19 13:15) update system_books set isborrowed0 where bookid902 and isborrowed1两步走先在借阅表留一条记录再把书的状态位翻成「已借出」。逻辑没错但这里没有事务包着。万一INSERT成功、UPDATE因为锁等待超时失败你就得到一本「账上借出去了、状态还显示在架」的书然后这本书能被再借一次同一本书同时出现在两个读者手上。这就是典型的「没有后悔药」场景。改写思路是用BEGIN TRAN把两条语句包起来并用ROWCOUNT校验BEGIN TRANSACTION; -- 只在书确实在架时才允许借出避免重复借阅 UPDATE system_books SET isborrowed 0 WHERE bookid 901 AND isborrowed 1; IF ROWCOUNT 0 BEGIN ROLLBACK TRANSACTION; THROW 50001, 该书不在架或已被借出, 1; -- 自定义错误号50000 以上留给应用层 END INSERT INTO borrow_record(bookid, readerid, borrowdate) VALUES (901, Q001, 2013-01-18 12:20); COMMIT TRANSACTION;顺序上我把UPDATE提到INSERT前面了原因是这样能靠ROWCOUNT做一次乐观并发检查——只有真把状态从1改成0的那一次才继续插借阅记录两个人同时点借书只有一个能成。这比先插后改稳妥得多。3.3 把造数脚本改成可重复执行课程设计的脚本通常只在干净库上跑一次你在本地反复调试跑第二遍就全是主键冲突。想省事开头加一段清理逻辑-- 按外键依赖反序删除先删引用方再删被引用方 IF OBJECT_ID(dbo.reader_fee, U) IS NOT NULL DROP TABLE dbo.reader_fee; IF OBJECT_ID(dbo.return_record, U) IS NOT NULL DROP TABLE dbo.return_record; IF OBJECT_ID(dbo.borrow_record, U) IS NOT NULL DROP TABLE dbo.borrow_record; IF OBJECT_ID(dbo.system_readers, U) IS NOT NULL DROP TABLE dbo.system_readers; IF OBJECT_ID(dbo.system_books, U) IS NOT NULL DROP TABLE dbo.system_books; IF OBJECT_ID(dbo.book_style, U) IS NOT NULL DROP TABLE dbo.book_style;OBJECT_ID(dbo.xxx, U)的第二个参数U限定只查用户表避免误删同名视图或存储过程。删除顺序必须是「先删子表、后删父表」反过来会被外键拦住报 3726。如果你不想删表重建那就改用MERGE或者先DELETE FROM再插注意DELETE的先后顺序同样受外键约束还得考虑给自增列做DBCC CHECKIDENT重置种子。4. 借还罚业务链路SQL 怎么把三件事串起来4.1 借书一条 INSERT 加一条 UPDATE 的完整事务上一节已经给出事务版借书。实际系统里借书还需要校验三件事读者存在且未被冻结、书在架、读者没超借阅上限。前两条靠外键和ROWCOUNT兜底第三条得单独查一次。-- 借书前先检查该读者当前在借数量上限 5 本 DECLARE readerid varchar(9) Q001; DECLARE bookid varchar(20) 901; DECLARE limit int 5; IF (SELECT COUNT(*) FROM borrow_record WHERE readerid readerid) limit BEGIN THROW 50002, 该读者借阅数量已达上限, 1; END参数limit抽成变量是方便后面改成从配置表读。COUNT(*)在借阅表上跑数据量小无所谓上万条之后就该建readerid上的非聚簇索引否则每次借书都全表扫一遍界面卡得用户以为死机。4.2 还书从借阅表搬到还书表原报告只说了「还书信息的输入包括借书证编号、书籍编号、还书日期」没给语句。合格的做法是三步删借阅记录、插还书记录、把书状态翻回在架。BEGIN TRANSACTION; -- 1. 先确认这本书确实是这个读者借的 DELETE FROM borrow_record WHERE bookid bookid AND readerid readerid; IF ROWCOUNT 0 BEGIN ROLLBACK TRANSACTION; THROW 50003, 借阅记录不存在无法还书, 1; END -- 2. 落一条还书流水保留历史 INSERT INTO return_record(bookid, readerid, returndate) VALUES (bookid, readerid, GETDATE()); -- 3. 书籍状态复位 UPDATE system_books SET isborrowed 1 WHERE bookid bookid; COMMIT TRANSACTION;注意这里我用DELETE删借阅记录而不是留着。为什么因为原始设计的borrow_record主键就是bookid不删掉同一本书永远借不了第二次。理想设计应该是借阅表保留所有历史、加一个「是否已还」状态位这属于第 6 章的改造内容。就原样跑删是最小改动方案。GETDATE()返回服务器当前时间如果你在容器里跑、时区没设写进去的就是 UTC 时间和读者看到的本地时间差八小时超期判断会跟着错。稳一点用SYSDATETIMEOFFSET()或者应用层传时间进来。4.3 超期罚款用 DATEDIFF 算出该罚多少报告里罚款是人工INSERT的bookfee还是个varchar(30)能存「五十块」这种字符串。真要自动化核心就是一个日期差函数。-- 计算所有未还图书的超期天数与应罚金额 SELECT b.readerid, r.readername, b.bookid, k.bookname, b.borrowdate, DATEDIFF(DAY, b.borrowdate, GETDATE()) AS 已借天数, CASE WHEN DATEDIFF(DAY, b.borrowdate, GETDATE()) 30 THEN (DATEDIFF(DAY, b.borrowdate, GETDATE()) - 30) * 0.20 ELSE 0 END AS 应罚金额 FROM borrow_record b JOIN system_readers r ON r.readerid b.readerid JOIN system_books k ON k.bookid b.bookid WHERE DATEDIFF(DAY, b.borrowdate, GETDATE()) 30; -- 只列超期的DATEDIFF(DAY, 开始, 结束)返回两个日期之间的天数差注意它的边界语义是按「跨过多少个日界」算的不是按 24 小时。今天下午三点借、明天上午十点还DATEDIFF会返回 1 而不是 0你做按天计费的业务时这个细节会咬人一口。0.20是每天罚金抽成参数或配置表更合适。CASE WHEN里把超期天数减 30 得到实际超期天数30 是借阅期限这个阈值在真实系统里应该按读者类型区分——教师借 60 天、学生借 30 天硬编码在 SQL 里以后改起来要翻遍所有查询。把上面这段包成存储过程还书时调用一次罚款就能自动落库CREATE PROCEDURE sp_CalcFee bookid varchar(20), readerid varchar(9) AS BEGIN SET NOCOUNT ON; DECLARE days int, fee decimal(10,2); SELECT days DATEDIFF(DAY, borrowdate, GETDATE()) FROM borrow_record WHERE bookid bookid AND readerid readerid; IF days 30 BEGIN SET fee (days - 30) * 0.20; INSERT INTO reader_fee(readerid, readername, bookid, bookname, bookfee, borrowdate) SELECT b.readerid, r.readername, b.bookid, k.bookname, CAST(fee AS varchar(30)), b.borrowdate FROM borrow_record b JOIN system_readers r ON r.readerid b.readerid JOIN system_books k ON k.bookid b.bookid WHERE b.bookid bookid AND b.readerid readerid; END ENDSET NOCOUNT ON关掉「N 行受影响」的返回消息不然应用层拿到的第一个结果集是行数统计而不是数据容易解析错。decimal(10,2)存金额比varchar靠谱排序、求和都不会出错。4.4 借阅统计与多表连接报告最后演示了select * from book_style和select * from system_books两条单表查询作为入门够用但要回答「哪类书最受欢迎」就得三表连接加聚合。-- 按类别统计借阅次数找出热门类别 SELECT s.bookstyle AS 类别, COUNT(*) AS 借阅次数, COUNT(DISTINCT b.readerid) AS 借阅读者数 FROM borrow_record b JOIN system_books k ON k.bookid b.bookid JOIN book_style s ON s.bookstyleno k.bookstyleno GROUP BY s.bookstyle ORDER BY 借阅次数 DESC;COUNT(DISTINCT b.readerid)和COUNT(*)的差别值得留意前者是「有多少不同的人借过这类书」后者是「总共借出去多少次」。一个读者反复借同一类书COUNT(*)会虚高。做运营报表时这两个指标要分开看混用会得出错误结论。GROUP BY里只能出现聚合函数和非聚合列SELECT里的s.bookstyle必须在GROUP BY中否则 SQL Server 报 8120。有些数据库比如 MySQL 的宽松模式允许你省略SQL Server 不允许从 MySQL 迁过来的人经常在这里卡住。5. 避坑与排查直接跑这份代码会翻车的几处5.1 建库时报 5123 或 1802现象执行CREATE DATABASE立刻报「无法创建文件 c:操作系统错误 5(拒绝访问)」或者 5123「CREATE FILE 遇到操作系统错误」。原因FILENAME c:\给的是目录不是文件SQL Server 服务账户对 C 盘根目录通常也没有写权限。解决改成完整路径比如D:\data\librarysystem.mdf并提前把D:\data建好、给 SQL Server 服务账户读写权限。路径里不要有中文和空格。5.2 读者数据插入时主键冲突现象跑初始化脚本第一条读者插入成功后面七条全部报 2627「违反 PRIMARY KEY 约束」。原因脚本里readerid全填了同一个占位符主键要求唯一。解决按规则重编号Q001到Q005这一类或者临时先把主键去掉插完再ALTER TABLE ADD PRIMARY KEY。根治办法是写脚本时养成习惯插入前先SELECT一下目标表确认没有重复。5.3 同一本书第二次借不出去现象还了书想再借同一本INSERT INTO borrow_record直接主键冲突。原因borrow_record拿bookid当主键一本书在借阅表里只允许一条记录历史被抹平了。解决短期用还书时DELETE掉借阅记录第 4.2 节的写法绕过长期按下节改造把主键换成自增 ID 或(bookid, readerid, borrowdate)复合主键。5.4 isborrowed 判断反了现象明明在架的书界面上显示「已借出」已经借走的书反而能再借。原因字段名叫isborrowed实际语义却是1 在架、0 已借出和命名完全相反。写查询的人凭直觉写成WHERE isborrowed 1查已借出结果查出来一堆在架书。解决二选一要么把字段改名为is_available1在架要么把取值语义翻过来。改名要同步改所有 SQL、视图、存储过程和前端代码别只改一半。5.5 删书删读者被外键拦住现象DELETE FROM system_books WHERE bookid 901报 547「DELETE 语句与 REFERENCE 约束冲突」。原因借阅表、还书表、罚款表都引用了这本书有子记录存在时父记录删不掉。解决先删子表记录再删主表或者建外键时指定ON DELETE CASCADE。级联删除用起来爽风险也大——删一本书连带把历史借阅流水全抹了审计就没了。我一般不用级联宁可写个存储过程按业务顺序清理并对有历史记录的书做「下架」而不是物理删除。6. 二次开发把这套课设改成能跑的小系统拿到这份代码最值钱的不是那些 INSERT是它把业务拆成了「类别—书—人—借—还—罚」六段。真要在它基础上做二次开发我建议按三步走。第一步改表结构。把三张记录表的主键从bookid换成自增id加一个状态位-- 借阅表改造历史全留用状态位区分在借/已还 CREATE TABLE borrow_record_v2 ( id int IDENTITY(1,1) PRIMARY KEY, -- 自增主键一条借阅一次生命周期 bookid varchar(20) NOT NULL, readerid varchar(9) NOT NULL, borrowdate datetime NOT NULL DEFAULT GETDATE(), returndate datetime NULL, -- 为空表示还没还 FOREIGN KEY (bookid) REFERENCES system_books(bookid), FOREIGN KEY (readerid) REFERENCES system_readers(readerid) ); -- 一本书同一时间只允许一条未还记录用筛选唯一索引兜住 CREATE UNIQUE INDEX ux_borrow_active ON borrow_record_v2(bookid) WHERE returndate IS NULL;那个带WHERE的筛选唯一索引是关键它保证「同一本书不能有两个未还记录」同时又不妨碍历史上多次借阅。这比在应用层写if判断可靠得多并发下也不会漏。第二步把借还书收进存储过程前端只负责传参。好处是业务规则只有一份改罚款规则不用重新发版。第三步加视图给报表用把三表连接、聚合这些重查询封装成vw_borrow_stat界面直接SELECT * FROM vw_borrow_stat。验证改造是否成功我的习惯是造三组数据压一遍同一本书连续借还三次、同一读者借满上限再借第六本、一本超期 45 天的书走完还书流程看罚款金额。三组都过基本逻辑就站住了。要是偷懒跳过这步等上线后读者拿两本书同时借走同一册你就得半夜爬起来手工核对台账。从那以后我每次改借还相关的 SQL都强制把这三组用例跑一遍才敢提交。希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →