简介:数据库课程设计完整版文档以学生成绩管理系统为实战主题,面向需要完成数据库课程设计、巩固Java编程与数据库理论的高校学生。文档从课程设计目的与需求分析切入,系统梳理数据流图与数据字典,随后展开数据库概念结构设计(E-R图)和逻辑结构设计(表结构与功能结构图),并给出系统实施运行结果与总结,末附完整源代码,清晰还原大学学籍管理系统的构建全过程。系统涵盖学生、教师、管理员三类角色,支持登录、密码修改、个人信息查看、成绩录入与查询、用户增删改查等模块,同时提供用户表、学生学籍表、成绩表、教师表等核心数据表定义,可作为课程设计报告撰写、数据库建模以及Java连接数据库操作的直接参考。资源为单个doc文档,压缩包大小269KB,已有330人浏览学习,适合正在准备数据库课程设计或复习相关知识点的读者。
1. 数据库与Java的课程设计:拿到这份.doc,你到底要交什么
很多同学在交作业前搜“数据库-Java课程设计完整版.doc”,以为改名提交就完事。其实这份doc只是别人把“数据库设计+Java源码+操作说明”打包后的一个索引,真正重要的是里面那套可复现的流程:从建库建表到增删改查,每一步都要能讲清楚。文章里我会从一线工程师的角度,把这个方向拆成一串标准动作:做什么题目、怎么建表、怎么写DAO、哪些坑必须避开,最后怎么让答辩老师愿意给高分。适合正准备数据库/Java课程设计、或者想快速补上项目经验的新手,也适合想把自己代码整理成文档的老手。
2. 拆需求与技术选型:课程设计不是“写代码”,是“答得上问”
2.1 先用最小闭环的增删改查定住业务范围
课程设计评分看的是完整度,不是复杂度。一个图书管理系统、学生信息管理、超市商品进销存,都是经久不衰的选题。原因很简单:它们都能用一张主表加一张关联表,完成最核心的增删改查,又能引出外键、事务这类必考知识点。
我一般拿到题目先写一句话:系统管什么数据、谁在用、操作后产生什么变化。然后把这句话拆成功能清单。以“图书借阅管理”为例,清单就三行:
- 图书:新增、修改、删除、按书名/作者查询。
- 读者:新增、修改、删除、查借阅历史。
- 借书/还书:关联读者和图书,记录借出日期、应还日期。
这三行就是整个课程设计的“需求边界”。再往多加权限、分类、统计报表,都可以做,但必须先把最小闭环跑通。你给答辩老师演示的时候,先走一遍“新增读者→新增图书→借书→还书→再查一下借阅记录”,老师心里已经有底了。
注意:做需求拆解时,不要把自己绕进大而全的功能里。我见过一个小组选了“高校教务系统”,光角色就搞了四种,最后一个月连登录都做得半吊子。课程设计的时间只够做一条清晰的主线,主线之外的都算加分项,不是必选项。把“核心CRUD”写进开题报告,比写十个含糊的功能点更有说服力。
2.2 技术栈选型:JDBC + DAO 保底,ORM 当亮点,别一上来就全家桶
现在Java做数据库操作的常见路线有三条:纯JDBC、JDBC+DAO封装、MyBatis/JPA这类ORM。课程设计阶段,我强烈建议用“JDBC + 手写DAO”当保底方案。理由很实际:纯JDBC能让你把Connection、Statement、ResultSet、事务的提交回滚都讲明白,这些是面试里反复问的“数据库基础”;而直接上MyBatis,虽然代码少一半,但很多同学连SqlSessionFactory怎么创建都说不清,答辩时反而被问穿。
技术对比说穿了就一句话:JDBC是Java访问数据库的基本能力,ORM是外包了SQL生成的脚手架。课程设计如果想要“亮点”又不丢基础,可以在保底方案上加一个Druid连接池,再用DBUtils简化ResultSet的转换。不过前提是你心里清楚JDBC自己做要经历哪几步,否则亮点会变成翻车点。
下面是一个最小的Maven依赖片段,我一般会把JDBC驱动、连接池和JUnit一起放进pom.xml,避免后面测试时到处找jar:
<dependencies> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> <scope>runtime</scope> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.22</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.13.2</version> <scope>test</scope> </dependency> </dependencies>这段依赖的三个作用:mysql-connector-java 是驱动,负责把Java的JDBC调用转成MySQL协议;Druid提供连接池,避免每次操作都新建物理连接;JUnit让你在写DAO之前先测通“连库-查一条数据”这个最简单的动作。
注意版本不是越新越好。mysql-connector-java 8.x对应MySQL 5.7/8.0都能用,但com.mysql.jdbc.Driver这个旧驱动类名在8.x里已经变成com.mysql.cj.jdbc.Driver。很多同学的连接失败,不是密码错,是驱动类名写成了老版本。这里要按你本地MySQL版本选驱动,别随手复制网上的连接串。
连接URL里有个关键参数,第一次写的时候要注意:
jdbc:mysql://localhost:3306/library?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai这个字符串里,useUnicode和characterEncoding解决中文字符乱码,useSSL=false关闭本地开发时的SSL握手干扰,serverTimezone指定时区,否则Java 8以后的驱动会报时间类型错误。每个参数都能在答辩时被问到,哪怕只记住中文乱码和时区这两条,也比照着抄有底。
如果还是纠结选型,可以看这个对比表,我把常见方案按答辩风险和工作量排了序:
| 方案 | 工作量 | 答辩风险 | 适合场景 |
|---|---|---|---|
| 纯JDBC | 大 | 中,容易暴露连接管理细节 | 想展示基础原理 |
| JDBC+DAO | 中 | 低,每层职责清楚 | 课程设计保底 |
| MyBatis+Spring | 小 | 高,必须能解释框架原理 | 憋大招或毕设 |
课程设计不是公司项目,评分的权重里,“你说的清楚”远大于“功能多”。选保底方案,把精力留给数据库设计和事务,才是性价比最高的做法。
2.3 把功能清单纯化成验收清单:让老师十分钟看到全貌
需求拆解之后,我习惯再画一张“验收清单”表格,放进报告的开头。表格的作用不是给代码加注释,而是让老师快速知道你做了什么、没做什么。比如:
| 编号 | 功能点 | 涉及表 | 是否必做 | 验证方式 |
|---|---|---|---|---|
| 1 | 图书新增 | book | 必做 | 插入后列表出现 |
| 2 | 图书删除 | book, borrow | 必做 | 无借阅记录时可删 |
| 3 | 借书 | book, borrow | 必做 | 库存减一,生成借阅记录 |
| 4 | 还书 | borrow | 必做 | 更新return_date |
| 5 | 未还列表 | borrow, reader | 加分 | 按return_date is null过滤 |
这个清单还能防止你自己做歪了。很多人写着写着就在查询里加了各种花活,回头一看,核心增删改查反而没走通。有验收清单在,每完成一项就勾一项,心里有数。
3. 数据库设计落地:从ER图到一份能交差的建表SQL
3.1 以图书管理系统为例:三张表把关系说清楚
数据库设计是课程设计的重头。评分标准里,概念设计(ER图)、逻辑设计(表结构)和数据字典通常占30%以上。别把建表SQL当成最后一步,应该先把表关系画出来。图书借阅管理最简模型是“读者借图书”的多对多关系,需要用一张借阅记录表破开。
- book:存图书信息,主键book_id,还有书名、作者、价格、库存。
- reader:存读者信息,主键reader_id,包括姓名、学号/工号、电话。
- borrow:借阅记录,主键borrow_id,外键book_id、reader_id,加上借书日期和还书日期。
这三张表的关系一句话:一本书可以被多个读者借,一个读者可以借多本书,一对多和多对一中间靠borrow表承接。设计时要注意,借阅记录里不该存冗余的图书价格和读者姓名,只存外键ID,需要显示时用JOIN查出来。这是数据库设计的基本原则,老师一眼就能看出你有没有概念。
3.2 建表SQL:主键、外键、索引和默认值一次配齐
下面这段SQL是我常用的一版,在MySQL 8.0下可以直接跑:
CREATE DATABASE IF NOT EXISTS library DEFAULT CHARSET utf8mb4; USE library; CREATE TABLE book ( book_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '图书ID', book_name VARCHAR(100) NOT NULL COMMENT '书名', author VARCHAR(50) NOT NULL COMMENT '作者', price DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT '价格', stock INT NOT NULL DEFAULT 1 COMMENT '当前库存', created_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '入库时间' ) ENGINE=InnoDB COMMENT='图书表'; CREATE TABLE reader ( reader_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '读者ID', reader_no VARCHAR(20) NOT NULL UNIQUE COMMENT '学号/工号', reader_name VARCHAR(30) NOT NULL COMMENT '姓名', phone VARCHAR(11) COMMENT '手机号' ) ENGINE=InnoDB COMMENT='读者表'; CREATE TABLE borrow ( borrow_id INT PRIMARY KEY AUTO_INCREMENT COMMENT '借阅ID', book_id INT NOT NULL COMMENT '图书ID', reader_id INT NOT NULL COMMENT '读者ID', borrow_date DATE NOT NULL COMMENT '借书日期', return_date DATE DEFAULT NULL COMMENT '还书日期', CONSTRAINT fk_borrow_book FOREIGN KEY (book_id) REFERENCES book(book_id), CONSTRAINT fk_borrow_reader FOREIGN KEY (reader_id) REFERENCES reader(reader_id), INDEX idx_borrow_reader (reader_id), INDEX idx_borrow_book (book_id) ) ENGINE=InnoDB COMMENT='借阅表';这里的几个设计选择值得解释。VARCHAR用100而不是255,是告诉老师你了解字段长度对索引的影响;DECIMAL而不是FLOAT存金额,是为了避免浮点误差;TIMESTAMP默认值可以让插入时少写一个字段;FOREIGN KEY保证借阅记录不会指向不存在的书或读者。INDEX on外键是因为外键列常用于JOIN查询和WHERE过滤,不加索引在这个数据量下也能跑,但加了在答辩时能答出“为了级联查找性能”。
关于字符集,我用utf8mb4而不是utf8,因为utf8mb4是MySQL对完整Unicode支持的字符集,能存emoji和生僻字。课程设计虽然不一定用到,但如果你用Navicat之类的工具导数据时遇到中文乱码,多半是这里不一致。建库时最好统一字符集,后续所有表都继承,避免一张表一个编码的尴尬。
3.3 测试数据:一份干净的insert胜过临场瞎点
很多同学写完建表后直接开写Java,这是反的。应该先插入几条代表性数据,把查询逻辑在SQL工具(Navicat/DataGrip/DBeaver)里验证一遍,再回到Java里写代码。这样出问题时能确定是SQL的问题还是Java的问题。
INSERT INTO book(book_name, author, price, stock) VALUES ('Java核心技术', 'Cay S. Horstmann', 149.00, 10), ('数据库系统概论', '王珊', 49.50, 5), ('重构', 'Martin Fowler', 92.00, 3); INSERT INTO reader(reader_no, reader_name, phone) VALUES ('2024001', '张伟', '13800000001'), ('2024002', '李娜', '13800000002'), ('2024003', '王芳', '13800000003'); INSERT INTO borrow(book_id, reader_id, borrow_date, return_date) VALUES (1, 1, '2024-11-01', NULL), (1, 2, '2024-11-02', '2024-11-10'), (2, 3, '2024-11-03', NULL);这份数据里特意设置了几个边界情况:有一条读者1未还书(return_date为NULL),有一条读者2已还,还有读者3借的是第二本。将来在Java里写“查询当前未归还的借阅记录”时,就能用WHERE return_date IS NULL来覆盖,这是课程设计最常见的查询类型。
测试数据还有个讲究:自增主键不要显式指定,让MySQL自动生成;外键值一定要在父表里存在,否则InnoDB会报外键约束错误。如果你在图形工具里手工插数据遇到“cannot add or update a child row”,先查父表里有没有对应ID,别怀疑工具。
3.4 视图和存储过程要不要加:给报告找个可答的加分项
很多课程设计范例会在数据库设计里加入视图或存储过程。我的建议是:不加则已,加就要能几句话讲清用途。视图适合用来展示“当前未归还的借阅明细”,因为它把三表JOIN的复杂查询封装成一个虚拟表;存储过程适合演示“还书时更新库存”的一串操作。这些知识点在数据库课程里讲过,写在报告里能加分。
但要注意别贪多。如果老师问“存储过程和Java里的Service层事务有什么区别”,答不上来反而扣分。我的习惯是:视图可以加一个,存储过程如果时间紧张就放弃。把“借书-扣库存”的事务用Java代码讲明白,效果不差于存储过程。
4. Java代码实现:连接池、DAO与CRUD,跑通一条完整链路
4.1 从驱动加载到连接池:别再每个方法里new Connection
课程设计最常见的幼稚写法,是在每个方法里Class.forName、DriverManager.getConnection、最后conn.close。代码跑得通,但有一个致命问题:每次数据库操作都建立和销毁物理连接,在高并发下会拖垮数据库,面试时也过不了关。常见的工程做法是用连接池,Druid/HikariCP都可以,课程设计用Druid更多,因为配置直观、自带监控页面。
下面是一个最简单的JdbcUtil,用Druid连接池代替手动获取连接:
package com.example.library.util; import com.alibaba.druid.pool.DruidDataSourceFactory; import javax.sql.DataSource; import java.io.InputStream; import java.util.Properties; public class JdbcUtil { private static DataSource dataSource; static { try (InputStream in = JdbcUtil.class.getClassLoader() .getResourceAsStream("druid.properties")) { Properties props = new Properties(); props.load(in); dataSource = DruidDataSourceFactory.createDataSource(props); } catch (Exception e) { throw new ExceptionInInitializerError("连接池初始化失败: " + e.getMessage()); } } public static DataSource getDataSource() { return dataSource; } }然后在resource目录放一个druid.properties:
driverClassName=com.mysql.cj.jdbc.Driver url=jdbc:mysql://localhost:3306/library?useUnicode=true&characterEncoding=UTF-8&useSSL=false&serverTimezone=Asia/Shanghai username=root password=123456 initialSize=3 maxActive=10 maxWait=3000这段配置里,initialSize是池子启动时创建的连接数,maxActive是最大连接数,maxWait是拿不到连接时的等待毫秒数。课程设计规模很小,initialSize=3、maxActive=10就够,不要写成100,否则本地MySQL会报too many connections。
注意:druid.properties里的driverClassName也是8.x驱动要写com.mysql.cj.jdbc.Driver,别和旧版混。password是你本机MySQL的实际密码,不要原样抄。如果改用HikariCP,配置项名字会略有不同,但思路一样。
4.2 DAO层与PreparedStatement:为什么你的SQL会报错或被注入
有了连接,下一步是写DAO。以BookDAO为例,最核心的方法是insert、update、delete、selectAll和selectById。这里必须使用PreparedStatement而不是Statement,原因有两点:一是预编译能减少SQL解析开销(虽然单条插入不明显),二是参数用占位符绑定,不会出现拼接字符串导致的单引号问题,也就是防SQL注入。答辩老师如果问“为什么不用Statement”,你能回答PreparedStatement防止注入,这就是基础分。
先看一个完整的BookDAO:
package com.example.library.dao; import com.example.library.entity.Book; import com.example.library.util.JdbcUtil; import java.sql.*; import java.util.ArrayList; import java.util.List; public class BookDAO { public int insert(Book book) { String sql = "INSERT INTO book(book_name, author, price, stock) VALUES(?,?,?,?)"; try (Connection conn = JdbcUtil.getDataSource().getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, book.getBookName()); ps.setString(2, book.getAuthor()); ps.setBigDecimal(3, book.getPrice()); ps.setInt(4, book.getStock()); return ps.executeUpdate(); } catch (SQLException e) { throw new RuntimeException("新增图书失败", e); } } public List<Book> selectList(String keyword) { String sql = "SELECT book_id, book_name, author, price, stock " + "FROM book WHERE book_name LIKE ? OR author LIKE ?"; List<Book> list = new ArrayList<>(); try (Connection conn = JdbcUtil.getDataSource().getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { String pattern = "%" + keyword + "%"; ps.setString(1, pattern); ps.setString(2, pattern); try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { Book b = new Book(); b.setBookId(rs.getInt("book_id")); b.setBookName(rs.getString("book_name")); b.setAuthor(rs.getString("author")); b.setPrice(rs.getBigDecimal("price")); b.setStock(rs.getInt("stock")); list.add(b); } } } catch (SQLException e) { throw new RuntimeException("查询图书失败", e); } return list; } public int deleteById(int id) { String sql = "DELETE FROM book WHERE book_id = ?"; try (Connection conn = JdbcUtil.getDataSource().getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, id); return ps.executeUpdate(); } catch (SQLException e) { throw new RuntimeException("删除图书失败", e); } } }这个DAO有三个值得抄作业的点。第一,try-with-resources让connection和statement自动关闭,即使SQL执行出错也不会泄漏连接。第二,PreparedStatement的setXxx方法根据字段类型选择setString/setBigDecimal/setInt,类型不匹配会在执行前报错,比等数据库报错更早。第三,selectList里的LIKE查询,参数直接传“%” + keyword + “%”,不要在SQL里写'%?%',那样?会被当成字符串字面量,查不出任何数据。
参数大小写:方法名insert、selectList、deleteById是常见命名。注意Book实体的字段类型要与表字段一一对应,比如价格用BigDecimal,日期用LocalDate或java.util.Date,不要所有数字都填int。实体的代码很简单,因篇幅就不贴了,但建议在设计报告里把实体字段和数据字典对应起来,方便老师比对。
4.3 业务层与事务:借书不是一条insert,而是一串必须同时成功的操作
课程设计里最能体现数据库知识的地方,就在业务层。以“借书”为例,光在borrow表里插一条记录不算完整,还要做一件事:把book表里的stock减1。这两步必须同时成功或同时失败,否则会出现库存没减但借阅记录存在的数据脏乱。这就是事务。
下面是一个简单的事务控制方法,不借助Spring,用手动commit/rollback演示原理:
public boolean borrowBook(int bookId, int readerId, LocalDate borrowDate) { String updateStock = "UPDATE book SET stock = stock - 1 WHERE book_id = ? AND stock > 0"; String insertBorrow = "INSERT INTO borrow(book_id, reader_id, borrow_date, return_date) VALUES(?,?,?,?)"; Connection conn = null; try { conn = JdbcUtil.getDataSource().getConnection(); conn.setAutoCommit(false); try (PreparedStatement ps1 = conn.prepareStatement(updateStock)) { ps1.setInt(1, bookId); if (ps1.executeUpdate() == 0) { conn.rollback(); return false; } } try (PreparedStatement ps2 = conn.prepareStatement(insertBorrow)) { ps2.setInt(1, bookId); ps2.setInt(2, readerId); ps2.setDate(3, Date.valueOf(borrowDate)); ps2.executeUpdate(); } conn.commit(); return true; } catch (SQLException e) { if (conn != null) { try { conn.rollback(); } catch (SQLException ignored) { } } throw new RuntimeException("借书失败,事务已回滚", e); } finally { if (conn != null) { try { conn.setAutoCommit(true); conn.close(); } catch (SQLException ignored) { } } } }这段代码的关键是conn.setAutoCommit(false)之后,所有SQL都处于同一个事务里,直到commit或rollback才结束。update语句里加了AND stock > 0,意思是如果库存已经为0,执行影响行数为0,这时候借书操作在该方法内返回false,不会继续插入借书记录。finally里把自动提交恢复为true再关闭连接,是为了确保连接回到连接池时是干净状态,避免下次使用还带着一个未提交事务。
这个“两步操作+事务”的模式,就是课程设计报告里“业务逻辑设计”一节的素材。写报告时,你可以画一个简单的判断图:检查库存-扣减库存-插入记录-提交;任何一步失败则回滚。老师看到这个就知道你理解了事务的ACID。
4.4 用一个JUnit测试把CRUD串起来
界面还没写的时候,怎么验证DAO层没问题?我习惯在写完DAO后写一个JUnit测试,直接打真实数据库跑一遍。测试类长这样:
public class BookDAOTest { @Test public void testInsertAndQuery() { Book book = new Book(); book.setBookName("测试书"); book.setAuthor("作者"); book.setPrice(new BigDecimal("30")); book.setStock(2); BookDAO dao = new BookDAO(); int rows = dao.insert(book); Assert.assertEquals(1, rows); List<Book> list = dao.selectList("测试"); Assert.assertFalse(list.isEmpty()); Assert.assertEquals("测试书", list.get(0).getBookName()); } }注意这个测试依赖真实数据库,跑之前必须先把init.sql执行过,而且连接池配置里的数据库名和账号密码正确。如果你用的是JUnit 4,测试方法要加@Test;如果你被老师要求用JUnit 5,import要从org.junit.jupiter.api包引入,断言类也略有不同。这个小测试最大的价值是验证“环境通没通”:如果测试通过,说明驱动、连接池、建表、DAO全链路都没问题,后面写界面就只剩UI的事。
5. 课程设计避坑指南:5个让答辩翻车的细节
5.1 驱动类找不到:这种翻车,八成是依赖没进classpath
现象:运行main方法直接报ClassNotFoundException: com.mysql.cj.jdbc.Driver,或者No suitable driver found。
原因:如果用了Maven,依赖作用域是runtime,却在编译时没有把驱动加到运行时classpath;如果用了普通工程,mysql-connector-java.jar根本没有放到lib目录。另外驱动类名写错也会报这个错,比如8.x驱动用com.mysql.jdbc.Driver可能不报编译错,但运行时找不到。
解决:先把pom.xml清理干净,确保mysql-connector-java没写成provided;普通工程把jar放进WEB-INF/lib或项目lib目录并右键Add as Library。再检查类名:MySQL 5.x用com.mysql.jdbc.Driver,8.x用com.mysql.cj.jdbc.Driver。最稳的办法是在JdbcUtil静态块里先Class.forName(driverClassName)加载一次,失败时能看到是jar缺失还是类名错误。
5.2 中文乱码:从Excel复制进库就变问号,原因不在Java而在链路上
现象:在Java插入中文后,查询出来是??,或者表里能显示但控制台打印乱码。
原因:中文乱码是“客户端字符集→JDBC URL→数据库表字符集”整条链路的任一环节不一致造成的。常见的组合是:URL里少了characterEncoding=UTF-8,数据库默认字符集是latin1,或者插入数据时用了直接拼SQL而不是PreparedStatement。
解决:统一三个地方。建库时用DEFAULT CHARSET utf8mb4;JDBC URL加useUnicode=true&characterEncoding=UTF-8;如果从外部文件导入,先检查文件编码是不是UTF-8。另外MySQL 8.0默认字符集虽然是utf8mb4,但连接层还是可能被驱动参数影响,所以URL里的参数别删。这个坑我至少填了三回,每次都是新建表时没注意继承父数据库的字符集。
5.3 明明没报错,数据却没插入:自动提交被关了
现象:执行executeUpdate返回1,但去Navicat里看表,没有新记录。再执行一次,当主键自增跳了几号,说明insert其实执行过,只是被回滚了。
原因:连接池的Connection可能被上一段代码设置过autoCommit(false),并且没有在finally里恢复。虽然手动事务代码里用try-with-resources关闭了Statement,但Conn返回连接池时把autoCommit状态保留了,下一个人拿到这条连接,执行insert后不commit,数据就不会落库。
解决:在事务方法的finally里,除了close,还要conn.setAutoCommit(true);更保险的方法是每次拿连接时检查连接状态,或用Druid连接池的配置项配置。也可以写一个简单过滤器拦截所有查询,但课程设计用不着,记住“谁改自动提交,谁负责恢复”就好。
5.4 删除父表记录失败:外键约束成了“绊脚石”
现象:想删除一本书,结果报Cannot delete or update a parent row: a foreign key constraint fails。有的同学直接把外键删掉来“解决”。
原因:borrow表通过外键引用了book表,只要存在借阅记录,直接删父表记录就会被InnoDB拦截。这是外键的默认行为,它保护数据完整性。直接删外键等于放弃了数据库设计里最重要的知识点,答辩时是送分变送命。
解决:有两种合理做法。第一种,先删除子表里对应的borrow记录,再删除book;第二种,给外键加ON DELETE CASCADE,让删除父表记录时自动清掉子表引用。但要注意,CASCADE在课程设计系统里可能太危险,比如删一本被借出去的书,会连历史借阅记录一起删。我一般推荐“先删子表再删父表”或者在DAO里用逻辑删除(给book加deleted字段),并把选择逻辑在报告里写清楚。
5.5 时间类型映射混乱:Date和LocalDate的转换踩坑
现象:从ResultSet里getDate("borrow_date")后赋给LocalDate,编译报错;或者在Java里setObject传LocalDateTime,数据库报类型转换错误。
原因:java.sql.Date是java.util.Date的子类,用于SQL层;LocalDate是Java 8时间类型,两者不能直接互转。JDBC 4.2之后,setObject(4, localDate)理论上可以支持LocalDate,但不同数据库驱动实现有差异,MySQL 8.x驱动支持,5.x驱动不一定支持。
解决:最稳的方式是显式转换:读出来用rs.getDate("borrow_date").toLocalDate(),插入用Date.valueOf(borrowDate)。如果字段带时间,用Timestamp.valueOf(LocalDateTime.atStartOfDay())。另外实体里的日期字段一律用LocalDate/LocalDateTime,不要为了省事用String,否则排序和范围查询都会出问题。
6. 从“能跑”到“高分”:三个交付技巧和我的文档习惯
6.1 用一份README把复现路径写清楚
不管老师要不要,我都建议在项目根目录放一个README.md,记录环境版本、建库步骤、启动命令、测试账号。这不是给老师看的,是给自己留的后悔药——两周后再打开代码,不用从头推。可以用一个小代码块:
- 环境:JDK 17、MySQL 8.0、Maven 3.8 - 建库:执行 sql/init.sql - 运行:mvn clean package 后启动 Main - 账号:admin / 1234566.2 答辩前把演示脚本背熟,别让老师看翻车
老师问你项目有什么功能,不要打开代码现找。准备一条演示路线:登录→添加一本测试书→查询→借书→还书→查库存。每个操作旁边准备一句“这里我用了PreparedStatement防注入”“这里我用了事务保证库存一致”。这是加分的核心。我见过太多项目代码没问题,结果现场输入中文乱码、删除时报外键错误,这种翻车完全可以靠提前走一遍脚本避免。
6.3 数据库设计文档里加一张数据字典表
我的习惯是给每张表做一张字段说明表,列出字段名、类型、长度、是否主键、是否外键、注释。这件事花不了半小时,但能让老师第一眼觉得你专业。可以用一个简表:
| 字段名 | 类型 | 说明 | 约束 |
|---|---|---|---|
| book_id | INT | 图书ID | 主键自增 |
| book_name | VARCHAR(100) | 书名 | 非空 |
| stock | INT | 库存 | 非空,默认1 |
最后说个我个人踩过无数次、现在变成习惯的规则:一切以“能复现”为准。报告里的SQL要自己跑过,代码里的连接池参数要按实际环境写,README里别写别人机器上没有的路径。这样哪怕功能朴素,老师也能看到你训练过工程交付的痕迹。希望帮到你。
本文还有配套的精品资源,点击获取