
简介一份面向重庆大学数据库系统课程 Project2 的完整工程包适合正在完成该课程设计或希望学习数据库系统实现原理的本科生、研究生使用。工程基于 Maven 构建核心代码以 Java 源码及编译后的 class 文件呈现配合 Maven 的 XML 配置管理依赖并内含 JUnit、Calcite 等 jar 库可直接导入 IDE 运行压缩包共 51 个文件除源码与配置外还附有说明文档整体约 9.92MB目录结构清晰便于按模块对照学习。目前已有 64 人浏览/学习在同类课程作业中具有一定参考价值。整个项目经过严格测试拿到后即可运行复现通过阅读源码、测试用例与工程配置可以快速理解数据库系统的解析、优化或执行等关键模块并在此基础上扩展二次开发。资源同时涵盖 IDE 工程配置与依赖管理文件导入 IntelliJ IDEA 等环境即可查看完整结构适合课程报告、期末大作业、项目实训、学科竞赛等场景。 每年数据库系统课的项目二一发布压缩包的命名基本都是一个套路“学校课程名project2日期.zip”。重庆大学版本的这个压缩包我见过不少同学交上来各种版本也看过很多人在群里问“里面这些东西到底怎么用”。其实这个项目二本质就是把数据库设计、SQL编写、JDBC联调、事务处理这一整条链路串起来做一个完整的小型系统只不过很多同学一上来就被压缩包里的文件结构、环境配置和一堆报错劝退了。这篇内容我不打算复述什么官方文档也不讲那种“第一步第二步第三步”的悬浮式教程。我想从拿到zip文件开始按实际推进项目的路径把每一环节的关键决策、底层原理和我自己的踩坑经历都摊开讲一遍。无论你是正在写这个project2还是其他学校的数据库课程项目二只要任务是“设计数据库写SQL用Java/C/Python连库做功能”这篇内容都值得你花十分钟看完。1. 拿到project2.zip的第一天先别急着写代码我见过太多人拿到压缩包的第一反应是双击解压然后立刻打开里面某个代码文件开始读读了一会儿发现看不懂又去打开SQL脚本然后陷入“这到底要我做什么”的迷茫。这个顺序是错的。正确的第一步是把整个压缩包的文件结构理清楚分清哪些是参考资料、哪些是必须交付的、哪些是老师预先搭好的脚手架。1.1 解压之后先找这三类文件一个典型的project2.zip里通常会有下面这些内容我按优先级排个序任务书或项目说明可能是PDF、MD或者一个README文本这里藏着所有考核点、功能要求和交付形式不看它等于裸奔上考场。SQL脚本常见命名是schema.sql、init.sql、create.sql这类里面是建表语句和初始数据这部分可能是老师给定的也可能需要你自己设计。代码工程目录如果是Java项目就是src目录如果是Python就可能是一堆.py文件。这部分通常是半成品留好了接口或者TODO注释需要你补齐业务逻辑。这三类文件一定要按“任务书 → SQL脚本 → 代码工程”的顺序看。原因很简单任务书告诉你“做什么”SQL脚本和代码工程告诉你“已经有什么、要改哪里”。如果反着来你会在别人已经写好的代码里迷失方向然后忍不住去删改那些其实不该动的东西。1.2 环境准备清单版本匹配比你想的更讲究这一环节最容易被忽略但也是后面所有痛苦的根源。我建议你建一个环境清单逐项核对组件版本建议说明数据库服务端MySQL 8.x 或 MariaDB 10.5语法规整窗口函数可用能跑大部分任务JDKJDK 8 或 11别用太新的版本某些老JDBC驱动兼容性差JDBC驱动对应版本的mysql-connector-j5.1.x和8.0.x的URL写法有差异连接串jdbc:mysql://localhost:3306/数据库名?useSSLfalseserverTimezoneAsia/Shanghai8.x驱动不配serverTimezone会直接报错字符集utf8mb4如果不设置中文数据插入后乱码的概率极高这里面最容易坑人的就是时区参数和驱动版本不匹配。我第一次跑通这个项目时用的MySQL 8.0.33驱动连接串里忘了加serverTimezone结果连接时报了个“The server time zone value ‘йʱ’ is unrecognized”的错光这个报错就卡了我一下午。所以你如果用的是MySQL 8.x的驱动连接串里直接把这行参数抄进去jdbc:mysql://localhost:3306/project2?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai至于数据库版本我个人建议直接用MySQL 8.x。数据库系统概论教材很多学校用第六版里讲的一些窗口函数、CTE表达式在MySQL 8.0里都能直接跑写SQL阶段会很省事。2. 把需求拆成能落地的数据模型project2的数据模型设计是整个项目的地基地基歪了后面写多少代码都救不回来。我见过有人拿到任务书之后连ER图都不画直接打开编辑器就开始写CREATE TABLE。这种做法不是说一定错但大概率会在做到一半的时候发现表缺字段、关系理不清然后反复修改最后反而更慢。2.1 从任务书里提炼实体与联系以典型的“学生选课/图书借阅/订单管理”这类系统为例任务书里会出现很多名词你要做的第一件事是把这些名词分类哪些是实体需要建表的对象哪些是属性实体下面的字段哪些只是查询条件不需要单独建表。这一步对应的就是教材里ER模型的建模过程实体用矩形、属性用椭圆、联系用菱形。我当时做project2时习惯先把所有名词写在纸上然后用连线把关系画出来。这个过程看起来很原始但它能让你在动手建表前就发现很多问题比如“一个学生可以选多门课一门课也可以被多个学生选”这种多对多关系如果一开始没画清楚后面必然会出现冗余表或者丢失关联表的问题。常见的实体和联系不外乎这几种1对多一个班级有多个学生学生表里通过班级ID关联班级表。多对多学生和课程需要一个中间表选课表来把两者的关系拆成两个1对多。1对1用户和身份证信息这种在实际项目中很少被拆成两张表如果任务书里没有强要求建议直接合并。2.2 关系模式设计中的三个决策点把ER图转换成关系模式的时候有三个点值得停下来想一想这三个点也是评分老师喜欢看设计文档时打钩的地方第一个决策点主键用业务字段还是代理主键。比如学生表可以用学号当主键也可以加一个自增的id字段当主键。学号是天然的业务主键看起来很方便但如果学校规定学号要支持修改比如学籍调整那所有引用学号的外键都要跟着改。稳妥做法是加一个自增id作为代理主键学号做成唯一索引。数据库系统课程项目一般规模不大两种做法都能跑但在设计文档里写清你选某一种的理由会显得你真正理解了这个决策背后的代价。第二个决策点外键约束到底开不开。这个很多教材没细讲但实际工程里特别重要。开了外键约束数据一致性有保障但插入和删除时要做额外检查而且删除父表记录时会受到子表限制。对于project2这种课程项目我建议把外键约束开着因为任务书考核点里大概率会有“验证数据完整性”这一项开着外键能帮你应付演示时的提问“怎么保证选课记录里不会出现不存在的学生”第三个决策点第三范式要不要遵守。教科书上讲BCNF、第三范式讲得很严肃但实际项目里完全守范式的代价很大。比如订单表里按范式设计应该只存用户ID查用户名时要连用户表但如果你有一个功能模块是查订单列表并显示用户名连表查询写起来就绕了一层。project2的任务书一般不会强制要求反范式但如果你愿意在订单表里多存一个“下单时用户名”字段然后在设计文档里说明这是为了“降低高频查询的连表开销”这种处理在评分时是可以加分的。2.3 物理设计与索引取舍数据模型设计完了就是建表吗还差一步物理层面的设计。这一步要做两件事定字符集、定索引。字符集不用多说直接utf8mb4。索引才是值得花心思的地方。你不需要给每个字段都建索引那会拖慢插入速度。优先考虑三类字段一是作为查询条件的字段比如“按课程名称查成绩”那课程表里的名称字段适合建索引二是经常被排序的字段比如按时间字段倒序查最新记录三是外键字段因为外键关联时数据库要按外键值去子表查没索引会很慢。有一个典型的反例网上很多建表教程里会在订单表的“状态”字段上建一个普通索引。但如果这个字段只有三五个取值待支付、已支付、已发货、已完成那索引的选择性太差数据库根本不会用它反而白白占用空间。这种字段真的不需要索引或者最多做复合索引的最后一列。设计索引时想清楚“这个字段到底是不是SQL的过滤条件”比盲目建索引重要得多。3. 建表与初始化数据很多人在这里翻车到了写CREATE TABLE这一步你可能会觉得终于进入正题了。但实际上这里是最容易被扣分的环节之一。我之前帮学弟学妹做代码Review发现大家的建表语句普遍存在三个问题外键依赖顺序不对、约束不完整、初始化数据数量太少。3.1 建表语句的执行顺序有硬性要求只要开了外键约束建表顺序就必须遵循依赖关系。也就是说先建被引用的表再建引用别人的表。比如你有一个院系表和一个学生表学生表外键指向院系表那必须先执行CREATE TABLE院系再CREATE TABLE学生。如果你按“想到哪建到哪”的顺序来大概率会报错ERROR 1215 (HY000): Cannot add foreign key constraint解决这个报错的思路有两个一是严格按照依赖顺序执行二是先不写外键约束等全部表建完之后用ALTER TABLE把外键补上。我个人的习惯是第一种因为建表脚本本身就承载了“记录表结构”的职责把外键关系放在CREATE TABLE里后续维护的人只看这一个文件就明白表之间的关系不用来回找ALTER语句。另外如果要重新建表你还需要想清楚删除顺序。有外键关系的表删除顺序和建表顺序相反先删子表再删父表。很多同学图省事用一句DROP TABLE把所有表名全部列出来结果报错提示外键约束导致无法删除。一个实用的技巧是在建表脚本的开头加一段SET FOREIGN_KEY_CHECKS 0; DROP TABLE IF EXISTS 所有表名; SET FOREIGN_KEY_CHECKS 1;这个写法放在本地开发环境没问题但如果你要提交给老师做自动化评测最好还是把外键检查开关的语句去掉或者脚本里严格按依赖顺序DROP毕竟自动化评测脚本不一定允许你关闭约束检查。3.2 约束定义得越细后面代码越省心很多同学建表只写字段名和类型主键一标就完事了。这样做在前期写SQL时确实没什么感觉但等你做JDBC联调时就会发现程序里需要大量判断空值、重复值代码写得又臭又长。问题出在建表时没把约束定义完整。我建议每个字段都问自己几个问题这个字段能不能为空有没有默认值取值范围是什么有没有唯一性要求例如用户表里的邮箱字段如果业务上要求一个邮箱只能注册一个账号那就直接加上UNIQUE约束应用层就不用写一遍查重了。学生表里的性别字段如果只允许“男/女/其他”可以用ENUM或者CHECK约束MySQL 8.0.16之后CHECK才真正生效。这类约束写在数据库里比写在Java代码里靠谱得多因为任何入口的数据写入都会被统一拦截。3.3 初始化数据数量太少会坑死自己project2的交付物里通常要求附带初始化数据用于演示。但很多同学测试时只用三四条数据看起来功能都能跑等到验收时老师随口说一句“你给我查一下选课人数超过3人的课程”才发现自己的SQL在真实数据下性能惨不忍睹或者聚合结果根本不对。造测试数据是有诀窍的。第一数据量往多了造每个核心表至少百条以上关联表比如选课记录造到千条级别这样写聚合查询、分组统计时才有真实感。第二数据要模拟真实分布比如学生分布在多个班级而不是集中在某一个班级课程要有热门和冷门的区分。第三可以考虑用存储过程或者脚本批量随机生成数据但要注意随机生成的数据更有可能踩到边界条件比如同一天多笔订单、同一学生多次退选这些恰恰是测试SQL逻辑的好用例。4. 核心SQL与存储过程的取舍数据模型和初始数据搞定后就进入SQL编写阶段了。project2对SQL的要求通常集中在几种场景多表连接查询、分组聚合统计、子查询或视图、触发器或存储过程。这个阶段不需要把任务书里的每个功能平均用力我按实际评分权重给你做个参考。4.1 多表连接与分组聚合是绝对主力数据库系统概论第六版的SQL章节里连接查询和分组查询是重点中的重点project2的考核绝大多数也落在这里。典型的需求是“查询每门课程的选课人数并按人数降序排列”对应的SQL长这样SELECT c.course_id, c.course_name, COUNT(sc.student_id) AS cnt FROM course c LEFT JOIN student_course sc ON c.course_id sc.course_id GROUP BY c.course_id, c.course_name ORDER BY cnt DESC;有两个地方要特别注意。第一JOIN类型选对。如果你用INNER JOIN那些“没有学生选的课程”就被过滤掉了但如果需求是“每门课程的选课人数包括没人选的课”就必须用LEFT JOIN让没匹配上的课程也在结果里保留COUNT是0。第二SELECT后面出现的非聚合字段course_id、course_name要出现在GROUP BY里否则在ONLY_FULL_GROUP_BY模式MySQL 5.7.5默认开启下会直接报错。这个错误非常常见几乎所有同学在写分组的SQL时都踩过。如果任务书里再难一点会考HAVING和WHERE的区别。简单说WHERE是在分组前过滤行HAVING是在分组后过滤分组。比如“查询选课人数超过10人的课程”这个“10人”是分组之后才能得到的数量所以只能放在HAVING里。SQL的执行顺序是FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY这一条记牢了写复杂查询时就能避免很多逻辑错误。4.2 视图、触发器、存储过程能用但别滥用project2的加分项往往包括视图、触发器和存储过程。我见过有些同学为了展示技术水平一口气建了十几个触发器结果程序一插入数据就触发一堆连锁操作最后数据被改得面目全非。这其实是没搞清楚触发器的定位。触发器适合做“一定会在写入时发生的”逻辑比如写日志、维护统计字段。一个典型场景选课表里每插入一条选课记录课程表的已选人数自动加1。这个用触发器实现确实合适因为不管你从哪个入口写入都会触发更新。但如果你的更新逻辑只在特定业务场景下才执行那写进触发器反而是负担。存储过程和函数的取舍也类似。如果你的项目里有一段SQL要在多个地方复用比如“根据学生ID查询成绩单”写成存储过程或函数会清爽很多。但如果只是一次性查询就没必要为了封装而封装。评判标准就一条这个SQL会不会被多处重复调用会就封装不会直接写在JDBC代码里。4.3 相关子查询与EXISTS的实战场景project2的高阶SQL里可能会考相关子查询。这里有个常见的误区用EXISTS还是IN。教材上会告诉你两种都能实现“包含”语义但实际性能差别很大。如果子查询的结果集很大而外层表较小IN的写法性能往往更差反过来如果子查询的结果集很小IN就够用。在MySQL 8.0里优化器其实做了很多改写但这个基本判断原则仍然适用。一个经典场景是“查询没有选过任何课程的学生”SELECT s.student_id, s.student_name FROM student s WHERE NOT EXISTS ( SELECT 1 FROM student_course sc WHERE sc.student_id s.student_id );这里用相关子查询对每个学生到选课表里查一遍有没有记录。数据量小的时候感觉不出来数据量一大你就会发现这个SQL会遍历外层表然后对每个学生执行一次内层查询。但只要你给student_course表的student_id建立了索引这个写法的性能在课程项目的数据规模下完全够用。5. JDBC联调与事务处理SQL写好了下一步就是通过代码去调用它。如果你们的project2要求用JavaJDBC来做那这一章的内容你要重点看如果用的是Python、C或者其他语言核心思想也类似只是API不同。5.1 JDBC连接代码的规范写法网上能找到的JDBC连接示例九成都是把Connection、Statement、ResultSet混在一个方法里用完不关连接只在main函数里跑一下。这种代码在课程项目里能跑但代码审查时非常容易被挑毛病。规范的做法是写一个专门的数据库工具类负责加载驱动、获取连接、关闭资源。最简单的模板长这样public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/project2?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/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) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }这里Class.forName的作用是加载驱动类到JVMMySQL 8.x的驱动类名是com.mysql.cj.jdbc.Driver老版本是com.mysql.jdbc.Driver。如果驱动版本和类名不匹配运行时会报ClassNotFoundException排查方法就是确认maven或jar包里的驱动版本。5.2 PreparedStatement安全性和性能都靠它JDBC里执行SQL有两种基础方式Statement和PreparedStatement。project2的代码里如果出现Statement大概率会被扣分原因是SQL注入。举个例子登录功能里拼接SQLString sql SELECT * FROM user WHERE username username AND password password ;如果用户输入的用户名是admin --那这条SQL就变成了SELECT * FROM user WHERE username admin -- AND password ...后面的条件被注释掉攻击者不需要密码就能登录。PreparedStatement之所以安全是因为它把SQL结构和参数分开了先让数据库编译SQL模板再把参数以纯数据的方式传进去参数里的任何引号都不会被当成SQL语法。写法如下String sql SELECT * FROM user WHERE username ? AND password ?; PreparedStatement ps conn.prepareStatement(sql); ps.setString(1, username); ps.setString(2, password); ResultSet rs ps.executeQuery();除了安全性PreparedStatement还有预编译优化。同一段SQL执行多次时数据库可以复用执行计划性能比Statement高。所以不管从哪个角度看都应当用PreparedStatement。5.3 事务边界什么时候commit什么时候rollbackproject2的业务场景里一定有几个操作需要事务保证。典型的就是“学生退课”要从选课表里删除记录又要把课程表的已选人数减一。这两步要么都成功要么都失败。如果在第一步删除之后、第二步更新之前程序崩溃了数据就处于不一致状态。JDBC默认是自动提交模式也就是说每条SQL执行结束后立即提交。要手工控制事务得先把自动提交关掉Connection conn DBUtil.getConnection(); try { conn.setAutoCommit(false); // 第一步从选课表删除 String sql1 DELETE FROM student_course WHERE student_id ? AND course_id ?; PreparedStatement ps1 conn.prepareStatement(sql1); ps1.setString(1, studentId); ps1.setString(2, courseId); ps1.executeUpdate(); // 第二步课程表选课人数减一 String sql2 UPDATE course SET selected_count selected_count - 1 WHERE course_id ?; PreparedStatement ps2 conn.prepareStatement(sql2); ps2.setString(1, courseId); ps2.executeUpdate(); conn.commit(); } catch (SQLException e) { conn.rollback(); e.printStackTrace(); } finally { conn.setAutoCommit(true); conn.close(); }这里有个容易被忽略的细节事务隔离级别。数据库系统概论里讲到多个事务并发访问时会遇到脏读、不可重复读、幻读问题而这取决于隔离级别。MySQL默认的隔离级别是REPEATABLE READ这个级别能避免脏读和不可重复读但对幻读只是部分解决。如果你在project2里做并发演示发现两个事务同时操作同一张表时结果和自己预期不一致优先检查是否有事务一直没提交导致另一个事务读到了旧数据快照。还有一点事务里如果锁了行后面又忘了提交很容易造成死锁或锁等待超时。有一个排查技巧本地如果出现Lock wait timeout exceeded这个报错执行下面这条SQL查一下当前有哪些事务在跑SELECT * FROM information_schema.INNODB_TRX\G看看trx_state字段是不是RUNNING在开发阶段如果发现事务挂着不结束大概率就是代码里漏了commit或者rollback。6. 从本地跑通到验收演示踩坑实录与查漏补缺清单最后一程不是代码写完就万事大吉了。我见过太多人本地跑得飞起一到验收现场就翻车。翻车的原因大多是环境差异、数据量差异、操作顺序差异。这一节我把自己和身边人踩过的坑集中列一下当做一份查漏补缺的清单。6.1 时区、字符集、中文乱码的连锁反应如果验收现场的数据库版本和你本地不一致第一个炸掉的往往是中文数据。核心检查点有三个数据库本身的字符集是utf8mb4表的字符集是utf8mb4连接串里指定了characterEncodingutf8。这三者缺一不可。另一个常见问题是在Windows下用cmd窗口执行SQL脚本因为cmd默认代码页是GBK如果你把含有中文的脚本用UTF-8编码保存然后在cmd里用source命令导入就会出现乱码。解决方法是执行SQL脚本前先执行chcp 65001把代码页切到UTF-8或者直接用Navicat/DBeaver这类图形化工具导入脚本它们对编码的处理通常更省心。6.2 大数据量下的慢查询处理验收演示时老师有可能直接在数据量比较大的表上跑复杂查询这时候如果响应特别慢场面会很尴尬。解决慢查询的思路很清晰先定位慢SQL再针对性优化。MySQL里可以开启慢查询日志也可以直接在客户端执行EXPLAIN来查看执行计划EXPLAIN SELECT ...;重点关注type列。如果出现ALL说明是全表扫描大概率缺索引如果是ref或range说明走了索引一般问题不大。还有一个值是rows它表示预估扫描行数你理想的情况是rows尽量接近最终返回的行数。如果发现某条查询全表扫描且业务上确实高频那就回到建表脚本里补一个索引然后在设计文档里把这次优化记录为“根据EXPLAIN结果对查询进行索引优化”。6.3 并发演示前务必做一次“双开测试”如果project2要求演示并发场景比如多人同时抢一门课的剩余名额有一个必踩的坑就是更新丢失。你可以在本地点开两个终端同时开启两个事务对同一条记录做更新观察最终结果。基于“读-改-写”的代码逻辑大多数情况下结果都会偏离预期。正确的处理方式是在更新语句里加条件让更新操作本身具备原子性。比如“课程剩余名额减一”可以写成UPDATE course SET remaining remaining - 1 WHERE course_id ? AND remaining 0;这种方式在数据库层面保证了一旦剩余名额为0后续更新操作的受影响行数就是0程序根据update返回的row count判断是否选课成功。这也体现了数据库系统课程里强调的“原子性”和“一致性”在实际代码中的落地以数据为中心而不是以程序代码为中心。6.4 提交前必须走一遍的完整测试路径最后我要给一份我在项目二提交前都会走的检查清单你可以按这个顺序在本地走一遍确认无误再打包交付建表脚本能不能在全新的空数据库上完整执行一遍且不报错。初始化数据是否足够覆盖所有查询场景每条SQL查询都有返回结果。所有JDBC功能是否在“关闭数据库服务→重启服务”后仍然正常工作排除依赖缓存假跑的代码。在同一套代码下换一个端口或换一个数据库实例能否通过修改配置文件实现切换而不是改动源代码。事务相关操作是否做了异常测试比如人为制造第二步SQL报错观察第一步是否被正确回滚。代码里是否有资源泄漏所有Connection、Statement、ResultSet是否在finally块或try-with-resources中关闭。以上六项只要有任意一项不过关都可能成为验收现场扣分的理由。我见过有人因为建表脚本里一个分号没写导致老师现场导入时屡屡报错最后手忙脚乱。这种问题不是技术难度问题纯粹是缺少“打包前自查”这一步。说到底project2这个压缩包考察的从来不只是“你会不会写SQL”而是你能否从头到尾把一个数据库应用从零落地并让它稳定运行。这一点想明白了你的项目二通关之路就顺了一半。本文还有配套的精品资源点击获取