☰
数据库课程设计图书管理系统:从ER建模到JDBC事务的完整实践路线
2026/9/25 8:46:17 网站建设 项目流程

简介:这是一份数据库课程设计报告,面向数据库初学者与高校软件工程、信息管理专业学生,围绕图书管理系统展开,解决传统人工管理图书馆存在的信息量庞大、人力物力浪费、管理费用增加等问题。资源包仅含1个doc文件,整体大小122KB,内容为完整课程设计报告。报告结构完整,涵盖系统背景、数据需求、事务需求、关系模式、E-R图设计、数据字典等关键环节,并具体展示了读者、书籍类别、借阅、还书、罚款等核心模块的表结构与关联关系,可帮助读者理解从需求分析到数据库实现的全过程。读者可参考其需求描述、实体关系建模与文档排版,也可直接借鉴用于完成同类课程设计或答辩准备。目前已有1897人学习下载,适合作为数据库课程设计参考。

1. 数据库课程设计图书管理系统:不是写业务,是考建模与落地

图书管理系统是数据库课程设计里出现频率最高的题目,也是每年答辩翻车率最高的一道。很多人拿到题先打开 IDE 写界面,最后交上去一个“能跑”的系统,却在老师问“借阅表为什么违反第三范式”“你这两张表怎么关联”的时候答不上来。这门课设真正比的是:把借还书流程拆成实体与联系,用范式约束表结构,再用 SQL 和 JDBC 把数据链路完整跑通。这篇笔记按一条完整路线推进——从 ER 设计到建表,从增删改查到事务边界,再到索引与死锁排查,最后给出一份答辩前的自检清单。适合正在做课设的本科生,也适合想系统过一遍关系型数据库核心知识的人。

2. 从借阅流程到数据模型:ER 设计与三大范式怎么落到表上

2.1 先别急着写代码:把业务拆成实体、属性与联系

我见过不少同学拿到题目后直接开建表,建出来的表字段堆在一起,读者信息、图书信息和借阅记录全塞在一张表里。这样做的后果是数据冗余严重,改一个作者名要更新几十行,还容易把约束逻辑搞乱。正确的起点是先做需求分析,把“谁、在什么时间、对哪本书、做了什么操作”这条主链路画出来。

图书管理系统里最基本的实体是三个:读者、图书、借阅记录。读者有读者号、姓名、学号/工号、电话、最大可借数量;图书有图书内部 ID、ISBN、书名、作者、出版社、分类、定价和库存数量;借阅记录则描述一次完整的“借出-归还”过程,包含借书时间、应还时间、实际归还时间和状态。这里要注意一个关键区别:ISBN 是书目的国际编号,不代表某一本实体书。同一本《数据库系统概论》可能有 20 个副本,如果拿 ISBN 当主键,就没法表达“这本书借走了一本还剩几本”。所以图书表里要有两个字段:total_stock表示馆藏总量,available_stock表示当前可借数量。

实体之间的关系也要在 ER 图上画清楚。一个读者可以借多本不同的书,一本书也可以在不同时间被多个读者借阅,这是典型的多对多关系。关系型数据库里多对多不能直接建表表达,必须拆成两个一对多:读者到借阅记录是一对多,图书到借阅记录也是一对多。中间的那张borrow表就是整个系统的枢纽,也是后面做并发控制和死锁分析的核心。把这一步想明白,后面的建表就顺理成章了。

2.2 用三大范式检查表结构:哪些冗余能接受,哪些是隐患

数据库基础知识里最常被答辩老师问到的就是范式。第一范式要求字段不可再分,比如“读者姓名”不能拆成“姓”和“名”两个业务字段塞在一个列里;第二范式要求非主键列完全依赖主键,不能只依赖联合主键的一部分;第三范式则要求非主键列之间不能有传递依赖。图书管理系统的表结构设计,基本就是对照这三条规则逐个检查。

最容易出问题的就是借阅记录表。有的同学为了方便,在borrow表里直接放上book_title和reader_name字段,省得联表查询。但这样一旦书名修改,所有历史借阅记录里的旧书名就都成了脏数据;读者改名同理。这就是典型的传递依赖,违反第三范式。正确的做法是借阅表只保留book_id和reader_id两个外键,展示的时候用 JOIN 去关联查询。

