小型MIS实验报告全流程:从ER模型到SQL建表与JavaWeb实现

发布时间:2026/10/6 4:31:43
小型MIS实验报告全流程:从ER模型到SQL建表与JavaWeb实现 简介这是一份南京邮电大学数据库系统课程的小型MIS开发实验报告适合正在学习数据库设计、C/S或B/S结构开发以及ODBC/ADO编程的高校学生参考。报告以航班信息管理系统为例完整记录了从创建数据库、建立四张核心数据表到添加索引、插入数据、创建视图与存储过程的实现过程并包含部分SQL语句与运行思路。压缩包内共1个doc文件大小约372KB内容集中、便于直接阅读和对照练习。该资源已有241人浏览学习可作为课程设计、期末实验或复习备考的参考资料帮助理解如何通过界面访问数据库并完成小型信息系统的整体开发流程。1. 小型MIS实验报告真正拉开分差的不是SQL而是需求边界数据库系统概论这门课的小型MIS开发实验十个人里有八个把精力全砸在写SQL和调界面上结果交上去的报告被老师一句话打回“你这个流程没闭环。”我做这类实验报告的评审和答辩辅导做了不少最直观的感受是能拿高分的报告往往不是SQL写得有多花哨而是从需求分析、ER模型到关系模式、再到建表脚本和页面操作整条链路是自洽的。老师要证明的是你有没有真的“开发过”一个管理信息系统而不是背了几条SELECT语句。这篇文章就把我从零做完一份南邮风格数据库课程实验报告的完整路径摊开来讲适合正在赶课设的学生也适合刚入职想补数据库落地常识的后端开发。2. 从题目到ER模型实体、属性和联系先画对再谈建表2.1 小型MIS的业务边界怎么识别单角色、单部门、5到10张表小型MIS的“小”字是有边界的。它不像ERP要管采购、库存、财务、人力十几个模块也不需要多个角色带着复杂权限协作。一个合格的实验级MIS通常就是一个部门内部、单业务流程、主要角色的操作闭环。拿最经典的“学生选课管理系统”来说业务链路就是学生登录、浏览课程、选课、教师录入成绩、学生查成绩、管理员做统计。这条链路上的名词是实体动词是联系。识别实体有个很土但好用的办法把业务流程写成一句话然后把句子里的名词全部圈出来。比如“学生选择课程教师给学生的选课记录录入成绩”这里“学生”“课程”“教师”“选课记录”“成绩”就是候选实体。再筛一遍把纯属性词去掉——比如“学号”是学生的属性不是实体“课程名”是课程的属性这样就能得到一张干净的五实体清单。候选实体出来后下一步是确定实体之间的联系方式。学生和课程是多对多必须拆成“选课记录”这张联系表这件事做对了后面建表就不会出现大量重复数据。教师和学生之间不直接发生关系教师和选课记录是一对多。课程和成绩是一对一但成绩一般作为选课记录的属性存在不需要单独开一张表。把这些联系标清楚ER图的骨架就有了。我用一个表格把这一层的设计结果固定下来后面所有脚本都以这张表为准这也是实验报告里“需求分析”章节最有含金量的部分。实体主要属性主键策略说人话的解释学生学号、姓名、学院、专业、入学年份自然键学号学号是全校园唯一的不需要自增教师工号、姓名、学院、职称自然键工号同上课程课程号、课程名、学分、开课学院自然键课程号课程号由教务处统一编码选课记录选课ID、学号、课程号、选课时间、成绩代理键自增ID学号课程号联合做业务唯一键管理员账号、密码、姓名、角色代理键自增ID密码不要明文存实验里至少做个MD52.2 把ER图转成关系模式主键、外键与联系表的落地规则ER图转关系模式教材《数据库系统概论》第六版里有标准规则但实验里真正要记住的只有四条。第一每个实体一张表主键如实落下来。第二一对多联系把“一”方的主键作为外键加到“多”方表中比如教师和选课记录选课记录表里加teacher_id。第三多对多联系必须单独建联系表选课记录就是这么来的它同时持有学生和课程的两把外键。第四属性继承不能乱来比如成绩只属于“学生选某门课”这次行为它该落在选课记录表里而不是学生表或课程表。做完这个映射五张表的关系模式我习惯用纯文本写出来方便贴到报告里做“逻辑设计”章节的配图说明。选课管理系统的关系模式如下学生学号姓名学院专业入学年份 教师工号姓名学院职称 课程课程号课程名学分开课学院授课教师工号 选课记录选课ID学号课程号教师工号选课时间成绩 管理员管理员ID账号密码姓名这里有个常见争议课程表里为什么要放“授课教师工号”课程和教师是典型的多对一一门课由一个老师上一个老师可以上多门课。把授课教师工号沉到课程表里查询“某位老师上了哪些课”就不需要额外的联系表。实验报告里把这一条写得清楚老师会认为你真正理解了一对多外键的语义。关系模式定了就可以开始构思建表脚本了。但在写CREATE TABLE之前我强烈建议把每张表的每一列定义好类型、约束、默认值做成一份“列定义清单”。这一份清单会在写SQL和文档时反复用到也能避免后面代码、报告、数据库三方字段对不上的血泪问题。列级约束的思考顺序是能否为空、是否唯一、默认值是什么、有没有取值范围四者想清楚一张表就稳了。3. 标准SQL建库建表DDL脚本、约束顺序与三个常用对象3.1 建库与建表字段类型、默认值、CHECK约束的实战选择写DDL的顺序是有讲究的必须先建被引用的父表再建持有外键的子表。按上面的五张表顺序应该是管理员、学生、教师、课程、选课记录。如果把课程表和选课记录的建表顺序写反脚本一执行就会报“无法添加外键约束”很多人的建表脚本翻车都是这个原因。MySQL 8.x下的建库建表脚本范例如下CREATE DATABASE IF NOT EXISTS course_select_system DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci; USE course_select_system; CREATE TABLE student ( student_id VARCHAR(20) PRIMARY KEY COMMENT 学号自然主键, student_name VARCHAR(50) NOT NULL COMMENT 姓名, college VARCHAR(50) NOT NULL COMMENT 学院, major VARCHAR(50) NOT NULL COMMENT 专业, enroll_year INT NOT NULL COMMENT 入学年份 ) ENGINEInnoDB COMMENT学生表; CREATE TABLE teacher ( teacher_id VARCHAR(20) PRIMARY KEY COMMENT 工号, teacher_name VARCHAR(50) NOT NULL COMMENT 姓名, college VARCHAR(50) NOT NULL COMMENT 学院, title VARCHAR(20) DEFAULT 讲师 COMMENT 职称 ) ENGINEInnoDB COMMENT教师表; CREATE TABLE course ( course_id VARCHAR(20) PRIMARY KEY COMMENT 课程号, course_name VARCHAR(100) NOT NULL COMMENT 课程名, credit DECIMAL(3,1) NOT NULL CHECK (credit 0) COMMENT 学分, college VARCHAR(50) NOT NULL COMMENT 开课学院, teacher_id VARCHAR(20) NOT NULL COMMENT 授课教师工号, CONSTRAINT fk_course_teacher FOREIGN KEY (teacher_id) REFERENCES teacher(teacher_id) ) ENGINEInnoDB COMMENT课程表;先解释字符集的选择。utf8mb4不是utf8的“升级版推荐”而是真正能存下emoji和完整中文生僻字的编码。实验报告里数据量极小不存在utf8mb4比utf8多占空间的取舍问题直接统一就行。字段类型上学号、工号、课程号这种编号一律用VARCHAR而不用INT原因是它们不参与数值运算而且可能带前缀字母INT一旦遇到“CS101”这种值就直接炸了。学分用DECIMAL(3,1)因为学分有0.5这种精度用FLOAT会在后续统计里产生莫名其妙的精度差。外键约束是实验报告里必须体现的点但也是最多人写错的地方。fk_course_teacher这条约束的名字要有语义约束必须落在子表。这里选课记录表还没建所以先不写。另外需要注意如果你的MySQL在导入时反复报外键错误可以先执行SET FOREIGN_KEY_CHECKS 0导入完成后再恢复为1但这个操作必须在实验报告里用文字注明原因否则答辩时老师会认为你不懂外键为什么存在。选课记录表是整份设计的核心它的建表脚本最能体现水平CREATE TABLE enroll_record ( enroll_id INT AUTO_INCREMENT PRIMARY KEY COMMENT 选课记录ID代理主键, student_id VARCHAR(20) NOT NULL COMMENT 学号, course_id VARCHAR(20) NOT NULL COMMENT 课程号, teacher_id VARCHAR(20) NOT NULL COMMENT 教师工号冗余自课程表, enroll_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 选课时间, score DECIMAL(5,2) DEFAULT NULL COMMENT 成绩未录入时为空, UNIQUE KEY uk_student_course (student_id, course_id), CONSTRAINT fk_enroll_student FOREIGN KEY (student_id) REFERENCES student(student_id), CONSTRAINT fk_enroll_course FOREIGN KEY (course_id) REFERENCES course(course_id), CONSTRAINT fk_enroll_teacher FOREIGN KEY (teacher_id) REFERENCES teacher(teacher_id) ) ENGINEInnoDB COMMENT选课记录表;这里有一个关键设计uk_student_course联合唯一键。它保证同一个学生不能重复选同一门课这个约束在应用层做要写一堆判断代码在数据库层做只需要一行UNIQUE KEY。教师工号在选课记录里显得冗余但它不是真正的冗余——它把“某老师教某门课”这个事实固化到选课记录里查询成绩单时少一次连表在实验报告里说明这个“受控冗余”的理由会是很漂亮的加分项。3.2 视图、索引与触发器小型MIS里真正会用的三个对象建完表之后基本CRUD谁都能写但实验报告要拿高分必须展示视图、索引、触发器三类对象。视图不是摆设它能把“学生选课成绩明细”这种高频三表连接查询封装成一个虚拟表让页面代码只做简单的SELECT * FROM view_name。索引的选位也有规律不要给每一列都建索引实验数据量虽然小但建索引的思路必须写明外键列必建WHERE条件高频列建低区分度的性别、学院这种列不要建。CREATE VIEW v_enroll_detail AS SELECT e.enroll_id, s.student_id, s.student_name, c.course_id, c.course_name, t.teacher_name, e.enroll_time, e.score FROM enroll_record e JOIN student s ON e.student_id s.student_id JOIN course c ON e.course_id c.course_id JOIN teacher t ON e.teacher_id t.teacher_id; CREATE INDEX idx_enroll_student ON enroll_record(student_id); CREATE INDEX idx_enroll_course ON enroll_record(course_id); CREATE INDEX idx_course_teacher ON course(teacher_id);视图的意义不在于“快”而在于把复杂的连接逻辑和字段语义封装好让页面开发者不关心底层表结构。索引的说明在报告里要写清楚“为什么在选课记录的student_id上建索引”因为业务查询多是“某个学生的选课列表”时这张表的查询过滤条件就以student_id为主。如果实验里做了“课程选课人数统计”那course_id上的索引也能派上用场。触发器是小型MIS实验里最能体现“数据库编程”能力的对象但也最容易写翻车。以选课人数限制为例假设课程表里有一个capacity字段每次插入新选课记录时自动判断是否已满。这个逻辑用触发器做看起来很高端但对初学者来说游标、条件判断、错误信号任何一个环节出错都会导致整个插入失败且排错困难。如果实在想展示触发器建议选一个简单且不易出错的场景给选课记录写审计日志。CREATE TABLE enroll_log ( log_id INT AUTO_INCREMENT PRIMARY KEY, enroll_id INT NOT NULL, action VARCHAR(10) NOT NULL, log_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE TRIGGER trg_enroll_after_insert AFTER INSERT ON enroll_record FOR EACH ROW BEGIN INSERT INTO enroll_log(enroll_id, action) VALUES (NEW.enroll_id, INSERT); END;这个触发器的逻辑固定、表结构简单比做库存拦截靠谱得多。写触发器时注意MySQL的默认DELIMITER设置在客户端执行多语句触发器时先执行DELIMITER //把语句分隔符临时换掉否则分号会在中途截断整个定义。很多人在Navicat里执行触发器报错十有八九是没处理DELIMITER的问题。4. 页面和数据连起来JavaWeb最小实现路径与事务边界4.1 JDBC连接与DAO层连接资源别在循环里开南邮这类课程实验前端技术栈通常是JavaWebJSPServlet或者Java Swing。虽然标题里没限定语言但实验室环境和评卷习惯上Java系是主流。前端做什么不重要重要的是老师一定会盯住“页面操作能不能落库”。如果连个JDBC都写不干净界面再好看也是白搭。我见过太多人的DAO层长这样在Servlet里直接写Class.forName、DriverManager.getConnection、PreparedStatement、ResultSet然后再DriverManager.getConnection一遍。这等于每来一个请求就新建一次数据库连接性能差且代码全是重复的样板。正确的做法是把连接获取集中在一个DBUtil类里表格里填好URL、用户名、密码再用一个简单的连接池概念管理连接。public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/course_select_system ?useSSLfalsecharacterEncodingutf8serverTimezoneAsia/Shanghai; private static final String USER root; private static final String PASSWORD 123456; static { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { throw new RuntimeException(MySQL驱动加载失败, e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }DBUtil里的URL参数值得逐项解释。useSSLfalse是本地开发时避免MySQL 8.0强制SSL握手导致警告刷屏characterEncodingutf8是保证中文能正确写入数据库这行不写后面必现乱码serverTimezoneAsia/Shanghai是因为MySQL 8.0的驱动要求显式指定时区漏了会直接报连接失败。USER和PASSWORD用常量写在这里实验报告里注明“生产环境必须用配置文件密文”这句话能体现工程意识。接下来写DAO层的标准样板。以学生选课动作为例它需要同时校验课程容量、插入选课记录、更新课程已选人数三个步骤必须在一个事务里完成。DAO层方法接收一个Connection参数由上层统一控制事务边界而不是自己在方法内部开连接和关连接。这样写虽然参数多了一个但代码结构与事务语义都清晰得多。public class EnrollDao { public boolean enroll(Connection conn, String studentId, String courseId) throws SQLException { String sql INSERT INTO enroll_record(student_id, course_id, teacher_id) VALUES (?, ?, (SELECT teacher_id FROM course WHERE course_id ?)); try (PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, studentId); ps.setString(2, courseId); ps.setString(3, courseId); return ps.executeUpdate() 1; } } }这里用了JDBC的try-with-resources语法PreparedStatement无论成功与否都会自动关闭省去finally块里繁琐的null判断。SQL里用子查询从course表带出teacher_id避免页面层再去查一次课程信息。INSERT本身由数据库唯一键约束兜底重复选课会抛DuplicateKeyException不用在DAO里手动判断“是否已选过”。说明一下坑不要在这个方法里调用conn.commit()事务的提交和回滚应该由调用方Service层或Servlet统一管控否则多个DAO方法协作时就没办法保证原子性了。4.2 从页面表单到数据库请求参数解析与事务提交JSP页面里放一个选课表单提交到InsertEnrollServletServlet里做的动作无外乎三步收参数、校验参数、调DAO落库。但在第三步里属于“写操作且涉及多张表”的必须包事务。选课这个动作表面上只插一张表但按业务流程它还应该记录日志、更新统计哪怕现在只做插入也应该养成“单次请求一个事务”的肌肉记忆。WebServlet(/enroll) public class EnrollServlet extends HttpServlet { protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String studentId req.getParameter(studentId); String courseId req.getParameter(courseId); if (studentId null || courseId null || studentId.trim().isEmpty()) { resp.sendError(400, 学号和课程号不能为空); return; } Connection conn null; try { conn DBUtil.getConnection(); conn.setAutoCommit(false); EnrollDao dao new EnrollDao(); boolean ok dao.enroll(conn, studentId.trim(), courseId.trim()); conn.commit(); if (ok) { resp.sendRedirect(req.getContextPath() /success.jsp); } else { resp.sendRedirect(req.getContextPath() /error.jsp); } } catch (Exception e) { if (conn ! null) { try { conn.rollback(); } catch (SQLException ex) { log(回滚失败, ex); } } throw new ServletException(选课失败请稍后重试, e); } finally { if (conn ! null) { try { conn.close(); } catch (SQLException e) { log(连接关闭失败, e); } } } } }把请求参数映射成SQL参数的过程是整条CRUD链路里最容易出低级bug的地方。页面传来的都是字符串先判断null和空字符串再交给DAO做类型转换。setAutoCommit(false)之后任何一步抛出异常finally块里rollback才能生效如果忘了这行前一步插入的数据就会残留在库里下一次调试时你会怀疑是自己的眼睛出了问题。连接记得在finally里关闭否则连接泄漏会把MySQL的连接数耗尽Tomcat在几分钟内就开始报“Too many connections”这是本地开发最常见也最隐蔽的翻车现场。前端这块如果不想写JSP标签库用纯HTML表单加Servlet也可以实验报告里重点描述的是数据流而不是页面效果。一个完整的“新增课程”表单action指向/course/addmethod用post字段名必须和后端req.getParameter里的key完全一致差一个字母查半天。5. 实验报告避坑指南四类导致改版重做的问题与排查方法5.1 建表脚本导入失败外键依赖顺序与约束命名冲突表现把CREATE TABLE复制进Navicat或命令行执行到一半报“Cannot add foreign key constraint”。原因多半有两个一是建表顺序错子表被提前执行二是外键引用的父表主键类型不匹配比如父表是VARCHAR(20)子表却写成了CHAR(20)。解决先把所有父表建完再建子表核对两张表字段的类型和长度完全一致。在MySQL里排查外键错误执行SHOW ENGINE INNODB STATUS从LATEST FOREIGN KEY ERROR段能看到具体是哪个约束在哪个字段上失败不用瞎猜。5.2 页面录入的中文变成问号字符集三处不统一表现JSP页面输入中文数据库里变成???但直接在SQL窗口里INSERT中文又是正常的。原因MySQL客户端连接层的字符集没指定表结构字符集与连接字符集不一致。解决第一步确认数据库建库时用了utf8mb4第二步检查JDBC连接串里有没有characterEncodingutf8第三步检查JSP页面最顶部是否正确写了% page contentTypetext/html; charsetUTF-8 pageEncodingUTF-8%。三处字符集全部统一乱码问题基本消失。如果还乱用SHOW VARIABLES LIKE character_set%;逐个核对系统变量。5.3 选课人数超出上限检查与插入之间的并发空隙表现单机调试时一切正常但压测或多人同时选课时课程已选人数超过capacity也没有被拦住。原因页面/DAO层的流程是“先查人数再插入”两个请求都查到还差一个名额然后都执行插入最后一个插入就超了。解决最直接的办法是事务里使用SELECT ... FOR UPDATE把课程行锁住锁住期间第二个事务会等待第一个事务提交后第二个事务再查到的已选人数就是最新的。实验报告里不需要把隔离级别展开讲但一定要把这个“检查-执行不是原子操作”的结论写明白这是并发场景下的标准踩坑记录。5.4 答辩被问倒报告里的ER图与代码里的表对不上表现报告里字数写得很满ER图画得也漂亮但老师一眼看出选课记录表在报告里是“处理多对多联系”代码里却只有student_id和course_id两个字段没有体现教师维度的冗余设计。还有一种情况是报告中写成绩字段在选课记录表代码里建的表却没有这一列查询成绩的功能用的是另一套临时拼凑的逻辑。解决动手写代码之前先定稿ER图和关系模式每改一次表结构同步更新报告里的关系模式描述和建表脚本答辩前用DESC命令把五张表的实际字段打印出来和报告逐列核对一遍保证文档与数据库完全一致。诚实地说这类“文档和代码脱节”的问题是几乎所有课程实验报告被扣分的最大来源。6. 加分技巧用统计视图和存储过程把报告做出区分度实验报告写到能跑通CRUD只是及格线想拿优秀建议再沉十分钟做一件事加一个统计视图和一个存储过程并用100行左右的模拟数据把结果跑出来。统计视图负责“每个学院选课的平均成绩、最高分、最低分、及格率”这类统计在SQL里写起来不复杂但放在视图里能让页面层变成一个纯展示层。存储过程更适合做“按课程统计选修人数并输出前三名”这种带排序和限制的查询因为MySQL的存储过程在这里能体现参数输入和结果集输出的完整链路更重要的是它让数据库端“可编程”的一面在报告里有了最直观的证据。CREATE VIEW v_college_score_stats AS SELECT s.college, COUNT(DISTINCT e.student_id) AS student_count, AVG(e.score) AS avg_score, MAX(e.score) AS max_score, MIN(e.score) AS min_score, SUM(CASE WHEN e.score 60 THEN 1 ELSE 0 END) / NULLIF(COUNT(e.score), 0) AS pass_rate FROM student s LEFT JOIN enroll_record e ON s.student_id e.student_id GROUP BY s.college; DELIMITER // CREATE PROCEDURE sp_top_courses(IN top_n INT) BEGIN SELECT c.course_id, c.course_name, COUNT(e.enroll_id) AS enroll_count FROM course c LEFT JOIN enroll_record e ON c.course_id e.course_id GROUP BY c.course_id, c.course_name ORDER BY enroll_count DESC LIMIT top_n; END // DELIMITER ;模拟数据不要手写100条INSERT用MySQL 8.0的WITH RECURSIVE生成连续编号再配合UUID或日期函数构造随机内容几十行SQL就能生成足够实验验证的数据集。存储过程执行时用CALL sp_top_courses(5)把执行结果截图放进报告旁边配一段文字解释输入参数top_n的含义和LIMIT的生效方式。真正的加分点是后面这段说明。不要只贴代码老师想看到的是你能讲清楚为什么用视图做统计、为什么存储过程里用LEFT JOIN而不是INNER JOIN——因为没选课的学生课程也要统计为0人而不是被过滤掉。我做了三年类似报告的评审最大的感触是高分实验报告背后从来不是某个惊艳的算法而是每一步都经得起追问的自洽性。视图为什么这么拆、索引为什么建在这列、事务边界为什么画在这里每一处都能讲出道理比堆十个触发器和五层嵌套子查询管用得多。我当年自己交的第一版报告就是吃了“只想着炫技”的亏把触发器、游标、临时表全用上结果整个脚本换个机器就跑不起来后来砍掉一半内容把事务和约束讲清楚反而拿了优秀。先跑通再讲清最后再谈优化这个顺序希望帮到你。本文还有配套的精品资源点击获取