简介:数据库课程设计的完整报告——图书馆管理信息系统,基于Eclipse与SQL Server 2000开发,覆盖从数据库规划、需求分析到物理设计、应用程序设计及测试运行的完整流程。文档以图书馆日常管理为业务背景,针对人工记录效率低、易出错的问题,给出书籍、读者、借阅、还书、罚款挂失等核心模块的解决方案。包体为1个doc文件,容量约239KB,内容包含任务陈述与目标、数据需求与事务需求、ER图与数据字典、关系表与索引视图、安全机制与触发器、功能模块与界面事务设计,以及测试运行和总结参考文献等,可直接作为课程设计报告写作范本。报告中还明确了读者类型与最大借阅数、30天借阅期及续借规则、超期每日0.1元罚款、遗失按原价赔偿等业务细节,适合学习数据库原理与应用的学生用于理解图书馆场景下的表结构设计、事务处理和权限管理思路。目前已有364人学习浏览,可作为课程设计参照模板及期末复习资料。
1. 数据库课程设计里的“图书馆管理信息系统”,到底在考核什么
拿到《数据库课程设计-图书馆管理信息系统.doc》这个标题,别急着把它当成一份复制粘贴就能交差的 Word 作业。图书馆管理信息系统是数据库课程设计里最稳的选题,没有之一:实体关系足够清晰,借书、还书、逾期罚款这些业务天然要求事务和并发控制,演示起来又非常直观。但它真正考核的不是你写了多少条 SQL,而是你有没有把整门《数据库原理》串起来——从需求分析、ER 图到范式、表结构,再到增删改查和异常处理。这篇文章就沿着这条链路拆开讲,适合正在排课设的学生按步骤复现,也适合带课设的助教拿来做评审清单。
2. 先建模再建表:从业务流程到表结构的落地路径
2.1 角色与流程先定死:读者、管理员和借还书闭环
很多课设翻车的第一步,是打开 MySQL 就直接 CREATE TABLE。表结构应该从业务流程里长出来,而不是拍脑袋决定字段。图书馆系统至少有两类角色,权限完全不一样:
| 角色 | 核心操作 | 涉及表 |
|---|---|---|
| 读者 | 注册、查询图书、借书、还书、查看逾期与罚款 | reader、book、borrow |
| 管理员 | 图书入库/下架、办理借还、逾期罚款处理、统计报表 | book、borrow、fine |
业务流程也要先在 Word 里用一段话写死,这段内容就是课设报告里“需求分析”那一章的底稿。借书流程是:校验读者身份与可借额度,查图书可借库存,可借则插入一条借阅记录,同时把可借库存减一。还书流程是:根据借阅记录计算是否逾期,逾期则生成罚款记录,更新借阅状态,最后把库存加一。注意这里每一步都不能拆开,比如“插入借阅记录”和“库存减一”必须绑定成原子操作,否则就会出现书借出去了、库存却没变的脏数据。
2.2 ER 图转关系模式:四张表的字段设计与范式检查
流程定死之后,实体关系图就是顺手的事。读者和图书之间是借阅关系,借阅记录本身带还书日期和状态;罚款挂在借阅记录下面,一对一;管理员独立成表,不跟读者混在一起。从 ER 图转成关系模式,最少四张表:
| 表名 | 关键字段 | 说明 |
|---|---|---|
| reader | reader_id、reader_name、phone、reg_date | 读者主档 |
| book | book_id、isbn、title、author、press、stock_total、stock_available | 库存总书与可借数分离 |
| borrow | borrow_id、book_id、reader_id、borrow_date、due_date、return_date、status | 借阅流水 |
| fine | fine_id、borrow_id、amount、paid、fine_date | 罚款记录,与 borrow 一对一 |
字段类型这里有几个容易踩的细节。ISBN 用 VARCHAR(20) 而不是 CHAR(13) 或 CHAR(17),因为很多 Excel 导入的 ISBN 带连字符,定长 CHAR 会在末尾补空格,导致查询时要用 TRIM 才能匹配。出版年份用 YEAR 类型够用,但如果你要兼容更早或更晚的数据,SMALLINT 反而更保险。借阅表的 status 字段值得专门写一段设计说明:逾期状态其实可以由due_date < CURDATE() AND return_date IS NULL推导出来,为什么还要单独存一个 status?因为统计视图、索引和查询都要频繁按状态过滤,存一个 TINYINT 字段可以用到联合索引,避免每行都跑日期函数。
范式检查是报告里必须写的一节。最常见的错误是把读者姓名和图书书名冗余进 borrow 表,这样确实查询方便,但它属于部分函数依赖,违反第二范式。想象一下读者改了个名字,你所有历史借阅记录里的姓名都得跟着改,这就是 UPDATE 异常。正确做法是 borrow 表只存 book_id 和 reader_id 这两个外键,要显示姓名或书名时 JOIN 回去。
2.3 建表 SQL:索引、外键和字符集的第一次定型
数据库我一般选 MySQL 8.x,字符集统一用 utf8mb4,排序规则用 utf8mb4_unicode_ci。建库建表脚本如下,可以直接抄,但每段我都标注了为什么这么写:
CREATE DATABASE IF NOT EXISTS library_system DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_unicode_ci; USE library_system; CREATE TABLE reader ( reader_id INT PRIMARY KEY AUTO_INCREMENT, reader_name VARCHAR(20) NOT NULL, phone VARCHAR(20), reg_date DATE DEFAULT (CURRENT_DATE) ); CREATE TABLE book ( book_id INT PRIMARY KEY AUTO_INCREMENT, isbn VARCHAR(20) NOT NULL, title VARCHAR(100) NOT NULL, author VARCHAR(50), press VARCHAR(50), publish_year YEAR, stock_total INT NOT NULL DEFAULT 1, stock_available INT NOT NULL DEFAULT 1, status TINYINT NOT NULL DEFAULT 1 COMMENT '1在架 2下架 3遗失', UNIQUE KEY uk_isbn (isbn) ); CREATE TABLE borrow ( borrow_id INT PRIMARY KEY AUTO_INCREMENT, book_id INT NOT NULL, reader_id INT NOT NULL, borrow_date DATE NOT NULL DEFAULT (CURRENT_DATE), due_date DATE NOT NULL, return_date DATE, status TINYINT NOT NULL DEFAULT 1 COMMENT '1借出中 2已归还 3逾期未还', CONSTRAINT fk_borrow_book FOREIGN KEY (book_id) REFERENCES book(book_id), CONSTRAINT fk_borrow_reader FOREIGN KEY (reader_id) REFERENCES reader(reader_id), KEY idx_reader_status (reader_id, status), KEY idx_book_status (book_id, status) ); CREATE TABLE fine ( fine_id INT PRIMARY KEY AUTO_INCREMENT, borrow_id INT NOT NULL, amount DECIMAL(6,2) NOT NULL DEFAULT 0.00, paid TINYINT NOT NULL DEFAULT 0, fine_date DATE NOT NULL DEFAULT (CURRENT_DATE), CONSTRAINT fk_fine_borrow FOREIGN KEY (borrow_id) REFERENCES borrow(borrow_id) );外键约束我建议保留,不要因为嫌麻烦就删掉。它是数据库层面最后一道防线,能拦住那些程序里没写到的非法 book_id 和 reader_id。索引这里有两个取舍:book 表的 isbn 建了唯一键,因为 ISBN 天然应该唯一,而且按 ISBN 查书的频率远高于按自增主键查;borrow 表则建了(reader_id, status)和(book_id, status)两个联合索引,因为大部分查询是“某个读者名下的未还记录”或“某本书的借出记录”,联合索引能让这两个高频查询走索引覆盖,避免回表。等数据量到几千条之后,你 EXPLAIN 一下就能看到差别。
字符集要在建库那一刻就定死,不然后面中文乱码的排查会非常痛苦。utf8mb4 兼容 emoji 和生僻字,比 utf8 更保险;排序规则选 unicode_ci 而不是 general_ci,主要是为了多语言环境下排序更符合直觉。
3. 借书、还书、统计排行:让增删改查跑在事务上
3.1 借书与还书事务:为什么不能拆成两条独立 SQL
课程设计文档里,“系统实现”这一章最容易写成简单的单表增删改查。但图书馆系统的核心业务恰恰是多表联动,这里必须用存储过程或事务把多条 SQL 包起来。借书这个动作,逻辑上等价于“插入一条借阅记录 + 扣减可借库存”,任何一步失败,另一步都必须回滚。
我一般直接用存储过程,因为演示的时候 CALL 一句就能看到效果,答辩老师也容易理解。下面这个版本做了简化,保留了一个最关键的并发控制点:
DELIMITER // CREATE PROCEDURE borrow_book( IN p_reader_id INT, IN p_book_id INT, IN p_days INT ) BEGIN DECLARE v_avail INT; START TRANSACTION; -- 行锁:防止两个会话同时读到库存为 1 都往下走 SELECT stock_available INTO v_avail FROM book WHERE book_id = p_book_id FOR UPDATE; IF v_avail IS NULL OR v_avail < 1 THEN ROLLBACK; SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'no stock available'; END IF; INSERT INTO borrow (book_id, reader_id, due_date) VALUES (p_book_id, p_reader_id, DATE_ADD(CURDATE(), INTERVAL p_days DAY)); UPDATE book SET stock_available = stock_available - 1 WHERE book_id = p_book_id; COMMIT; END// DELIMITER ;这段代码里最关键的不是 INSERT 和 UPDATE,而是SELECT ... FOR UPDATE。它会给 book 表这一行加排他锁,保证两个管理员同时给同一位读者借同一本库存只剩 1 的书时,后一个事务会等前一个提交后才读到新库存,然后正确地报出“无库存”。如果没有这行锁,两个事务可能同时读到 stock_available=1,都执行插入和扣减,最后库存变成 -1,这就是典型的超卖。
p_days参数控制借期,一般传 30,对应默认借阅期限。SIGNAL SQLSTATE '45000'是 MySQL 里手工抛异常的标准写法,作用相当于程序里的 throw RuntimeException,用 ROLLBACK 保证库存检查失败时不留下任何半截数据。如果你用的是 MySQL 5.x,DATE_ADD和INTERVAL的语法完全一样,可以放心跑。
3.2 逾期罚款与状态流转:还书逻辑里的边界处理
还书比借书多一个分支:要判断是否逾期,逾期了要写罚款记录,而且罚款和还书必须同生共死——不能出现书已经还了,罚款记录却没生成的场景。否则读者下次来借书,管理员查不到他名下的欠款,系统就漏了一笔应收款。
DELIMITER // CREATE PROCEDURE return_book( IN p_borrow_id INT ) BEGIN DECLARE v_book_id INT; DECLARE v_due_date DATE; DECLARE v_over_days INT; START TRANSACTION; SELECT book_id, due_date INTO v_book_id, v_due_date FROM borrow WHERE borrow_id = p_borrow_id FOR UPDATE; IF v_book_id IS NULL THEN ROLLBACK; SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'borrow record not found'; END IF; IF DATEDIFF(CURDATE(), v_due_date) > 0 THEN SET v_over_days = DATEDIFF(CURDATE(), v_due_date); INSERT INTO fine (borrow_id, amount, fine_date) VALUES (p_borrow_id, v_over_days * 0.50, CURDATE()); END IF; UPDATE borrow SET return_date = CURDATE(), status = 2 WHERE borrow_id = p_borrow_id; UPDATE book SET stock_available = stock_available + 1 WHERE book_id = v_book_id; COMMIT; END// DELIMITER ;这里用SELECT ... FOR UPDATE锁住的是 borrow 记录,防止同一笔借阅被两个会话重复归还。重复归还是个很隐蔽的坑:如果两个管理员同时打开同一笔记录操作,没有行锁的话,第二次执行会把 book 表的库存多加一次,book 的 stock_available 就会超过 stock_total,数据一查就露馅。
逾期计算用的是DATEDIFF(CURDATE(), v_due_date),这是 MySQL 的写法,计算的是“当前日期减去截止日期”的天数差。罚款金额按每天 0.50 元算,DECIMAL(6,2)最多表示 9999.99 元,对课设场景绝对够用。status = 2表示已归还,逾期这个状态没有单独写进表里,因为 overdue 可以由return_date IS NULL AND due_date < CURDATE()推导,只在需要统计的时候查一下就行。
3.3 统计查询与视图:答辩时最好讲的三类查询
课程设计报告里必须有一段“典型查询”,这些查询要覆盖聚合、连表、分组排序,而不是只在表上做单表 SELECT。下面三类查询是图书馆系统最常被答辩老师问到的,每一个背后都有知识点。
-- 1. 读者借阅排行:谁借书最多 SELECT r.reader_name, COUNT(*) AS borrow_count FROM borrow b JOIN reader r ON b.reader_id = r.reader_id GROUP BY r.reader_id, r.reader_name ORDER BY borrow_count DESC LIMIT 10; -- 2. 图书热度排行:哪些书流通最频繁 SELECT bk.title, COUNT(*) AS borrow_count FROM borrow b JOIN book bk ON b.book_id = bk.book_id GROUP BY bk.book_id, bk.title ORDER BY borrow_count DESC LIMIT 10; -- 3. 当前逾期未还清单:视图封装复杂条件 CREATE VIEW v_overdue AS SELECT b.borrow_id, r.reader_name, bk.title, b.due_date, DATEDIFF(CURDATE(), b.due_date) AS overdue_days FROM borrow b JOIN reader r ON b.reader_id = r.reader_id JOIN book bk ON b.book_id = bk.book_id WHERE b.return_date IS NULL AND b.due_date < CURDATE();前两个查询是 GROUP BY 加 ORDER BY 加 LIMIT 的经典组合。这里有一个特别容易翻车的新版 MySQL 坑:默认sql_mode里有only_full_group_by,SELECT 出来的非聚合列必须出现在 GROUP BY 里。所以我在分组时把reader_id, reader_name一起写了,而不是只按reader_id分组却去 SELECTreader_name。如果你在别的机器上跑这段 SQL 报错,先看SELECT @@sql_mode;是不是带了only_full_group_by。
第三个视图把“逾期未还”这个逻辑封装成了一个表,答辩演示的时候一句SELECT * FROM v_overdue;就能出效果。视图的好处是调用方不用关心底下三张表的 JOIN 和日期判断,也方便在程序层直接把视图映射成只读实体。视图在文档里要单独占一节,讲清楚它屏蔽了哪些复杂度。
4. 程序层接入 MySQL:连接池、DAO 与批量导入
4.1 告别 DriverManager:连接池参数怎么设
很多课设代码里写的是DriverManager.getConnection(url, user, password),每次查询都新建一个物理连接。这在演示时看不出问题,因为数据量才几百条,连接开销被查询时间盖住了。但只要答辩老师问一句“并发几十个人同时借书怎么办”,这个实现就露馅了——每次连接都要走 TCP 握手和 MySQL 认证,数据库端的最大连接数很快被打满。
常见的做法是用连接池,Java 这边我用 HikariCP,配置极简,性能也够。它的初始化代码长这样:
HikariConfig config = new HikariConfig(); config.setJdbcUrl("jdbc:mysql://localhost:3306/library_system?useSSL=false&characterEncoding=utf8"); config.setUsername("root"); config.setPassword("your_password"); config.setMaximumPoolSize(10); config.setConnectionTimeout(3000); config.setMaxLifetime(1800000); HikariDataSource dataSource = new HikariDataSource(config);连接池参数不是越大越好,这是课设报告里值得写一段“参数设计依据”的地方:
| 参数 | 建议值 | 理由 |
|---|---|---|
| maximumPoolSize | 10 | 课设场景并发个位数,10 个连接足够,开多了只是占着数据库资源 |
| minimumIdle | 5 | 保底连接数,避免突发请求时临时建连接 |
| connectionTimeout | 3000ms | 3 秒拿不到连接直接报错,不让调用方无限等待 |
| maxLifetime | 1800000ms | 30 分钟强制换连接,小于 MySQL 默认 8 小时 wait_timeout,防止连接被服务端断开 |
设置maxLifetime的 30 分钟上限是我踩过坑才加上的。MySQL 默认wait_timeout是 8 小时,空闲超过这个时间的连接会被服务端主动断开。连接池如果拿着一个已经断掉的连接继续用,第一次查询就会报“Connection is not available”或通信链路异常。把maxLifetime设得比服务端超时短,是连接池的标准自救姿势。
4.2 PreparedStatement 与 DAO 封装:SQL 注入这道送分题
程序层访问数据库,最忌讳的就是字符串拼接 SQL。比如查询读者:
// 错误示范:参数直接拼进 SQL String sql = "SELECT * FROM reader WHERE reader_name = '" + name + "'";这段代码只要传入' OR '1'='1,SQL 就变成了WHERE reader_name = '' OR '1'='1',把整张表查了出来。课设答辩时老师问“什么是 SQL 注入”,拿你这段代码当反例,分就没了。正确写法是用 PreparedStatement,让参数只作为占位符的数据,不参与 SQL 语法解析:
String sql = "SELECT * FROM reader WHERE reader_name = ?"; try (PreparedStatement ps = dataSource.getConnection() .prepareStatement(sql)) { ps.setString(1, name); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { // 处理结果集 } } }同一个 SQL 字符串在 MySQL 端只需要解析一次,后面传入不同的参数直接复用执行计划,这也是 PreparedStatement 比 Statement 快的第二层原因。你的 DAO 层应该长成“一个方法对应一条 SQL”的样子,比如findReaderByName(String name)、insertBook(Book book)、updateStock(int bookId, int delta)。业务层只调 DAO 方法,不直接拼 SQL,这是分层最基本的约束。
4.3 Excel 批量导入测试数据:LOAD DATA 与 JDBC Batch
课程设计演示需要几百条像样的图书数据,手工 INSERT 不现实,最省事的路径是从 Excel 整理数据、另存 CSV,再用 LOAD DATA 一次性灌进去。Excel 里第一行写列名,第二行开始是数据,另存时编码选 UTF-8。导入命令:
LOAD DATA INFILE '/tmp/books.csv' INTO TABLE book CHARACTER SET utf8mb4 FIELDS TERMINATED BY ',' ENCLOSED BY '"' IGNORE 1 LINES (isbn, title, author, press, publish_year, stock_total, stock_available);FIELDS TERMINATED BY ','告诉 MySQL 逗号是列分隔符;ENCLOSED BY '"'处理 Excel 导出时给含逗号的字段自动加的双引号;IGNORE 1 LINES跳过表头。注意LOAD DATA INFILE读取的是 MySQL 服务端本地的文件,如果报ERROR 1290,说明服务端开了secure_file_priv,把文件放到它指定的目录,比如/var/lib/mysql-files/下面再跑。
如果不是外部文件,而是 Java 程序里解析到的对象列表,就换成 JDBC 批量插入。只要在连接串上加一个参数rewriteBatchedStatements=true,然后addBatch、executeBatch循环提交,MySQL 会把多条 INSERT 合并成一条多值 INSERT,几千条数据一次往返就完成。注意 batch 也不是越大越好,一般每 500 条执行一次 executeBatch,避免单条 SQL 过长超过 max_allowed_packet 的限制。
5. 图书馆课设的避坑清单:从乱码到死锁的四条血泪记录
5.1 中文乱码:插入“三国演义”变成“????”
现象:用图形客户端或 Java 程序插入中文书名,查出来是问号或者乱码,英文数据完全正常。
原因:这条坑的根源是字符集链路某一段不是 utf8mb4。数据库建库时用了默认字符集,而 MySQL 8.x 之前默认是 latin1;或者建库定了 utf8mb4,但 JDBC 连接串没加characterEncoding=utf8;又或者 CSV 文件本身是 GBK 编码,LOAD DATA 时按 utf8mb4 解析。
解决:按三段链路逐一排查。建库时显式写DEFAULT CHARSET utf8mb4;JDBC 连接串加characterEncoding=utf8;CSV 文件用 Excel 另存时明确选择“CSV UTF-8”格式,而不是默认的“CSV”或“CSV UTF-8 (逗号分隔)”。最后用SHOW VARIABLES LIKE 'character_set_server';确认服务端默认值,再用SELECT * FROM book WHERE title = '三国演义';反向验证数据是不是真的写坏了。如果已经写坏,只能删掉重导,没有后悔药。
5.2 外键删不掉:DELETE FROM book 报错 1451
现象:想清理测试数据,执行DELETE FROM book WHERE book_id = 1;,MySQL 直接报Cannot delete or update a parent row: a foreign key constraint fails。
原因:borrow 表里有 book_id=1 的借阅记录,外键约束默认是ON DELETE RESTRICT,有子表引用时不允许删父表。这不是数据库出 bug,是约束在起作用,但很多课设同学到这一步就慌着去删外键,等于把最后一道防线拆了。
解决:分清“物理删除”和“逻辑删除”。借阅记录是历史存档,本来就不该物理删除;图书下架应该用UPDATE book SET status = 2 WHERE book_id = 1;做逻辑删除,查询时默认过滤status != 2。如果确实要物理清理测试数据,按依赖顺序先删子表再删父表:
DELETE FROM fine WHERE borrow_id IN (SELECT borrow_id FROM borrow WHERE book_id = 1); DELETE FROM borrow WHERE book_id = 1; DELETE FROM book WHERE book_id = 1;5.3 并发借同一本书:数据库死锁的经典现场
现象:两个管理员工位同时给读者办理借书,借的是同一本库存只剩 1 的书。本来应该有一个报“无库存”,实际上却可能有一个窗口报Deadlock found when trying to get lock; try restarting transaction。
原因:死锁的本质是两个事务各自持有一把锁,又都在等对方手里的那把锁。借书事务里既有SELECT ... FOR UPDATE锁 book 行,又有 INSERT 写 borrow 历史表,如果两个事务以不同顺序去碰这两类记录,就可能形成循环等待。数据库的并发锁机制本身没问题,是事务的锁顺序没设计好。
解决:固定所有事务访问资源的顺序。借书流程统一“先锁 book 行、再写 borrow 记录”,不要让其中一个事务反着写。同时把事务尽量缩短,锁的时间越短,冲突概率越低。“尽量缩短”的落地姿势是:在事务里只做数据库操作,不写网络请求、不读文件,这些动作放到事务外。对库存扣减还有一种更激进的原子写法:UPDATE book SET stock_available = stock_available - 1 WHERE book_id = ? AND stock_available > 0;,单条 UPDATE 自带行锁,检查affected rows是否为 1 就能代替先 SELECT 再判断的流程,事务会短很多。
5.4 DATEDIFF 与 GROUP BY 翻车:同一个库不同版本行为不一致
现象:还书存储过程在自己的笔记本上跑得好好的,拷到答辩机器上执行,算出来的罚款金额变成负数;另一个现象是统计排行时报Expression #1 of SELECT list is not in GROUP BY clause and contains nonaggregated column。
原因:两个问题背后都是“口径”问题。DATEDIFF(date1, date2)在不同数据库方言里的参数顺序不同——MySQL 是 date1 减 date2,SQL Server 是DATEDIFF(datepart, start, end)。如果你在文档里同时提了 MySQL 和 SQL Server,代码却只在 MySQL 上跑,很容易写混。GROUP BY 的报错则是 MySQL 5.7.5 之后默认开启了only_full_group_by模式,SELECT 的非聚合列必须出现在 GROUP BY 里,老版本的宽松检查把这个问题掩盖掉了。
解决:在课设文档开头就声明“本系统基于 MySQL 8.x 开发测试”,所有 SQL 以这个版本为准。DATEDIFF 统一写成DATEDIFF(CURDATE(), due_date)这种能看出“当前日期减截止日期”的形式,不要写倒数。GROUP BY 查询把所有需要的列都放进分组,或者用ANY_VALUE(reader_name)包裹非聚合列。这两个问题的共同教训是:换环境跑课设前,先跑一遍你的存储过程和统计 SQL,不要在演示现场才发现行为不一致。
6. 答辩演示前的自测:验证数据一致性的一套组合拳
演示翻车最惨的不是系统崩溃,而是老师随口问一个问题你就接不上。在去答辩之前,我建议你把这套验证脚本完整跑一遍,每个点都对应一个可能被追问的知识点。
-- 1. 逾期视图是否工作 SELECT * FROM v_overdue; -- 2. 借书后库存是否扣减 CALL borrow_book(1, 1, 30); SELECT book_id, stock_total, stock_available FROM book WHERE book_id = 1; -- 3. 重复借同一本书:第二次应该报 no stock available -- 开两个数据库会话同时执行 CALL borrow_book(1, 1, 30); -- 4. 权限验证:只读账号不能写 CREATE USER IF NOT EXISTS 'readonly'@'localhost' IDENTIFIED BY 'readonly123'; GRANT SELECT ON library_system.* TO 'readonly'@'localhost';用只读账号执行一条 INSERT,比如INSERT INTO reader(reader_name) VALUES('hacker');,数据库应在权限层直接拒绝。这一步验证的是你文档里写的“不同角色权限分离”不是空话,老师很吃这一套。借书演示则分两步:先正常借一本看库存减一,再打开两个会话同时借同一本库存为 1 的书,观察一个成功、一个报错或死锁重试。这个过程把事务、行锁、并发控制三个概念一次演完。
文档里还要有一节“异常处理”,别只写成功路径。把第 5 章里的乱码、外键删除失败、死锁各写一段“现象 + 原因 + 解决方案”,答辩时主动讲自己踩过哪些坑,比被问到时支支吾吾强得多。老师问的问题,说白了就是数据库面试题的范围:事务隔离级别、索引为什么快、死锁怎么避免、范式怎么判断。你只要在产品里真能指出对应代码,这门课的分数就稳了。
我当年第一次答辩,数据库设计抄了学长的文档,表结构、存储过程都能背,老师一句“两个人同时借最后一本书,你的系统怎么保证不错”就把我问住了。后来自己把事务和行锁跑通,才觉得这门课真正过了关。希望帮到你。
本文还有配套的精品资源,点击获取