简介这是四川大学数据库系统原理课程设计项目源自“陈鹏班”2021年完成的图书馆管理系统实践。项目围绕用户管理、图书信息管理、借阅管理、搜索查询与统计报表等核心模块完整呈现数据库从概念建模、关系模式设计到SQL实现与优化的过程涉及事务处理、存储过程、触发器及数据备份等关键知识点。压缩包共1862个文件以js、json、md、ts、py等类型为主包含前后端源码、数据库脚本、配置说明与Markdown文档整体约16.84MB目录层级清晰便于按模块查阅。目前已有139人学习适合数据库课程学生、毕业设计者以及希望了解图书管理系统实现细节的开发者。借助该资源可学习图书借阅流程、报表统计等功能的代码组织方式也能对照源码理解数据库表关系建模和接口设计思路为同类系统开发提供参考。1. 数据库系统原理课程设计图书管理这个经典题目到底考什么2021 年陈鹏班的《数据库系统原理》课程设计任务就是 A-Simple-Library-Management——一个看起来人畜无害的简单图书管理系统。正因为业务太常见新手最容易做成 CRUD 拼盘建三张表、写增删改查、跑通就交。但数据库系统原理这门课考的不是你会不会写 SQL而是能不能把关系模式、函数依赖、事务、约束这些理论真正落到代码里。这篇文章按我做同类项目的方式拆给你先立数据模型再写功能层最后是答辩前要避开的坑和能加分的设计。想拿高分或者想借这个题目把数据库基本功补齐的读者按这个顺序走不会错。2. 从需求到关系模式先把数据模型立起来别急着建表答辩时老师最爱问的第一个问题就是表结构怎么设计的依据是什么。要答得上就得从需求拆起。我先讲拆法再给完整建表 SQL并把字段参数和范式依据一并说清楚。2.1 拆业务实体把需求文本变成表清单回到 A-Simple-Library-Management 这个题目本身拿到需求我第一件事不是建表而是把需求文本里的名词和动词圈出来。名词是实体候选读者、图书、出版社、管理员、借阅记录动词是操作候选借书、还书、续借、查询逾期。课程设计的典型功能要求只有读者管理、图书管理、借书还书、逾期统计这几项所以核心实体收敛成三个读者、图书、借阅记录。出版社是新手最容易纠结的地方。我见过的做法分两种一种是把出版社名称、联系方式建成独立表与图书建立外键另一种是把出版社名称直接做成图书表的一个字段。我的判断标准是看功能需求里有没有维护出版社档案这个动作。课程设计里十有八九没有那就并入图书表更合理——少一张表就少一套增删改查答辩时也少一个解释负担。实体属性也在这步顺手列出来。读者有编号、姓名、性别、电话、注册日期图书有编号、ISBN、书名、作者、出版社、分类、库存借阅记录有读者编号、图书编号、借出日期、应还日期、实际归还日期。这份清单列完字段基本就位了。2.2 关系与主键M:N 怎么用中间表承接实体确定之后要回答关系类型。一个读者可以借多本图书从同一时刻一本图书只能被一个读者持有这个约束看当前借阅是 1:N但从长期历史看一本图书会被多个读者先后借过所以图书与读者整体上是 M:N。M:N 关系不能直接用两张表表达必须拆成两个 1:N借阅记录这张中间表就是干这个的。借阅表主键的设计值得专门讲一下。常见做法是加自增主键 borrow_id简单省事另一种做法是用 (reader_id, book_id, borrow_date) 做联合主键。我建议课程设计里用联合主键它天然防止同一个读者在同一天重复借同一本书而且答辩时你能说清楚我用联合主键是为了表达业务上的唯一性约束这比为了让程序好写专业得多。当然如果需求允许同一个人同一天借两本完全相同的书现实里不太可能联合主键就不合适这时退回自增主键。外键方向也要想清楚。借阅表里的 reader_id 引用读者表主键book_id 引用图书表主键这两列必须非空这是借阅行为成立的前提。不要在课程设计里引入图书表指向管理员表管理员表又指向图书表这类循环依赖功能没要求就是给自己添堵。2.3 建表 SQL字段类型、默认值与第三范式三张表的 DDL 我放在一个初始化脚本里。注意字段类型、默认值和注释这些是老师会追问的细节。-- 读者表基本信息与注册日期 CREATE TABLE reader ( reader_id INT NOT NULL AUTO_INCREMENT COMMENT 读者编号主键, name VARCHAR(20) NOT NULL COMMENT 姓名, gender CHAR(1) DEFAULT M COMMENT 性别M/F默认M, phone VARCHAR(15) COMMENT 联系电话允许为空, reg_date DATE NOT NULL COMMENT 注册日期, PRIMARY KEY (reader_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT读者表; -- 图书表ISBN与库存 CREATE TABLE book ( book_id INT NOT NULL AUTO_INCREMENT COMMENT 图书编号主键, isbn VARCHAR(20) NOT NULL COMMENT ISBN允许带连字符, title VARCHAR(100) NOT NULL COMMENT 书名, author VARCHAR(50) COMMENT 作者, publisher VARCHAR(50) COMMENT 出版社名称直接冗余, category VARCHAR(20) COMMENT 分类如Java/数据库/文学, stock INT NOT NULL DEFAULT 1 COMMENT 库存数量默认1, PRIMARY KEY (book_id), UNIQUE KEY uk_isbn (isbn) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT图书表; -- 借阅表联合主键防止重复借阅 CREATE TABLE borrow ( reader_id INT NOT NULL COMMENT 读者编号外键, book_id INT NOT NULL COMMENT 图书编号外键, borrow_date DATE NOT NULL COMMENT 借出日期, due_date DATE NOT NULL COMMENT 应还日期借出后30天, return_date DATE COMMENT 实际归还日期NULL表示在借, PRIMARY KEY (reader_id, book_id, borrow_date), KEY idx_book_id (book_id), KEY idx_return_date (return_date), CONSTRAINT fk_borrow_reader FOREIGN KEY (reader_id) REFERENCES reader (reader_id), CONSTRAINT fk_borrow_book FOREIGN KEY (book_id) REFERENCES book (book_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT借阅表;这段 SQL 里有几个参数值得展开。ISBN 为什么用 VARCHAR(20) 而不是 BIGINTISBN 在印刷时经常带连字符早期 ISBN-10 的校验位还会出现 X用数值类型会丢失格式。gender 用 CHAR(1) 而不是 VARCHAR(1)定长字符在存储和比较上都更快取值只有两个不需要留大空间。日期字段统一用 DATE 而不是 DATETIME借还书业务粒度是天DATE 足够用 DATETIME 反而会让逾期天数统计多出时分秒的边界问题。reg_date 和 borrow_date 设置 NOT NULL因为这两项在业务流程里必然存在。范式层面这套设计满足第三范式3NF没有部分依赖也没有传递依赖。图书信息只存在图书表里借阅表只存 book_id不冗余书名和作者读者信息同理。唯一看着像冗余的是图书表里的 publisher 字段但它是标量属性只要不把出版社地址、电话也塞进来就不会出现多处重复存储、改一处漏一处的更新异常。课程设计做到 3NF 足够别为了追求 BCNF 去硬拆出版社表除非功能明确要求维护出版社档案。提示字符集统一用 utf8mb4不要用 utf8。utf8 在 MySQL 里实际是 utf8mb3 的别名存不了 Emoji 和生僻字老版本还可能导致中文相关查询异常。数据库、表、连接串三层都对齐成 utf8mb4能避开后面绝大多数乱码问题。3. 功能实现JDBC 连接与借还书事务的关键写法数据模型立住之后控制台或页面代码只是壳真正的核心在数据访问层和事务控制。课程设计用 JDBC 直连最常见不需要上 MyBatis 这类框架——老师想看你对 Connection、PreparedStatement、事务边界的理解是否到位。3.1 数据访问层JDBC 工具类与 DAO 的封装写法我一般先做一个 JDBC 工具类把驱动加载、获取连接、关闭资源三件事收口避免每个 DAO 方法里写一大堆重复样板代码。public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/library ?useSSLfalsecharacterEncodingutf8serverTimezoneAsia/Shanghai; private static final String USER root; private static final String PASSWORD your_password; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { throw new RuntimeException(MySQL驱动加载失败, e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } public static void close(Connection conn, Statement stmt, ResultSet rs) { try { if (rs ! null) rs.close(); } catch (SQLException ignored) {} try { if (stmt ! null) stmt.close(); } catch (SQLException ignored) {} try { if (conn ! null) conn.close(); } catch (SQLException ignored) {} } }URL 里的参数直接决定你后面少踩多少坑。characterEncodingutf8 解决中文写入乱码serverTimezoneAsia/Shanghai 解决 JDBC 8.x 连 MySQL 时的时区报错useSSLfalse 是本地开发不强制 SSL 握手。驱动类名也要注意MySQL 5.x 用 com.mysql.jdbc.DriverMySQL 8.x 用 com.mysql.cj.jdbc.Driver写反了直接 ClassNotFound。为什么课程设计用 JDBC 而不是 ORM我的理由很简单MyBatis 和 Hibernate 帮你省掉的 SQL 细节恰恰是这门课要考察的。手写 PreparedStatement 虽然啰嗦但你能清楚看到参数怎么绑定、结果集怎么转换、事务在哪里提交。这份手感带进工作后再用任何 ORM 都不会被黑盒迷惑。DAO 方法以查询在借图书列表为例public ListBorrowRecord listBorrowing() throws SQLException { String sql SELECT r.name, b.title, br.borrow_date, br.due_date FROM borrow br JOIN reader r ON br.reader_id r.reader_id JOIN book b ON br.book_id b.book_id WHERE br.return_date IS NULL ORDER BY br.borrow_date DESC; ListBorrowRecord result new ArrayList(); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql); ResultSet rs ps.executeQuery()) { while (rs.next()) { BorrowRecord rec new BorrowRecord(); rec.setReaderName(rs.getString(name)); rec.setTitle(rs.getString(title)); rec.setBorrowDate(rs.getDate(borrow_date)); rec.setDueDate(rs.getDate(due_date)); result.add(rec); } } return result; }这里用了 try-with-resources三个资源都会在方法退出时按逆序自动关闭省掉冗长的 finally。WHERE 条件直接筛 return_date IS NULL把是否在借的判断下推到数据库而不是把全部借阅记录拉到 Java 内存里再过滤。课程设计数据量看不出差别但这个习惯在数据量上来之后是性能和可读性的双重收益。3.2 借书还书的原子性一个事务里的两条 UPDATE借书流程表面上是两步往借阅表插一条记录把图书表库存减一。如果两步分开提交第二步失败时借阅表里就多了一条没有库存支撑的记录。事务存在的意义就是把两步框进同一个边界——要么都成功要么都回滚。public void borrowBook(int readerId, int bookId) throws SQLException { String updateStock UPDATE book SET stock stock - 1 WHERE book_id ? AND stock 0; String insertBorrow INSERT INTO borrow (reader_id, book_id, borrow_date, due_date) VALUES (?, ?, CURDATE(), DATE_ADD(CURDATE(), INTERVAL 30 DAY)); try (Connection conn DBUtil.getConnection()) { conn.setAutoCommit(false); try (PreparedStatement ps1 conn.prepareStatement(updateStock); PreparedStatement ps2 conn.prepareStatement(insertBorrow)) { ps1.setInt(1, bookId); int changed ps1.executeUpdate(); if (changed 0) { throw new SQLException(库存不足或图书不存在); } ps2.setInt(1, readerId); ps2.setInt(2, bookId); ps2.executeUpdate(); conn.commit(); } catch (SQLException e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); } } }这段代码有三个值得抄走的点。第一个updateStock 里特意加了 stock 0 条件executeUpdate 返回 0 就说明库存不够直接抛异常走回滚。这比先 SELECT 查库存再决定是否 UPDATE 安全因为两条 SQL 之间可能有并发修改SELECT 的结果到 UPDATE 执行时已经过期。第二个due_date 用 DATE_ADD(CURDATE(), INTERVAL 30 DAY) 在数据库侧计算而不是在 Java 里算好再传进去避免 Java 日期计算的时区边界问题。第三个conn.setAutoCommit(true) 放在 finally 里保证异常路径下连接回到自动提交状态否则这条连接被后续代码复用时可能继承一个未提交的事务状态。还书流程是同构的一条 UPDATE borrow SET return_date CURDATE() WHERE reader_id ? AND book_id ? AND return_date IS NULL一条 UPDATE book SET stock stock 1。同样放进事务同样检查更新行数是否为 0防止对同一本书重复还。思路和借书完全一致不再贴重复代码。3.3 查询优化EXPLAIN 告诉你索引有没有生效借阅列表常用查询是按读者查历史、按状态查在借。数据量小的时候感觉不到等图书几千本、借阅记录过万条不带索引的全表扫描就会拖慢页面。至少给 borrow 表的 reader_id 和 return_date 建索引前面 DDL 里已经建了这里补一句按需创建的 SQL 并验证它CREATE INDEX idx_borrow_reader ON borrow(reader_id); EXPLAIN SELECT r.name, b.title, br.borrow_date, br.due_date FROM borrow br JOIN reader r ON br.reader_id r.reader_id JOIN book b ON br.book_id b.book_id WHERE br.reader_id 123 AND br.return_date IS NULL;EXPLAIN 是答辩里能拿出来的干货。看到 key 列是 idx_borrow_reader说明查询走了索引type 列如果是 ref表示索引等值查找属于健康的访问类型如果 type 是 ALL说明这个查询没吃到索引。JOIN 在课程设计里最常见的错误是过滤条件所在的列没索引导致驱动表选错。我的习惯是先确认 WHERE 里等值判断的列都有索引再确认 JOIN 关联列是主键或外键两件事都满足性能一般不会出问题。注意性别、状态这种取值只有两三类的字段不要单独建索引。区分度太低MySQL 可能放弃索引直接全表扫反而白白维护一棵 B 树。能在答辩时说出这一条比背十个索引概念都加分。4. 课程设计避坑指南答辩前最容易翻车的 5 个点这一章集中写我见过的同类项目踩坑记录。每一条按现象 → 原因 → 解决来你可以直接对照自己的项目排查。4.1 日期类型与时区为什么查出来的日期不在预期范围现象程序里 setDate 存入的日期查出来和预期不一样或者连接时报 The server time zone value is unrecognized。原因如果把 borrow_date 设计成 DATETIME 或 TIMESTAMPJDBC 驱动 8.x 会强制要求连接串里有 serverTimezoneJava 运行时区与 MySQL 的 time_zone 不一致时日期时间在转换层被按错误时区解释。本设计的借阅表用 DATE 类型只精到天一般不触发时区漂移但很多同学喜欢用 DATETIME 记录借出时刻这一步就埋雷了。解决连接串显式写 serverTimezoneAsia/Shanghai日期传递统一走 java.sql.DateJava 8 里更好的是 LocalDate配合 rs.getObject(borrow_date, LocalDate.class) 绕过旧 API 的转换问题。如果业务只需要天就用 DATE别用 DATETIME。4.2 外键约束卡住删除先删子表还是改下架状态现象删除一本没有被借过的图书DELETE 照样报 Cannot delete or update a parent row。原因如果这本书曾经出现在借阅表里哪怕已经归还borrow 表里仍保留着历史记录外键约束不允许直接删主表。解决两个选择。业务上图书可以下架但不能物理删除更常见那就把 DELETE 改成 UPDATE 下架状态彻底避开外键如果确实要物理删除就得先处理子表DELETE FROM borrow WHERE book_id ?再删除图书。课程设计里我推荐前者因为它体现业务思考ON DELETE CASCADE 也能用但答辩时老师追问级联删了借阅历史怎么办时不如下架回答得从容。4.3 中文乱码字符集三层对齐现象Java 里查出来的中文全是问号或者插入后数据库里是乱码。原因三层编码不一致——表是 utf8mb4连接串没带 characterEncodingutf8控制台本身又是 Windows 默认的 GBK。解决先 SHOW CREATE TABLE 确认表字符集再给 JDBC 连接串加上 characterEncodingutf8最后给控制台设置 -Dfile.encodingUTF-8。三层对齐后中文基本稳定。我遇到过最玄学的场景本地全对换台机器打包运行就乱码查下来是 JDK 版本不同导致默认平台编码不一样代码里每次字符串转换都显式指定 UTF-8 才根治。4.4 并发扣库存变负数条件更新挡住超借现象两个终端同时借同一本书最后库存变成 -1。原因两条 UPDATE 并发执行都先读到 stock1各自减一最后一个回写覆盖了前一个。解决在 UPDATE 语句里加 stock 0 条件数据库的行锁会让后执行的更新返回 0从而拒绝超借。这个方案本质上是一种乐观的条件更新比在 Java 代码里加 synchronized 靠谱得多——锁只应该加在真正保护数据的那一层。答辩时能把这个讲清楚等于主动送自己一个加分题。4.5 连接不关闭演示现场越跑越慢现象功能单测都过了连续操作十几分钟后程序变慢最后报 Too many connections。原因每次查询都 new 一个 Connection用完不关连接数耗尽。解决用 try-with-resources 保证自动关闭如果用自写工具类确保 finally 里关闭每个 Connection。课程设计是控制台程序连接开着不关就是泄漏谁打开谁关闭要养成肌肉记忆。我见过不止一个组在现场演示到第三个功能时卡死最后发现是数据库连接数被自己写满了。5. 最后的加分动作补一份数据字典和备份恢复脚本答辩时老师翻代码之前通常先看设计文档。与其交一份凑字数的实验报告不如把数据字典和备份命令放进去一眼就能看出你懂工程实践。5.1 数据字典把字段含义和参照关系写清楚数据字典不是把 CREATE TABLE 抄一遍而是要交代字段为什么存在、被谁引用、约束是什么。下面这个表对应前文的建表 SQL字段不用列全重点是把约束讲准确字段类型约束参照说明reader.reader_idINTPK, AUTO_INCREMENT无读者唯一编号book.isbnVARCHAR(20)NOT NULL, UNIQUE无带连字符的 ISBNbook.stockINTNOT NULL, DEFAULT 1无当前可借库存非负borrow.reader_idINTNOT NULL, PK 一部分reader.reader_id借阅人borrow.return_dateDATE允许 NULL无NULL 表示在借写的时候注意两点borrow.return_date 允许为空的业务含义是在借这个语义必须写明白外键参照要写清楚说明你用它保证了借阅记录不会指向不存在的读者或图书。老师看到这种细节心里基本就有底了。5.2 备份与恢复三条命令留好后悔药课程设计数据量小不需要装备份工具mysqldump 足够mysqldump -uroot -p library library_backup_$(date %Y%m%d).sql mysql -uroot -p -e CREATE DATABASE IF NOT EXISTS library DEFAULT CHARSET utf8mb4; mysql -uroot -p library library_backup_20250101.sql第一行是备份文件名带日期防止覆盖前一天版本后两行是恢复先建库再导数据因为 mysqldump 默认不创建数据库。答辩前跑一次备份脚本演示时把数据搞乱了一分钟就能还原。我的习惯是每次改表结构之前先备份一次这习惯就是从课程设计养成的。数据库系统原理教的是理论课程设计是唯一一次让你在会崩、能救的完整环境里练手的机会多留一份后悔药比答辩现场手忙脚乱地重来体面得多。希望帮到你。本文还有配套的精品资源点击获取