但范式也不是越高越好。图书表里的total_stock和available_stock,严格来说属于冗余设计——可用库存可以由total_stock减去正在借出中的记录数推导出来。但在课设和真实业务里,每次查询都去实时计算一次 COUNT 代价太高,所以保留冗余字段,用事务保证它跟借阅记录一致。这种“有意识的冗余”在答辩时是可以讲清楚的,前提是你要能说出取舍理由。

2.3 建表 SQL 与字段类型:自增主键、外键约束、utf8mb4

完成 ER 设计后就可以落 SQL 了。图书、读者、借阅记录三张表的建表语句如下,我一般会统一使用 InnoDB 引擎和 utf8mb4 字符集。InnoDB 支持事务和外键,utf8mb4 能完整存储中文和生僻字符,避免在“书名”这种字段上出现乱码。

CREATE TABLE book ( book_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '书目ID', isbn VARCHAR(20) NOT NULL COMMENT 'ISBN号', title VARCHAR(128) NOT NULL COMMENT '书名', author VARCHAR(64) DEFAULT NULL COMMENT '作者', publisher VARCHAR(64) DEFAULT NULL COMMENT '出版社', category VARCHAR(32) DEFAULT NULL COMMENT '分类', price DECIMAL(8,2) DEFAULT NULL COMMENT '定价', total_stock INT NOT NULL DEFAULT 1 COMMENT '馆藏总量', available_stock INT NOT NULL DEFAULT 1 COMMENT '当前可借数量', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_category (category), KEY idx_title (title) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; CREATE TABLE reader ( reader_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '读者ID', reader_no VARCHAR(20) NOT NULL UNIQUE COMMENT '学号或工号', name VARCHAR(32) NOT NULL COMMENT '姓名', phone VARCHAR(20) DEFAULT NULL COMMENT '联系电话', max_borrow INT NOT NULL DEFAULT 5 COMMENT '最大可借数量', created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci; CREATE TABLE borrow ( borrow_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '借阅记录ID', reader_id INT NOT NULL COMMENT '读者ID', book_id INT NOT NULL COMMENT '图书ID', borrow_date DATE NOT NULL COMMENT '借出日期', due_date DATE NOT NULL COMMENT '应还日期', return_date DATE DEFAULT NULL COMMENT '实际归还日期', status TINYINT NOT NULL DEFAULT 1 COMMENT '1借出 2已还 3逾期', CONSTRAINT fk_borrow_reader FOREIGN KEY (reader_id) REFERENCES reader(reader_id), CONSTRAINT fk_borrow_book FOREIGN KEY (book_id) REFERENCES book(book_id), KEY idx_reader_status (reader_id, status), KEY idx_book_status (book_id, status) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;

这里解释几个关键选择。主键全部用自增 INT,不用 UUID,原因是课设规模下自增主键占用空间小、B+Tree 叶子节点能存更多行,查询性能更好;ISBN 只做普通索引,不做主键,因为它不能唯一标识一个物理副本。外键约束建议保留,虽然它会在删除时带来一些麻烦,但答辩时外键是完整性设计里很直接的加分点,第 4 章我会讲怎么绕过它删数据。字段类型方面,价格用DECIMAL(8,2)而不是 FLOAT,避免浮点误差;日期用 DATE,状态用 TINYINT,不用 varchar 存“借出/已还”,这样查询更快也更好写约束。

3. 用 JDBC + MySQL 跑通数据库增删改查:最小实现

3.1 工程骨架与连接管理:连接串单独放配置,别硬编码

建完表之后,下一步是把数据链路打通。课程设计里最常见的实现组合是 Java + JDBC + MySQL,不需要引入 MyBatis 或 Spring Data JPA,因为课设的重点是让你亲手写出 SQL 和事务控制,框架会把关键细节藏起来。工程结构上我建议很朴素:一个DBUtil类管连接,一个dao包管 SQL,一个service包管业务,再加一个Main或简单控制台菜单演示。

首先把数据库连接参数放到配置文件里,而不是写在 Java 代码中。常见做法是在src/main/resources下放一个db.properties,内容如下:

jdbc.url=jdbc:mysql://localhost:3306/library?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false jdbc.username=root jdbc.password=123456

连接串里的characterEncoding=utf8是中文不乱码的前提,serverTimezone=Asia/Shanghai是 MySQL 8.x 驱动的要求。然后写一个最简单的连接工具类,把每次获取连接的逻辑收敛到一处:

import java.io.InputStream; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; import java.util.Properties; public class DBUtil { private static String url; private static String username; private static String password; static { try (InputStream in = DBUtil.class.getClassLoader() .getResourceAsStream("db.properties")) { Properties props = new Properties(); props.load(in); url = props.getProperty("jdbc.url"); username = props.getProperty("jdbc.username"); password = props.getProperty("jdbc.password"); Class.forName("com.mysql.cj.jdbc.Driver"); } catch (Exception e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(url, username, password); } }

这个工具类最核心的价值是把“加载驱动”和“读取配置”放到静态块里只执行一次,后续每次调用getConnection()都不会重复读文件。要注意com.mysql.cj.jdbc.Driver是 MySQL 8.x 的驱动类名,老版本是com.mysql.jdbc.Driver,两者不要混用,否则会报找不到驱动。

3.2 核心 SQL 操作:PreparedStatement 解决注入与类型映射

数据库增删改查是课设的骨干。网上能查到很多拼接字符串的版本,比如"SELECT * FROM book WHERE title = '" + keyword + "'",这种写法在答辩现场很容易被老师挑刺,因为单引号和拼接注入的问题在这门课里属于必考概念。正确做法是统一使用PreparedStatement,参数用占位符?传递。下面是一个按书名或作者模糊查询图书的 DAO 方法:

import java.sql.*; import java.util.ArrayList; import java.util.List; public class BookDao { public List<Book> searchBooks(String keyword) { String sql = "SELECT book_id, title, author, publisher, price, available_stock " + "FROM book WHERE title LIKE ? OR author LIKE ?"; List<Book> list = new ArrayList<>(); try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, "%" + keyword + "%"); ps.setString(2, "%" + keyword + "%"); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { Book b = new Book(); b.setBookId(rs.getInt("book_id")); b.setTitle(rs.getString("title")); b.setAuthor(rs.getString("author")); b.setPublisher(rs.getString("publisher")); b.setPrice(rs.getBigDecimal("price")); b.setAvailableStock(rs.getInt("available_stock")); list.add(b); } } } catch (SQLException e) { throw new RuntimeException("查询数据库失败", e); } return list; } }

这里有两个容易被忽略的细节。第一,LIKE的模糊匹配通配符是写在参数里的,而不是拼在 SQL 字符串里,这样数据库驱动会把%当作普通参数处理,同时杜绝注入;第二,ResultSet和PreparedStatement都放进 try-with-resources 块里,能保证无论是否抛异常,连接都会被归还到池中。如果你用了 mysql 的数据库连接池,忘了关闭ResultSet虽然不致命,但长期跑会累计游标泄漏,课设 demo 跑到一半报连接超时就是因为这些小地方。

新增图书的 SQL 逻辑完全一样,只是把?绑定到具体字段。注意日期类型要调用setDate而不是setString,比如插入借阅记录时,borrow_date传java.sql.Date.valueOf(LocalDate.now()),否则 MySQL 可能隐式转换出错,报Data truncation异常。

3.3 借书与还书:事务边界必须覆盖“检查-更新-插入”三步

图书管理里最有业务含量的是借书操作。借书不是一条 INSERT 那么简单,它包含三步:判断这本书还有没有库存、把available_stock减一、插入一条借阅记录。任何一步失败,前面成功的行为都必须撤销,否则就会出现库存扣了但借阅记录不存在的情况。这个场景是数据库事务最经典的演示,也是答辩时老师最爱让你写的一个方法。

import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; public class BorrowService { public void borrowBook(int bookId, int readerId) { String updateStockSql = "UPDATE book SET available_stock = available_stock - 1 " + "WHERE book_id = ? AND available_stock > 0"; String insertBorrowSql = "INSERT INTO borrow " + "(reader_id, book_id, borrow_date, due_date, status) " + "VALUES (?, ?, CURDATE(), DATE_ADD(CURDATE(), INTERVAL 30 DAY), 1)"; Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); try (PreparedStatement ps1 = conn.prepareStatement(updateStockSql)) { ps1.setInt(1, bookId); int affected = ps1.executeUpdate(); if (affected == 0) { throw new SQLException("库存不足或图书不存在"); } } try (PreparedStatement ps2 = conn.prepareStatement(insertBorrowSql)) { ps2.setInt(1, readerId); ps2.setInt(2, bookId); ps2.executeUpdate(); } conn.commit(); } catch (SQLException e) { if (conn != null) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } } throw new RuntimeException("借书失败:" + e.getMessage(), e); } finally { if (conn != null) { try { conn.setAutoCommit(true); conn.close(); } catch (SQLException e) { e.printStackTrace(); } } } } }

