简介:这份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', '言情小说');注意类别编号用'1'~'7'这种字符串数字当主键,后面如果要做「按类别排序」,字符串排序会变成'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 isborrowed='0' where bookid='901' insert into borrow_record(bookid,readerid,borrowdate) values('902','Q','2013-01-19 13:15') update system_books set isborrowed='0' where bookid='902' and isborrowed='1'两步走:先在借阅表留一条记录,再把书的状态位翻成「已借出」。逻辑没错,但这里没有事务包着。万一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_available('1'在架),要么把取值语义翻过来。改名要同步改所有 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,都强制把这三组用例跑一遍才敢提交。希望帮到你。
本文还有配套的精品资源,点击获取