简介:这份数据库课程设计文档面向高校计算机相关专业学生,聚焦图书馆管理信息系统的完整设计流程,可作为课程设计、期末大作业或数据库原理实践的参考方案。资源包共1个doc文件,约239KB,内容按标准课程设计报告结构组织,涵盖系统开发平台、数据库规划、系统定义、需求分析、逻辑设计、物理设计、应用程序设计、测试运行与总结等章节。文档以SQL Server 2000为数据库、Eclipse为开发工具,详细给出管理员与读者两类用户视图,梳理书籍、副本、借阅记录、罚款账目等实体的数据需求与事务需求,并配有ER图、数据字典、关系表、索引、视图、安全机制与触发器设计,以及功能模块、界面与事务设计说明。已有362人学习,适合需要参考完整设计思路、快速搭建报告框架或对照查漏补缺的读者。
1. 从一份 22 页的课程设计报告说起:图书馆管理信息系统到底能跑通什么
如果你手头正压着一个数据库课程设计,选题是图书馆管理信息系统,大概率会遇到同一个尴尬:需求文档写得像模像样,ER 图画了一版又一版,真到建表、写触发器、跑借还书事务的时候,数据对不上、副本状态乱掉、罚款金额算错。这份 22 页的课程设计报告,核心价值不在于它有多完整,而在于它把「需求分析 → ER 图 → 数据字典 → 关系表 → 索引/视图/触发器 → 应用程序事务」这条链路完整走了一遍,而且每一步都落到了具体的字段类型、主外键约束和 SQL 语句上。它适合两类人:一是正在做同类课程设计、需要一份可参照的数据库设计骨架的在校生;二是想快速回顾「一个中小型管理系统从需求到物理设计该怎么落地」的开发者。开发工具是 Eclipse,数据库是 SQL Server 2000,操作系统 Windows XP,这套技术栈虽然老,但数据库设计的思路和 SQL 语法放到今天依然能直接迁移到 MySQL 或 SQL Server 更高版本上。
2. 需求分析怎么落到字段:从借阅规则反推数据字典
2.1 先看业务规则,再定表结构
很多课程设计翻车,不是因为 SQL 写得不好,而是因为需求分析阶段漏掉了关键约束,导致后面表结构反复改。这份报告在需求分析部分把规则写得比较细,值得逐条拆开看。
读者类型决定了最大借阅数:本科生 8 册,研究生和教师 10 册。这意味着reader表里必须有type字段和max_no字段,而且max_no的值不是随便填的,它应该由type推导出来。常见做法是单独建一张type表,把类型编号和类型名称存进去,reader表通过外键关联。报告里就是这么做的:type (type_no, t_name),reader表里存type作为外键。
借阅期限默认 30 天,续借加 30 天,只可续借一次。这条规则直接影响loan表的设计。loan表需要out_date(借出日期)和due_date(应还日期),续借操作本质上就是修改due_date。但「只可续借一次」这个约束,单靠loan表本身很难表达,需要在应用层或者加一个续借次数字段来控制。报告里没有单独设续借次数字段,而是通过应用逻辑判断,这是一个可以讨论的设计取舍。
罚款规则:超期每天 0.1 元,从应还时间开始算;遗失按原价赔偿。这要求history表里有fine_type(赔偿类型)、fine_pay(应赔金额)、fine_paid(实赔金额)三个字段。注意fine_type是int类型,说明它可能是一个枚举值,比如 0 表示正常、1 表示超期、2 表示遗失。
违章未缴罚款的读者不能借书。这个约束在借书事务里必须检查,报告里在借书登记部分明确列出了三种禁止借书的情况:达到最大借阅量、有超期未还、有罚款未缴。
2.2 数据字典的字段类型不是拍脑袋定的
报告里的数据字典给出了每个字段的类型和长度,这些选择背后有实际考虑。比如librarian.id用char(5),管理员编号固定 5 位;reader.id也是char(5),读者编号同样固定长度。isbn用varchar(20),因为 ISBN 长度不固定,有 10 位和 13 位两种。price用float(8),金额字段用浮点数在正式项目里其实有精度风险,常见做法是改用decimal,但课程设计阶段用float也能跑。
copy表的on_loan字段用int(4),报告里触发器把它当布尔用:借出时设为 0,归还时设为 1。这种用整数模拟布尔的做法在老系统里很常见,迁移到 MySQL 时可以直接用TINYINT(1)。
loan表的主键是copy_id,这意味着一个副本同时只能有一条借阅记录。这个设计是合理的,因为一个副本不可能同时被两个人借走。但要注意,loan表的主键只有copy_id一个字段,而history表的主键是(copy_id, reader_id, out_date)三字段联合主键,因为历史记录里同一个副本可能被同一个人多次借阅,需要借出日期来区分。
2.3 关系表与外键约束的落地写法
报告给出了七张关系表:librarian、reader、book、copy、loan、history、type、account。外键关系如下:
book.type引用type.type_nocopy.isbn引用book.isbnloan.copy_id引用copy.copy_idloan.reader_id引用reader.idhistory.copy_id引用copy.copy_idhistory.reader_id引用reader.idaccount.reader_id引用reader.id
建表时外键的顺序很重要,必须先建被引用的表。比如book引用了type,所以type要先建。下面是一段可以直接在 SQL Server 里跑的建表脚本,我按报告里的关系表整理成了可执行版本:
-- 先建被引用的表 CREATE TABLE type ( type_no CHAR(1) PRIMARY KEY, t_name VARCHAR(50) NOT NULL ); CREATE TABLE librarian ( id CHAR(5) PRIMARY KEY, name VARCHAR(30) NOT NULL, tel VARCHAR(11), password VARCHAR(30) NOT NULL ); CREATE TABLE reader ( id CHAR(5) PRIMARY KEY, name VARCHAR(30) NOT NULL, sex CHAR(2), enter INT, type CHAR(1), max_no INT, cur_no INT DEFAULT 0, password VARCHAR(30) NOT NULL, FOREIGN KEY (type) REFERENCES type(type_no) ); CREATE TABLE book ( isbn VARCHAR(20) PRIMARY KEY, title VARCHAR(50) NOT NULL, author VARCHAR(50), publisher VARCHAR(50), price FLOAT, type CHAR(1), suocopy_no INT, in_copy INT, FOREIGN KEY (type) REFERENCES type(type_no) ); CREATE TABLE copy ( copy_id CHAR(10) PRIMARY KEY, isbn VARCHAR(20), on_loan INT DEFAULT 1, FOREIGN KEY (isbn) REFERENCES book(isbn) ); CREATE TABLE loan ( copy_id CHAR(10) PRIMARY KEY, reader_id CHAR(5), out_date DATETIME, due_date DATETIME, FOREIGN KEY (copy_id) REFERENCES copy(copy_id), FOREIGN KEY (reader_id) REFERENCES reader(id) ); CREATE TABLE history ( copy_id CHAR(10), reader_id CHAR(5), out_date DATETIME, in_date DATETIME, fine_type INT, fine_pay FLOAT, fine_paid FLOAT, PRIMARY KEY (copy_id, reader_id, out_date), FOREIGN KEY (copy_id) REFERENCES copy(copy_id), FOREIGN KEY (reader_id) REFERENCES reader(id) ); CREATE TABLE account ( id CHAR(5) PRIMARY KEY, reader_id CHAR(5), time DATETIME, type VARCHAR(8), money FLOAT, FOREIGN KEY (reader_id) REFERENCES reader(id) );这段脚本里几个关键点:reader.cur_no给了默认值 0,因为新注册读者当前借阅数肯定是 0;copy.on_loan默认 1,表示新副本默认在馆可借;history的主键是三字段联合主键,这是为了允许同一副本被同一读者多次借阅时产生多条历史记录。外键约束保证了引用完整性,但要注意 SQL Server 2000 默认不启用级联删除,删除book记录时如果copy表里有引用会报错,需要先处理副本。
3. 触发器与事务:借还书操作的数据一致性怎么保证
3.1 借书触发器:一次插入,三表联动
借书这个动作,表面上是往loan表插一条记录,实际上要同时更新三张表:copy表的on_loan改为 0(借出),book表的in_copy减一(在馆副本数减一),reader表的cur_no加一(当前借阅数加一)。如果不用触发器,就得在 Java 代码里依次执行四条 SQL,任何一条失败都会导致数据不一致。报告里用触发器把这三步更新绑在一起:
CREATE TRIGGER LoanInsert ON loan FOR INSERT AS -- 更新副本状态为借出 UPDATE copy SET on_loan = 0 FROM copy c INNER JOIN inserted i ON c.copy_id = i.copy_id; -- 更新图书在馆副本数减一 UPDATE book SET in_copy = in_copy - 1 FROM book b, copy c, inserted i WHERE b.isbn = c.isbn AND c.copy_id = i.copy_id; -- 更新读者当前借阅数加一 UPDATE reader SET cur_no = cur_no + 1 FROM reader r INNER JOIN inserted i ON r.id = i.reader_id;inserted是 SQL Server 触发器里的特殊表,存放本次插入的新行。触发器在loan表插入后自动执行,三步更新要么全成功要么全回滚。这里有个细节:book表的更新用了逗号连接而不是INNER JOIN,在 SQL Server 2000 里这种写法能跑,但迁移到 MySQL 时语法不兼容,需要改成标准JOIN。
3.2 还书事务:为什么没用触发器
报告里明确说了,还书操作没有用触发器,而是在 Java 应用层用多条 SQL 拼起来执行。原因是还书有两种情况:正常归还和挂失。正常归还时,删除loan记录、更新copy状态为在馆、book在馆数加一、reader当前借阅数减一、往history插一条记录。挂失时,操作类似但赔偿类型和金额不同。两种情况逻辑分叉,用触发器反而不好写条件判断,所以放在应用层控制。
报告给出的还书 SQL 片段如下:
// 删除借阅记录 sql = "delete from loan where copy_id='" + isbn.getText() + "'"; // 更新副本状态为在馆 sql = "update copy set on_loan=1 where copy_id='" + isbn.getText() + "'"; // 更新图书在馆副本数加一 sql = "update book set in_copy=in_copy+1 where book.isbn in (select isbn from copy where copy_id='" + isbn.getText() + "')"; // 判断是否超期,决定罚款金额 if (cal.compareTo(duecal) <= 0) { // 未超期,正常归还 sql = "insert into history(copy_id,reader_id,out_date,in_date,fine_type,fine_pay,fine_paid) values('" + isbn.getText() + "','" + reader_id + "','" + out_date + "','" + in_date + "','正常',0,0)"; } else { // 超期,计算罚款 money = 0.1 * val; df = new DecimalFormat("#0.0"); money = 0.1 * val; sql = "insert into history(copy_id,reader_id,out_date,in_date,fine_type,fine_pay,fine_paid) values('" + isbn.getText() + "','" + reader_id + "','" + out_date + "','" + in_date + "','超期'," + money + ",0)"; }这段代码有几个值得注意的地方。第一,cal.compareTo(duecal) <= 0判断当前日期是否早于或等于应还日期,是则未超期。第二,罚款金额money = 0.1 * val,val应该是超期天数,但代码里没给出val的计算过程,实际实现时需要自己补上:val = (当前日期 - 应还日期) / 一天的毫秒数。第三,fine_paid设为 0,表示应赔但未实赔,等读者缴款后再更新。第四,SQL 是字符串拼接的,存在 SQL 注入风险,正式项目应该用PreparedStatement,但课程设计阶段这样写能跑通。
3.3 副本入馆触发器:库存管理的自动化
当新副本入馆时,往copy表插一条记录,同时要更新book表的suocopy_no(总副本数)和in_copy(在馆副本数)各加一。报告里的触发器写法:
CREATE TRIGGER copy_insert ON copy FOR INSERT AS -- 总副本数加一 UPDATE book SET suocopy_no = suocopy_no + 1 FROM book b INNER JOIN inserted i ON b.isbn = i.isbn; -- 在馆副本数加一 UPDATE book SET in_copy = in_copy + 1 FROM book b INNER JOIN inserted i ON b.isbn = i.isbn;这个触发器逻辑简单直接,但要注意:如果一次插入多条副本记录,inserted表里有多行,UPDATE语句会一次性更新所有涉及的book记录,不会重复加。这是 SQL Server 触发器的特性,inserted表包含本次插入的所有行,UPDATE ... FROM ... INNER JOIN inserted会按isbn分组更新。
3.4 视图:简化多表查询的实用手段
报告里建了两个视图,OnloanView和HistoryView,分别用于查询当前借阅和历史借阅的详细信息。这两个视图把book、copy、loan(或history)三张表连接起来,应用层直接查视图就能拿到书名、作者、出版社、借出日期、应还日期等字段,不用每次写三表连接。
CREATE VIEW OnloanView AS SELECT book.isbn, title, author, publisher, enter, reader_id, out_date, due_date FROM book, copy, loan WHERE book.isbn = copy.isbn AND copy.copy_id = loan.copy_id; CREATE VIEW HistoryView AS SELECT book.isbn, title, author, reader_id, out_date, in_date FROM book, copy, history WHERE book.isbn = copy.isbn AND history.copy_id = copy.copy_id;视图的好处是查询逻辑封装,坏处是如果底层表结构变了,视图可能失效。课程设计里用视图简化查询是加分项,但要注意视图本身不存储数据,每次查询都是实时执行底层 SQL。
4. 索引与安全机制:物理设计里最容易忽略的两块
4.1 索引不是越多越好,要看事务频率
报告里列了一张索引表,按事务原因给不同表的字段建索引。比如librarian.id因为搜索条件(事务 a、h、o)建索引,reader.id因为搜索条件(事务 d、e、f、g、k、m、n、r、s、t、u)建索引,book.isbn因为搜索条件(事务 p、v)建索引,copy.copy_id因为搜索条件(事务 c、j、q)建索引。
这些索引的选择逻辑是:频繁作为查询条件的字段建索引。但要注意,索引会拖慢插入和更新速度,因为每次写操作都要维护索引。loan表的reader_id建了索引,因为经常要查某个读者的当前借阅记录;history表的copy_id和reader_id建了联合索引,因为历史查询经常按这两个字段过滤。
一个容易被忽略的点是:主键自动建唯一索引,外键字段如果不建索引,连接查询时性能会差。报告里loan.reader_id建了索引,但loan.copy_id是主键,已经自动有索引了。history表的主键是(copy_id, reader_id, out_date),这个联合主键本身就是一个复合索引,按copy_id或copy_id + reader_id查询时能命中索引,但单独按reader_id查询时用不上,所以报告额外给history.reader_id建了索引。
4.2 安全机制:报告里的做法和更稳妥的方案
报告里安全机制部分写得很直白:系统没有给每个数据库用户分配认证标识,所有操作都用超级用户sa连接数据库,权限控制在应用程序里做。这种做法的风险很明显:一旦应用层被绕过,数据库就完全暴露。但在课程设计场景下,这种简化可以理解,因为重点在数据库设计而不是安全架构。
如果要把这个设计改得更稳妥,常见做法是:给应用创建一个专用数据库账号,只授予必要的SELECT、INSERT、UPDATE、DELETE权限,不给DROP、ALTER等 DDL 权限。读者和管理员在应用层用不同的数据库连接账号,读者账号只能查视图和部分表,管理员账号才能操作全部表。这样即使应用层有漏洞,数据库层的权限也能兜底。
报告里还提到「图书管理员和读者只能在适合他们完成工作的需要的窗口中看到需要的数据」,这是用户视图层面的权限控制,在应用层实现。比如读者登录后只能看到自己的借阅记录和罚款记录,看不到其他读者的信息。这种控制靠 SQL 的WHERE条件实现,比如查询借阅记录时强制加reader_id = 当前登录读者编号。
4.3 备份策略:每天 24 点备份的落地方式
报告里写了「每天 24 点备份」,但没有给出具体实现。在 SQL Server 2000 里,可以通过 SQL Server Agent 创建作业,定时执行BACKUP DATABASE语句。下面是一个备份脚本的示例:
-- 每天 24 点执行完整备份 BACKUP DATABASE LibraryDB TO DISK = 'D:\Backup\LibraryDB_Full.bak' WITH INIT, NAME = 'LibraryDB Full Backup';WITH INIT表示覆盖之前的备份文件,NAME是备份集名称。如果要保留多份备份,可以去掉INIT,或者用日期动态生成文件名。课程设计里如果不想配 SQL Server Agent,也可以在 Java 应用里用Timer或ScheduledExecutorService定时调用备份 SQL,但这种方式依赖应用进程一直运行,不如数据库自带的作业调度可靠。
5. 避坑与排查:这份设计里最容易翻车的五个地方
5.1 触发器里更新 book 表时 in_copy 变成负数
现象:借书后book.in_copy变成负数,或者还书后in_copy超过suocopy_no。
原因:触发器的更新条件写错了,或者inserted表里有多行时重复更新。比如借书触发器里UPDATE book SET in_copy = in_copy - 1 FROM book b, copy c, inserted i WHERE b.isbn = c.isbn AND c.copy_id = i.copy_id,如果copy表里同一个isbn有多个副本,而inserted里只有一条记录,这个连接条件会匹配到多行copy,导致book表被更新多次。
解决:把更新条件改成直接通过inserted关联copy再关联book,确保每个inserted行只触发一次更新。或者改用子查询:UPDATE book SET in_copy = in_copy - 1 WHERE isbn = (SELECT isbn FROM copy WHERE copy_id = (SELECT copy_id FROM inserted))。更稳妥的做法是在触发器开头加SET NOCOUNT ON,避免行数统计干扰。
5.2 还书时 history 表插入失败,因为主键冲突
现象:还书时往history表插记录报主键冲突。
原因:history表的主键是(copy_id, reader_id, out_date),如果同一个读者借同一本书两次,且借出日期相同(比如同一天借了又还、还了又借),第二次插入时主键重复。
解决:out_date用DATETIME类型,精确到秒甚至毫秒,同一天借两次的概率极低。但如果确实需要支持同一天多次借阅,可以把主键改成自增 ID,或者把out_date的精度提高到毫秒。报告里用的是DATETIME(8),SQL Server 2000 的DATETIME精度是 3.33 毫秒,基本够用。
5.3 借书时没有检查读者是否有未缴罚款
现象:读者有未缴罚款,但系统仍然允许借书。
原因:借书事务里只检查了cur_no < max_no和是否有超期未还,漏掉了罚款检查。
解决:在借书前先查account表,看该读者是否有fine_paid < fine_pay的记录。如果有,拒绝借书并提示先缴款。报告里在借书登记部分列出了三种禁止借书的情况,其中第三种就是「有违章罚款未缴纳」,但具体 SQL 实现需要自己补。
5.4 删除图书时外键约束报错
现象:删除book表里的记录时报外键冲突。
原因:copy表里有引用该isbn的记录,外键约束阻止删除。
解决:先删除该图书的所有副本,再删除图书。或者在建外键时加ON DELETE CASCADE,但级联删除风险大,容易误删数据。报告里的做法是:删除指定书刊时,先检查是否有外借副本,如果有则提示暂无法删除;如果没有外借,则删除该书刊及所有副本。这个逻辑在应用层实现,先DELETE FROM copy WHERE isbn = ?,再DELETE FROM book WHERE isbn = ?。
5.5 罚款金额计算精度丢失
现象:超期罚款算出来是 0.30000000000000004 而不是 0.3。
原因:float类型在计算机里是二进制浮点数,无法精确表示 0.1 这样的十进制小数。
解决:金额字段改用DECIMAL(10,2)或NUMERIC(10,2),Java 里用BigDecimal而不是double。报告里用的是float(8),课程设计阶段能跑,但正式项目一定要改。如果不想改表结构,至少在 Java 里用DecimalFormat格式化输出,报告里也用了DecimalFormat("#0.0")来保留一位小数。
6. 从课程设计到能跑的系统:三个进阶技巧
6.1 用存储过程封装借还书事务
触发器能保证单条插入后的联动更新,但借书前的检查(读者是否存在、是否达到最大借阅数、是否有超期、是否有罚款)和借书后的更新如果分开写,中间任何一步失败都会导致状态不一致。更稳妥的做法是把整个借书流程封装成一个存储过程,在数据库层用事务包起来:
CREATE PROCEDURE BorrowBook @copy_id CHAR(10), @reader_id CHAR(5) AS BEGIN SET NOCOUNT ON; BEGIN TRANSACTION; -- 检查副本是否在馆 IF NOT EXISTS (SELECT 1 FROM copy WHERE copy_id = @copy_id AND on_loan = 1) BEGIN ROLLBACK; RAISERROR ('该副本当前不可借', 16, 1); RETURN; END -- 检查读者是否存在且未达最大借阅数 IF NOT EXISTS (SELECT 1 FROM reader WHERE id = @reader_id AND cur_no < max_no) BEGIN ROLLBACK; RAISERROR ('读者不存在或已达最大借阅数', 16, 1); RETURN; END -- 检查是否有未缴罚款 IF EXISTS (SELECT 1 FROM account WHERE reader_id = @reader_id AND money > 0) BEGIN ROLLBACK; RAISERROR ('有未缴罚款,请先缴款', 16, 1); RETURN; END -- 插入借阅记录,触发器会自动更新 copy、book、reader INSERT INTO loan (copy_id, reader_id, out_date, due_date) VALUES (@copy_id, @reader_id, GETDATE(), DATEADD(DAY, 30, GETDATE())); COMMIT; END这个存储过程把检查逻辑和插入操作放在一个事务里,任何检查失败就回滚,不会留下脏数据。DATEADD(DAY, 30, GETDATE())计算应还日期,比在 Java 里算好再传进来更可靠,因为数据库服务器的时间是统一的。
6.2 用视图加权限控制实现读者只能看自己的数据
读者登录后,应该只能看到自己的借阅记录、罚款记录和账目。如果应用层用同一个数据库账号查所有数据,靠WHERE reader_id = ?过滤,一旦应用层有漏洞就可能越权。更安全的做法是给读者创建一个专用数据库账号,只授予视图的查询权限,视图里用SUSER_SNAME()或应用传入的上下文变量过滤。
在 SQL Server 里可以用USER_NAME()获取当前数据库用户,但读者登录用的是应用层账号,不是数据库账号。所以更实际的做法是:读者账号只能查OnloanView和HistoryView,而这两个视图的定义里不包含其他读者的数据,应用层查询时强制加reader_id条件。如果要在数据库层强制,可以用行级安全(SQL Server 2016 及以上支持),但 SQL Server 2000 没有这个功能。
6.3 热门借阅统计的 SQL 写法
报告里统计报表模块要求「近 30 天内借阅情况,按借阅次数排行,显示前 20 本书刊」。这个查询需要从history表里按isbn分组统计,再按次数降序取前 20。下面是一个可用的 SQL:
SELECT TOP 20 b.isbn, b.title, b.author, COUNT(*) AS borrow_count FROM history h INNER JOIN copy c ON h.copy_id = c.copy_id INNER JOIN book b ON c.isbn = b.isbn WHERE h.out_date >= DATEADD(DAY, -30, GETDATE()) GROUP BY b.isbn, b.title, b.author ORDER BY borrow_count DESC;DATEADD(DAY, -30, GETDATE())计算 30 天前的日期,COUNT(*)统计每本书的借阅次数,TOP 20取前 20 条。注意GROUP BY里要包含SELECT里所有非聚合列,否则 SQL Server 会报错。如果要在 MySQL 里跑,TOP 20改成LIMIT 20,DATEADD改成DATE_SUB(NOW(), INTERVAL 30 DAY)。
平均借阅时间的统计类似,从history表里算DATEDIFF(DAY, out_date, in_date)的平均值:
SELECT AVG(DATEDIFF(DAY, out_date, in_date)) AS avg_borrow_days FROM history WHERE out_date >= @start_date AND in_date <= @end_date;DATEDIFF(DAY, out_date, in_date)计算借出到归还的天数,AVG求平均。报告里提到「对时间的输入有良好的校验」,意思是用户输入的起止日期要验证格式和逻辑(起始日期不能晚于结束日期),这个在应用层做。
6.4 我踩过的一个坑:触发器递归
SQL Server 的触发器默认是递归的,也就是说,如果loan表的触发器更新了copy表,而copy表上也有触发器更新loan表,就会无限循环。报告里的触发器没有这个问题,因为copy表的触发器只更新book表,不碰loan。但如果你后来给copy表加了触发器去更新loan,就要小心。解决办法是用RECURSIVE_TRIGGERS数据库选项关闭递归,或者在触发器里加条件判断,避免循环触发。
从那以后我每次写触发器之前,都会先画一张表之间的更新关系图,确认没有环,再动手写 SQL。希望帮到你。
本文还有配套的精品资源,点击获取