这个方法的要点有两个。第一,UPDATE ... WHERE available_stock > 0是原子操作,MySQL 在执行这一行时会对命中行加行级锁,两个连接同时借同一本书时,只有一个能成功扣减,另一个受影响行数为 0,直接抛异常回滚。这不是玄学,是 InnoDB 行锁保证的。第二,setAutoCommit(false)之后必须手动 commit,任何异常都要 rollback,最后在 finally 里恢复自动提交并关闭连接。如果你忘了setAutoCommit(true),连接归还到池里后可能带着未提交事务状态,影响下一个请求。

还书操作就是反向逻辑:把available_stock + 1,同时把borrow表对应记录的return_date设为当天、status改为 2。注意还书同样要走事务,因为“更新库存”和“更新借阅状态”是两个写操作,不能一个成功一个失败。

4. 图书管理系统避坑指南:课设里最常见的 5 个翻车点

4.1 中文乱码:库、表、连接三层都要查

现象:用 Java 程序向数据库插入中文书名,写进去之后在 Navicat 里看到的是???,或者从数据库查出来显示乱码。

原因:字符集不一致。常见的是 MySQL 服务端默认字符集是latin1,建表时又没指定utf8mb4,加上 JDBC 连接串没带characterEncoding=utf8,三层里只要有一层不对,中文就保不住。

