2026/10/15 21:42:51

C#数据库增删改查实战:Access与SQL Server双路径落地

C#数据库增删改查实战:Access与SQL Server双路径落地 简介本资源是一份面向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 Servernorthwind示例库的 SqlDataAdapter DataSet 完整事务流。它不讲 ORM不碰 Entity Framework专治“连不上、插不进、删不掉、改不动、查不到”这五类新手高频翻车现场。适合正在做课程设计、实训项目或快速原型开发的 C# 初学者也适合需要临时搭个本地管理工具的老手——毕竟不是每个场景都需要上 Web API 和 EF Core。2. 从零建连接OleDb 与 SqlConnection 的选型逻辑与初始化实操2.1 为什么先用 OleDbAccess 是你的“后悔药”很多教程一上来就推 SQL Server但新手常卡在“装 SQL Server Express 太重”“sa 密码设错连不上”“Windows 身份验证失败”这类环境问题上。而.mdb文件是纯文件双击就能用 Access 打开验证数据OleDb 提供的是最轻量级的“数据库存在性证明”。项目正文里第一段代码用的就是OleDbConnectionMicrosoft.Jet.OLEDB.4.0它对应的是旧版 Access 2003 格式.mdb兼容性极强Win7/Win10/Win11 原生支持无需额外安装驱动。注意它不支持.accdbAccess 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( ProviderMicrosoft.Jet.OLEDB.4.0;Data Source path ;Persist Security InfoFalse); 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 SqlConnectionSQL Server 连接字符串的三个生死参数当你升级到 SQL Server如northwind示例库SqlConnection是唯一选择。项目正文第二段代码中DRIVER字符串为server.;databasenorthwind;uidsa;pwdsa这里藏着三个必须显式确认的参数server.表示本地默认实例。若你用的是命名实例如SQLEXPRESS必须写成serverYOURPCNAME\\SQLEXPRESS或serverlocalhost\\SQLEXPRESSdatabasenorthwind库名必须已存在且已附加。Northwind 是微软经典示例库需手动下载.bak文件并用 SSMS 还原不能靠代码自动创建uidsa;pwdsasa是系统管理员账户但默认禁用且密码为空。你必须先用 Windows 身份验证登录 SSMS右键sa→ 属性 → 启用登录 设置强密码不能是sa这种弱口令SQL Server 2019 会拒绝。// ✅ 正确写法显式指定连接超时和加密生产环境必备 string connectionString Serverlocalhost\SQLEXPRESS;Databasenorthwind;User Idsa;PasswordYourStrongPass123!;Connection Timeout30;EncryptFalse;TrustServerCertificateTrue;; 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}); } }注意EncryptFalse;TrustServerCertificateTrue是开发机临时方案生产环境必须配证书并设EncryptTrue。此处仅为让代码跑通不构成安全建议。2.3 连接池别在每次操作后Close()但必须Dispose()初学者常犯的错误是每次增删改查都新建Connection、Open()、Close()。这看似稳妥实则摧毁性能。ADO.NET 默认启用连接池Connection PoolingClose()并非断开物理连接而是将连接归还池中复用。真正该做的是用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.1a的本质占位符不是字符串拼接项目正文里所有 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 11它也会被当做一个普通字符串插入而非执行恶意逻辑。// ✅ 安全参数值与 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 对参数名的处理略有差异OleDbAccess参数名不区分大小写但必须与 SQL 中的xxx完全一致包括符号。UserName和username在 OleDb 中被视为同一参数SqlConnectionSQL Server参数名区分大小写且要求严格匹配。UserName和username是两个不同参数。项目正文第一段用a、b是为了简化但实际工程中必须用语义化名称如Name、Password、RegionId。原因有三一是避免abc混乱二是方便调试时看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)size50对于 VARCHAR(100)size100不指定则按默认长度可能不够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 INSERTExecuteNonQuery()返回值是“影响行数”不是“是否成功”ExecuteNonQuery()是增删改的统一执行方法它返回一个int表示受 SQL 语句影响的行数。项目正文里所有INSERT、DELETE、UPDATE都用了它但新手常误读返回值INSERT INTO t1 VALUES (...)→ 成功返回1失败如主键冲突返回0或抛异常DELETE FROM t1 WHERE id999→ 若id999不存在返回0这不算错误只是没删到UPDATE t1 SET namen WHERE idid→ 若idid不存在返回0同样不是异常。所以不能只靠if (count 0)判断业务成功必须结合业务逻辑。比如“添加用户”应检查count 1而“删除选中行”应先确认dataGridView1.CurrentRow ! null再执行否则count0是预期行为。// ✅ 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 SELECTExecuteReader()与Fill(DataSet)的适用场景查操作有两种主流模式ExecuteReader()流式读取内存占用低和DataAdapter.Fill()批量加载到内存支持离线编辑。项目正文第一段用ExecuteReader()做登录校验第二段用SqlDataAdapter.Fill()绑定 DataGridView这是典型的场景划分ExecuteReader()适合“查一条、校验、跳转”类操作如登录、权限检查、存在性验证。它返回OleDbDataReader只能向前读且连接必须保持打开状态直到Read()完成。dr.Read()返回true表示有数据false表示无结果。// ✅ 登录校验只关心是否存在匹配记录 string loginSql SELECT COUNT(*) FROM t1 WHERE namename AND pwdpwd; 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 UPDATEDataRowVersion.Original是修改同步的“时间锚点”项目正文第二段BtnUpdate()方法里sp.SourceVersion DataRowVersion.Original这行是关键。SqlDataAdapter的Update()方法要生成UPDATE语句必须知道“原始值”是什么才能写出WHERE regionidoldid。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 $ProviderMicrosoft.Jet.OLEDB.4.0;Data Source{path};Jet OLEDB:Database Locking Mode0;; // Jet OLEDB:Database Locking Mode0 表示乐观锁推荐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 已 Opencmd 安全 { // ... } }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()新增行但未设RowStateAdded。解决永远通过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 自动为 Added6. 进阶技巧用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仅删除行“软删除”用户点删除按钮实际只设IsDeleted1不真删// 场景用户点了“保存修改”但不想提交新增和删除 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。这是实现“CtrlZ 撤销”的底层支撑。// ✅ 在用户开始编辑前备份原始状态通常在 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。这套组合拳下来用户再怎么狂点增删改数据状态始终可控。希望帮到你。本文还有配套的精品资源点击获取