C#与SQL Server交互实战:从ADO.NET基础到性能优化

📅 发布时间:2026/8/17 12:15:53
C#与SQL Server交互实战:从ADO.NET基础到性能优化 1. 项目概述从零到一掌握C#与SQL Server的交互核心如果你刚开始用C#做项目十有八九绕不开要和数据库打交道。而SQL Server作为微软技术栈里的“老搭档”与C#的配合可以说是天衣无缝。但很多新手朋友一上来就被“增删改查”这四个字给难住了感觉每个操作背后都有一堆SqlConnection、SqlCommand、SqlDataReader还有令人头疼的异常处理和资源释放。别担心这篇文章就是来帮你把这些看似零散的操作串成一条清晰、可复现的实战路径。我干了十多年后端开发用C#操作SQL Server的代码写过不计其数。今天我们不谈那些高大上的ORM框架就扎扎实实地回到最根本的ADO.NET把“增、删、改、查”这四项基本功掰开揉碎了讲清楚。你会发现掌握了这些底层操作无论后面是用Dapper、Entity Framework还是自己封装数据访问层心里都会特别有底。这篇文章的目标就是让你看完之后能独立、安全、高效地写出与SQL Server交互的C#代码并且清楚每一个步骤背后的“为什么”。2. 环境准备与核心对象解析在动手写代码之前我们必须把“舞台”搭好并且认识台上的每一位“演员”。很多问题其实都出在准备工作没做到位。2.1 项目环境搭建与NuGet包管理首先创建一个新的C#控制台应用项目。打开Visual Studio或者你喜欢的IDE比如Rider、VS Code选择“控制台应用”模板即可。项目创建好后我们需要引入操作SQL Server的核心程序集。在.NET Framework项目中你通常可以在“引用”里直接添加System.Data.SqlClient。但在现代的.NET Core或.NET 5/6/7/8中我们通过NuGet包管理器来添加。打开“工具”-“NuGet包管理器”-“管理解决方案的NuGet程序包”搜索并安装Microsoft.Data.SqlClient。这是微软官方维护的、支持新版本.NET的SQL Server数据提供程序比旧的System.Data.SqlClient功能更新性能也更好。注意如果你的项目是.NET Framework 4.7.2或更高版本也可以使用Microsoft.Data.SqlClient它提供了更好的兼容性和新功能。对于全新的项目我强烈建议直接使用它。安装好后别忘了在代码文件的顶部引入命名空间using Microsoft.Data.SqlClient; // 或者 using System.Data.SqlClient; (针对旧版.NET Framework) using System;2.2 ADO.NET核心四大件详解ADO.NET是与数据库交互的底层库它的核心是几个对象理解它们各自的生命周期和职责至关重要。SqlConnection连接对象这是与数据库服务器的物理通道。创建它需要连接字符串Connection String就像你要打电话必须知道对方的电话号码。它的主要职责是打开和关闭连接。一个非常重要的原则是连接是稀缺资源用完后必须立即关闭。通常我们会使用using语句块来确保即使发生异常连接也能被正确释放。SqlCommand命令对象它代表要对数据库执行的一条SQL语句或存储过程。你需要为它指定要执行的命令文本CommandText和关联的连接Connection。它才是真正执行“增删改查”动作的执行者。你可以通过它执行不返回结果集的操作如INSERT, UPDATE, DELETE也可以执行返回结果集的操作如SELECT。SqlDataReader读取器对象这是一个只进、只读的数据流专门用于高效地从SELECT查询中读取数据。你可以把它想象成一个从头走到尾的扫描仪一次只能读取当前行的一列数据。它非常高效因为数据不是一次性全部加载到内存而是边读边处理。它必须在一个打开的连接上工作并且在读取数据时该连接被独占不能用于其他操作。SqlDataAdapter与DataSet适配器与数据集这是一对“重量级”组合。SqlDataAdapter像一个搬运工它利用SqlCommand执行查询然后将结果填充到DataSet这个内存中的离线数据库容器里。DataSet可以包含多个DataTable并且维护数据之间的关系和状态如新增、修改、删除。这种方式适合处理复杂的数据关系或需要离线操作的场景但内存开销较大。对于简单的“增删改查”我们更多使用前三个对象。理解这几个对象的关系是关键SqlCommand挂在SqlConnection上执行产生的结果可以由SqlDataReader流式读取或者由SqlDataAdapter搬运到DataSet。2.3 连接字符串的安全配置与管理连接字符串是安全重灾区绝对不能硬编码在代码里。一个典型的连接字符串长这样Serverlocalhost\SQLEXPRESS; DatabaseYourDatabase; Trusted_ConnectionTrue; TrustServerCertificateTrue;或者使用用户名密码ServermyServerAddress; DatabasemyDataBase; User IdmyUsername; PasswordmyPassword; TrustServerCertificateTrue;Trusted_ConnectionTrue使用Windows身份验证更安全无需在连接串中暴露密码。TrustServerCertificateTrue在开发环境尤其是使用本地SQL Express时跳过证书验证避免连接错误。生产环境应配置有效的证书。EncryptTrue在生产环境中强烈建议启用加密以保护数据传输安全。安全实践绝对不要将带密码的连接字符串提交到源代码版本控制系统如Git。在开发中可以使用appsettings.json文件存储并通过IConfiguration读取。在.NET Core中典型做法是在appsettings.Development.json中配置并通过UserSecrets管理敏感信息。在生产环境使用环境变量或Azure Key Vault等安全存储服务。3. 查Read操作高效读取数据的多种姿势查询是数据库操作中最频繁的动作。如何根据场景选择正确的方法是写出高效代码的第一步。3.1 使用SqlDataReader进行流式高效查询当你需要快速、顺序地处理大量数据并且不需要在内存中保留所有数据时SqlDataReader是最佳选择。它的模式是“连接式”的即在整个读取过程中数据库连接必须保持打开状态。string connectionString Your_Connection_String_Here; string queryString SELECT CustomerID, CompanyName FROM Customers WHERE Country Country; using (SqlConnection connection new SqlConnection(connectionString)) { SqlCommand command new SqlCommand(queryString, connection); // 使用参数化查询防止SQL注入 command.Parameters.AddWithValue(Country, Germany); try { connection.Open(); // 执行命令获取DataReader using (SqlDataReader reader command.ExecuteReader()) { // 判断是否有数据返回 if (reader.HasRows) { // 循环读取每一行 while (reader.Read()) { // 通过列名或序号获取数据。推荐使用列名代码更清晰。 Console.WriteLine($ID: {reader[CustomerID]}, Name: {reader[CompanyName]}); // 也可以使用强类型方法提高性能并避免装箱拆箱 // string id reader.GetString(reader.GetOrdinal(CustomerID)); } } else { Console.WriteLine(No rows found.); } // reader在using块结束时自动关闭 } } catch (SqlException ex) { // 处理数据库特定异常如连接失败、语法错误等 Console.WriteLine($Database error: {ex.Message}); } // connection在using块结束时自动关闭 }关键点与避坑指南using语句确保SqlConnection和SqlDataReader被及时释放。这是防止连接泄漏Connection Leak的关键连接泄漏会导致数据库连接池耗尽应用崩溃。参数化查询Country这是抵御SQL注入攻击的唯一正确方式。永远不要用字符串拼接的方式来构造SQL语句比如$“SELECT ... WHERE Country ‘{country}’”。reader.Read()将读取器前进到下一行。它返回一个布尔值指示是否还有行。必须在读取数据前调用。读取数据的方法reader[“列名”]返回object方便但涉及装箱。对于性能敏感的场景使用GetString、GetInt32等强类型方法并配合GetOrdinal获取列索引。连接打开时机在即将执行命令前ExecuteReader才打开连接操作完成后立即关闭以最大化连接池的利用率。3.2 使用SqlDataAdapter与DataSet进行离线灵活操作如果你的业务逻辑需要复杂的数据遍历、前后滚动或者需要将数据绑定到UI控件如WinForms的DataGridViewDataSet是更合适的选择。它将数据一次性加载到内存中断开与数据库的连接允许你自由操作。string connectionString Your_Connection_String_Here; string queryString SELECT * FROM Orders WHERE OrderDate StartDate; using (SqlConnection connection new SqlConnection(connectionString)) { SqlDataAdapter adapter new SqlDataAdapter(queryString, connection); // 同样参数需要配置给SqlDataAdapter内部的SelectCommand adapter.SelectCommand.Parameters.AddWithValue(StartDate, new DateTime(2023, 1, 1)); DataSet orderDataSet new DataSet(); try { // Fill方法会自动打开和关闭连接如果连接未打开 adapter.Fill(orderDataSet, Orders); // “Orders”是为这个DataTable起的名字 DataTable ordersTable orderDataSet.Tables[Orders]; if (ordersTable.Rows.Count 0) { // 遍历所有行 foreach (DataRow row in ordersTable.Rows) { Console.WriteLine($OrderID: {row[OrderID]}, Date: {row[OrderDate]}); // 可以随意访问任何行任何列 DataRow firstRow ordersTable.Rows[0]; // 可以修改数据 row[ShipCity] Berlin; } // DataSet可以包含多个表并建立关系 // DataRelation relation new DataRelation(CustOrderRel, // orderDataSet.Tables[Customers].Columns[CustomerID], // orderDataSet.Tables[Orders].Columns[CustomerID]); // orderDataSet.Relations.Add(relation); } } catch (SqlException ex) { Console.WriteLine($Database error: {ex.Message}); } // 无需手动关闭连接Fill方法已处理 }核心区别与选择建议SqlDataReader快、省内存但只进只读且占用连接。适合后台任务、数据导出、流式处理。DataSet功能强大、灵活可离线操作但内存开销大。适合复杂业务逻辑、小型数据全集操作、需要数据绑定的桌面应用场景。一个常见的误区是无论数据量大小都用DataSet。对于一次读取几百条、几千条记录并在内存中频繁查找、计算的情况DataSet确实方便。但对于一次读取十万、百万条记录SqlDataReader是唯一的选择否则内存会瞬间爆掉。3.3 执行标量查询与单值获取有时候我们只需要查询一个值比如记录总数COUNT(*)、最大值MAX或某个聚合结果。这时使用ExecuteScalar方法最高效。string sql “SELECT COUNT(*) FROM Products WHERE UnitsInStock ReorderLevel”; using (SqlConnection conn new SqlConnection(connString)) using (SqlCommand cmd new SqlCommand(sql, conn)) { cmd.Parameters.AddWithValue(“ReorderLevel”, 10); conn.Open(); // ExecuteScalar返回结果集中第一行的第一列类型是object int lowStockCount (int)cmd.ExecuteScalar(); Console.WriteLine($“有{lowStockCount}种产品库存低于警戒线。”); }ExecuteScalar的返回值是object需要根据查询结果进行类型转换。如果查询没有返回任何行它会返回null在转换前需要进行判断。4. 增Create、改Update、删Delete操作与事务控制非查询操作INSERT UPDATE DELETE相对简单但它们涉及到数据的一致性因此事务Transaction的使用至关重要。4.1 基本的增删改实现这三种操作都使用SqlCommand的ExecuteNonQuery方法它返回受影响的行数。插入Insert示例string insertSql ” INSERT INTO Employees (FirstName, LastName, BirthDate, HireDate) VALUES (FirstName, LastName, BirthDate, HireDate); SELECT SCOPE_IDENTITY();”; // 获取刚插入记录的自增ID using (SqlConnection conn new SqlConnection(connString)) using (SqlCommand cmd new SqlCommand(insertSql, conn)) { cmd.Parameters.AddWithValue(“FirstName”, “张”); cmd.Parameters.AddWithValue(“LastName”, “三”); cmd.Parameters.AddWithValue(“BirthDate”, new DateTime(1990, 5, 15)); cmd.Parameters.AddWithValue(“HireDate”, DateTime.Today); conn.Open(); // ExecuteScalar用于获取SCOPE_IDENTITY()返回的新ID int newEmployeeId Convert.ToInt32(cmd.ExecuteScalar()); Console.WriteLine($“新员工ID: {newEmployeeId}”); }这里用了一个技巧在INSERT语句后加上SELECT SCOPE_IDENTITY()然后使用ExecuteScalar执行可以一次性完成插入并获取数据库自动生成的主键ID比如自增列。SCOPE_IDENTITY()函数返回当前作用域内最后生成的标识值比IDENTITY更安全。更新Update与删除Delete示例// 更新 string updateSql “UPDATE Products SET UnitPrice NewPrice WHERE ProductID ProductID”; using (SqlCommand cmd new SqlCommand(updateSql, conn)) { cmd.Parameters.AddWithValue(“NewPrice”, 25.99m); cmd.Parameters.AddWithValue(“ProductID”, 1); conn.Open(); int rowsAffected cmd.ExecuteNonQuery(); Console.WriteLine($“更新了{rowsAffected}条记录。”); } // 删除 string deleteSql “DELETE FROM Customers WHERE CustomerID CustomerID AND LastOrderDate CutoffDate”; using (SqlCommand cmd new SqlCommand(deleteSql, conn)) { cmd.Parameters.AddWithValue(“CustomerID”, “OLDCO”); cmd.Parameters.AddWithValue(“CutoffDate”, DateTime.Today.AddYears(-5)); int rowsAffected cmd.ExecuteNonQuery(); // 可以根据rowsAffected判断是否真的删除了记录 }ExecuteNonQuery返回受影响的行数。这个返回值非常有用你可以用它来验证操作是否如预期执行例如更新时判断是否只更新了1条目标记录。4.2 事务Transaction的必须性与正确使用事务是保证一组数据库操作要么全部成功要么全部失败回滚的机制。想象一下银行转账从A账户扣钱和向B账户加钱必须作为一个整体。任何一步失败另一步也必须撤销。在C#中使用ADO.NET事务的基本模式如下string connString “Your_Connection_String”; using (SqlConnection conn new SqlConnection(connString)) { conn.Open(); // 1. 开启事务 SqlTransaction transaction conn.BeginTransaction(); // 可以指定隔离级别如 IsolationLevel.ReadCommitted try { string sql1 “UPDATE Account SET Balance Balance - 100 WHERE AccountId A”; string sql2 “UPDATE Account SET Balance Balance 100 WHERE AccountId B”; // 操作1 using (SqlCommand cmd1 new SqlCommand(sql1, conn, transaction)) { cmd1.Parameters.AddWithValue(“A”, “Account001”); int r1 cmd1.ExecuteNonQuery(); if (r1 ! 1) throw new Exception(“扣款账户更新失败”); } // 模拟一个可能失败的操作 // bool somethingWrong true; // if (somethingWrong) throw new ApplicationException(“模拟的异常”); // 操作2 using (SqlCommand cmd2 new SqlCommand(sql2, conn, transaction)) { cmd2.Parameters.AddWithValue(“B”, “Account002”); int r2 cmd2.ExecuteNonQuery(); if (r2 ! 1) throw new Exception(“收款账户更新失败”); } // 2. 一切顺利提交事务 transaction.Commit(); Console.WriteLine(“转账成功”); } catch (Exception ex) { // 3. 发生异常回滚事务 Console.WriteLine($“操作失败正在回滚。错误: {ex.Message}”); try { transaction.Rollback(); } catch (Exception rollbackEx) { // 处理回滚本身可能发生的异常例如连接已断开 Console.WriteLine($“回滚时发生错误: {rollbackEx.Message}”); } // 向上抛出或处理业务异常 throw; } // using块会确保连接关闭 }事务使用核心要点同一个连接事务内的所有命令必须使用同一个SqlConnection对象。关联事务每个SqlCommand对象必须通过构造函数或.Transaction属性与开启的SqlTransaction对象关联。BeginTransaction在连接打开后调用开始一个事务。Commit在所有操作成功后调用将更改永久保存到数据库。Rollback在catch块中调用撤销事务内所有未提交的更改。异常处理必须用try-catch包裹事务操作并在异常时回滚。回滚操作本身也可能失败需要单独处理。隔离级别BeginTransaction(IsolationLevel level)可以设置隔离级别如ReadCommitted默认避免脏读、Serializable最高隔离避免幻读等。级别越高一致性越强但并发性能越差。大多数业务场景ReadCommitted已足够。一个常见的坑是忘记将命令对象与事务关联。如果命令没有指定Transaction属性它会在自己的隐式事务中执行不受你控制的显式事务影响这就破坏了原子性。5. 高级技巧与性能优化实战掌握了基础操作后一些高级技巧能让你代码的健壮性和性能提升一个档次。5.1 使用Using语句管理资源与连接池机制前面反复强调了using这里深入一下。SqlConnection和SqlDataReader都实现了IDisposable接口。using语句会在代码块结束时自动调用它们的Dispose方法。对于SqlConnectionDispose()会先检查连接状态如果打开则关闭它然后将连接释放回连接池。对于SqlDataReaderDispose()会关闭读取器并释放相关资源。连接池Connection Pooling是ADO.NET一个至关重要的性能优化特性。默认是开启的。当你“关闭”一个连接时物理TCP连接并没有真正断开而是被标记为空闲放回池子里。下次请求相同连接字符串的连接时直接从池子里取出一个空闲的连接复用避免了建立TCP连接和数据库登录验证的巨大开销通常是几十到几百毫秒。using语句是确保连接能被正确放回池子的最佳实践。千万不要手动调用.Close()后就觉得万事大吉Dispose()包含了Close()并做了更多清理工作。5.2 参数化查询深入类型、精度与性能参数化查询不仅是防注入的盾牌也对性能有帮助。数据库服务器可以对参数化查询的查询计划进行缓存和复用。// 不推荐的字符串拼接危险 string badSql $“SELECT * FROM Users WHERE Name ‘{userInput}’”; // 推荐的参数化查询 string goodSql “SELECT * FROM Users WHERE Name UserName”; cmd.Parameters.AddWithValue(“UserName”, userInput);关于AddWithValue的注意事项AddWithValue非常方便但它有时会推断出不准确的数据类型可能导致索引失效或隐式类型转换影响性能。对于有严格类型要求的列如DECIMAL的精度和小数位NVARCHAR的长度更推荐使用Add方法显式指定。// 使用 Add 方法明确指定参数类型更优 SqlParameter param new SqlParameter(“Price”, SqlDbType.Decimal); param.Value 19.99m; param.Precision 10; // 总位数 param.Scale 2; // 小数位 cmd.Parameters.Add(param); // 或者使用简化的Add方法重载 cmd.Parameters.Add(“Price”, SqlDbType.Decimal).Value 19.99m; // 对于字符串可以指定长度有助于生成更优的执行计划 cmd.Parameters.Add(“Description”, SqlDbType.NVarChar, 500).Value description;5.3 存储过程Stored Procedure的调用对于复杂的业务逻辑将其封装在数据库的存储过程中然后在C#中调用是一种常见的架构模式。这样做可以利用数据库的计算能力减少网络传输也便于逻辑的集中管理和复用。假设数据库中有一个存储过程sp_GetEmployeeOrdersCREATE PROCEDURE sp_GetEmployeeOrders EmployeeID INT, Year INT NULL -- 默认参数 AS BEGIN SELECT * FROM Orders WHERE EmployeeID EmployeeID AND (Year IS NULL OR YEAR(OrderDate) Year) ORDER BY OrderDate DESC; END在C#中调用它string connString “...”; using (SqlConnection conn new SqlConnection(connString)) using (SqlCommand cmd new SqlCommand(“sp_GetEmployeeOrders”, conn)) { // 1. 指定命令类型为存储过程 cmd.CommandType CommandType.StoredProcedure; // 2. 添加参数 cmd.Parameters.AddWithValue(“EmployeeID”, 5); // 为可选参数赋值如果不赋值存储过程会使用其默认值(NULL) cmd.Parameters.AddWithValue(“Year”, 2023); conn.Open(); using (SqlDataReader reader cmd.ExecuteReader()) { while (reader.Read()) { // 处理结果... } } }调用存储过程的关键是将SqlCommand的CommandType属性设置为CommandType.StoredProcedure。参数添加方式与普通SQL语句完全一致。如果存储过程有输出参数OUTPUT则需要设置参数的Direction属性为ParameterDirection.Output并在执行命令后从参数中取值。6. 异常处理、日志记录与连接管理最佳实践健壮的程序必须能妥善处理错误并留下可供排查的线索。6.1 结构化异常处理与重试策略数据库操作可能抛出多种异常最常见的是SqlException。你应该捕获特定的异常而不是通用的Exception。try { // 数据库操作 conn.Open(); // ... } catch (SqlException sqlEx) { // 处理数据库特定错误 foreach (SqlError error in sqlEx.Errors) // SqlException可能包含多个错误 { Console.WriteLine($“SQL Error Number: {error.Number}, Message: {error.Message}”); // 错误号Number可以用来判断具体错误类型如超时( -2 )、死锁(1205)等 if (error.Number -2) // 超时 { // 可以考虑实现重试逻辑 Console.WriteLine(“查询超时建议优化查询或增加超时时间。”); } else if (error.Number 1205) // 死锁 { Console.WriteLine(“发生死锁事务已被选为牺牲品。应重试事务。”); // 实现带退避策略的重试 } } } catch (InvalidOperationException invOpEx) { // 例如在连接未打开时尝试执行命令 Console.WriteLine($“无效操作: {invOpEx.Message}”); } catch (Exception ex) // 最后捕获其他所有异常 { // 记录未预期的异常 Console.WriteLine($“未预期的错误: {ex.Message}”); } finally { // 确保资源清理的代码using语句通常已涵盖 }对于网络波动或死锁等暂时性错误实现一个简单的重试机制能大大提高系统的韧性。public static void ExecuteWithRetry(Action action, int maxRetries 3, int baseDelayMs 100) { int retries 0; while (true) { try { action(); break; // 成功则跳出循环 } catch (SqlException ex) when (IsTransientError(ex) retries maxRetries) { retries; int delay baseDelayMs * (int)Math.Pow(2, retries - 1); // 指数退避 Console.WriteLine($“暂时性错误{delay}ms后重试第{retries}次...错误: {ex.Message}”); Thread.Sleep(delay); } } } private static bool IsTransientError(SqlException ex) { // 判断是否为暂时性错误超时(-2)、死锁(1205)、连接中断等 foreach (SqlError err in ex.Errors) { if (err.Number -2 || err.Number 1205 || err.Number 4060 /*数据库不可用*/) { return true; } } return false; } // 使用方式 ExecuteWithRetry(() { using (var conn new SqlConnection(connString)) using (var cmd new SqlCommand(“...”, conn)) { conn.Open(); cmd.ExecuteNonQuery(); } });6.2 连接字符串管理与安全实践进阶在真实项目中连接字符串的管理需要更严谨。1. 使用配置源在.NET Core/5中标准做法是使用appsettings.json。// appsettings.json { “ConnectionStrings”: { “DefaultConnection”: “Serverlocalhost;DatabaseMyAppDb;Trusted_ConnectionTrue;TrustServerCertificateTrue;” } }在Program.cs或启动类中var builder WebApplication.CreateBuilder(args); var connectionString builder.Configuration.GetConnectionString(“DefaultConnection”); // 然后通过依赖注入等方式使用2. 开发与生产环境分离创建appsettings.Development.json用于开发环境其中可以包含本地数据库的连接字符串。这个文件应该被加入.gitignore避免敏感信息泄露。生产环境的连接字符串则通过环境变量或部署平台的配置界面设置。3. 使用用户机密User Secrets进行本地开发对于ASP.NET Core项目可以使用用户机密管理工具来存储开发环境的敏感数据。# 在项目目录下初始化 dotnet user-secrets init # 设置连接字符串 dotnet user-secrets set “ConnectionStrings:DefaultConnection” “你的敏感连接字符串”代码中通过Configuration可以像读取普通配置一样读取它但在源代码中看不到明文。4. 生产环境使用托管标识或密钥保管库在Azure等云平台上最佳实践是使用托管标识Managed Identity进行身份验证完全无需在代码或配置中存储密码。或者将连接字符串存储在Azure Key Vault等安全服务中应用程序通过配置的标识去访问。6.3 性能监控与简易日志记录即使是一个简单的控制台应用加入一些基本的日志也能在出问题时救命。public static class DbLogger { public static void LogOperation(string operation, string commandText, long elapsedMilliseconds, bool success, string? error null) { string logEntry $“[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] Operation: {operation}, “ $“Command: {commandText.Substring(0, Math.Min(commandText.Length, 100))}..., “ $“Duration: {elapsedMilliseconds}ms, Success: {success}”; if (!string.IsNullOrEmpty(error)) { logEntry $“, Error: {error}”; } // 简单输出到控制台实际项目中可写入文件、数据库或日志框架如Serilog, NLog Console.WriteLine(logEntry); // System.IO.File.AppendAllText(“C:\logs\dbops.log”, logEntry Environment.NewLine); } } // 在数据库操作中使用 var stopwatch System.Diagnostics.Stopwatch.StartNew(); try { // ... 执行数据库命令 stopwatch.Stop(); DbLogger.LogOperation(“SELECT”, queryString, stopwatch.ElapsedMilliseconds, true); } catch (SqlException ex) { stopwatch.Stop(); DbLogger.LogOperation(“SELECT”, queryString, stopwatch.ElapsedMilliseconds, false, ex.Message); throw; }记录操作类型、简化的SQL注意不要记录参数值以防泄露敏感数据、耗时和成功状态。这对于发现慢查询耗时过长和诊断错误非常有用。在正式项目中应集成成熟的日志框架。7. 从ADO.NET到现代ORM的平滑过渡思考虽然本文聚焦于ADO.NET基础但了解其与现代ORM对象关系映射框架的关系能帮助你做出更好的技术选型。1. DapperDapper是一个“微ORM”它本质上是对ADO.NET的轻量级封装。它扩展了IDbConnection接口让你用一行代码就能执行查询并将结果映射到强类型对象上同时保留了手写SQL的灵活性和对性能的极致控制。如果你觉得纯ADO.NET代码太繁琐但又不想被Entity Framework这样的全功能ORM约束Dapper是完美的中间选择。using Dapper; var products connection.QueryProduct(“SELECT * FROM Products WHERE CategoryID CatID”, new { CatID 1 });2. Entity Framework (EF) CoreEF Core是一个全功能的ORM它让你用操作C#对象实体的方式来操作数据库自动生成SQL语句。它提供了迁移Migration、变更跟踪、LINQ查询等高级功能极大地提升了开发效率尤其适合领域驱动设计DDD和快速迭代的项目。它的学习曲线更陡并且需要你放弃对最终生成SQL的精细控制。为什么还要学ADO.NET因为ORM框架不是银弹。当你遇到复杂的报表查询、需要调用特定数据库函数、进行大批量数据操作或调试性能瓶颈时最终往往还是要回到SQL和数据库连接的本源上来。理解ADO.NET就是理解了所有.NET数据库访问技术的基石。当EF Core生成的SQL效率低下时你可以通过FromSqlRaw执行自己优化的SQL当Dapper不能满足需求时你知道如何回退到更底层的方法。掌握本文所述的“增删改查”及事务控制意味着你拥有了直接与数据库对话的能力。这份能力能让你在使用任何高级框架时都充满自信因为你知道它们背后发生了什么也知道当它们不够用时该如何自己动手解决问题。从今天起试着在你的下一个项目中有意识地运用参数化查询、妥善管理连接和事务并加入适当的日志记录你会发现代码的稳定性和可维护性会有立竿见影的提升。