简介:本资源是一份面向C#初学者与ASP.NET入门开发者的数据库基础操作教学代码包,聚焦增删改查(CRUD)核心能力训练。内容以完整可运行的C#源码为主,涵盖OleDbConnection连接Access数据库、OleDbCommand执行参数化SQL语句(INSERT/DELETE/UPDATE/SELECT)、OleDbParameter防注入处理等关键知识点,并提供结构清晰的固定模板与WinForm界面交互示例(如登录验证、数据绑定至DataGridView),便于理解数据库操作全流程。资源为1个203KB的Word文档(.doc),内含详细代码片段、注释说明及分步骤实现逻辑,适合作为课堂补充材料或自学速查参考。目前已有819人学习下载,读者可直接复用模板代码,快速掌握C#数据库编程基础规范与安全实践。
1. C# 增删改查代码:不是模板,是能跑通的“数据库操作最小闭环”
你刚写完一个 WinForms 登录界面,TextBox 里输完账号密码,点登录按钮——结果弹出System.Data.OleDb.OleDbException: 未指定的错误,连数据库文件路径都对得上,就是连不上;或者更玄学的是:增删改明明执行了ExecuteNonQuery(),返回值是 1,但刷新 DataGridView 就是没新数据;又或者Update(ds)一调用就报Dynamic SQL generation for the DeleteCommand is not supported against a SelectCommand that does not return any key column information,查文档像读天书。这不是你手生,是缺一套带上下文、含边界判断、经实测能闭环验证的 C# 数据库操作骨架。这份 C# 增删改查代码不是教科书式伪代码,它包含两个真实可运行路径:一是基于 Access(.mdb)的 OleDb 快速验证流,二是基于 SQL Server(northwind示例库)的 SqlDataAdapter + DataSet 完整事务流。它不讲 ORM,不碰 Entity Framework,专治“连不上、插不进、删不掉、改不动、查不到”这五类新手高频翻车现场。适合正在做课程设计、实训项目或快速原型开发的 C# 初学者,也适合需要临时搭个本地管理工具的老手——毕竟,不是每个场景都需要上 Web API 和 EF Core。
2. 从零建连接:OleDb 与 SqlConnection 的选型逻辑与初始化实操
2.1 为什么先用 OleDb?Access 是你的“后悔药”
很多教程一上来就推 SQL Server,但新手常卡在“装 SQL Server Express 太重”“sa 密码设错连不上”“Windows 身份验证失败”这类环境问题上。而.mdb文件是纯文件,双击就能用 Access 打开验证数据,OleDb 提供的是最轻量级的“数据库存在性证明”。项目正文里第一段代码用的就是OleDbConnection+Microsoft.Jet.OLEDB.4.0,它对应的是旧版 Access 2003 格式(.mdb),兼容性极强,Win7/Win10/Win11 原生支持,无需额外安装驱动。注意:它不支持.accdb(Access 2007+),若你手头是.accdb,必须换用Microsoft.ACE.OLEDB.12.0或更高版本驱动(需单独下载 Microsoft Access Database Engine),否则会报Provider cannot be found。这是第一个硬边界。
string path = Application.StartupPath + "\\db1.mdb"; // ⚠️ 关键:路径必须真实存在,且程序有读写权限(尤其 Win10+ 默认禁止写 Program Files) OleDbConnection con = new OleDbConnection( @"Provider=Microsoft.Jet.OLEDB.4.0;Data Source=" + path + ";Persist Security Info=False"); try { con.Open(); MessageBox.Show("数据库连接成功!"); } catch (OleDbException ex) { // 不要只抛 ex.ToString(),要抓具体错误号 MessageBox.Show($"连接失败:{ex.Message}(错误号:{ex.ErrorCode})"); // 常见 ErrorCode:-2147467259(文件不存在)、-2147217887(权限不足)、-2147467259(驱动未注册) } finally { if (con.State == ConnectionState.Open) con.Close(); }提示:
Application.StartupPath返回的是可执行文件(.exe)所在目录,不是项目根目录。调试时确保db1.mdb真的被复制到了bin\Debug\下(属性 → 复制到输出目录 → 始终复制)。这是新手最常忽略的“路径幻觉”。
2.2 SqlConnection:SQL Server 连接字符串的三个生死参数
当你升级到 SQL Server(如northwind示例库),SqlConnection是唯一选择。项目正文第二段代码中DRIVER字符串为"server=.;database=northwind;uid=sa;pwd=sa",这里藏着三个必须显式确认的参数:
server=.:表示本地默认实例。若你用的是命名实例(如SQLEXPRESS),必须写成server=YOURPCNAME\\SQLEXPRESS或server=localhost\\SQLEXPRESS;database=northwind:库名必须已存在且已附加。Northwind 是微软经典示例库,需手动下载.bak文件并用 SSMS 还原,不能靠代码自动创建;uid=sa;pwd=sa:sa是系统管理员账户,但默认禁用且密码为空。你必须先用 Windows 身份验证登录 SSMS,右键sa→ 属性 → 启用登录 + 设置强密码(不能是sa这种弱口令,SQL Server 2019+ 会拒绝)。
// ✅ 正确写法:显式指定连接超时和加密(生产环境必备) string connectionString = @"Server=localhost\SQLEXPRESS;Database=northwind;User Id=sa;Password=YourStrongPass123!;Connection Timeout=30;Encrypt=False;TrustServerCertificate=True;"; SqlConnection conn = new SqlConnection(connectionString); try { conn.Open(); Console.WriteLine($"连接成功!服务器版本:{conn.ServerVersion}"); } catch (SqlException ex) { // SqlException 有 Number 属性,比 Message 更精准定位问题 switch (ex.Number) { case 18456: MessageBox.Show("登录失败:用户名或密码错误"); break; case 4060: MessageBox.Show("数据库不存在或未附加"); break; case 53: MessageBox.Show("SQL Server 服务未启动,请检查 SQL Server (SQLEXPRESS) 服务状态"); break; default: MessageBox.Show($"SQL 错误 {ex.Number}:{ex.Message}"); } }注意:
Encrypt=False;TrustServerCertificate=True是开发机临时方案,生产环境必须配证书并设Encrypt=True。此处仅为让代码跑通,不构成安全建议。
2.3 连接池:别在每次操作后Close(),但必须Dispose()
初学者常犯的错误是:每次增删改查都新建Connection、Open()、Close()。这看似稳妥,实则摧毁性能。ADO.NET 默认启用连接池(Connection Pooling),Close()并非断开物理连接,而是将连接归还池中复用。真正该做的是:用using语句块确保Dispose()被调用,它会触发Close()并清理资源。
// ❌ 错误:手动 Close,但异常时可能跳过 OleDbConnection con = new OleDbConnection(connStr); con.Open(); // ... 操作 con.Close(); // 若中间 throw exception,这行永不执行 // ✅ 正确:using 自动保证 Dispose(等价于 Close + 清理) using (OleDbConnection con = new OleDbConnection(connStr)) { con.Open(); // ... 所有数据库操作 } // 此处自动调用 con.Dispose()using不是语法糖,它是 .NET 资源管理的铁律。所有IDisposable对象(Connection、Command、DataReader、DataAdapter)都必须走using,否则内存泄漏和连接数爆满是分分钟的事。
3. 参数化查询:为什么@a不是变量,而是防注入的“安全阀”
3.1@a的本质:占位符,不是字符串拼接
项目正文里所有 SQL 都用了@a、@b这类参数,比如"insert into t1(name,pwd) values (@a,@b)"。新手常误以为这只是“写法好看”,甚至偷偷改成"values ('" + textBox1.Text + "','" + textBox2.Text + "')"—— 这是 SQL 注入的直通车。@a的真实身份是Parameter 占位符,它和OleDbParameter对象绑定后,ADO.NET 会将参数值以二进制方式传给数据库引擎,完全绕过 SQL 解析器。这意味着:即使用户在 TextBox 里输入' OR '1'='1,它也会被当做一个普通字符串插入,而非执行恶意逻辑。
// ✅ 安全:参数值与 SQL 结构彻底分离 string sql = "SELECT * FROM users WHERE username = @user AND password = @pwd"; using (OleDbCommand cmd = new OleDbCommand(sql, con)) { // Add 方法会自动推断类型,但显式指定更稳 cmd.Parameters.Add("@user", OleDbType.VarChar).Value = textBox1.Text; cmd.Parameters.Add("@pwd", OleDbType.VarChar).Value = textBox2.Text; // 执行查询... } // ❌ 危险:字符串拼接,用户输入直接进入 SQL 流水线 string unsafeSql = $"SELECT * FROM users WHERE username = '{textBox1.Text}' AND password = '{textBox2.Text}'"; // 用户输:admin'-- 就变成 SELECT ... WHERE username = 'admin'--' AND ... → 绕过密码校验3.2 参数命名规范:大小写敏感?下划线要不要?
OleDb 和 SqlConnection 对参数名的处理略有差异:
- OleDb(Access):参数名不区分大小写,但必须与 SQL 中的
@xxx完全一致(包括@符号)。@UserName和@username在 OleDb 中被视为同一参数; - SqlConnection(SQL Server):参数名区分大小写,且要求严格匹配。
@UserName和@username是两个不同参数。
项目正文第一段用@a、@b是为了简化,但实际工程中必须用语义化名称,如@Name、@Password、@RegionId。原因有三:一是避免@a@b@c混乱;二是方便调试时看cmd.Parameters集合;三是为后续迁移到 EF 或 Dapper 做准备(它们都依赖语义化参数名)。
// ✅ 推荐:语义清晰,大小写统一(PascalCase) cmd.Parameters.Add("@RegionId", SqlDbType.Int).Value = Convert.ToInt32(textBoxId.Text); cmd.Parameters.Add("@RegionDescription", SqlDbType.NVarChar, 50).Value = textBoxDesc.Text; // ⚠️ 注意:Add 方法的第三个参数是 size,对字符串类型必须指定(防止截断),对 int 可省略 // 对于 NVARCHAR(50),size=50;对于 VARCHAR(100),size=100;不指定则按默认长度(可能不够)3.3AddWithValue是把双刃剑:何时用,何时禁
cmd.Parameters.AddWithValue("@name", value)看似方便,但它会自动推断参数类型,导致隐式转换错误。例如:
AddWithValue("@id", "123")→ 推断为VarChar,但字段是INT,查询时可能无法走索引;AddWithValue("@date", DateTime.Now)→ 推断为DateTime,但 SQL Server 中smalldatetime精度不够,存入后秒数丢失。
项目正文里没用AddWithValue,而是显式new OleDbParameter("@a", textBox1.Text),这是老派但稳健的做法。现代推荐写法是:
// ✅ 显式指定类型,杜绝隐式转换 cmd.Parameters.Add("@RegionId", SqlDbType.Int).Value = regionId; cmd.Parameters.Add("@RegionDescription", SqlDbType.NVarChar, 50).Value = description; // ⚠️ 仅当确定类型且无精度风险时,才用 AddWithValue(如简单字符串) cmd.Parameters.AddWithValue("@Status", "Active"); // 字符串字面量,安全4. 四大操作落地:INSERT/DELETE/UPDATE/SELECT 的代码级差异与陷阱
4.1 INSERT:ExecuteNonQuery()返回值是“影响行数”,不是“是否成功”
ExecuteNonQuery()是增删改的统一执行方法,它返回一个int,表示受 SQL 语句影响的行数。项目正文里所有INSERT、DELETE、UPDATE都用了它,但新手常误读返回值:
INSERT INTO t1 VALUES (...)→ 成功返回1,失败(如主键冲突)返回0或抛异常;DELETE FROM t1 WHERE id=999→ 若id=999不存在,返回0,这不算错误,只是没删到;UPDATE t1 SET name=@n WHERE id=@id→ 若id=@id不存在,返回0,同样不是异常。
所以,不能只靠if (count > 0)判断业务成功,必须结合业务逻辑。比如“添加用户”,应检查count == 1;而“删除选中行”,应先确认dataGridView1.CurrentRow != null,再执行,否则count==0是预期行为。
// ✅ INSERT:明确期望影响 1 行 string insertSql = "INSERT INTO t1 (name, pwd) VALUES (@name, @pwd)"; using (OleDbCommand cmd = new OleDbCommand(insertSql, con)) { cmd.Parameters.Add("@name", OleDbType.VarChar, 50).Value = textBox1.Text; cmd.Parameters.Add("@pwd", OleDbType.VarChar, 50).Value = textBox2.Text; int rowsAffected = cmd.ExecuteNonQuery(); if (rowsAffected == 1) MessageBox.Show("添加成功!"); else MessageBox.Show("添加失败:可能用户名已存在"); } // ✅ DELETE:先校验行存在性,再执行 if (dataGridView1.CurrentRow == null) { MessageBox.Show("请先选择一行数据"); return; } int selectedId = (int)dataGridView1.CurrentRow.Cells["id"].Value; // 假设列名为 id string deleteSql = "DELETE FROM t1 WHERE id = @id"; using (OleDbCommand cmd = new OleDbCommand(deleteSql, con)) { cmd.Parameters.Add("@id", OleDbType.Integer).Value = selectedId; int rowsAffected = cmd.ExecuteNonQuery(); if (rowsAffected == 0) MessageBox.Show("删除失败:该记录可能已被其他用户删除"); else MessageBox.Show("删除成功!"); }4.2 SELECT:ExecuteReader()与Fill(DataSet)的适用场景
查操作有两种主流模式:ExecuteReader()(流式读取,内存占用低)和DataAdapter.Fill()(批量加载到内存,支持离线编辑)。项目正文第一段用ExecuteReader()做登录校验,第二段用SqlDataAdapter.Fill()绑定 DataGridView,这是典型的场景划分:
ExecuteReader():适合“查一条、校验、跳转”类操作(如登录、权限检查、存在性验证)。它返回OleDbDataReader,只能向前读,且连接必须保持打开状态直到Read()完成。dr.Read()返回true表示有数据,false表示无结果。
// ✅ 登录校验:只关心是否存在匹配记录 string loginSql = "SELECT COUNT(*) FROM t1 WHERE name=@name AND pwd=@pwd"; using (OleDbCommand cmd = new OleDbCommand(loginSql, con)) { cmd.Parameters.Add("@name", OleDbType.VarChar, 50).Value = textBox1.Text; cmd.Parameters.Add("@pwd", OleDbType.VarChar, 50).Value = textBox2.Text; object result = cmd.ExecuteScalar(); // ExecuteScalar 专用于单值查询 if (result != null && (int)result > 0) MessageBox.Show("登录通过!"); else MessageBox.Show("用户名或密码错误"); }注意:登录校验用
COUNT(*)比SELECT *更高效,且避免dr.Read()后还要dr.Close()。
DataAdapter.Fill():适合“查多条、展示、编辑、回写”类操作(如主从表、数据网格)。它把结果集一次性加载到DataSet或DataTable,断开连接后仍可操作,DataGridView绑定的就是这个内存对象。
4.3 UPDATE:DataRowVersion.Original是修改同步的“时间锚点”
项目正文第二段BtnUpdate()方法里,sp.SourceVersion = DataRowVersion.Original这行是关键。SqlDataAdapter的Update()方法要生成UPDATE语句,必须知道“原始值”是什么,才能写出WHERE regionid=@oldid。DataRowVersion.Original告诉 ADO.NET:这个参数的值取自DataRow被修改之前的状态(即从数据库读出来的值),而不是当前编辑后的值。
// ✅ 正确:@oldid 取原始值,确保只更新目标行 da.UpdateCommand.Parameters.Add("@oldid", SqlDbType.Int, 4, "regionid").SourceVersion = DataRowVersion.Original; // ❌ 错误:若不设 SourceVersion,默认取 Current(当前编辑值),WHERE 条件就变成 WHERE regionid=新值,可能误更新其他行DataRowVersion枚举还有Current(当前值)、Proposed(暂定值)、Default(根据上下文自动选),但Original是UPDATE和DELETE的黄金标准。
5. 避坑指南:五个血泪经验总结的“必现翻车点”
5.1 现象:OleDbException: 未指定的错误
原因:.mdb文件被其他进程(如 Access、Excel、甚至另一个 VS 实例)独占打开,OleDb 无法获取写锁。Access 数据库是文件级锁,不像 SQL Server 有行级锁。
解决:
- 关闭所有可能访问该
.mdb的程序; - 在代码中加
FileShare.ReadWrite(虽不总有效,但值得一试):string connStr = $@"Provider=Microsoft.Jet.OLEDB.4.0;Data Source={path};Jet OLEDB:Database Locking Mode=0;"; // Jet OLEDB:Database Locking Mode=0 表示乐观锁(推荐),=1 表示悲观锁(更易锁死)
5.2 现象:SqlException: Cannot open database "northwind" requested by the login
原因:northwind库未正确附加到 SQL Server 实例,或附加后未设为“在线”状态(右键库 → 属性 → 状态 → “联机”)。
解决:
- 用 SSMS 连接
localhost\SQLEXPRESS→ “数据库”节点右键 → “附加” → 选择northwind.mdf; - 附加后,右键
northwind→ “属性” → “选项” → 确认“状态”为“联机”; - 若附加失败,检查
northwind_log.ldf是否与.mdf在同一目录,或勾选“自动增长日志文件”。
5.3 现象:InvalidOperationException: Connection property has not been initialized
原因:OleDbCommand或SqlCommand创建时未传入Connection对象,或传入了null。项目正文第一段com = new OleDbCommand(sql, ole)中ole变量未定义(应为con),属笔误。
解决:
- 严格检查
new OleDbCommand(sql, connection)的第二个参数是否为已Open()的有效连接; - 使用
using块嵌套,避免连接作用域混乱:using (var con = new OleDbConnection(connStr)) { con.Open(); using (var cmd = new OleDbCommand(sql, con)) // con 已 Open,cmd 安全 { // ... } }
5.4 现象:UpdateCommand生成失败,报Dynamic SQL generation for the DeleteCommand is not supported...
原因:SqlDataAdapter的SelectCommand查询的表没有主键(Primary Key),或DataSet的DataTable没有设置PrimaryKey,导致 ADO.NET 无法生成WHERE子句。northwind.region表有RegionID主键,但若你换其他表(如regiondescription字段无索引),就会失败。
解决:
- 确保查询的表有主键;
- 手动为
DataTable设置主键(即使数据库有,DataSet 也不自动继承):da.Fill(ds, "region"); ds.Tables["region"].PrimaryKey = new DataColumn[] { ds.Tables["region"].Columns["RegionID"] };
5.5 现象:DataGridView 编辑后Update()无反应,或报Concurrency violation: the UpdateCommand affected 0 of the expected 1 records
原因:DataRow的RowState不是Modified(修改态)。常见于:
- 直接改
dataGridView1[0,0].Value,但未触发DataTable的变更跟踪; DataTable绑定后,又用Rows.Add()新增行,但未设RowState=Added。
解决:- 永远通过
DataTable操作数据,而非直接操作DataGridView:// ✅ 正确:通过 DataTable 修改 DataRow row = ds.Tables["region"].Rows[dataGridView1.CurrentRow.Index]; row["RegionDescription"] = textBoxDesc.Text; // 此时 RowState 自动变为 Modified // ❌ 错误:dataGridView1[0,0].Value = "xxx" 不会改变 DataTable - 新增行必须用
DataTable.NewRow():DataRow newRow = ds.Tables["region"].NewRow(); newRow["RegionID"] = 999; newRow["RegionDescription"] = "New Region"; ds.Tables["region"].Rows.Add(newRow); // RowState 自动为 Added
6. 进阶技巧:用DataTable.GetChanges()实现“局部提交”与脏数据隔离
6.1 为什么需要GetChanges()?—— 避免全表刷写
SqlDataAdapter.Update(ds)默认会遍历ds.Tables["table"]中所有RowState为Added、Modified、Deleted的行,并逐条执行对应的InsertCommand、UpdateCommand、DeleteCommand。但在真实业务中,你往往只想提交用户当前编辑的几行,而不是整个DataTable(可能有上千行,只改了3行)。全表刷写不仅慢,还可能因并发导致Optimistic Concurrency冲突(别人改了同一行,你的UPDATE就失败)。
DataTable.GetChanges()就是为此而生:它返回一个只包含变更行的新DataTable,你可以把它交给DataAdapter单独更新,实现“局部提交”。
// ✅ 步骤1:捕获用户本次操作的所有变更 DataTable changes = ds.Tables["region"].GetChanges(); if (changes == null || changes.Rows.Count == 0) { MessageBox.Show("没有数据变更,无需提交"); return; } // ✅ 步骤2:创建专用 DataAdapter 处理变更(复用原命令,但只传 changes) SqlDataAdapter updateDa = new SqlDataAdapter(); updateDa.InsertCommand = da.InsertCommand; updateDa.UpdateCommand = da.UpdateCommand; updateDa.DeleteCommand = da.DeleteCommand; // ✅ 步骤3:只更新变更集,返回实际影响行数 int updatedRows = updateDa.Update(changes); // ✅ 步骤4:合并结果回原 DataTable,清除变更标记 ds.Tables["region"].Merge(changes); // 合并后,RowState 重置为 Unchanged ds.Tables["region"].AcceptChanges(); // 确认所有变更,下次 GetChanges() 返回 null MessageBox.Show($"成功提交 {updatedRows} 行变更");6.2GetChanges()的三种模式与业务映射
GetChanges()接受一个DataRowState枚举参数,控制返回哪些变更:
| 参数值 | 返回行类型 | 典型业务场景 |
|---|---|---|
DataRowState.Added | 仅新增行 | “批量导入”功能,用户粘贴100行新数据,只提交新增部分 |
DataRowState.Modified | 仅修改行 | “审核模式”,用户只改了价格列,只提交价格变更 |
DataRowState.Deleted | 仅删除行 | “软删除”,用户点删除按钮,实际只设IsDeleted=1,不真删 |
// 🌟 场景:用户点了“保存修改”,但不想提交新增和删除 DataTable modifiedOnly = ds.Tables["region"].GetChanges(DataRowState.Modified); if (modifiedOnly != null && modifiedOnly.Rows.Count > 0) { int modCount = updateDa.Update(modifiedOnly); ds.Tables["region"].Merge(modifiedOnly); ds.Tables["region"].AcceptChanges(); MessageBox.Show($"仅保存了 {modCount} 处修改"); }6.3 脏数据隔离:用RejectChanges()实现“撤销到上次提交”
DataTable.RejectChanges()是AcceptChanges()的反向操作:它将所有RowState为Added的行删除,Modified的行恢复为Original值,Deleted的行恢复为Unchanged。这是实现“Ctrl+Z 撤销”的底层支撑。
// ✅ 在用户开始编辑前,备份原始状态(通常在 Form_Load 或数据加载后) ds.Tables["region"].AcceptChanges(); // 确保初始状态为 Unchanged // ✅ 用户编辑中,随时可撤销 private void buttonUndo_Click(object sender, EventArgs e) { ds.Tables["region"].RejectChanges(); // 一键回到上次 AcceptChanges() 之后的状态 dataGridView1.Refresh(); // 刷新界面 } // ✅ 但要注意:RejectChanges() 会丢弃所有未 Accept 的变更,包括新增行 // 所以,若需保留新增行(只撤销修改),应先分离: DataTable addedRows = ds.Tables["region"].GetChanges(DataRowState.Added); ds.Tables["region"].RejectChanges(); // 撤销修改和删除 if (addedRows != null) // 再把新增行加回来 foreach (DataRow r in addedRows.Rows) ds.Tables["region"].ImportRow(r);从那以后我每次做数据编辑窗体,都会在Form_Load末尾加一行dataTable.AcceptChanges(),在保存按钮里用GetChanges()分离处理,在撤销按钮里用RejectChanges(),绝不依赖dataGridView1.CancelEdit()这种表面功夫——因为CancelEdit()只影响界面控件,根本没碰DataTable的RowState。这套组合拳下来,用户再怎么狂点增删改,数据状态始终可控。希望帮到你。
本文还有配套的精品资源,点击获取