简介:数据库课程设计的关键不只是写代码,而是交付一份能跑、能查、能答辩的完整系统。从关系型数据库的选型逻辑出发,MySQL凭借免费、轻量、资料丰富成为默认选择,但版本差异、驱动兼容和连接参数往往决定项目能否顺利启动。理解JDBC连接、PreparedStatement参数绑定以及连接池的配置,是工程实践的基础能力。事务的边界控制与索引设计,则直接体现对数据一致性和查询性能的把握。本文以图书管理系统为例,梳理从E-R图设计、三大范式落地到并发避坑的完整路径,帮助学生在答辩现场从容应对。
1. 数据库课程设计.doc:一份要能跑、能写、能答辩的完整交付
拿到一份《数据库课程设计.doc》时,大多数人的第一反应是打开模板,往里面填需求分析和运行截图。但数据库课程设计这门课真正卡人的地方,是老师会在验收现场点开你的系统、对着数据库查数据、随机挑一条 SQL 让你讲执行过程。这门课要求你把关系模型、范式、事务、索引这些抽象概念编译成一个能点开的界面,再把全过程的证据沉淀进 doc 里。它适合正在赶课程设计的学生、带课助教,以及在企业里想快速搭一套可演示增删改查系统的工程师。先想清楚交付物边界:能跑的代码、能查的数据、能讲清的设计,缺一个都算没做完。
2. 选型先行:数据库与开发栈怎么定,才不会做到一半返工
课程设计里最贵的时间开销不是写代码,是中途换数据库。选型这一步花半小时想清楚,后面能省出两天重写连接代码和建表脚本的时间。选型的核心原则只有一条:以验收环境为准,而不是以技术时髦度为准。下面拆成数据库、开发栈、连接参数三层讲。
2.1 关系型数据库选型:MySQL 是课程设计的默认答案
常见做法是默认选 MySQL 5.7 或 8.0,因为绝大多数高校机房和教师验收机器上都装了 MySQL,Navicat、命令行工具随手可用,遇到报错也总能搜到同款复现。课程设计的数据规模一般在 5 到 15 张表、几万条记录以内,MySQL 免费、安装包小、远程连接方便,性能绰绰有余。Oracle 不是不好,而是安装重量级、内存占用大、验收机上未必有实例,你用 Windows 连远程 Oracle 服务器时,光配一个 SQL*Plus 客户端就得折腾半天,这不是课程设计该花时间的地方。
达梦、人大金仓这类国产数据库,只有在题目明确指定时才碰。它们的思路和 MySQL 大体一致,但驱动不通用、连接工具要单独配,比如用 Navicat 连接达梦数据库时,需要先下载达梦专属 JDBC 驱动,这个步骤在答辩现场一旦卡住,整个演示就没了。SQLite 单文件零配置,适合个人工具,但课程设计要求的多用户并发和事务演示,SQLite 的锁粒度扛不住。选型的优先级应该是:题目指定库 > 机房已装库 > MySQL 8.0。
数据库选型的另一个坑是版本区分。MySQL 8.0 把默认认证插件改成了 caching_sha2_password,老一代驱动 5.1.x 连 8.0 会直接报认证失败;反过来,8.0 的驱动连 5.7 一般没问题。如果机房装 5.7,驱动用 5.1.49;如果装 8.0,驱动用 8.0.33。这个对应关系在写进设计文档之前,先确认一下你本机装的是哪个版本。
| 数据库 | 课程设计的优势 | 容易踩的坑 |
|---|---|---|
| MySQL 5.7/8.0 | 免费、资料多、默认端口 3306 | 8.0 认证插件与旧驱动不兼容 |
| Oracle | 企业级功能全,面试常问 | 安装重、验收环境难复现 |
| 达梦/金仓 | 国产化,题目可能指定 | 工具链不通用,驱动需单独下载 |
| SQLite | 单文件、零配置 | 并发写入弱,事务演示效果差 |
2.2 开发栈怎么选:Swing、JSP 还是 Spring Boot
界面层的选择直接决定了答辩现场能不能在一分钟内跑起来。常见路线有三条:Java Swing 桌面端、JSP/Servlet 网页端、Spring Boot 加前端框架的前后端分离。我一般按年级和题目难度来定:大二大三的数据库课程设计,Swing 或 JSP 足够,老师能在教室机器上快速部署;毕业设计级别的综合系统,才值得上 Spring Boot。
Swing 的优势是零部署,打了 jar 包双击就能运行,不依赖 Tomcat,也不碰 Maven 拉依赖的问题。缺点是界面确实旧,但课程设计评分看的是功能完整度、数据库设计是否合理、并发和事务有没有处理,界面只是加分项。JSP/Servlet 是经典课设形态,在浏览器里演示更像一个"系统",但需要配置 Tomcat 和打包 war,现场环境一旦缺 JDK 版本不对,启动时报错很随机。Spring Boot 加 Vue 这类前后端分离,技术含量最高,但需要联网拉依赖、前端要 Node 环境,答辩现场断网或者镜像源抽风,项目连编译都过不了。
还有一个决策项:数据库访问层必须独立于界面层。无论选哪条路线,都先把 DAO 层写好,界面层的按钮只负责调用 DAO 方法。这样万一选题阶段拿不准界面技术,数据库表结构和 DAO 代码完全不用返工,后端代码可以无缝切到另一个界面框架。这算课程设计里的一个"后悔药"设计:把变的部分和不变的部分切开。
2.3 字符集、驱动与连接串:第一轮就要定死的三个参数
选型阶段就应该把三个字符串参数钉在设计文档里,后面所有连接代码都从文档抄,而不是每次重新凭记忆写。第一个是字符集,库、表、连接串三处统一用 utf8mb4;第二个是 JDBC 驱动 jar 和驱动类名,对应 2.1 的 MySQL 版本;第三个是连接串里的附属参数。
| 参数 | 推荐值 | 作用 |
|---|---|---|
| characterEncoding | utf8mb4 | 解决中文存储乱码 |
| serverTimezone | Asia/Shanghai | MySQL 8.0 连接时必加,否则时区报错 |
| useSSL | false | 本地演示关闭 SSL 握手,减少警告与延迟 |
| useUnicode | true | 与 characterEncoding 配合生效 |
String url = "jdbc:mysql://localhost:3306/book_db" + "?useUnicode=true" + "&characterEncoding=utf8mb4" + "&serverTimezone=Asia/Shanghai" + "&useSSL=false"; String user = "root"; String password = "123456"; Connection conn = DriverManager.getConnection(url, user, password);这段代码里的四个参数逐个说:useUnicode 和 characterEncoding 是成对出现的,只写一个有时不生效,这是中文乱码的第一道防线;serverTimezone 在 MySQL 8.0 下必须显式指定,否则驱动拿系统时区和服务器时区对比时会抛 SQLException;useSSL=false 在本地演示只是为了省去证书握手那一层无意义的开销。这些参数不属于业务代码,写一次就不动它。
需要一并定死的是驱动类名。MySQL 5.7 配 mysql-connector-java 5.1.49,驱动类名用 com.mysql.jdbc.Driver;MySQL 8.0 配 8.0.33,驱动类名用 com.mysql.cj.jdbc.Driver。类名写错是课程设计里最高频的启动报错之一,后面避坑章节会专门讲现场。
3. 把需求拆成库表:E-R 设计与三大范式在课程设计里的落地
课程设计的题目描述往往只有一句话,比如"设计一个图书管理系统,实现读者管理、图书管理和借还书功能"。这一句话里藏着实体、联系、约束和业务规则。数据库设计部分的得分点,全看你有没有能力把这句话翻译成规范的表结构。
3.1 从题目到实体:先画 E-R 图,再写建表语句
以图书管理系统为例。先列自然语言里的名词:读者、图书、借阅记录、管理员。再圈出业务动词:读者借书、读者还书、管理员维护图书和读者信息。可以确定的实体有三个:读者、图书、借阅记录。管理员在课程设计级别通常可以并入借阅记录的操作字段,或者单独建一张极简管理员表,我一般建议先不做权限,把篇幅留给借阅核心流程,答辩时再说明"权限部分可以通过字段扩展"。
借阅记录是读者和图书之间"多对多"联系的载体,在关系模式里体现为第三个表。三段关系模式如下:
- 读者(读者ID,读者编号,姓名,读者类型,联系电话)
- 图书(图书ID,ISBN,书名,作者,出版社,价格,库存数量)
- 借阅记录(记录ID,读者ID,图书ID,借书日期,应还日期,实际还书日期)
E-R 图要画进 Word 文档里,但不能只画一个矩形加连线就完事。要在每个实体旁标出主键和关键属性,在联系上标出基数(1:n 还是 m:n),这是老师看设计文档时最先扫的位置。关系模式转成建表语句后,再回过去检查一句话:每个表的主外键在 E-R 图里是否能对应上。对应不上,说明前面的分析漏了实体或联系,这时候改成本最低。
3.2 字段类型与约束:主外键、唯一键、默认值怎么给
字段类型是数据库课程设计里最容易被扣分也最好补的环节。常见误用是把所有字符字段都建成 VARCHAR(255),把学号、ISBN 这些业务编号建成自增数字。学号和借书证号本质上不是数字,不需要参与运算,用 VARCHAR(20) 加唯一索引更合理;ISBN 固定 13 位,用 CHAR(13) 或者 VARCHAR(20) 都行;金额用 DECIMAL(10,2),不能用 FLOAT,浮点算金额会出精度问题;日期统一用 DATE,不要用 VARCHAR,否则日期区间查询和逾期天数的计算都会非常别扭。
| 业务字段类型 | 推荐类型 | 理由 |
|---|---|---|
| 学号 / 证号 | VARCHAR(20) | 不参与算术运算,业务编号不应自增 |
| ISBN | CHAR(13) / VARCHAR(20) | 定长标识,但需兼容带连字符的写法 |
| 价格 | DECIMAL(10,2) | 避免浮点精度误差 |
| 日期 | DATE | 支持区间查询,可算日期差 |
| 状态标记 | TINYINT | 比字符串省空间,可扩展枚举 |
CREATE TABLE reader ( reader_id BIGINT AUTO_INCREMENT COMMENT '读者ID,表内主键', reader_no VARCHAR(20) NOT NULL COMMENT '学号或借书证号', reader_name VARCHAR(50) NOT NULL COMMENT '姓名', reader_type TINYINT DEFAULT 1 COMMENT '1学生 2教师', phone VARCHAR(20) DEFAULT NULL, PRIMARY KEY (reader_id), UNIQUE KEY uk_reader_no (reader_no) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='读者表'; CREATE TABLE borrow ( borrow_id BIGINT AUTO_INCREMENT, reader_id BIGINT NOT NULL, book_id BIGINT NOT NULL, borrow_date DATE NOT NULL, due_date DATE NOT NULL, return_date DATE DEFAULT NULL, PRIMARY KEY (borrow_id), KEY idx_reader (reader_id), KEY idx_book (book_id), CONSTRAINT fk_borrow_reader FOREIGN KEY (reader_id) REFERENCES reader (reader_id), CONSTRAINT fk_borrow_book FOREIGN KEY (book_id) REFERENCES book (book_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='借阅记录表';上面两张表的参数说明要讲清楚:reader_id 设计成自增主键,是表内部的代理键,对外不可见;读者编号 reader_no 加唯一索引,保证同一个学号不能被插入两次,这是唯一键和主键的分工。borrow 表的外键约束指向 reader 和 book 的主键,保证借阅记录不会指向不存在的读者或图书。引擎选 InnoDB 而不是 MyISAM,是为了支持外键约束和后面第 4 章要讲的事务。字符集统一写在建表语句里,避免继承库级默认值带来乱码。
3.3 索引设计:每个表的高频查询路径
索引不是装饰,要体现"为什么建这个索引"。课程设计里的索引一般来自三个查询需求:按读者编号查读者信息、按 ISBN 查图书、按借阅记录查某个读者的全部借阅历史。前两个用唯一索引或普通索引覆盖,第三个是高频查询,借阅表要为外键列建索引,否则每次查询都走全表扫描。
以借阅记录表为例,reader_id 和 book_id 是两个外键列,建表语句里的 KEY idx_reader 和 KEY idx_book 就是给它们建普通索引。理由是:外键约束本身不会自动建索引,InnoDB 在更新父表或删除父表时,要在子表上按外键列查找对应记录,没有索引就会全表扫,既慢又容易触发死锁。借阅历史查询的典型 SQL 是 WHERE reader_id = ?,这个条件命中 idx_reader,访问类型能从全表扫描变成索引查找。
建议在文档里留一张小表说明每个索引服务的查询场景:主键索引服务按 reader_id 的等值查找,唯一索引 uk_reader_no 服务按学号的登录校验,普通索引 idx_reader 服务借阅历史列表。答辩问"为什么给这个列加索引"时,答"因为这个列出现在 WHERE 高频查询条件里"比答"老师说要建索引"要扎实得多。同时注意,不是每个列都要建索引,写操作频繁的列索引过多会拖慢插入和更新,课程设计规模下每个表最多 2 到 3 个索引就足够了。
3.4 从三范式到反范式:课程设计该守到什么程度
老师爱问的一句话是"你的表满足第几范式"。默认答案是好设计按第三范式来,但要能解释清楚每个表为什么满足,也要能说清哪里做了取舍。第三范式的要求是非主属性不依赖其他非主属性,落到借阅系统里就是:借阅记录表里只存 reader_id,不存读者姓名;只存 book_id,不存书名。查询时需要姓名和书名,通过 JOIN 去读者表和图书表取,这是标准做法。
有些课程设计为了页面展示方便,在借阅记录表里冗余了书名和读者姓名,这就是反范式设计。反范式不是错,但要有代价意识:如果读者改名字,所有历史借阅记录里的冗余姓名都要同步改,漏掉一条就出现数据不一致。我的判断标准很简单:如果冗余能减少一次高频 JOIN,且同步更新逻辑在代码里能写到同一个事务内,可以冗余;如果只是为了少写一行 JOIN 语句,不值得。答辩时主动说"借阅表里没有冗余书名,因为这个查询是等值 JOIN,成本可控",比无脑反范式更能体现设计意识。
4. 代码连库:从 JDBC 到连接池的最小可运行路径
功能代码千差万别,连接数据库的路径只有几条。课程设计里最常见的卡点是不管 MySQL 还是别的库,连接都跑不通。这一章把从裸 JDBC 到连接池的路径完整走一遍,代码可以直接抄进 DAO 层。
4.1 JDBC 六步:DriverManager 时代的最小连接代码
JDBC 连接数据库的六步是固定的:加载驱动、获取连接、创建语句、执行 SQL、处理结果、关闭资源。第一步和最后一步最容易丢。加载驱动的经典写法是 Class.forName,虽然 JDBC 4 之后驱动 jar 可以自动注册,但课程设计里保留这一行能第一时间发现驱动缺失,报错也直观。
public List<Reader> queryByReaderNo(String readerNo) { String url = "jdbc:mysql://localhost:3306/book_db" + "?useUnicode=true&characterEncoding=utf8mb4" + "&serverTimezone=Asia/Shanghai&useSSL=false"; String sql = "SELECT reader_id, reader_no, reader_name " + "FROM reader WHERE reader_no = ?"; try (Connection conn = DriverManager.getConnection(url, "root", "123456"); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, readerNo); try (ResultSet rs = ps.executeQuery()) { List<Reader> list = new ArrayList<>(); while (rs.next()) { Reader r = new Reader(); r.setReaderId(rs.getLong("reader_id")); r.setReaderNo(rs.getString("reader_no")); r.setReaderName(rs.getString("reader_name")); list.add(r); } return list; } } catch (SQLException e) { e.printStackTrace(); return Collections.emptyList(); } }这段代码用 try-with-resources 自动关闭 Connection、PreparedStatement 和 ResultSet,注意关闭顺序是 ResultSet 先关,然后是 Statement,最后是 Connection。如果不用 try-with-resources 而手动关闭,必须在 finally 里按这个反序写,否则先关连接再关结果集会报错。参数占位符 ? 从 1 开始编号,ps.setString 的第一个参数是占位符下标,不是列序号,这是新手最容易搞混的地方。
裸 JDBC 的问题在于每次 DriverManager.getConnection 都建立一条物理连接,课程设计单人演示完全够用,但答辩老师很可能追问"100 个人同时借书会不会崩"。这个问题用连接池回答,代码改动只有一行。
4.2 用连接池改造:Druid 的配置与三个必调参数
连接池的常见选择是 Druid 或 HikariCP。课程设计里 Druid 更常见,因为它的监控页面可以额外当展示点。引入依赖后,写一个 druid.properties 配置文件,再在 DAO 层用 DruidDataSource 初始化一次,之后所有方法都从 dataSource.getConnection() 拿连接,资源复用后性能表现完全不同。
jdbc.driverClassName=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/book_db?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai&useSSL=false jdbc.username=root jdbc.password=123456 initialSize=5 maxActive=20 maxWait=5000 poolPreparedStatements=true最需要调的是三个参数。initialSize 是启动时预建的连接数,设 5 就够课程设计用;maxActive 是最大活跃连接数,设 20 意味着同时最多 20 个连接在工作,超过就排队;maxWait 是拿不到连接时的等待毫秒数,设 5000 表示等待 5 秒后抛异常。maxWait 太短,演示时稍微慢一点就报获取连接超时;太长,程序看起来像卡死。poolPreparedStatements 开启后 PreparedStatement 被缓存复用,增删改查频繁的场景能明显降低解析开销。
Properties props = new Properties(); try (InputStream in = Files.newInputStream(Paths.get("druid.properties"))) { props.load(in); } DataSource dataSource = DruidDataSourceFactory.createDataSource(props); Connection conn = dataSource.getConnection();从裸 JDBC 切换到连接池,DAO 层只需要把获取连接的一行换掉:原来是 DriverManager.getConnection(url, user, pwd),现在是 dataSource.getConnection()。sql 语句和结果集处理完全不变。这个切换过程在课程设计文档的"系统设计"章节写一句"使用数据库连接池复用连接,避免频繁建立物理连接",比堆一页概念更能说明你理解了连接管理的意义。
4.3 增删改查闭环:PreparedStatement 与结果集映射
增删改查四个操作里,查询用 executeQuery 返回 ResultSet,插入、更新、删除都用 executeUpdate 返回受影响行数。课程设计里最容易忽略的不是 SQL 写法,而是对返回值的处理:插入图书后应该判断返回值是否为 1,是 0 说明没插入成功,这时候还继续往下走就会产生逻辑缺口。
public boolean addBook(Book book) { String sql = "INSERT INTO book (isbn, book_name, author, price, stock) " + "VALUES (?, ?, ?, ?, ?)"; try (Connection conn = dataSource.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, book.getIsbn()); ps.setString(2, book.getBookName()); ps.setString(3, book.getAuthor()); ps.setBigDecimal(4, book.getPrice()); ps.setInt(5, book.getStock()); return ps.executeUpdate() == 1; } catch (SQLException e) { e.printStackTrace(); return false; } }参数说明:setBigDecimal 对应数据库里的 DECIMAL(10,2),用 BigDecimal 而不是 double 才能保住精度;setInt 对应 INT 和 BIGINT 都可以;日期字段用 setObject 传 LocalDate 配合 JDBC 4.2,避免手动转 java.sql.Date 的麻烦。PreparedStatement 用 ? 占位符绑定参数,而不是拼字符串,这是防止 SQL 注入的基本要求,课程设计里必须用这种写法而不是 Statement 拼 SQL。
使用连接池后有一个新坑:ResultSet 和 Statement 不关闭的话,连接不会真正归还给池,时间一长 maxActive 会被占满,后续请求全部超时。裸 JDBC 时代连接泄漏的后果是进程连带崩溃,连接池时代只是慢慢变慢,更难发现。所以 DAO 里必须保证每一个连接都在 finally 或 try-with-resources 中被释放。
4.4 事务边界:借书操作的两个 SQL 必须一起成功
借书操作在数据库层包含两步:往借阅记录表插入一条记录,再把图书表的库存减一。这两步要么都成功,要么都失败,否则会出现"借阅记录存在但库存没减"或反过来"库存减了但查不到借阅记录"的数据不一致。事务的边界就画在两步之间。
public boolean borrowBook(long readerId, long bookId) { String insertBorrow = "INSERT INTO borrow (reader_id, book_id, borrow_date, due_date) " + "VALUES (?, ?, CURDATE(), DATE_ADD(CURDATE(), INTERVAL 30 DAY))"; String updateStock = "UPDATE book SET stock = stock - 1 WHERE book_id = ? AND stock > 0"; try (Connection conn = dataSource.getConnection()) { conn.setAutoCommit(false); try (PreparedStatement ps1 = conn.prepareStatement(insertBorrow)) { ps1.setLong(1, readerId); ps1.setLong(2, bookId); ps1.executeUpdate(); } try (PreparedStatement ps2 = conn.prepareStatement(updateStock)) { ps2.setLong(1, bookId); int rows = ps2.executeUpdate(); if (rows == 0) { throw new SQLException("库存不足或图书不存在"); } } conn.commit(); return true; } catch (SQLException e) { try { conn.rollback(); } catch (SQLException ex) { ex.printStackTrace(); } e.printStackTrace(); return false; } finally { try { conn.setAutoCommit(true); } catch (SQLException e) { e.printStackTrace(); } } }这段代码的逻辑说明:setAutoCommit(false) 之后,当前连接上的所有 SQL 都不会自动提交,直到显式调用 commit 才一并生效;任何一个环节抛异常,进入 catch 分支执行 rollback,把前面插入的记录和库存更新一起撤销;finally 里把 autoCommit 复位成 true,是因为连接要归还给连接池,下一个使用者不知道这个连接的 commit 模式已被改过,不复位会引发难以追踪的 bug。
第三个细节是 updateStock 里加了 AND stock > 0 和受影响行数判断。这是把"库存不足"的业务规则下沉到 SQL 层,比先查库存再更新的方式更安全,因为两个操作之间不会被并发插入打破。课程设计把这两行写出来,事务和并发意识就都有了。另外注意:事务里不要夹杂 UI 弹窗或耗时操作,事务时间越长,持有锁的时间越长,第 5 章的卡死现场就是这么来的。
5. 数据库课程设计避坑清单:从连不上库到死锁的五个现场
这一章的五条记录全部来自课设验收前的高频现场,按"现象、原因、解决"的节奏排查,可以省去在教室当众翻车的尴尬。
5.1 现象:启动报“No suitable driver”或“ClassNotFoundException”
现象是程序一启动,控制台抛 java.sql.SQLException: No suitable driver found for jdbc:mysql://localhost:3306/...,或者直接 ClassNotFoundException。原因一般是三个:驱动 jar 没有放进 classpath;MySQL 版本换了但驱动 jar 没换;驱动类名写错,5.x 的 com.mysql.jdbc.Driver 和 8.x 的 com.mysql.cj.jdbc.Driver 混用。解决时先确认三件事:jar 是否在构建路径里,用 jar tf 检查驱动包内有没有对应的 Driver 类;连接串是否以 jdbc:mysql:// 开头;类名是否和驱动包版本匹配。Web 项目里还要确认 jar 放在 WEB-INF/lib 而不是只在 IDE 的 classpath 里,Tomcat 运行时只看前者。
5.2 现象:中文写入变成问号
现象是界面输入中文保存后,数据库里看到的是 ? 或乱码。原因几乎都是三层字符集不一致:库默认字符集是 latin1,表继承了 latin1,连接串没带 characterEncoding。解决是从上到下统一成 utf8mb4。建库时用 CREATE DATABASE book_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci,建表语句里的 CHARSET=utf8mb4 也不能省,连接串第 2 章里已经加过 characterEncoding=utf8mb4。这里有个血泪教训:用 ALTER TABLE ... CHARSET=utf8mb4 之后,旧数据如果是已经按 latin1 存储的乱码,并不会自动恢复,最干净的做法是导出数据、重建库表、再导入。所以最好在设计文档的第一步就定死字符集,后面不改。
5.3 现象:两个窗口操作同一张表,程序卡死
现象是界面点保存后一直转圈,数据库命令窗口执行同一条 UPDATE 也卡住。原因大多是某个事务开启了却没提交,行锁被第一个连接占着;另一个更隐蔽的原因是两个事务按相反顺序更新同一批数据,互相等对方的锁,形成死锁。解决办法分三步:先在命令行查 SHOW ENGINE INNODB STATUS 看最后一段 LOCK WAIT 或 DEADLOCK 信息;代码里给事务设 innodb_lock_wait_timeout=5,超过 5 秒直接超时报错,至少能让程序弹错而不是假死;最后从根上改代码,把多张表的更新顺序固定下来,比如先更新 book 表再更新借阅表,全项目统一这个顺序,交叉等待就不会出现。课程设计里最常见的是在一个长方法开头开了事务,中间弹了对话框等用户输入,事务一直不提交,锁就这么一直悬着。
5.4 现象:SQL 在客户端能跑通,程序查不出数据
现象是同样的 WHERE 条件在 Navicat 里查得到数据,程序运行结果却是空列表。原因最常见的是连的不是同一个库:连接串里的库名写错、大小写不一致,或者本机有多个 MySQL 实例;其次是 autocommit 被关掉后插入没提交,事务里的查询看不到未提交数据;还有一种坑是查询条件里字符串和数字类型不匹配,比如把 VARCHAR 条件数字当数字传,MySQL 会做隐式转换,索引失效但不报错。解决时先打印 dataSource.getConnection().getCatalog() 确认当前连的是哪个库,然后把插入方法的事务提交改为自动提交验证一次,最后删掉 WHERE 条件的分号或者换参数类型。这类问题不报错,全靠对照检查。
5.5 现象:答辩时发现 doc 里的表结构和代码不一致
现象是文档里 E-R 图有三个表,代码里实际建了四个;文档里 book 表有 stock 字段,数据库里没有。原因很直白:开发时为了加需求改了表,文档没同步,毕竟没人喜欢返工写文档。解决办法是在交稿前做一次"从零复现":删掉整个库,按 doc 里的建表语句重建,再用 doc 里的步骤运行程序。如果程序在这一步跑不通,那就说明文档和代码对不上,提交前还有机会改;如果跑通了,这份文档就真的能指导另一个人部署。这是课程设计里唯一靠谱的"后悔药",能在发现问题时永远来得及。
6. 答辩前必查:验证清单、性能演示与文档闭环
6.1 五类必测用例
答辩前至少用五类用例把系统过一遍:正常增删改查、重复主键插入、空值提交、超长字符串输入、并发借书。每一类对应一个扣分点:正常路径保证能演示完所有功能;重复主键验证唯一约束和异常处理;空值验证 NOT NULL 约束和页面校验;超长字符串验证字段长度是否与表结构一致;并发借书在开两个客户端窗口同时借同一本书时,验证库存不会变负数。
| 用例 | 操作 | 期望结果 |
|---|---|---|
| 正常新增 | 添加一本图书 | 列表刷新后出现新记录 |
| 重复主键 | 插入相同 reader_no | 抛出唯一键冲突并被提示 |
| 空值 | 不填姓名保存 | 被 NOT NULL 或页面校验拦截 |
| 并发借书 | 两个窗口借同一本书 | 库存不为负,借阅记录各一条 |
6.2 性能演示:用一万条数据证明索引不是装饰
课程设计数据量小,索引效果不明显。答辩前可以造一万条测试数据,用存储过程循环插入,再执行 EXPLAIN SELECT * FROM borrow WHERE reader_id = 10,看访问类型和扫描行数。有索引时是 ref,扫描行数是个位数;去掉索引后是全表扫描,扫描行数是全表条数。这一条对比图放进 doc 的"数据库优化"部分,比写一百字关于索引原理的文字更有说服力。
6.3 文档闭环:数据字典、E-R 图和代码注释对得上
交稿前的最后动作是数据字典与 SHOW CREATE TABLE 输出逐行比对,E-R 图实体属性与 DAO 层的 VO 字段逐个核对。我自己的习惯是提交前把电脑借给同学,让他只照着 doc 从零部署一次,能跑通才提交。这门课真正收获不是把系统交上去,而是让另一个人能顺着文档把系统完整跑起来。希望帮到你。
本文还有配套的精品资源,点击获取