解决:依次检查三层。第一,建库时执行CREATE DATABASE library DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci,或对已有库执行ALTER DATABASE library CHARACTER SET utf8mb4;第二,建表语句里明确写DEFAULT CHARSET=utf8mb4;第三,连接串加上useUnicode=true&characterEncoding=utf8。我一般习惯在建表 SQL 里每次都写全,不依赖全局默认配置,这样换一台电脑部署也不会翻车。

4.2 Navicat 能查到数据,程序查出来是空的

现象:在 Navicat 里SELECT * FROM book明明有 5 条记录,Java 程序跑查询却返回空列表。

原因:大概率是程序连错了库,或者表名大小写对不上。MySQL 在 Windows 上表名不区分大小写,在 Linux 上区分,很多课设同学在 Windows 本地开发,交付的时候把项目挪到云服务器或老师的 Linux 机器上跑,SELECT * FROM BOOK就会查不到。另一个隐性原因是事务隔离级别是默认的REPEATABLE READ,同一个事务里对表的快照在第一次查询时确定,如果别的会话在事务开始后插入数据,本事务再查也看不到。

解决:先把代码里的连接 URL 打出来确认databaseName指向的是library而不是test。再看表名是否与建表语句完全一致,Linux 环境下统一用小写。如果确认不是这些原因,检查是否在 service 层手动开启了事务但没 commit,把事务改成“用完即提交”再试。排这个问题的思路是先SHOW PROCESSLIST看程序实际连的是哪个库,不要靠猜。

4.3 借书操作扣了库存但没生成借阅记录

现象:借书流程走完后,book.available_stock减了 1,但borrow表里查不到任何新记录。

原因:这是最典型的事务边界错误。很多实现是先用一个updateStock()方法执行 UPDATE,再用另一个insertBorrow()方法执行 INSERT,两个方法各自通过DBUtil.getConnection()拿新连接,天然就是两个独立事务。UPDATE 成功后自动提交生效了,INSERT 那边抛了异常回滚,前后就失衡了。

解决:把这两个写操作放进同一个事务里,共享同一个 Connection,按第 3 章borrowBook的写法处理。记住一个判断标准:在图书管理系统里,“扣库存”和“写借阅记录”必须同时成功或同时失败,否则数据一致性就破了。类似的还有“还书加库存”和“更新还书状态”。

