
1. 项目背景与整体设计思路1.1 为什么拿“学生信息”当练手项目要做一个学生信息管理系统绕不开的最基础功能就是增删改查。我见过不少初学者一上来就去找商城、博客、订单这类项目练手结果被权限体系、购物车状态、支付回调这些业务逻辑拖住数据库操作反而没吃透。所以我还是推荐先用“学生信息”这种经典场景起步字段不多不少既有字符串又有数字和日期覆盖了增删改查里最常见的类型处理。这套项目用的是 VS2019 社区版 C# WinForms MySQL 8.0核心就一句话界面上录入学生信息点按钮写入 MySQL选中列表里的某一行点按钮把它删掉。本期是系列第一篇只做“增”和“删”两个动作“改”和“查”放在下一篇。并不是说改查不重要而是增删这两个动作涉及到的连接管理、参数化SQL、影响行数判断、界面刷新这些基础点一旦吃透后面改查就是照葫芦画瓢。1.2 技术选型VS2019、C#、MySQL 各承担什么很多人在选型上纠结为什么不用 SQL Server为什么不用 WPF为什么不用控制台我逐个说一下我的理由。VS2019社区版免费工作负载勾选“.NET 桌面开发”就能用环境成本几乎为零。VS2022 虽然也出了但 VS2019 依然是很多公司还在用的版本教程按 VS2019 写兼容性反而更好。C# WinFormsWinForms 拖拽控件就能出界面代码量比 WPF 少得多。对于“数据库增删改查”这种以数据操作为核心的需求WinForms 的 DataGridView 和 TextBox 组合起来非常顺手。控制台虽然更聚焦但看不到数据在表格里刷新的效果学习成就感低。MySQL免费、安装包不大、资料多而且 MySQL 的 SQL 语法和 SQL Server 有细微差别比如自增列写法、LIMIT 分页提前用 MySQL 练手后面遇到真实项目迁移也能更快适应。另外提一个很多人问过的问题MySql.Data这个驱动库一定要通过 NuGet 装不要手动去网上找个 DLL 就往项目里扔。NuGet 会帮你处理依赖和版本出问题也好排查。后面我还会专门讲一个和它相关的坑。1.3 系列文章规划增删改查分两期写的理由增删改查四个功能我故意拆成两篇先讲“增删”再讲“改查”。原因是这两个组合的技术侧重点不一样。“增删”的核心在于如何安全地拼接用户输入、如何判断操作影响的行数、如何在操作后刷新列表。这两个操作一旦出错轻则抛异常重则可能删错数据所以需要用 MessageBox 做二次确认。“改查”的核心则在于如何回填选中行数据、如何构造 UPDATE 语句、如何做模糊搜索和分页。逻辑链条比增删长单独一篇能讲得更细。这种拆分方式也方便读者按需查阅不用为了看一个删除功能翻一大篇文章。2. 环境准备与数据库设计2.1 开发环境搭建先说环境。VS2019 的安装路径一般默认在 C 盘如果你像我一样想装到 D 盘安装器里选自定义路径即可。安装时一定记得勾选“.NET 桌面开发”工作负载这个工作负载包含了 WinForms 所需的所有组件。如果勾漏了后面新建项目时找不到“Windows 窗体应用”还得回头补装非常麻烦。MySQL 的安装我建议用 MySQL Installer 的 Developer Default 模式它会顺带装上 MySQL Workbench 和 MySQL Shell。Workbench 用来建库建表、执行 SQL 很方便比命令行对新手友好太多。安装过程中会让你设置 root 密码这个密码一定要记牢后面连接字符串里要用。安装完 MySQL 后打开 Windows 服务管理器确认 MySQL 服务状态是“正在运行”这一步很多人忽略导致后面 C# 程序连接时报“无法连接到任何指定的 MySQL 主机”实际上就是服务没起来。数据库装好后在 VS2019 里新建一个 Windows 窗体应用.NET Framework然后在“工具—NuGet 包管理器—管理解决方案的NuGet程序包”里搜索MySql.Data选择最新稳定版安装。这里提醒一句MySQL 8.0 的默认认证插件是caching_sha2_password老版本的 MySql.Data 可能连不上所以尽量装 8.0.x 以上的版本。2.2 建表语句与字段说明我用 Workbench 执行下面这段 SQL建一个StudentDB库和Student表CREATE DATABASE IF NOT EXISTS StudentDB DEFAULT CHARACTER SET utf8mb4; USE StudentDB; DROP TABLE IF EXISTS Student; CREATE TABLE Student ( Id INT PRIMARY KEY AUTO_INCREMENT COMMENT 主键、自增, StudentNo VARCHAR(20) NOT NULL COMMENT 学号, Name VARCHAR(50) NOT NULL COMMENT 姓名, Gender CHAR(1) DEFAULT 男 COMMENT 性别, Age INT DEFAULT 0 COMMENT 年龄, ClassName VARCHAR(50) DEFAULT COMMENT 班级, Phone VARCHAR(20) DEFAULT COMMENT 手机号, CreateTime DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;字段设计上我特意保留了StudentNo学号和Name姓名两个 NOT NULL 字段用来演示“必填校验”。Id用自增主键好处是新增时不用手动传 Id数据库自己分配避免并发场景下主键冲突。CreateTime用DEFAULT CURRENT_TIMESTAMP插入数据时如果没传这个字段数据库会自动填当前时间。字符集统一用utf8mb4而不是utf8因为utf8在 MySQL 里最多只支持 3 个字节遇到 emoji 这类 4 字节字符会报错。虽然学生表里大概率不会存 emoji但养成好习惯能少踩一个坑。2.3 连接字符串配置与读取在项目里添加一个应用程序配置文件App.config写入连接字符串?xml version1.0 encodingutf-8 ? configuration connectionStrings add nameMySqlConnection connectionStringServerlocalhost;DatabaseStudentDB;Uidroot;Pwd你的密码;Charsetutf8mb4;SslModeNone; providerNameMySql.Data.MySqlClient / /connectionStrings /configuration这里有两个容易踩坑的配置项Charsetutf8mb4没加的话中文在部分环境下可能出现乱码。虽然 Connector/NET 默认字符集可能没问题但显式声明更稳妥。SslModeNoneMySQL 8.0 默认启用 SSL 连接本地开发时如果没配置证书连接可能会报 SSL 错误。本地开发用None就够了。在 C# 代码里通过ConfigurationManager读取using System.Configuration; private static string connStr ConfigurationManager.ConnectionStrings[MySqlConnection].ConnectionString;如果没有 System.Configuration 这个引用需要在项目里手动添加引用这是新手很容易卡住的地方。3. 数据访问层的封装3.1 为什么要把数据库操作封装成 DBHelper很多初学 C# 的人第一次写连接数据库的代码是每个窗体事件里都写一遍MySqlConnection、MySqlCommand、DataAdapter。我之前也这么干过后来发现两个问题一是代码严重重复一个项目里光连接对象的创建就有十几处二是如果哪天连接字符串变了比如密码改了得把所有窗体翻一遍。所以我在这个项目里做了一个简单的DBHelper静态类把“执行查询返回 DataTable”和“执行增删改返回影响行数”这两个高频操作统一封装。窗体层只需要传 SQL 和参数不用关心连接怎么开、怎么关、异常怎么处理。这里要特别说明DBHelper 不是越复杂越好初学者不要一上来就仿照网上那种带连接池、带事务、带日志的“通用框架”。这个阶段封装两个方法够用就行等真遇到高并发场景再考虑引入更重的数据访问框架。3.2 DBHelper 完整代码using System.Configuration; using System.Data; using MySql.Data.MySqlClient; public class DBHelper { private static readonly string connStr ConfigurationManager.ConnectionStrings[MySqlConnection].ConnectionString; public static DataTable Query(string sql, MySqlParameter[] parameters null) { using (MySqlConnection conn new MySqlConnection(connStr)) { conn.Open(); using (MySqlCommand cmd new MySqlCommand(sql, conn)) { if (parameters ! null) cmd.Parameters.AddRange(parameters); DataTable dt new DataTable(); using (MySqlDataAdapter adapter new MySqlDataAdapter(cmd)) { adapter.Fill(dt); } return dt; } } } public static int ExecuteNonQuery(string sql, MySqlParameter[] parameters null) { using (MySqlConnection conn new MySqlConnection(connStr)) { conn.Open(); using (MySqlCommand cmd new MySqlCommand(sql, conn)) { if (parameters ! null) cmd.Parameters.AddRange(parameters); return cmd.ExecuteNonQuery(); } } } }这里用using块来管理连接和命令对象方法执行完连接会自动释放。很多人写代码不习惯用using连接用完不关本地测试感觉不到问题一旦放到服务器上跑几天连接池耗尽程序就卡死了。所以从第一课就养成用using的习惯后面会省很多事。3.3 关于参数化 SQL为什么永远不要拼字符串我在不少教程里见过这种写法string sql INSERT INTO Student (Name) VALUES ( txtName.Text );这段代码在功能上确实能跑但它有两个致命问题。第一是 SQL 注入用户在文本框里输入); DROP TABLE Student; --这种内容拼出来的 SQL 就成了破坏性语句。第二是引号转义用户名字里如果带了单引号比如OBrien拼接后的 SQL 直接语法错误。参数化 SQL 的写法是把用户输入当作参数传给数据库而不是拼接进 SQL 字符串。数据库驱动会正确处理特殊字符也能防止恶意内容被当成 SQL 执行。所以在这期项目里不管是一条 INSERT 还是一条 DELETE我都用了MySqlParameter传参。这个习惯强烈建议从练手阶段就固定下来别等项目上线再去还技术债。4. 增删功能完整实现4.1 窗体界面布局在 WinForms 里我拖了这些控件6 个 TextBox学号、姓名、年龄、班级、手机号1 个 ComboBox性别下拉项为“男”“女”2 个按钮新增、删除1 个 DataGridView用来展示当前表里所有学生数据布局没有固定标准做到“输入区域在上方按钮在中部表格占下方大头”这种直觉布局就够了。关键是给 DataGridView 几个列设置中文表头不然用户看到的是 Id、StudentNo 这种英文字段名体验太差。我给 DataGridView 加上列的方式是直接跑一句 SQL 查询再把结果集绑定到 DataSource同时通过 SQL 别名把列名映射成中文这样就不用手动一列一列去配置省事很多。后面讲刷新列表时会贴出这行关键的 SQL。4.2 添加学生功能实现新增按钮的点击事件我拆成四步读取输入、必填校验、执行插入、刷新列表。private void btnAdd_Click(object sender, EventArgs e) { string studentNo txtStudentNo.Text.Trim(); string name txtName.Text.Trim(); string gender cmbGender.Text.Trim(); int age 0; int.TryParse(txtAge.Text.Trim(), out age); string className txtClassName.Text.Trim(); string phone txtPhone.Text.Trim(); if (string.IsNullOrEmpty(studentNo) || string.IsNullOrEmpty(name)) { MessageBox.Show(学号和姓名不能为空); return; } string sql INSERT INTO Student (StudentNo, Name, Gender, Age, ClassName, Phone) VALUES (StudentNo, Name, Gender, Age, ClassName, Phone);; MySqlParameter[] parameters { new MySqlParameter(StudentNo, studentNo), new MySqlParameter(Name, name), new MySqlParameter(Gender, gender), new MySqlParameter(Age, age), new MySqlParameter(ClassName, className), new MySqlParameter(Phone, phone) }; int rows DBHelper.ExecuteNonQuery(sql, parameters); if (rows 0) { MessageBox.Show(添加成功); LoadStudentList(); ClearInputs(); } else { MessageBox.Show(添加失败); } }这里用到Trim()去掉用户输入前后的空格避免误输入一个空格导致校验通过。年龄用int.TryParse做转换如果用户填的不是数字或者留空age就是 0既不会抛异常也能被数据库接受。判断是否成功的标准是ExecuteNonQuery的返回值它表示受影响的行数。插入一行数据返回 1返回 0 就说明 SQL 执行了但没有实际插入这种情况通常是 WHERE 条件不匹配INSERT 场景下少见但 DELETE 场景很常用。4.3 删除学生功能实现删除的界面逻辑是先在 DataGridView 里选中一行然后点删除按钮。这里有两个关键点第一是拿当前选中行的主键 Id第二是删除前必须弹确认框。private void btnDelete_Click(object sender, EventArgs e) { if (dgvStudents.CurrentRow null) { MessageBox.Show(请先选择要删除的行); return; } int id Convert.ToInt32(dgvStudents.CurrentRow.Cells[Id].Value); string name dgvStudents.CurrentRow.Cells[Name].Value.ToString(); DialogResult result MessageBox.Show( $确定要删除学生“{name}”吗删除后不可恢复。, 删除确认, MessageBoxButtons.YesNo, MessageBoxIcon.Warning); if (result ! DialogResult.Yes) { return; } string sql DELETE FROM Student WHERE Id Id;; MySqlParameter[] parameters { new MySqlParameter(Id, id) }; int rows DBHelper.ExecuteNonQuery(sql, parameters); if (rows 0) { MessageBox.Show(删除成功); LoadStudentList(); } else { MessageBox.Show(删除失败); } }删除确认框不能省。实际项目里误删数据的成本非常高虽然我们这期是本地 Demo但养成“删除前弹确认”的习惯非常必要。按 Id 删除而不是按姓名删除是因为 Id 是主键唯一且稳定。姓名有可能重名如果按姓名删一删就是好几条很容易误伤。4.4 刷新列表与整体联调新增和删除之后都要重新查一次数据库让 DataGridView 显示最新数据private void LoadStudentList() { string sql SELECT Id, StudentNo AS 学号, Name AS 姓名, Gender AS 性别, Age AS 年龄, ClassName AS 班级, Phone AS 手机号, CreateTime AS 创建时间 FROM Student ORDER BY Id DESC;; DataTable dt DBHelper.Query(sql); dgvStudents.DataSource dt; dgvStudents.Columns[Id].Visible false; }把 Id 列隐藏掉是因为它只是用来定位删除行的内部主键展示给用户没意义。Sql里用了ORDER BY Id DESC新插入的数据会显示在表格最上面视觉反馈更明显。最后再补一个ClearInputs方法把输入框清空性别重置为“男”方便连续添加private void ClearInputs() { txtStudentNo.Text ; txtName.Text ; txtAge.Text ; txtClassName.Text ; txtPhone.Text ; cmbGender.SelectedIndex 0; }到这里增删两个核心功能就能完整跑通了。5. 常见问题与排查技巧实录5.1 连接数据库失败报“无法连接到任何指定的 MySQL 主机”这个错误八成不是代码问题而是环境问题。按顺序排查MySQL 服务有没有启动连接字符串的 Server 和 Port 对不对root 密码是否正确3306 端口有没有被防火墙挡住。我见过一个比较隐蔽的情况电脑上装了多个 MySQL 实例比如 5.7 和 8.0 同时存在3306 端口被旧实例占用新实例用的 3307连接字符串里却写的默认端口自然连不上。用 Workbench 能连、C# 连不上的时候重点查你实际连的端口和密码。5.2 报“Authentication method caching_sha2_password failed”这是 MySQL 8.0 默认认证插件和旧版驱动不兼容导致的。解决办法优先更新 MySql.Data 到 8.0 以上。如果项目比较老无法升级也可以在 MySQL 里执行ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 你的密码;但我不推荐长期用这个方案毕竟驱动是不断更新的尽量让你的驱动匹配数据库版本。5.3 插入或删除后界面没变化先手动在 Workbench 里执行一下 SQL看看数据到底有没有变。如果数据变了但界面没刷新说明没调用LoadStudentList()如果数据也没变说明 SQL 执行失败了而你又没看到异常。这里有个新手很容易踩的坑ExecuteNonQuery返回 0 不代表异常比如 DELETE 的 WHERE 条件没匹配到任何行BCL 不会抛错只会悄悄返回 0。所以代码里一定要体现出“rows 0 才能提示成功”的逻辑这样问题就能定位到条件是否匹配。5.4 中文乱码乱码问题 90% 是字符集不一致。检查三处MySQL 表字符集是否为 utf8mb4连接字符串里有没有Charsetutf8mb4DataGridView 里显示的内容是否在写入前就已经是乱码。如果是写入前就乱那是输入框的问题如果写入后从数据库查出来才乱才是字符集的问题。5.5 DataGridView 删除时报“列 Id 不存在”或取值不对我在示例里用dgvStudents.CurrentRow.Cells[Id].Value取主键前提是生成的 DataGridView 列名和 SQL 里的字段名一致。如果你手动配置过列或者查询时用了别名列名可能已经变了取值时就要用实际列名。最稳妥的办法是删除前用 MessageBox 或者断点看一眼CurrentRow.Cells都有哪些列。还有一种情况是用户对表格重新排序或者分了页CurrentRow指向的行不是你以为的那一行。对于本期这种单页数据量问题不大以后做分页时要注意取当前行 Id 的逻辑在分页后要重新处理。6. 一些实打实的体会这期代码写下来我最想留给大家的一段经验是不要嫌流程繁琐。新增前校验、删除前确认、操作后刷新每一步都有人觉得“是不是多余”但实际项目里正是这些“多余”的检查拦下了大量低级事故。另外关于 MySQL 连接这个事我再提一句本地调试建议把连接字符串写到 App.config 里不要写死在代码中。我见过有人把连接字符串直接嵌在DBHelper里后来数据库密码一改整个项目找连接字符串找半天。本期只讲了增和删下一篇我会把“改”和“查”补完重点是选中行一键回填到输入框、UPDATE 语句怎么配合 Id 精确更新、以及带条件的模糊查询和分页。你可以先把增删的代码敲熟尤其把参数化 SQL 和影响行数判断这两个点消化掉后面学起来会快很多。