ARTICLE DETAIL

资讯详情

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

精通C#+SQLServer增删:参数化查询、批量入库与软删除设计

精通C#+SQLServer增删:参数化查询、批量入库与软删除设计 1. 整体思路C#连着SQLServer增删到底在做什么干了这么多年开发接手过不少五花八门的项目从工厂里的上位机数据采集到学校的综合教务管理系统再到公司内部的小工具几乎所有需要“存数据”的软件最后都绕不开同一个组合C#连着SQLServer做增加、删除。这个标题看起来简单但实际里面坑不少。先把这个组合拆开。SQLServer是微软家的关系型数据库专门负责把数据规规矩矩地存起来C#则是开发语言负责写业务逻辑、做界面、跟用户交互。增加删除这个动作本质上是C#把用户的操作翻译成SQLServer能听懂的SQL语句送到数据库那边执行完再把结果拿回来告诉界面“存好了”或者“删掉了”。听起来就是“发个请求、拿个结果”这么简单但真正做起来你才会发现连接串配没配对、SQL注入防没防、删错了数据能不能回滚全都是事儿。这篇文章我就从头到尾讲讲实际项目里怎么把“C# SQLServer 增加删除”这套功能做得既稳又安全。适合什么样的人看呢第一类是刚接触C#数据库开发的入门者照着做能跑通增删功能知道每一步在干什么第二类是做过一点开发、但一直用拼接SQL方式写增删的老手这篇文章里的参数化查询、软删除设计、批量插入方案应该能帮你把代码质量往上提一档第三类是准备面试上位机或管理系统岗位的求职者下面的常见问题排查部分很多都是面试官爱问的实操细节。2. 准备工作连接串配不好什么都没得聊2.1 SQLServer实例和连接串到底怎么配很多人第一步就摔在连接串上。明明代码看着没问题一跑就报“建立与服务器的连接时出错”或者“找不到数据库引擎启动句柄”。先确认SQLServer服务是活着的打开“SQL Server配置管理器”看一下SQL Server服务节点下面的实例是不是在“正在运行”状态。很多本机装了SQLServer 2016、2019、2022但连不上十有八九是服务压根没启动或者安装时实例名跟你代码里写的不一致。常见的本地连接串这么写Server.;DatabaseTestDB;User Idsa;Password你的密码;Server.代表本机默认实例。如果是命名实例比如你装的是“SQLExpress”就要写成Server.\\SQLEXPRESS注意反斜杠要转义。还有一种更省心的写法用Windows身份验证Server.;DatabaseTestDB;Integrated SecurityTrue;我个人的建议是自己开发调试用Windows身份验证最省心不用管密码策略、不用记账号只要当前Windows用户有访问数据库的权限就能连。但如果是部署到服务器上或者上位机要给客户用一般会用SQL Server身份验证也就是sa账号或者单独建一个专用账号。单独建账号比直接用sa安全因为sa权限太大了万一应用被人注入了SQL整个库都危险。还有几个连接串参数我建议加上Server.;DatabaseTestDB;User Idsa;Password你的密码;TrustServerCertificateTrue;EncryptOptional;EncryptOptional是给SQLServer 2022用的新版驱动默认会开启强制加密连接跟老版本的SQLServer通信时容易报证书相关的错加上这个能避免很多莫名其妙的问题。TrustServerCertificateTrue则是跳过证书校验本地开发或内网环境用没什么问题。2.2 先写个5秒验证连接的小工具不要急着写业务逻辑先写一个最小验证程序确认连接串没问题再往下做。这个过程能帮你把“数据库连不上”和“SQL语句写错”这两个问题彻底分开。using System; using System.Data.SqlClient; class Program { static void Main() { string connStr Server.;DatabaseTestDB;Integrated SecurityTrue;TrustServerCertificateTrue;; using (SqlConnection conn new SqlConnection(connStr)) { try { conn.Open(); Console.WriteLine(连接成功); } catch (Exception ex) { Console.WriteLine(连接失败: ex.Message); } } } }这段代码核心就干了三件事创建SqlConnection对象、调用Open()建立连接、用using保证用完释放。using是C#处理数据库连接非常关键的一个东西它保证代码块执行完无论成功还是抛异常连接都会自动关闭并释放不会把数据库连接池占满。如果这个程序能输出“连接成功”恭喜你环境没问题可以继续往下写增删了。如果失败看报错信息报“用户xx登录失败”就是账号密码问题报“网络相关错误”就是实例名或服务问题报超时多半是防火墙把1433端口拦了。3. 增加操作从一行INSERT到批量入库3.1 最基础的插入先跑通再谈优化假设有张用户表字段是Id自增主键、UserName、Age往里面加一条用户记录常规操作是构建一条INSERT语句然后用SqlCommand的ExecuteNonQuery去执行。using (SqlConnection conn new SqlConnection(connStr)) { conn.Open(); string sql INSERT INTO Users(UserName, Age) VALUES(UserName, Age); using (SqlCommand cmd new SqlCommand(sql, conn)) { cmd.Parameters.AddWithValue(UserName, 张三); cmd.Parameters.AddWithValue(Age, 25); int rows cmd.ExecuteNonQuery(); Console.WriteLine(rows 0 ? 插入成功 : 插入失败); } }这里有几个细节值得展开讲。ExecuteNonQuery的返回值是受影响的行数插入一条成功就返回1返回0说明SQL执行了但没影响任何行。Parameters.AddWithValue是参数化查询的写法作用是把UserName、Age当占位符执行时SQLServer会把参数值单独传进去跟SQL语句本身分开。为什么非得用参数化而不是拼接字符串看看反面教材你就明白了// 千万别这么写 string userName 张三; DROP TABLE Users;--; string sql INSERT INTO Users(UserName, Age) VALUES( userName , 25);这行代码拼出来之后SQLServer实际执行的是INSERT INTO Users(UserName, Age) VALUES(张三; DROP TABLE Users;--, 25)传说中的SQL注入就这么产生了。参数化查询则是把用户名当成一个“值”传进去就算输入再多单引号和分号它也只是这个字段的一串普通文本永远不会被当成SQL语法执行。这就是我不管写什么增删改查都坚持参数化的原因不是为了显摆而是这个习惯能救命。3.2 插入的记录怎么取回自增主键实际开发里有一个非常常见的需求插入一条记录之后马上要拿到这个新记录的自增主键Id因为后面还得拿这个Id去关联子表、生成日志或者返回给前端刷新列表。解决办法是跟INSERT一起执行SCOPE_IDENTITY()函数来取当前会话刚插入的自增Id。注意SELECT IDENTITY也可以取但IDENTITY是全局的如果表上你有触发器插入了其他自增表它返回的是触发器里那条记录的Id不是你要的那条。SCOPE_IDENTITY()只返回当前作用域内的自增Id更靠谱。string sql INSERT INTO Users(UserName, Age) VALUES(UserName, Age); SELECT SCOPE_IDENTITY();; using (SqlCommand cmd new SqlCommand(sql, conn)) { cmd.Parameters.AddWithValue(UserName, 李四); cmd.Parameters.AddValueWithValue(Age, 30); // 注意是 AddWithValue object result cmd.ExecuteScalar(); int newId Convert.ToInt32(result); Console.WriteLine(新记录Id: newId); }这里用了ExecuteScalar而不是ExecuteNonQuery因为要拿返回值。ExecuteScalar专门用来执行SQL并返回结果集中第一行的第一列正好匹配SCOPE_IDENTITY()的返回。这是实际写增删里非常实用的一个技巧我在做订单系统的时候订单主表和订单明细表就是这么串起来的。3.3 批量插入循环逐条插的坏处和SqlBulkCopy的好做上位机数据采集时我接过一个需求设备每秒回传几十条数据一个班次下来几千条刚开始图省事用循环ExecuteNonQuery逐条插结果数据一多界面直接卡死写入速度也不理想。每条记录都要走一遍“建立命令、解析SQL、发送给SQLServer、等待返回”的完整流程几千条下来来回通信成本太高。后来改用SqlBulkCopy批量插入速度提升非常明显。它的原理是把数据先装进DataTable然后一次性“灌”进目标表减少了几千次网络往返。DataTable dt new DataTable(); dt.Columns.Add(UserName, typeof(string)); dt.Columns.Add(Age, typeof(int)); for (int i 0; i 5000; i) { dt.Rows.Add(用户 i, 20 (i % 30)); } using (SqlConnection conn new SqlConnection(connStr)) { conn.Open(); using (SqlBulkCopy bulk new SqlBulkCopy(conn)) { bulk.DestinationTableName Users; bulk.ColumnMappings.Add(UserName, UserName); bulk.ColumnMappings.Add(Age, Age); bulk.WriteToServer(dt); } }用SqlBulkCopy有几个坑要提醒目标表字段名必须跟DataTable的列名对得上所以ColumnMappings一定要写清楚不然导进去全是默认值或者直接报错。还有SqlBulkCopy不支持触发器如果表上有复杂的插入触发器这个方案会把触发器绕过去业务上如果有依赖要自己处理。另外大批量导入时日志文件增长很快建议配合简单恢复模式操作不然硬盘容易被日志撑爆。逐条插入还是用SqlBulkCopy我一般这么判断数据量在百条以内直接循环插入完全没问题代码还直观。数据量上千尤其是采集类、上报类的连续写入直接上SqlBulkCopy。4. 删除操作语法不难难的是别删错4.1 DELETE基础写法Where条件是灵魂删除操作最简单的形式就是按主键删一条DELETE语句搞定using (SqlConnection conn new SqlConnection(connStr)) { conn.Open(); string sql DELETE FROM Users WHERE Id Id; using (SqlCommand cmd new SqlCommand(sql, conn)) { cmd.Parameters.AddWithValue(Id, 1001); int rows cmd.ExecuteNonQuery(); Console.WriteLine(删除了 rows 行); } }看着简单但WHERE条件稍有差池后果非常严重。写DELETE语句不带WHERE就是把整个表清空这个操作连确认都不带弹的。我见过不止一次开发人员在测试环境写了一条不带条件的DELETE然后又带着这条语句去连生产库测试结果整张表记录全没了。所以我给团队定过一个死规矩任何删除SQL在写出来之后先改成SELECT COUNT(*)去查一下确认影响的行数在自己预期范围内再换回DELETE执行。在实际代码里还有一个经验删除之前最好先把要删的数据查出来显示给用户确认或者至少打印日志。因为数据这东西删了就真的没了程序再强大也变不出来。4.2 按条件删多条用WHERE而不是循环删有一个常见错误是拿到一批Id之后写个循环一条一条删除。比如用户勾选了100条记录要批量删除新手最容易这么写循环。这种写法的问题跟逐条插入一样——通信开销大、效率低而且中途失败会出现“删了一半”的脏数据状态。正确做法是构造一个IN条件。但这里有个细节要注意IN后面不能直接用参数化方式传一个列表变量要么用动态拼接参数要么用表值参数要么用STRING_SPLIT函数。SQLServer 2016及以上版本可以用STRING_SPLITstring ids 1,2,3,4,5; string sql DELETE FROM Users WHERE Id IN (SELECT value FROM STRING_SPLIT(Ids, ,)); using (SqlCommand cmd new SqlCommand(sql, conn)) { cmd.Parameters.AddWithValue(Ids, ids); int rows cmd.ExecuteNonQuery(); }STRING_SPLIT是SQLServer 2016引入的字符串拆分函数把Ids按逗号拆成多行外边IN子查询就能直接匹配。这个方案胜在一条SQL就搞定而且完全是参数化的不用动态拼一串Id0, Id1, Id2。如果是SQLServer 2008或2012这些老版本没有STRING_SPLIT可以用临时表或者XML拆分。不过现在还在用老版本SQLServer的项目真的建议升级了性能和安全性都跟不上。还有一种更规范的做法是提前在数据库里建好自定义表类型然后C#这边传DataTable进去当参数。表值参数适合复杂场景但需要额外建类型日常用STRING_SPLIT已经足够了。4.3 软删除和硬删除删数据前先想清楚这是个设计层面的问题。硬删除就是上面这种DELETE物理上把数据从表里干掉查不到、找不回。软删除则是加一个IsDeleted字段删除时执行的是UPDATEUPDATE Users SET IsDeleted 1 WHERE Id Id查询列表时统一带上WHERE IsDeleted 0虽然数据还在表里但对用户来说“已删除”的记录已经看不到了。这两种删法怎么选我的经验是凡是涉及用户操作痕迹、订单记录、财务报表这类要留痕的数据一律用软删除。比如你做教务管理系统学生成绩被老师误删如果走的是硬删除连恢复都无从谈起到时候哭都来不及。但如果是临时采集的中间数据、缓存数据、日志数据这些没有保留价值的硬删更干脆还能控制表体积。能不能软硬结合可以。很多系统就是这么设计的界面点删除时先做软删除让数据对用户不可见后台另设一个“数据清理”功能定期把超过保留期限的软删除记录真正物理清掉。这样既有软删除的安全性又不会让表无限膨胀。这个模式我强烈建议做管理系统的人都考虑进去光这一个决定就能帮你省掉后面无数的“求恢复数据”的麻烦。4.4 级联删除和事务数据关联时要动脑子如果表之间有外键关系删除主表记录时子表还有关联数据SQLServer会直接报错。比如用户表和订单表存在外键约束用户存在订单时直接删用户会提示“DELETE 语句与 REFERENCE 约束冲突”。处理方式有两种。第一种先删子表再删主表代码里写两遍DELETE用SqlTransaction包起来保证要么全删干净要么全不删。第二种在数据库外键上设置级联删除ON DELETE CASCADE删主表时自动删子表。我个人的建议是业务简单的场景可以用级联但一旦涉及多方关联比如用户-订单-订单明细这种多层结构级联删除的连锁反应很难一眼看清最好还是在代码里手动控制删除顺序。事务的写法看起来就是给连接开个BeginTransaction()然后把命令都挂到这个事务上using (SqlTransaction tran conn.BeginTransaction()) { try { // 先删订单明细 // 再删订单 // 最后删用户 tran.Commit(); Console.WriteLine(全部删除成功); } catch { tran.Rollback(); Console.WriteLine(删除失败已回滚); } }这里的关键是cmd.Transaction tran要把每个SqlCommand的事务属性都赋上不然命令默认不在事务里执行一旦中途出错前面删掉的数据照样提交事务就名存实亡了。5. 常见问题与排查技巧实录5.1 连接不上SQLServer怎么按顺序排查这个问题的出现频率居于所有数据库问题的第一位每次帮别人调代码我基本按固定顺序查服务启动了吗打开配置管理器看服务状态。实例名对吗默认实例用.或localhost命名实例用.\SQLEXPRESS。认证模式对吗SQL Server身份验证的话检查目标数据库账号是否启用密码是否输对。防火墙放行了吗SQLServer默认端口是1433进防火墙高级设置加一条入站规则开放这个端口。远程连接允许了吗SQLServer安装后在“属性-连接”里默认可能不允许远程连接需要勾选“允许远程连接到此服务器”。很多被这个问题卡到崩溃的案例排查到这一步就豁然开朗了。有一个例外值得单独说SQLServer 2016/2017/2019安装时报“无法找到数据库引擎启动句柄”这不是连接问题而是安装问题一般是安装过程中服务没起来可以去事件查看器里找具体的错误记录常见诱因是安装时选了“Windows身份验证模式”但当前用户没有SQLServer管理员权限。装上之后连接就好使了。5.2 插入的中文变成乱码怎么解决中文乱码大多数情况是数据库表的字符集或者字段排序规则不对。SQLServer跟MySQL不同不用utf8mb4这种存储引擎级别的字符集概念但每个字段的Collation排序规则会影响中文的显示和排序。最典型的场景是把中文存成了类似于乱码的方块。检查方案是看建表语句SELECT name, collation_name FROM sys.columns WHERE object_id OBJECT_ID(Users);如果看到排序规则是Chinese_PRC_CI_AS说明支持中文问题不大。如果是SQL_Latin1_General_CP1_CI_AS存中文就有可能出现乱码。解决办法是在建表或者建字段时把排序规则改成中文的ALTER TABLE Users ALTER COLUMN UserName NVARCHAR(50) COLLATE Chinese_PRC_CI_AS;另外一点很多人容易忽略C#连SQLServer插入中文时字段类型一定要用NVARCHAR而不是VARCHAR。VARCHAR是按单字节存储的中文字符存进去后按多字节解析会乱NVARCHAR是Unicode存储能直接存中文。这个在表设计的时候就该定下来。5.3 字符串转数字TryParse才是C#的答案热词里搜过“sqlserver 字符串转数字”和“c#怎样截取字符串”的应该是在做数据处理时遇到类型转换问题了。SQLServer那边可以用CAST和CONVERT在SQL层面转SELECT CAST(123 AS INT); SELECT CONVERT(INT, 123);如果字符串里带非数字字符比如表格里导过来的“金额123元”CAST会直接报错转换失败。SQLServer 2012以上可以用TRY_CAST转失败返回NULL而不是报错SELECT TRY_CAST(123元 AS INT); -- 返回 NULLC#这边处理字符串转数字永远优先int.TryParse而不是Convert.ToInt32string input 123; if (int.TryParse(input, out int result)) { Console.WriteLine(result); } else { Console.WriteLine(转换失败处理默认逻辑); }TryParse的好处是转换失败不会抛异常而是返回false。用Convert.ToInt32遇到非法字符串会直接程序崩在上位机那种长时间无人值守的环境里这种小细节就能决定程序是不是稳定。5.4 ExecuteNonQuery返回值是0但不报错写增加删除时发现代码不报错界面也提示“执行成功”但数据没有任何变化ExecuteNonQuery返回0。这个情况大概率是SQL的WHERE条件没匹配到任何行。比如删除用户Id10086但表里根本没有这个IdSQLServer不认为这是错误它只是告诉你“没删到”。处理方法是把返回值利用起来但这还不够因为0行也可能是“该删的确实不存在”。我通常在删除前先查一下记录是否存在SELECT COUNT(1) FROM Users WHERE Id Id;返回0就直接提示用户“这条记录不存在了”返回1再执行删除。多一条查询虽然多了点开销但在关键业务上能避免很多“假成功”的误判。5.5 WinForm卡死数据库操作别占UI线程热词里搜过“c#线程”“c# task的用法”“winform卡”的多半是遇到了界面卡死的问题。凡是在点击事件里直接跑数据库增删一旦数据量大SQL执行几百毫秒甚至几秒界面会一直转圈像死机一样。解决方案就是用异步方法。从.NET 4.5开始SqlConnection和SqlCommand都有了异步版本private async void btnDelete_Click(object sender, EventArgs e) { using (SqlConnection conn new SqlConnection(connStr)) { await conn.OpenAsync(); string sql DELETE FROM Users WHERE Id Id; using (SqlCommand cmd new SqlCommand(sql, conn)) { cmd.Parameters.AddWithValue(Id, currentId); int rows await cmd.ExecuteNonQueryAsync(); // 回到UI线程刷新列表 LoadUserList(); } } }async方法内部遇到await会立刻把控制权交还给UI线程界面不会卡住等数据库操作完成后自动回来继续执行。这是WinForm和上位机开发里非常值得养成的好习惯。注意async void只能用在事件处理程序上普通方法就别用async void了异常处理是麻烦事普通方法用async Task更合适。6. 实操心得把这套增删练成肌肉记忆带过不少新人也面试过不少做上位机和信息管理系统的候选人我对“C#联合SQLServer增删”这个能力的判断标准就三条第一条能不能说清楚为什么用参数化查询防注入第二条删除操作之前有没有想过数据恢复的问题第三条批量操作时脑子里面有没有效率的概念知不知道循环逐条写是下策。我自己写增删代码时已经养成了几个固定的习惯分享给大家。连接对象和命令对象一定用using包着这比在finally里写Dispose优雅还不容易漏资源。所有用户输入一律走参数化不管这个输入是界面文本框还是配置文件读取出来的没有任何例外。删除操作一定带上事务和日志事务保证原子性日志保证出问题时能追溯现场。涉及大批量数据一定先想想有没有比循环更好的方案。最后再分享一个小技巧。我一般在写完一段增删代码之后会顺手在SQLServer Management Studio里用事务包着调试一下比如删除用户时先写BEGIN TRAN; DELETE ...; SELECT * FROM Users WHERE IdId; ROLLBACK;这四行会告诉你删除逻辑是否正确但不会真正删掉数据。代码走一遍数据还在这条秘诀帮我排掉了无数次潜在的误删事故。这套组合看着基础但能把细节做到位的人写出的系统在使用的时候才真正让人放心。
返回列表