ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

数据库课程设计全流程:图书管理系统从E-R图到SQL实现

数据库课程设计全流程:图书管理系统从E-R图到SQL实现 简介这是一份面向数据库初学者的 SQL 图书管理系统课程设计完整文档适合计算机相关专业学生在数据库应用技术、信息系统设计等课程中参考或直接对照实现。文档围绕“读者、图书馆馆员、系统管理员”三类角色完整覆盖系统目标、数据库存储设计、基础与业务数据划分、六大关系表书籍类别、读者、书籍、借阅、还书、罚款、E-R 图、数据字典、关系模式与 SQL 查询上机实现等环节可帮助读者理解从需求分析到系统测试的全流程。资源共 1 个文件为 doc 格式压缩包大小 739KB属于典型的单文件课程设计报告类型便于打印和修改。已有 6488 人学习下载内容包含设计报告书和实现要点既可用于完成课程作业也可作为毕业设计或小型管理信息系统的开发参考。1. 数据库课程设计文档怎么用一套覆盖 E-R 图到 SQL 执行的完整图书管理系统做数据库课程设计时最头疼的不是写代码而是「不知道一份合格的设计报告该长什么样」。这份《图书管理系统》课程设计文档恰好补上了这个缺口——它来自某职业技术学院信息工程系的数据库应用技术课程完整包含设计目标、E-R 图、数据字典、关系模式、数据流程图、建库建表 SQL、数据初始化脚本和查询结果展示总共梳理出书籍类别、读者、书籍、借阅、还书、罚款六张核心表覆盖读者、馆员、管理员三类角色的主要业务场景。它适合正在做数据库课设的在校生也适合需要快速搭一个图书管理演示系统的从业者。哪怕文档年代较早SQL Server 的建表语法至今仍可复用表结构设计的思路也一点不过时。从「这是什么」到「怎么落地」这份文档能帮你省掉从零构思的功夫直接照着一套已验证过的方案推演。2. 表结构设计从角色与数据流到六张核心表的字段细节2.1 先理清业务边界三类角色与三类数据图书管理系统听起来简单但凡是踩过课设坑的人都知道业务边界理不清后面建表全是债。这份文档在一开始就把角色和数据分得很清楚读者负责借书还书图书馆馆员负责登记操作系统管理员负责维护基础数据。对应到数据层面又拆成三类——读者信息、图书信息、操作员信息属于基础数据借还书记录登记、罚款登记属于业务数据书籍借阅情况统计、读者借阅情况统计属于统计数据。这个划分直接决定了表的设计粒度。基础数据单独建表业务数据通过外键引用基础数据统计数据不单独存表而是靠查询实时算出。比如借阅记录只需要存借书证编号、书籍编号、借书时间三个字段读者姓名和书名都通过外键关联去查这样避免了一个读者借十本书就要重复存十次姓名的数据冗余。从实现上看关系模式的设计也遵循了同样的思路。文档定义了六个关系模式书籍类别种类编号、种类名称、读者借书证编号、读者姓名、读者性别、读者种类、登记日期、书籍书籍编号、书籍名称、书籍类别、书籍作者、出版社名称、出版日期、登记日期、借阅借书证编号、书籍编号、读者借书时间、还书借书证编号、书籍编号、读者还书时间、罚款借书证编号、读者姓名、书籍编号、读者借书时间。这里有个细节值得注意罚款关系模式里同时出现了借书证编号和读者姓名按第三范式的要求读者姓名应该只存在读者表里罚款表通过外键关联即可。但文档这样设计也有现实考量——罚款记录属于历史快照如果读者改名或者读者被删罚款单据上仍需保留当时的信息。这属于典型的「空间换一致性」设计课程设计里这样写完全说得通答辩时能解释清楚就行。2.2 六张核心表的主外键设计与关联逻辑物理表设计沿用了关系模式的拆分思路但做了两处重要补充。第一处是书籍表增加了isborrowed字段用来标记书籍是否已借出这是关系模式里没有的。第二处是借阅记录表用bookid作为主键而不是用借阅流水号这意味着同一本书同时只能有一条借阅记录从源头杜绝了一书多借的并发问题。这六张表的关联关系可以用一句话讲清书籍类别是一级表书籍通过bookstyleno外键挂在类别下读者独立成表借阅记录同时引用书籍和读者还书记录与借阅记录结构对称罚款记录挂在借阅链路的末端。文档里给出的关系图图 2-8把这种星型结构画得很直观照着 E-R 图核对表关系时效率很高。有一点容易踩坑borrow_record表的bookid既是主键又是外键return_record表同样如此。这个设计隐含了一个前提——一本书只能被借一次还书后如果想再借需要删除旧记录或另开新表存历史。课程设计阶段这样设计没问题但如果你打算做成长期运行的系统最好给借阅记录增加独立的流水号主键同时把isborrowed作为借还状态的标记后面第 4 章我会展开讲这个坑。2.3 数据字典里容易被忽略的字段约束文档的数据字典部分用了六个表格逐一说明字段仔细看会发现几个对后续写 SQL 影响很大的约束。book_style表的bookstyleno是 varchar(30) 主键system_readers表的readerid是 varchar(9) 主键而且readername也设了 not nullsystem_books表的bookid是 varchar(20) 主键isborrowed是 varchar(2) 且 not null。字段类型选 varchar 而不是 int 是常见做法——借书证编号像Q20120401这种格式带字母前缀用 int 就存不进去。varchar(9) 的 9 是留给纯数字编号的教师证和Q 8位数字的格式如果你自己扩展编号规则长度要提前算好否则插入数据时会被截断。isborrowed用 varchar(2) 而不是 bit原因是 SQL Server 早期版本对 bit 在查询展示上的体验一般用 1 和 0 两个字符更直观。但要注意这个字段本身没有默认值约束如果插入书籍数据时忘了填就会出现 NULL 值后续where isborrowed1这种条件查询会把这本书记录漏掉。3. 建库建表与数据初始化照着敲就能跑通的 SQL 流程3.1 创建数据库路径、初始大小与增长选项文档第一步是创建数据库这段 SQL 放在任何 SQL Server 2008 及以上的环境里都能直接执行。核心参数有三个初始大小 10MB、最大限制 50MB、文件增长 5MB。对于课程设计这种量级的数据10MB 起步已经完全够用设置最大 50MB 是防止日志文件无限膨胀撑爆 C 盘。USE master GO CREATE DATABASE tangzhangsentsg ON ( NAME librarysystem, FILENAME c:\tangzhangsenlibrary.mdf, SIZE 10, MAXSIZE 50, FILEGROWTH 5 ) LOG ON ( NAME library, FILENAME c:\tangzhangsenlibrary.ldf, SIZE 5MB, MAXSIZE 25MB, FILEGROWTH 5MB ) GOFILENAME路径是写死的c:\tangzhangsenlibrary.mdf如果你本机 C 盘没有写权限或者想换到 D 盘直接改这两行路径即可。SIZE可以不写单位默认是 MB。FILEGROWTH支持 KB/MB/GB 后缀不写后缀时默认按 MB 增长。一个常见的做法是将数据文件和日志文件分开存放在不同物理磁盘上以减轻 I/O 竞争。但课设环境通常只有一台机器一个盘文档这样设计已经足够。如果你用的是 SQL Server Express 版本MAXSIZE会被实例级别的 10GB 上限约束课程数据根本触达不到不用担心。3.2 建表顺序先父表后子表否则外键报错建表顺序在 SQL Server 里不是可选项而是硬性约束——被引用的父表必须先创建。文档的实际顺序是book_style→system_books→system_readers→borrow_record→return_record→reader_fee。如果你先建borrow_record再建system_books会直接报外键引用错误。把建表 SQL 拆开看核心是这六段。先看父表book_styleCREATE TABLE book_style( bookstyleno varchar(30) PRIMARY KEY, bookstyle varchar(30) )bookstyleno是类别编号bookstyle是类别名称。主键建在编号上后续system_books表引用它时外键才能建立索引关联。这个表只有两个字段属于典型的字典表。接着是书籍表它是整张表结构里最重的CREATE TABLE system_books( bookid varchar(20) PRIMARY KEY, bookname varchar(30) NOT NULL, bookstyleno varchar(30) NOT NULL, bookauthor varchar(30), bookpub varchar(30), bookpubdate datetime, bookindate datetime, isborrowed varchar(2), FOREIGN KEY (bookstyleno) REFERENCES book_style(bookstyleno) )bookid用 varchar(20) 是因为原始编号是 11 位数字未来扩展加前缀也够用。外键挂在bookstyleno上这保证了插入书籍时类别编号必须在book_style表里已存在。isborrowed没有默认值文档的数据初始化脚本里都会显式赋值 1含义是「1 表示未借出0 表示已借出」。读者表和借阅记录表放在一起看主外键关系更清晰CREATE TABLE system_readers( readerid varchar(9) PRIMARY KEY, readername varchar(9) NOT NULL, readersex varchar(2) NOT NULL, readertype varchar(10), regdate datetime ) CREATE TABLE borrow_record( bookid varchar(20) PRIMARY KEY, readerid varchar(9), borrowdate datetime, FOREIGN KEY (bookid) REFERENCES system_books(bookid), FOREIGN KEY (readerid) REFERENCES system_readers(readerid) )system_readers里readerid是主键borrow_record里readerid是外键方向不能反。borrow_record的bookid同时是主键和外键——主键保证一本书只有一条借阅记录外键保证引用的书真实存在。这个设计简洁但有限制一本书被还了之后想再借出去就必须先删除旧记录再插入新记录。最后是还书表和罚款表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) ) 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) )注意reader_fee表里bookfee用的是 varchar(30) 而不是 decimal这在文档的数据字典里能看到。用字符串存金额是早期课设常见写法但如果你自己扩展成实用系统建议改成decimal(10,2)否则计算罚款总和时要先做类型转换而且字符串比较大小会出现 9 大于 10 的玄学错误。建表顺序的终极提醒如果脚本执行到一半报错大概率是表依赖关系没理清。用DROP TABLE清理时也要按子表到父表的顺序删否则会因外键约束被阻止。3.3 初始化数据INSERT 与 UPDATE 联动的业务逻辑数据初始化脚本是这份文档里信息量最大的部分。它不只是简单地插入测试数据而是演示了一个借书业务流程先在borrow_record表插入借阅记录再把system_books表里对应的isborrowed从 1 改成 0。这相当于用两个 SQL 语句模拟了「借书」这个动作。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(4, 都市小说) INSERT INTO book_style(bookstyleno, bookstyle) VALUES(5, 科幻小说) INSERT INTO book_style(bookstyleno, bookstyle) VALUES(6, 仙侠小说) INSERT INTO book_style(bookstyleno, bookstyle) VALUES(7, 言情小说)书籍类别共 7 类编号从 1 到 7。这里的类别名带有明显的小说类型特征直接把业务场景固定在了「文艺类图书馆」。如果换成通用图书管理系统类别应该是文学、历史、科技、艺术等分类但表结构不用改只改插入值就行。书籍数据的插入是重头戏INSERT INTO system_books( bookid, bookname, bookstyleno, bookauthor, bookpub, bookpubdate, bookindate, isborrowed ) VALUES( 20135678901, 飘渺之旅, 1, 萧潜, 鲜网, 2005-09-01, 2013-05-25, 1 )每本书的bookid是 11 位数字前 4 位 2013 是年份后面 7 位是序号。bookstyleno对应book_style表里已存在的编号isborrowed初值 1 代表在库可借。日期格式是标准的 YYYY-MM-DDSQL Server 能直接识别。读者数据的插入覆盖了三种读者类型INSERT INTO system_readers(readerid, readername, readersex, readertype, regdate) VALUES(Q20120401, 李雷, 男, 学生, 2013-01-18 12:20) INSERT INTO system_readers(readerid, readername, readersex, readertype, regdate) VALUES(201005, 毛正标, 男, 教师, 2013-01-23 18:50) INSERT INTO system_readers(readerid, readername, readersex, readertype, regdate) VALUES(GL001, 李燕玲, 女, 管理, 2013-01-01 16:20)学生证号以 Q 开头加 8 位数字教师证号是纯 6 位数字管理员用 GL 前缀。readerid字段长度是 9刚好适配前两种格式。这表明在设计阶段就已经考虑了不同角色的编号规则差异如果你要扩展新的读者类型第一件事就是确认编号长度在 varchar(9) 范围内。借书操作的联动更新是文档里最值得学的部分INSERT INTO borrow_record(bookid, readerid, borrowdate) VALUES(20135678901, Q20120401, 2013-01-18 12:20) UPDATE system_books SET isborrowed 0 WHERE bookid 20135678901借书记录插入后立刻把对应书籍标记为已借出。这两条语句必须在一个事务里执行否则会出现「借阅记录存在但书还在架上」的脏数据。文档没有显式写事务包裹但实际运行时你要么手动开启事务要么在应用程序层保证这两步的原子性。这条UPDATE语句还有一个版本带了额外条件WHERE bookid 20135678902 AND isborrowed 1。这个条件的意思是只有书当前可借时才执行借出相当于一个简单的并发保护。如果这本书已经被借走isborrowed已经是 0这个 UPDATE 会匹配 0 行借阅动作实际上失败——但INSERT已经执行了就产生了脏数据。生产环境里应该先查再借或者把判断逻辑放在存储过程中用事务包裹。4. 查询实现与业务场景单表查询到借还联动以及五个常见翻车点4.1 单表查询验证数据落地的正确姿势文档第 4 章开头是两张单表查询用SELECT *分别查book_style和system_books。看起来简单但它承担了第一个验证职责建表和初始化数据是否成功。执行SELECT * FROM book_style后应该看到 7 行类别数据执行SELECT * FROM system_books后应该看到 8 本书的记录。在课程设计报告里这个查询结果需要截图贴在文档中作为实验成果。实际操作时我一般会先查COUNT(*)确认行数符合预期再查明细数据核对字段内容因为如果初始化数据脚本里某条 INSERT 写错了字段顺序SELECT *的结果看起来会很混乱。-- 确认类别表行数 SELECT COUNT(*) AS style_count FROM book_style -- 确认书籍表行数 SELECT COUNT(*) AS book_count FROM system_books -- 查看当前所有在库书籍isborrowed 1 表示可借 SELECT bookid, bookname, bookstyleno, isborrowed FROM system_books WHERE isborrowed 1参数说明isborrowed字段的取值约定是 1 表示在库可借0 表示已借出。查询在库书籍时只要把这个条件加到 WHERE 子句即可。这里要特别注意字段是 varchar 类型条件里必须用引号包裹写成isborrowed 1在 SQL Server 里会自动转换但性能较差且容易踩隐式转换的坑不建议这样写。4.2 多表查询把 E-R 图里的关系翻译成 JOIN单表查询覆盖的是基础数据但图书管理系统的核心价值在于多表关联。E-R 图里画出的借阅关系映射到 SQL 里就是borrow_record表同时 JOINsystem_readers和system_books。SELECT r.readerid, r.readername, b.bookid, b.bookname, br.borrowdate FROM borrow_record br JOIN system_readers r ON br.readerid r.readerid JOIN system_books b ON br.bookid b.bookid这个查询把「谁在什么时候借了哪本书」一次性查出来。JOIN的方向是从事实表borrow_record出发左连接两张维度表。如果改用 LEFT JOIN能查出有借阅记录但读者或书籍信息缺失的异常数据——正常设计下不会出现但排查脏数据时很有用。还书查询的结构与借阅查询对称SELECT r.readerid, r.readername, rt.bookid, b.bookname, rt.returndate FROM return_record rt JOIN system_readers r ON rt.readerid r.readerid JOIN system_books b ON rt.bookid b.bookid WHERE rt.returndate IS NOT NULL注意returndate是 datetime 类型判断是否为空要用IS NOT NULL不能写成! NULL。这一点初学者经常搞混——NULL 在 SQL 里不是值而是「未知」的标记任何与 NULL 的比较都会返回未知。4.3 超期罚款的查询与数据口径问题罚款业务是文档需求列表里的最后一项对应reader_fee表。这个表的字段设计比较精简——bookfee存了罚款金额borrowdate存了借阅时间但没有还书时间也没有超期天数字段。计算超期要从borrowdate加上借阅期限后与当前日期比较但借阅期限本身没有单独的表或字段来定义。-- 查询所有罚款记录 SELECT readerid, readername, bookid, bookname, bookfee, borrowdate FROM reader_fee -- 按读者汇总罚款金额 SELECT readerid, readername, SUM(CAST(bookfee AS DECIMAL(10,2))) AS total_fee FROM reader_fee GROUP BY readerid, readername第一段查询直接列出罚款明细第二段用GROUP BY汇总每个读者的累计罚款。这里暴露了bookfee用 varchar 存储的弊端——必须CAST成 DECIMAL 才能做 SUM 运算。如果某一行的bookfee里混入了非数字字符这个查询会直接报错。这也是我在第 3 章强调改成 decimal 类型的实际理由。罚款记录的生成逻辑在文档中没有单独的存储过程或触发器的实现只在需求描述里提到「超期还书罚款输入」。课程设计层面用简单的INSERT INTO reader_fee手动录入罚款数据即可但如果你想展示更完整的业务闭环可以写一个存储过程来自动计算超期天数并生成罚款记录这个在后面一节展开讲。4.4 避坑清单五个高频翻车点与排查路径翻车点 1借书后忘记更新isborrowed现象borrow_record表有借阅记录但system_books.isborrowed仍然是 1同一本书还能被再次插入借阅记录。原因INSERT借阅记录和UPDATE书籍状态是两个独立语句没有事务包裹或应用程序没做联动操作。解决把两步操作放到一个显式事务中用 BEGIN TRANSACTION 和 COMMIT 包裹如果是在应用程序里用同一数据库连接按顺序执行并捕获异常回滚。我一般还会加一道保险——插入借阅记录前先查isborrowed是否为 1等于 0 就直接拒绝借出。翻车点 2bookid做主键导致一本书无法重复借阅现象还书后想再借同一本书INSERT INTO borrow_record报主键冲突。原因borrow_record表的主键设计在bookid上一本书同时只能存在一条记录而不是一个读者同时只能借一本相同的书。解决课程设计里可以直接删掉旧的借阅记录再插入新记录。更合理的做法是把主键改成自增流水号bookid降级为普通外键。这样能保留完整的借阅历史也方便统计借阅次数。翻车点 3bookfee用 varchar 存金额导致汇总报错现象SUM(bookfee)查询报「操作数数据类型 varchar 对 sum 运算符无效」。原因varchar 类型不能直接做聚合运算必须先转换。更隐蔽的问题是如果某条记录的值是 abc 或空字符串CAST会直接报转换失败。解决建表时就用decimal(10, 2)定义金额字段。如果表已建成先清理脏数据再执行ALTER TABLE reader_fee ALTER COLUMN bookfee DECIMAL(10, 2)。翻车点 4日期格式不一致导致比较结果异常现象查询超期图书时有的记录算得出超期天数有的算成负数。原因borrowdate字段的类型不统一或者插入时用了 2013-01-18 12:20 这种带时间的格式而另一条记录只给了日期。datetime 类型在做日期差时会精确到秒如果超期规则只看日期需要先截断时间部分。解决用DATEDIFF(day, borrowdate, GETDATE())计算天数差值DAY 粒度会忽略时间部分。前提是borrowdate本身是 datetime 类型如果存的是 varchar得先CONVERT(datetime, borrowdate, 120)标准格式转换。翻车点 5删除父表数据时被外键约束卡住现象DELETE FROM system_readers WHERE readerid Q20120401报外键冲突错误。原因borrow_record表里仍有引用该读者的记录外键约束阻止删除被引用的数据。解决先删子表数据再删父表数据或者把外键改成ON DELETE CASCADE。课设里用前者更稳妥避免误删连锁数据。如果要清空所有表重新初始化建议按borrow_record、return_record、reader_fee、system_books、system_readers、book_style的顺序执行 DELETE然后重新跑初始化脚本。5. 验证设计报告的正确性用 SQL 反向校验 E-R 图与数据字典拿到这份文档后很多人直接照抄 SQL 建库建表跑通了就觉得完事。但课程设计答辩时老师最喜欢问的是「你怎么证明你的表设计是对的」。我习惯用一套反向验证方法从数据字典出发逐个验证 E-R 图里画的实体关系是否真的落了地。第一步验证实体完整性。数据字典里每个表都必须有主键主键字段不允许 NULL。跑一段系统查询就能检查出所有问题SELECT t.name AS table_name, c.name AS column_name FROM sys.tables t JOIN sys.columns c ON t.object_id c.object_id WHERE c.is_nullable 1 AND c.column_id (SELECT MIN(column_id) FROM sys.columns WHERE object_id t.object_id)这段脚本找出每张表的第一个字段是否可空。如果主键字段出现在结果集里说明建表脚本和文档不一致。第二步验证参照完整性。E-R 图里画的每一根关系线都应该能在sys.foreign_keys系统表里找到对应记录。执行以下查询能列出所有外键关系SELECT fk.name AS constraint_name, tp.name AS parent_table, ref.name AS referenced_table FROM sys.foreign_keys fk JOIN sys.tables tp ON fk.parent_object_id tp.object_id JOIN sys.tables ref ON fk.referenced_object_id ref.object_id拿这个结果和文档里的 E-R 图对比书籍表引用类别表、借阅表引用书籍表和读者表、还书表引用书籍表和读者表、罚款表引用书籍表和读者表六张表间的五组外键关系必须全部出现。少了任何一组都说明建表脚本漏了外键。第三步验证业务约束。isborrowed字段只允许 0 和 1 两个值但表结构里没有 CHECK 约束这意味着靠应用程序自觉保证。验证时跑一个检查非法值的查询SELECT bookid, isborrowed FROM system_books WHERE isborrowed NOT IN (0, 1)结果如果是空集说明初始化数据干净。如果查出 NULL 或者 2 之类的值就是插入脚本没控制好。第四步用一组模拟业务场景把借书、还书、罚款全流程跑一遍同时记录每一步的 SQL 和查询结果作为设计报告里的「实验结果」部分。比如模拟借书就是执行第 3 章那组 INSERT UPDATE模拟超期还书就是手工向reader_fee插入一笔罚款数据再用SELECT查出来。每一步的截图就是你报告里的实据——这比空口说「系统能跑」有说服力得多。我自己的习惯是拿到类似的课设文档后永远先执行一遍建表和初始化脚本然后马上跑这几条验证查询。数据能对上文档里的 E-R 图和关系模式才算真正落地。要是哪次发现SELECT查出来的行数和预期不符不用怀疑——一定是初始化的 INSERT 脚本里有字段位置写错或日期格式不统一的问题但这份文档的脚本在这一点上写得挺干净照着敲基本不会翻车。希望帮到你。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进