简介:这份数据库课程设计文档面向高校计算机相关专业学生与数据库初学者,围绕“学生选课管理系统”这一经典实践课题,提供从需求分析到系统实现的完整设计思路。内容涵盖学生、教师、管理员三类角色的权限划分,以及学生表、教师表、专业表、课程表、专业课程表、学生课程信息表、班级表等数据字典定义,并给出关系模式、主外键约束与参照关系图。文档还讨论了完整性、安全性、密码加密、索引优化与范式理论等设计要点,并附有SQL聚合统计总分、平均分与排名的实现思路及C#界面代码示例。资源包为1个docx文档,约597KB,结构紧凑,适合作为课程设计报告参考或数据库建模练习的对照材料。目前已有552人学习,可帮助读者快速理清选课管理系统的表结构设计与功能模块划分。
1. 学生选课管理系统:从课程设计到能跑通的数据库实战
学生选课管理系统几乎是每个计算机专业学生绕不开的数据库课程设计题目,但大部分人在动手时才发现,真正难的不是写 C# 界面,而是把选课这件事背后的并发、约束和查询逻辑用数据库讲清楚。这个系统要解决的核心问题是:学生能选课、退课,教师能录入成绩,管理员能管理课程容量,而数据库必须保证同一门课不会被超额选中、同一学生不会重复选同一门课。适合正在做数据库课程设计的学生,也适合想用 C# 加 MySQL 练一遍完整增删改查和事务控制的开发者。下面按实际落地顺序,从建库建表到 C# 连接、事务处理和排错,一步步拆开讲。
2. 需求拆解与数据库表设计:选课系统的实体和关系怎么定
2.1 先确定实体和关系,再动手建表
学生选课管理系统的实体并不复杂,但关系容易理乱。常见做法是拆成四张核心表:学生表、课程表、教师表、选课记录表。学生和课程是多对多关系,选课记录表就是中间表,同时承载成绩字段。教师和课程是一对多,一个教师可以教多门课。管理员不单独建表,用角色字段区分即可。
这里有一个容易翻车的地方:很多人把选课记录直接塞进学生表或课程表,用逗号分隔的课程 ID 存,后期查成绩、统计选课人数时非常痛苦。正确做法是独立出enrollment表,字段包括学生 ID、课程 ID、选课时间、成绩,主键用联合主键或自增 ID 加唯一约束。
选课容量控制也在这张表上做文章。课程表里放一个capacity字段表示容量上限,选课时统计enrollment里该课程的有效记录数,超过就不让选。这个逻辑放在事务里执行,避免并发时超选。
2.2 建库建表的完整 SQL 与字段说明
下面这套 SQL 可以直接在 MySQL 里执行,建库、建表、加约束一次完成。注意字符集用utf8mb4,否则学生姓名里的生僻字会出问题。
-- 创建数据库,字符集用 utf8mb4 支持完整 Unicode CREATE DATABASE IF NOT EXISTS course_selection DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_unicode_ci; USE course_selection; -- 学生表:学号唯一,姓名非空 CREATE TABLE student ( student_id VARCHAR(20) PRIMARY KEY COMMENT '学号', name VARCHAR(50) NOT NULL COMMENT '姓名', gender CHAR(1) DEFAULT 'M' COMMENT '性别 M/F', major VARCHAR(50) COMMENT '专业', password VARCHAR(64) NOT NULL COMMENT '登录密码哈希', created_at DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB; -- 教师表:工号唯一 CREATE TABLE teacher ( teacher_id VARCHAR(20) PRIMARY KEY COMMENT '工号', name VARCHAR(50) NOT NULL COMMENT '姓名', title VARCHAR(30) COMMENT '职称', password VARCHAR(64) NOT NULL ) ENGINE=InnoDB; -- 课程表:容量字段用于选课人数上限控制 CREATE TABLE course ( course_id VARCHAR(20) PRIMARY KEY COMMENT '课程号', course_name VARCHAR(100) NOT NULL COMMENT '课程名', credit DECIMAL(3,1) NOT NULL COMMENT '学分', capacity INT NOT NULL DEFAULT 50 COMMENT '容量上限', teacher_id VARCHAR(20), semester VARCHAR(20) NOT NULL COMMENT '开课学期', FOREIGN KEY (teacher_id) REFERENCES teacher(teacher_id) ) ENGINE=InnoDB; -- 选课记录表:学生和课程联合唯一,防止重复选课 CREATE TABLE enrollment ( id INT AUTO_INCREMENT PRIMARY KEY, student_id VARCHAR(20) NOT NULL, course_id VARCHAR(20) NOT NULL, select_time DATETIME DEFAULT CURRENT_TIMESTAMP, score DECIMAL(5,1) DEFAULT NULL COMMENT '成绩,NULL 表示未录入', status TINYINT DEFAULT 1 COMMENT '1 有效 0 已退课', UNIQUE KEY uk_student_course (student_id, course_id), FOREIGN KEY (student_id) REFERENCES student(student_id), FOREIGN KEY (course_id) REFERENCES course(course_id) ) ENGINE=InnoDB;字段设计里有几个关键点。enrollment表的uk_student_course唯一约束是防止重复选课的第一道防线,比在 C# 代码里查一遍再插入更可靠。status字段用软删除标记退课,而不是直接删记录,这样成绩和选课历史可追溯。score允许为 NULL,表示还没录入成绩,查询时用IS NULL判断。
注意:外键约束在批量导入数据时可能拖慢速度,如果只是做课程设计演示,可以保留;如果数据量大,常见做法是先禁用外键检查再导入,导入后重新启用。
2.3 索引和查询性能的取舍
课程设计的数据量通常不大,但养成加索引的习惯没坏处。除了主键和唯一约束自带的索引,选课记录表上按course_id和student_id分别建索引,能加快“查某门课选了哪些人”和“查某个学生选了什么课”这两类高频查询。
-- 按课程查选课名单 CREATE INDEX idx_enrollment_course ON enrollment(course_id); -- 按学生查已选课程 CREATE INDEX idx_enrollment_student ON enrollment(student_id);索引不是越多越好。每加一个索引,插入和更新都会变慢。选课系统里enrollment表的写入频率高,索引控制在两个以内比较合适。如果发现选课变慢,先看是不是索引缺失,而不是盲目加索引。
3. C# 连接 MySQL 实现选课核心逻辑:连接、事务与并发控制
3.1 用 MySqlConnector 建立数据库连接
C# 连接 MySQL 常见做法是用MySqlConnector这个 NuGet 包,它比旧的MySql.Data在异步和连接池上表现更稳。在项目里通过 NuGet 安装MySqlConnector,然后在代码里用连接字符串连库。
using MySqlConnector; // 连接字符串:服务器、端口、数据库、账号密码、字符集 string connStr = "Server=localhost;Port=3306;Database=course_selection;" + "User=root;Password=your_password;CharSet=utf8mb4;"; // 使用 using 确保连接释放回连接池 using var conn = new MySqlConnection(connStr); conn.Open(); Console.WriteLine("数据库连接成功");连接字符串里的CharSet=utf8mb4必须和建库时一致,否则中文课程名会乱码。using语句保证连接用完自动关闭,实际是归还到连接池,不是真正断开。连接池默认开启,最小连接数 0,最大 100,课程设计场景完全够用。
如果连接报错“无法连接到任何指定的 MySQL 主机”,先确认 MySQL 服务是否启动、端口是否被防火墙拦截、账号密码是否正确。常见坑是 root 用户只允许 localhost 登录,远程连接需要单独授权。
3.2 选课事务:容量检查和插入必须原子执行
选课的核心逻辑是:先查课程已选人数是否小于容量,再插入选课记录。这两步如果分开执行,并发时会出现超选。比如两个学生同时选同一门只剩一个名额的课,都查到人数没满,都插入成功,结果超了一个。解决办法是把检查和插入放在同一个事务里,并对课程行加锁。
public bool SelectCourse(string studentId, string courseId) { using var conn = new MySqlConnection(connStr); conn.Open(); using var tx = conn.BeginTransaction(); try { // 锁定课程行,防止并发修改容量判断 string lockSql = "SELECT capacity FROM course WHERE course_id=@cid FOR UPDATE"; using var lockCmd = new MySqlCommand(lockSql, conn, tx); lockCmd.Parameters.AddWithValue("@cid", courseId); var capacityObj = lockCmd.ExecuteScalar(); if (capacityObj == null) { tx.Rollback(); return false; } int capacity = Convert.ToInt32(capacityObj); // 统计当前有效选课人数 string countSql = "SELECT COUNT(*) FROM enrollment " + "WHERE course_id=@cid AND status=1"; using var countCmd = new MySqlCommand(countSql, conn, tx); countCmd.Parameters.AddWithValue("@cid", courseId); int selected = Convert.ToInt32(countCmd.ExecuteScalar()); if (selected >= capacity) { tx.Rollback(); return false; } // 插入选课记录,唯一约束兜底防重复 string insertSql = "INSERT INTO enrollment(student_id, course_id) " + "VALUES(@sid, @cid)"; using var insertCmd = new MySqlCommand(insertSql, conn, tx); insertCmd.Parameters.AddWithValue("@sid", studentId); insertCmd.Parameters.AddWithValue("@cid", courseId); insertCmd.ExecuteNonQuery(); tx.Commit(); return true; } catch (MySqlException ex) when (ex.Number == 1062) { // 1062 是唯一约束冲突,说明重复选课 tx.Rollback(); return false; } catch { tx.Rollback(); throw; } }这段代码的关键在FOR UPDATE,它会对课程行加排他锁,其他事务在这一行上必须等待,从而保证容量判断和插入之间没有其他事务插队。MySqlException的 1062 错误码对应唯一约束冲突,捕获后返回 false 表示重复选课,不用再查一次数据库。
参数说明:@cid和@sid用参数化查询,避免 SQL 注入。status=1只统计有效选课,退课的记录不计入人数。事务提交前任何异常都要回滚,否则连接归还池时可能带着未提交事务。
3.3 退课和成绩录入的 SQL 写法
退课不是删除记录,而是把status置为 0。这样选课历史保留,成绩也不会丢。
public bool DropCourse(string studentId, string courseId) { using var conn = new MySqlConnection(connStr); conn.Open(); string sql = "UPDATE enrollment SET status=0 " + "WHERE student_id=@sid AND course_id=@cid AND status=1"; using var cmd = new MySqlCommand(sql, conn); cmd.Parameters.AddWithValue("@sid", studentId); cmd.Parameters.AddWithValue("@cid", courseId); return cmd.ExecuteNonQuery() > 0; }成绩录入用 UPDATE,只允许教师给自己教的课程录成绩。SQL 里加一个子查询校验课程归属,避免越权。
UPDATE enrollment e JOIN course c ON e.course_id = c.course_id SET e.score = @score WHERE e.student_id = @sid AND e.course_id = @cid AND c.teacher_id = @tid AND e.status = 1;如果ExecuteNonQuery返回 0,说明没有匹配的记录,可能是学生没选这门课、课程不属于该教师、或者已经退课。排查时按这三个条件逐一核对。
4. 避坑与排查:选课系统开发中最容易翻车的五个地方
4.1 中文乱码:从建库到连接字符串要统一
现象:课程名或学生姓名在 C# 界面显示成问号或乱码。原因:建库时字符集用了latin1,或者连接字符串没指定CharSet=utf8mb4。解决:建库、建表、连接字符串三处字符集必须一致,都用utf8mb4。已经建好的库可以用ALTER DATABASE course_selection CHARACTER SET utf8mb4;修改,但已有数据需要重新导入。
4.2 选课超员:并发下容量检查失效
现象:课程容量 50,实际选了 52 人。原因:容量检查和插入不在同一事务,或者没用FOR UPDATE锁行。解决:按 3.2 的写法,把SELECT ... FOR UPDATE和INSERT放在同一事务里。如果不想用行锁,也可以在enrollment表上加触发器统计人数,但触发器调试麻烦,课程设计里不推荐。
4.3 重复选课:唯一约束没生效或没捕获异常
现象:同一学生同一课程出现两条记录。原因:enrollment表没加UNIQUE KEY,或者加了但 C# 代码没捕获 1062 异常导致程序崩溃。解决:建表时加联合唯一约束,插入时捕获MySqlException且Number == 1062,返回友好提示而不是抛异常。
4.4 连接池耗尽:连接没关或事务没提交
现象:程序运行一段时间后报“超时时间已到,但是未能从池中获取连接”。原因:MySqlConnection没有用using包裹,或者事务异常后没回滚导致连接被占用。解决:所有连接用using,事务用try-catch确保Rollback或Commit一定执行。连接池最大连接数可以在连接字符串里用MaximumPoolSize=50调整,但根本办法是及时释放。
4.5 成绩录入越权:教师改了别人的课
现象:教师 A 能录入教师 B 课程的成绩。原因:UPDATE 语句只按学生和课程过滤,没校验课程归属。解决:UPDATE 时 JOIN 课程表,加c.teacher_id = @tid条件。返回影响行数为 0 就说明越权或记录不存在,前端给出对应提示。
5. 用存储过程和视图把统计查询做利索
课程设计答辩时,老师常会问“怎么查某门课的平均分”“怎么查选课人数最多的课”。这些统计查询如果每次都在 C# 里拼 SQL,代码又长又容易错。我一般会把高频统计做成视图,把选课和退课的核心逻辑封装成存储过程,C# 只负责调用。
先建一个视图,把选课记录、学生、课程、教师连在一起,查成绩和名单时直接查视图。
CREATE VIEW v_enrollment_detail AS SELECT e.id, s.student_id, s.name AS student_name, c.course_id, c.course_name, c.credit, t.name AS teacher_name, e.score, e.status, e.select_time FROM enrollment e JOIN student s ON e.student_id = s.student_id JOIN course c ON e.course_id = c.course_id LEFT JOIN teacher t ON c.teacher_id = t.teacher_id;视图的好处是字段名统一,C# 里不用再写多表 JOIN。查某门课平均分:
SELECT course_id, course_name, COUNT(*) AS selected_count, AVG(score) AS avg_score FROM v_enrollment_detail WHERE status = 1 GROUP BY course_id, course_name;AVG会自动忽略 NULL 成绩,所以未录入成绩的学生不影响平均分计算。如果要统计“已录入成绩的人数”,用COUNT(score)而不是COUNT(*)。
再把选课逻辑封装成存储过程,C# 调用时只传学号和课程号,事务和锁都在数据库端完成。
DELIMITER // CREATE PROCEDURE sp_select_course( IN p_student_id VARCHAR(20), IN p_course_id VARCHAR(20), OUT p_result INT ) BEGIN DECLARE v_capacity INT; DECLARE v_selected INT; DECLARE EXIT HANDLER FOR SQLEXCEPTION BEGIN ROLLBACK; SET p_result = -1; END; START TRANSACTION; SELECT capacity INTO v_capacity FROM course WHERE course_id = p_course_id FOR UPDATE; SELECT COUNT(*) INTO v_selected FROM enrollment WHERE course_id = p_course_id AND status = 1; IF v_selected >= v_capacity THEN SET p_result = 0; -- 容量已满 ROLLBACK; ELSE INSERT INTO enrollment(student_id, course_id) VALUES(p_student_id, p_course_id); SET p_result = 1; -- 选课成功 COMMIT; END IF; END // DELIMITER ;C# 调用存储过程时用CommandType.StoredProcedure,输出参数用MySqlParameter的Direction设为Output。这样业务逻辑集中在数据库端,C# 代码更薄,也更容易在答辩时讲清楚“事务是在数据库里保证的”。
存储过程的EXIT HANDLER捕获任何 SQL 异常后回滚并返回 -1,调用方根据返回值判断结果:1 成功,0 容量满,-1 系统异常。唯一约束冲突也会走异常分支,返回 -1,前端提示“请勿重复选课”。
最后说一个我踩过的坑:存储过程里ROLLBACK之后不要再执行SELECT或INSERT,否则会报“当前事务已结束”。所有判断逻辑要在START TRANSACTION之后、COMMIT或ROLLBACK之前完成。这个习惯帮我省了很多调试时间,希望帮到你。
本文还有配套的精品资源,点击获取