4.4 删除图书被外键约束挡住

现象:执行DELETE FROM book WHERE book_id = 3,MySQL 报错Cannot delete or update a parent row: a foreign key constraint fails。

原因:borrow表里有记录通过外键fk_borrow_book引用了这本book_id = 3的书,而且借阅记录还没归还或尚未删除。数据库为了不让外键变成悬空引用,拒绝执行。

解决:分两种情况。如果是逻辑删除,把book表加一个is_deleted TINYINT字段,删除时置 1,查询时过滤,这样既不破坏借阅历史,又能让图书从可借列表消失。如果是物理删除,先处理关联数据:要么把这本书的借阅记录归还,要么先删借阅记录再删书。课设里我一般建议加逻辑删除字段,因为老师问起时你能给出“为什么要保留历史借阅记录”的回答,比硬删有说服力。

4.5 mysql 的数据库连接池配置不当导致服务假死

现象:课设 demo 连续操作一段时间后,页面一直转圈,控制台报Connection is not available, request timed out或Too many connections。

原因:常见的是用DriverManager.getConnection()拿连接后没有关闭,或者自己手写了一个简易连接池但池大小配得不对。连接数是数据库有上限的,默认max_connections是 151,泄漏到满后新请求直接排队超时。另一个极端是池子配得太大,比如把 HikariCP 的maximum-pool-size设成 200,数据库本身扛不住,也会崩。

解决:课设阶段最简单的办法是确保每个连接都在 try-with-resources 里关闭,而不是只关ResultSet不关Connection。如果你用了 Spring Boot 的默认 HikariCP,把maximum-pool-size设置在 10~20 之间就够了,minimum-idle保持默认。连接池这一层在答辩里属于加分细节,能讲清楚“为什么池大小不是越大越好”比单纯配置正确更让老师认可。

5. 数据库优化:从“能跑”到“答辩能讲”

5.1 用 EXPLAIN 判断索引是否生效:不要凭感觉加索引

图书管理系统的数据量在课设阶段很小,性能问题不明显,但老师常会问一句:“如果借阅记录有 100 万行,查询某读者当前借了哪些书,怎么让它快一点?”这个问题考的是索引设计。先把慢查询拿出来看执行计划,这是数据库优化习惯的开端。

EXPLAIN SELECT * FROM borrow WHERE reader_id = 1001 AND status = 1;

执行结果里重点看type和key两列。如果type是ALL,说明这条 SQL 做了全表扫描,也就是把整张borrow表一行行翻了个遍,数据量一大就会慢。解决办法是加一个复合索引:

ALTER TABLE borrow ADD INDEX idx_reader_status (reader_id, status);

再跑一次EXPLAIN,type应该变成ref,key显示idx_reader_status,代表索引生效了。这里注意索引列的顺序:查询条件是reader_id等值加status等值,所以复合索引把reader_id放前面。如果你只对status加单列索引,上面这条查询不会用它,因为不符合最左前缀原则。这个原则是索引面试题的高频考点,答辩时主动讲出来能加分。反过来,borrow表上的borrow_date字段适合单独建索引,因为逾期统计经常按日期范围扫描,范围查询用不上复合索引里的后续列。

5.2 并发借同一本书:事务隔离级别与数据库并发锁

课设做到“能跑”很容易,做到“能扛”就需要考虑并发。典型场景是两个同学同时借同一本书,而且这本书只剩最后一本库存。如果不做并发控制,两个请求都能读到available_stock = 1,然后各自执行available_stock - 1,最后库存变成 0 而不是 -1 的翻车现场少见,但更隐蔽的问题是可能产生重复的借阅记录。

第 3 章的写法已经处理了这个问题,核心是这一句:

UPDATE book SET available_stock = available_stock - 1 WHERE book_id = ? AND available_stock > 0;

这条 UPDATE 在 InnoDB 里是原子操作,会对命中的行加写锁。两个并发事务同时执行时,后执行的那个会等前一个提交或回滚后才继续,然后发现affected rows = 0,于是业务层抛出“库存不足”。这里隐含着两个概念:第一是行级锁,锁的是book_id对应的那一行,不是整张表;第二是事务隔离级别,MySQL 默认的REPEATABLE READ下,更新操作使用当前读,不会读到旧值。这种“条件更新 + 受影响行数判断”是数据库并发锁里比较优雅的写法,比先SELECT ... FOR UPDATE再判断库存要少一条语句,也更不容易出死锁。

