ARTICLE DETAIL

资讯详情

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

基于C#与SQL Server的学生选课管理系统:数据库课程设计实战指南

基于C#与SQL Server的学生选课管理系统:数据库课程设计实战指南 简介这份数据库课程设计文档面向高校计算机相关专业学生与数据库初学者围绕“学生选课管理系统”这一经典实践课题提供从需求分析到系统实现的完整设计思路。内容涵盖学生、教师、管理员三类角色的权限划分以及学生表、教师表、专业表、课程表、专业课程表、学生课程信息表、班级表等数据字典定义并给出关系模式、主外键约束与参照关系图。逻辑结构设计部分还涉及1NF、2NF、3NF范式理论、密码加密存储、索引优化与完整性、安全性考量系统实现则结合SQL语句与C#界面代码演示登录、增删改查及总分、平均分、排名统计等操作。资源包共1个docx文件约597KB结构紧凑适合直接参考撰写课程设计报告或搭建数据库实验。目前已有552人学习下载可作为数据库课程设计、期末大作业与关系模型练习的实用范本。1. 学生选课管理系统从课程设计到能跑起来的数据库应用学生选课管理系统几乎是每届计算机专业都绕不开的数据库课程设计题目但真正把它做扎实的人并不多。多数人卡在两个地方一是数据库表结构设计得经不起推敲选课冲突、容量超限、重复选课这些约束全靠代码兜底二是用 C# 写出来的界面和数据库之间只有最原始的增删改查事务、并发、连接池这些真正体现工程能力的东西一个都没碰。这篇笔记就围绕「数据库课程设计—学生选课管理系统」这个标题把一套能直接复现的方案讲清楚数据库怎么建、C# 怎么连、选课逻辑怎么保证一致性、哪些参数必须调、哪些坑一定会踩。适合正在做课程设计的学生也适合想用一个小项目把数据库知识点串起来的 C# 入门开发者。整套方案用 SQL Server 或 MySQL 都行代码以 C# WinForms/WPF 加 ADO.NET 为主线不依赖任何重型框架跑得起来、看得见数据、改得动逻辑。2. 表结构定生死学生选课管理系统的库表设计与约束2.1 四张核心表怎么拆字段怎么定学生选课管理系统的数据模型其实不复杂核心就是学生、课程、教师、选课记录四个实体。但恰恰是这种「看起来简单」的题目最容易在设计阶段埋雷。我一般会先画清楚实体关系一个学生可以选多门课一门课可以被多个学生选所以学生和课程之间是多对多必须有一张中间表来承载选课关系。教师和课程是一对多一个教师可以开多门课。这四张表拆出来就是 Student、Course、Teacher、SC选课记录。字段设计上有几个必须想清楚的点。学生表里学号做主键不要用自增 ID 做主键再给学号加唯一索引课程设计阶段直接用学号做业务主键更直观查询和调试都方便。课程表里课程号做主键同时要有容量字段 Capacity 和已选人数字段 SelectedCount这两个字段是后面并发控制的关键。选课表 SC 用学号加课程号的联合主键天然防止同一个学生重复选同一门课这个约束比在代码里写 if 判断可靠得多。-- 学生表学号做主键姓名、专业、年级为基本属性 CREATE TABLE Student ( Sno VARCHAR(12) PRIMARY KEY, -- 学号业务主键 Sname NVARCHAR(20) NOT NULL, -- 姓名 Major NVARCHAR(30), -- 专业 Grade INT, -- 年级 Password VARCHAR(64) NOT NULL DEFAULT 123456 -- 登录密码课程设计够用 ); -- 课程表容量和已选人数分开存方便做并发扣减 CREATE TABLE Course ( Cno VARCHAR(10) PRIMARY KEY, -- 课程号 Cname NVARCHAR(40) NOT NULL, -- 课程名 Credit DECIMAL(3,1), -- 学分 Capacity INT NOT NULL DEFAULT 50, -- 课容量上限 SelectedCount INT NOT NULL DEFAULT 0, -- 当前已选人数 Tno VARCHAR(10) -- 授课教师工号 ); -- 选课表联合主键防重复选课这是最省事的约束手段 CREATE TABLE SC ( Sno VARCHAR(12) NOT NULL, Cno VARCHAR(10) NOT NULL, SelectTime DATETIME DEFAULT GETDATE(), -- 选课时间便于排查 Score DECIMAL(5,1) NULL, -- 成绩选课时为空 PRIMARY KEY (Sno, Cno), FOREIGN KEY (Sno) REFERENCES Student(Sno), FOREIGN KEY (Cno) REFERENCES Course(Cno) );这段建表语句里联合主键PRIMARY KEY (Sno, Cno)是最值得说的一处。很多同学喜欢在 SC 表里加一个自增 ID 做主键然后指望代码去判断重复结果测试时手快点了两次选课按钮数据库里就多了一条记录。联合主键把「一个学生对一门课只能有一条选课记录」这件事交给数据库来保证代码只需要处理插入失败的情况就行。外键约束则保证了不会出现选了不存在的课这种脏数据。参数上要注意Capacity和SelectedCount的默认值。容量默认给 50 是课程设计的常见规模实际调的时候根据课程性质改选修课可以给 100实验课给 30。SelectedCount默认 0每次选课成功加一退课减一这个字段和容量字段配合使用是判断课程是否已满的依据。2.2 索引和约束让查询和并发都稳表建好之后索引不是可有可无的装饰。学生选课管理系统里最频繁的查询有两类一是按学号查某个学生选了哪些课二是按课程号查某门课有哪些学生选。SC 表的联合主键已经覆盖了按学号加课程号的查询但单独按课程号查的时候用不上这个索引的最左前缀所以需要额外加一个课程号索引。-- 按课程号查选课名单是高频操作单独建索引 CREATE INDEX idx_sc_cno ON SC(Cno); -- 课程表按教师查课也是常见需求 CREATE INDEX idx_course_tno ON Course(Tno); -- 学生表按姓名模糊查询课程设计阶段数据量小可以不建 -- 但如果要做姓名搜索建议用前缀索引而不是全表扫描约束方面除了主键和外键还应该给Capacity加一个检查约束防止有人把容量设成负数。SelectedCount也要保证不大于Capacity但这个约束在并发场景下用数据库检查约束会带来额外的锁开销课程设计阶段我一般放在应用层用事务保证数据库层只加一个非负检查。注意MySQL 和 SQL Server 在检查约束的语法上略有差异MySQL 8.0 之后才真正强制执行 CHECK 约束5.7 版本会忽略。如果用的是旧版本 MySQL别指望 CHECK 能拦住脏数据。索引建多了会影响插入性能选课表在选课高峰期写入频繁所以索引数量要克制。SC 表上除了主键和课程号索引不要再加其他索引。课程表和学生表都是读多写少索引可以稍微宽松一点。3. C# 连数据库连接池、参数化查询和事务的落地写法3.1 连接字符串怎么配连接池参数怎么调C# 连数据库最常见的方式是 ADO.NET核心对象是SqlConnectionSQL Server或MySqlConnectionMySQL。连接字符串里藏着很多课程设计里不会讲但实际很重要的参数。以 SQL Server 为例一个能用的连接字符串长这样// 连接字符串数据库地址、库名、账号密码、连接池参数 string connStr Serverlocalhost;DatabaseCourseSelection; User Idsa;PasswordYourPassword; Min Pool Size5;Max Pool Size100; Connection Timeout15;;这里Min Pool Size和Max Pool Size是连接池的核心参数。默认情况下连接池最小是 0最大是 100。课程设计阶段并发不高但如果你在循环里反复开连接关连接没有连接池的话每次都要走一次 TCP 握手和身份验证几十次循环就能感觉到明显卡顿。把Min Pool Size设成 5意味着应用启动后连接池里至少保持 5 个空闲连接第一次查询就不用等建连。Max Pool Size设成 100 是上限超过这个数的新请求会排队等待等待时间由Connection Timeout控制默认 15 秒超时抛异常。一个血泪经验是不要用using包住连接之后在using块外面继续用这个连接对象。using结束时会调用Dispose连接被归还到池里但对象本身已经不能用了。正确做法是把数据库操作封装成方法连接在方法内部开、内部关外部只拿结果。// 封装查询方法连接在方法内开闭依赖连接池复用 public static DataTable Query(string sql, params SqlParameter[] parameters) { using (var conn new SqlConnection(connStr)) using (var cmd new SqlCommand(sql, conn)) { if (parameters ! null) cmd.Parameters.AddRange(parameters); var adapter new SqlDataAdapter(cmd); var dt new DataTable(); adapter.Fill(dt); // 连接由 using 自动关闭并归还池 return dt; } }这个方法每次调用都 new 一个连接看起来开销很大实际上因为连接池的存在new SqlConnection只是从池里拿一个现成的物理连接Dispose也只是归还真正的 TCP 连接并没有反复建立。这就是连接池的价值也是为什么推荐「短连接」写法而不是全局维护一个长连接。3.2 参数化查询别再用字符串拼接 SQL课程设计里最常见的翻车现场就是 SQL 注入。比如登录功能写成SELECT * FROM Student WHERE Sno txtSno.Text 学生在学号框里输入 OR 11直接绕过密码登录。这不是危言耸听每年课程设计答辩都有人的系统被这么攻破。参数化查询的写法很简单把用户输入的值用SqlParameter传进去数据库驱动会负责转义和类型处理。// 参数化查询用户输入永远不拼进 SQL 文本 string sql SELECT COUNT(*) FROM Student WHERE SnoSno AND PasswordPwd; var parameters new SqlParameter[] { new SqlParameter(Sno, txtSno.Text.Trim()), new SqlParameter(Pwd, txtPwd.Text.Trim()) }; int count Convert.ToInt32(Query(sql, parameters).Rows[0][0]); if (count 0) { /* 登录成功 */ }参数化查询除了防注入还有一个好处是执行计划复用。数据库会对带参数的 SQL 做一次编译后续相同结构不同参数的查询直接复用执行计划省去了解析和优化的开销。字符串拼接的 SQL 每次文本都不一样执行计划缓存命中率极低。这一点在选课高峰期批量操作时差别很明显。参数类型也要注意。SqlParameter不指定类型时会根据值自动推断但字符串推断成NVARCHAR还是VARCHAR会影响索引使用。学号字段是VARCHAR(12)参数最好显式指定SqlDbType.VarChar长度 12避免隐式转换导致索引失效。3.3 选课事务容量扣减和插入必须原子完成选课这个动作在数据库层面包含两步往 SC 表插一条记录同时把 Course 表的SelectedCount加一。这两步必须一起成功或一起失败否则会出现「选了课但人数没加」或者「人数加了但没选上课」的脏数据。这就是事务的用武之地。// 选课事务插入选课记录 更新已选人数原子提交 public static bool SelectCourse(string sno, string cno) { using (var conn new SqlConnection(connStr)) { conn.Open(); using (var tran conn.BeginTransaction()) { try { // 第一步检查容量并锁定该课程行防止并发超选 string checkSql SELECT Capacity, SelectedCount FROM Course WITH (UPDLOCK, ROWLOCK) WHERE CnoCno; int capacity, selected; using (var cmd new SqlCommand(checkSql, conn, tran)) { cmd.Parameters.AddWithValue(Cno, cno); using (var reader cmd.ExecuteReader()) { if (!reader.Read()) return false; // 课程不存在 capacity reader.GetInt32(0); selected reader.GetInt32(1); } } if (selected capacity) return false; // 已满直接返回 // 第二步插入选课记录联合主键会拦住重复选课 string insertSql INSERT INTO SC(Sno, Cno) VALUES(Sno, Cno); using (var cmd new SqlCommand(insertSql, conn, tran)) { cmd.Parameters.AddWithValue(Sno, sno); cmd.Parameters.AddWithValue(Cno, cno); cmd.ExecuteNonQuery(); } // 第三步已选人数加一 string updateSql UPDATE Course SET SelectedCountSelectedCount1 WHERE CnoCno; using (var cmd new SqlCommand(updateSql, conn, tran)) { cmd.Parameters.AddWithValue(Cno, cno); cmd.ExecuteNonQuery(); } tran.Commit(); return true; } catch (SqlException ex) when (ex.Number 2627) // 主键冲突 { tran.Rollback(); return false; // 重复选课 } catch { tran.Rollback(); throw; } } } }这段代码里有三个关键点。第一WITH (UPDLOCK, ROWLOCK)是 SQL Server 的锁提示在读取课程容量的时候就对该行加更新锁防止两个事务同时读到相同的SelectedCount然后都认为还有名额。第二插入选课记录时如果联合主键冲突SQL Server 会抛 2627 号错误捕获这个错误返回 false 比先查再插更可靠因为查和插之间有时间窗口。第三所有操作在同一个事务里任何一步失败都回滚不会留下半截数据。MySQL 的写法类似锁提示换成SELECT ... FOR UPDATE主键冲突错误码是 1062。参数上要注意事务的隔离级别默认的READ COMMITTED在 SQL Server 里配合UPDLOCK已经够用如果改成READ UNCOMMITTED虽然并发高但会读到脏数据选课场景不能接受。4. 避坑与排查学生选课管理系统最常见的五个翻车点4.1 选课人数超了容量但数据库里查不出原因现象是课程容量 50实际选课记录有 52 条。原因通常是没用事务或者没用锁提示两个学生同时选最后一个名额都读到了SelectedCount49都判断没满都插入成功。解决就是上面 3.3 节的写法读取容量时加UPDLOCK把检查和更新放在一个事务里。如果已经出现了超选数据用一条 SQL 修正UPDATE Course SET SelectedCount(SELECT COUNT(*) FROM SC WHERE SC.CnoCourse.Cno)。4.2 连接池耗尽程序卡死无响应现象是程序运行一段时间后所有数据库操作都超时日志里出现「超时时间已到但是尚未从池中获取连接」。原因是某处代码打开了连接但没有关闭比如在try块里conn.Open()之后抛了异常catch里只记了日志没关连接。解决是统一用using包裹连接或者用try-finally确保Close被调用。排查方法是在连接字符串里加Poolingtrue并观察 SQL Server 的sys.dm_exec_sessions看连接数是否持续增长不释放。4.3 中文姓名存进去变成问号现象是学生姓名「张三」存到数据库里变成「??」。原因是数据库字段用了VARCHAR而不是NVARCHAR或者连接字符串没有指定字符集。SQL Server 里中文必须用NVARCHARC# 参数要指定SqlDbType.NVarChar。MySQL 里要确保库和表的字符集是utf8mb4连接字符串加CharSetutf8mb4。已经存错的数据没法自动恢复只能删掉重插。4.4 选课按钮点一次插两条记录现象是学生点一次选课SC 表里出现两条相同记录。如果联合主键建对了第二条会插入失败但界面上可能因为没处理异常而显示成功。原因是按钮的 Click 事件被触发了两次可能是双击也可能是代码里手动绑定了两次事件。解决是在插入前禁用按钮操作完成后恢复同时捕获主键冲突异常并提示「请勿重复选课」。更彻底的做法是在数据库层用联合主键兜底代码层用if (!SelectCourse(...))判断返回值。4.5 退课后容量没有释放课程显示已满现象是学生退了课SC 表里记录删了但 Course 表的SelectedCount没减导致课程一直显示满员。原因是退课逻辑只删了 SC 记录忘了更新计数。解决是把退课也包在事务里删除 SC 记录的同时UPDATE Course SET SelectedCountSelectedCount-1。另外要注意退课时的边界如果SelectedCount已经是 0减一会变成负数所以更新语句要加WHERE SelectedCount 0。5. 进阶技巧用存储过程和触发器把选课逻辑下沉到数据库课程设计做到后面如果想让系统更「数据库味」一点可以把选课和退课的核心逻辑写成存储过程C# 端只负责调用。这样做的好处是逻辑集中在数据库层换前端不用重写业务规则而且存储过程在数据库内部执行省去了网络往返。-- 选课存储过程把容量检查、插入、计数更新封装在一起 CREATE PROCEDURE sp_SelectCourse Sno VARCHAR(12), Cno VARCHAR(10), Result INT OUTPUT AS BEGIN SET NOCOUNT ON; BEGIN TRY BEGIN TRANSACTION; DECLARE Capacity INT, Selected INT; SELECT Capacity Capacity, Selected SelectedCount FROM Course WITH (UPDLOCK, ROWLOCK) WHERE Cno Cno; IF Capacity IS NULL BEGIN SET Result -1; ROLLBACK; RETURN; END -- 课程不存在 IF Selected Capacity BEGIN SET Result -2; ROLLBACK; RETURN; END -- 课程已满 INSERT INTO SC(Sno, Cno) VALUES(Sno, Cno); UPDATE Course SET SelectedCount SelectedCount 1 WHERE Cno Cno; COMMIT; SET Result 0; -- 成功 END TRY BEGIN CATCH IF TRANCOUNT 0 ROLLBACK; SET Result -3; -- 重复选课或其他错误 END CATCH ENDC# 端调用时用SqlCommand的CommandType.StoredProcedure把Result设为Output参数根据返回值判断结果。-1是课程不存在-2是已满-3是重复选课0是成功。这样前端只需要弹对应的提示不用关心数据库细节。触发器可以用在退课场景在 SC 表上建一个AFTER DELETE触发器删除选课记录时自动把对应课程的SelectedCount减一。这样即使有人直接操作数据库删记录计数也能保持一致。但触发器是把双刃剑调试困难逻辑隐蔽课程设计里用一两个体会一下就行生产环境要谨慎。验证存储过程是否生效最直接的方法是在 SSMS 里执行EXEC sp_SelectCourse 2023001, C001, r OUTPUT; SELECT r看返回值和 SC 表、Course 表的数据变化。如果返回 0 但 SC 表没记录检查事务是否提交如果返回 -3 但确实是第一次选课检查 SC 表里是不是已经有这个学号加课程号的记录。我自己做课程设计时最深的教训是别等到答辩前一天才去测并发。平时用单线程点按钮一切正常答辩时老师随口说「两个人同时选最后一门课会怎样」现场写个循环开十个线程一跑数据就乱了。后来我养成的习惯是任何涉及计数更新的功能写完先开两个数据库连接手动模拟并发确认锁和事务生效了再往下做。希望帮到你。本文还有配套的精品资源点击获取
返回列表