ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

C#与MySQL实战:学生选课管理系统数据库设计与并发控制

C#与MySQL实战:学生选课管理系统数据库设计与并发控制 简介这份数据库课程设计文档面向高校计算机相关专业学生与数据库初学者围绕“学生选课管理系统”这一经典实践课题提供从需求分析到系统实现的完整设计思路。内容涵盖学生、教师、管理员三类角色的权限划分以及学生表、教师表、专业表、课程表、专业课程表、学生课程信息表、班级表等数据字典定义并给出关系模式、主外键约束与参照关系图。文档还讨论了完整性、安全性、密码加密、索引优化与范式理论等设计要点并附有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 ) ENGINEInnoDB; -- 教师表工号唯一 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 ) ENGINEInnoDB; -- 课程表容量字段用于选课人数上限控制 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) ) ENGINEInnoDB; -- 选课记录表学生和课程联合唯一防止重复选课 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) ) ENGINEInnoDB;字段设计里有几个关键点。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 Serverlocalhost;Port3306;Databasecourse_selection; Userroot;Passwordyour_password;CharSetutf8mb4;; // 使用 using 确保连接释放回连接池 using var conn new MySqlConnection(connStr); conn.Open(); Console.WriteLine(数据库连接成功);连接字符串里的CharSetutf8mb4必须和建库时一致否则中文课程名会乱码。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_idcid 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_idcid AND status1; 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 注入。status1只统计有效选课退课的记录不计入人数。事务提交前任何异常都要回滚否则连接归还池时可能带着未提交事务。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 status0 WHERE student_idsid AND course_idcid AND status1; 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或者连接字符串没指定CharSetutf8mb4。解决建库、建表、连接字符串三处字符集必须一致都用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一定执行。连接池最大连接数可以在连接字符串里用MaximumPoolSize50调整但根本办法是及时释放。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之前完成。这个习惯帮我省了很多调试时间希望帮到你。本文还有配套的精品资源点击获取
返回列表