如果老师追问“为什么要用AND available_stock > 0而不是先查再改”,你要答得出来:查和改之间有窗口期,只有把条件写进 UPDATE,让数据库在加锁的同一瞬间判断条件,才能真正堵住并发。这也是为什么数据库同步软件和中间件再怎么封装,底层还是离不开这些原子操作。

5.3 数据库死锁的排查与避免

现象:借书和还书同时操作,控制台报Deadlock found when trying to get lock; try restarting transaction。

原因:死锁的本质是两个事务各自持有一把锁,又在等对方释放另一把锁。在图书管理系统里最常见的触发姿势是这样的:事务 A 先更新book表再插入borrow表,事务 B 先插入borrow表再更新book表,两者以不同顺序加锁,就可能互相等待。课设数据量小,死锁概率低,但并发线程一多就能复现。

解决:先看死锁现场。执行SHOW ENGINE INNODB STATUS,在输出里找到LATEST DETECTED DEADLOCK段落,里面会列出两个事务各持有什么锁、在等什么锁。排掉死锁的第一原则是让所有事务按相同顺序加锁,比如统一先更新book再操作borrow,不穿插执行;第二原则是缩短事务持有锁的时间,不要在事务里做网络请求或循环查询。还有一个小技巧:把事务隔离级别从REPEATABLE READ改成READ COMMITTED可以减少间隙锁带来的死锁,但如果老师问起,你要能说清楚间隙锁是什么。课设里我建议先用“统一加锁顺序”解决问题,因为它不依赖修改全局隔离级别,风险更小。

6. 答辩前必做的 4 项验证与两个加分方向

6.1 数据完整性自检:把这几条 SQL 跑一遍再交

检查项验证 SQL期望结果
借阅记录无悬空引用SELECT COUNT(*) FROM borrow b LEFT JOIN book bk ON b.book_id=bk.book_id WHERE bk.book_id IS NULL0
库存不越界SELECT COUNT(*) FROM book WHERE available_stock < 0 OR available_stock > total_stock0
未归还记录有应还日期SELECT COUNT(*) FROM borrow WHERE status = 1 AND due_date IS NULL0
演示数据可讲逾期SELECT r.reader_no, r.name, bk.title, b.due_date 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.status=1 AND b.due_date < CURDATE()有数据

前两条保护的是数据不出逻辑错误,第三条查空值,第四条是为了给老师演示“逾期未还”这个查询场景。我每次交课设前都会先跑一遍这四条 SQL,全过了再开界面演示,避免演示现场被老师查到脏数据。

6.2 加分项:视图与存储过程,触发器慎用

一个不会给演示添乱的加分项是建两张视图。比如借阅明细视图,把三张表 JOIN 的常用查询固化下来:

CREATE VIEW v_borrow_detail AS SELECT b.borrow_id, r.reader_no, r.name, bk.title, b.borrow_date, b.due_date, b.return_date, b.status FROM borrow b JOIN reader r ON b.reader_id = r.reader_id JOIN book bk ON b.book_id = bk.book_id;

视图的好处是让业务代码变短,Java 里SELECT * FROM v_borrow_detail WHERE reader_id = ?一行就查完,不用每次拼三个 JOIN。存储过程适合做统计报表,比如统计每类图书的借出次数,30 行 SQL 的体量正好能体现“过程化逻辑封装”的价值。触发器我建议谨慎使用,虽然用来自动维护“归还状态”很省事,但它会隐藏执行流程,老师追问“这条数据为什么变了”的时候,新手往往说不清触发时机和递归触发的问题,反而扣分。

我第一次做图书管理系统时,把所有逻辑堆在 main 方法里,建表只用 Navicat 手动点,答辩时被老师连续问了“外键在哪”“事务边界在哪”,当场答不上来。从那以后我养成一个习惯:哪怕是最简单的课设,也把建表 SQL、事务方法和自检脚本当成代码的一部分写完,而不是交一个甩给鼠标操作的黑匣子。验证数据要提前准备,逻辑要能用一句话讲明白,这份功夫比多写几个页面更能让你站得住。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询