5.1 从 ADO.NET 到 ORM 的取舍 ADO.NET 是 .NET 数据访问的最底层手工件:自己开连接、写命令、读结果、关资源,换来对 SQL 的完全掌控;ORM(EF Core)用映射与跟踪把这四步自动化,代价是引入一层需要理解的抽象。选型不是信仰问题,是成本问题——本节把两边的成本都摆上桌。 进入数据支线的第一站,先不碰 EF Core,而是亲手写一遍最原始的数据访问。理由很直接:不手工做过一遍,就体会不到 ORM 在替你做什么、也判断不出它什么时候碍事。观察哨本节的视角是"数据访问的四个动作":连接、命令、读取、释放——任何数据层都绕不开这四步。 手工数据访问长什么样 手工代码的控制力货真价实:SQL 一字不改地上库、没有翻译层、连接时机完全自己定。
ADO.NET 是 .NET 数据访问的最底层手工件:自己开连接、写命令、读结果、关资源,换来对 SQL 的完全掌控;ORM(EF Core)用映射与跟踪把这四步自动化,代价是引入一层需要理解的抽象。选型不是信仰问题,是成本问题——本节把两边的成本都摆上桌。
进入数据支线的第一站,先不碰 EF Core,而是亲手写一遍最原始的数据访问。理由很直接:不手工做过一遍,就体会不到 ORM 在替你做什么、也判断不出它什么时候碍事。观察哨本节的视角是"数据访问的四个动作":连接、命令、读取、释放——任何数据层都绕不开这四步。
// ADO.NET 风格:四个动作全部手工,以查询商品为例 public Product? FindById(int id) { // 动作一:建连接(using 确保归还) using var conn = new SqliteConnection("Data Source=shop.db"); conn.Open(); // 动作二:写命令——注意参数化占位,绝不拼字符串 using var cmd = conn.CreateCommand(); cmd.CommandText = "SELECT Id, Name, Price FROM Products WHERE Id = @id"; cmd.Parameters.AddWithValue("@id", id); // 参数化:防注入的关键 // 动作三:读结果,逐行逐列手工搬运到对象 using var reader = cmd.ExecuteReader(); if (!reader.Read()) return null; return new Product { Id = reader.GetInt32(0), Name = reader.GetString(1), Price = reader.GetDecimal(2) }; } // 动作四:三个 using 结束,命令、读取器、连接按序释放
手工代码的控制力货真价实:SQL 一字不改地上库、没有翻译层、连接时机完全自己定。它的账单同样真实:每张表每列都要手写搬运代码;改一列要同步改 SQL 与搬运两处;连接忘记释放就是事故;插入并取回自增主键这种小事要写一串收尾 SQL。
ORM 的本质是"对象与关系之间的翻译官":你操作对象图(订单带着明细集合),它负责翻成表与连接,再把结果翻回来。翻译之外还捎带三件事:变更跟踪(记住你改过哪些属性,保存时只发差异 SQL)、身份映射(同一主键在一个上下文里只有一个对象实例)、迁移生成(模型改动变成建表改表脚本)。

背景:商品搜索要按关键字与价格区间过滤,团队里"手写派"与"ORM 派"各写一版,比一比可读性与风险点。
操作:手写版(节选):
// 手写版:SQL 拼接 + 手工搬运,控制力强但两处同步 cmd.CommandText = @"SELECT Id, Name, Price FROM Products WHERE Name LIKE @kw AND Price BETWEEN @lo AND @hi"; cmd.Parameters.AddWithValue("@kw", $"%{keyword}%"); cmd.Parameters.AddWithValue("@lo", low); cmd.Parameters.AddWithValue("@hi", high); // ...逐列搬运进 List<Product>
ORM 版:
// ORM 版:同一查询的 LINQ 表达,翻译交给框架 var products = await db.Products .Where(p => p.Name.Contains(keyword) && p.Price >= low && p.Price <= high) .ToListAsync(); // SQL 观察窗(5.3 节)可看到翻译结果与手写版几乎一致
结果:两版发出的 SQL 大体相同,性能差异可忽略。差别全在维护面:手写版改排序字段要动 SQL 与搬运两处;ORM 版改 Lambda 一处即可。但手写版对索引命中的确定性更高——DBA 评审 SQL 时看到的就是最终文本。
解读:ORM 的比较优势在"变更频率高、表多、团队大"时放大;手写的优势在"查询形态特殊、性能极度敏感、SQL 需要评审"时保留。成熟项目的常态是主干用 EF Core、报表与热点查询走逃生门。
变式:EF Core 混用三种形态——LINQ 为主,FromSql 原生 SQL 接实体,SqlQueryRaw 直接取标量或非实体类型。从整段原生到纯 LINQ 是连续光谱,按查询逐条选择,不必全站二选一。
拿真实维度替信仰站队。我的论证框架四条:项目规模——表过三十、迭代快,搬运代码的成本压垮手工收益;团队构成——DBA 主导评审文化的团队保留 SQL 可见性更重要;查询形态——大量报表、复杂分析 SQL,逃生门比例会很高;人员流动——ORM 的代码风格统一性让新人接手更快。四条打完分,选型结论自然浮出,而且能向团队解释——能解释的选型才是好选型。
能,而且这正是两条路线并存的用武之地:新模块用上下文与 LINQ,存量手工层原样运行,两者通过同一个数据库会话策略(连接串、事务边界)协调。迁移按模块推进,每迁一块补一块集成测试——观察哨的建议是先迁"简单增删改查",把复杂查询留在逃生门里最后处理,风险最小、收益最快兑现。
⚠️ 常见坑:手写路线里最危险的不是麻烦,是"偶尔拼一次字符串"。只要存在一条拼接路径,注入面就存在。参数化是纪律,不是建议——代码评审对拼接 SQL 应当零容忍。
💡 关键直觉:ORM 不是"替你写 SQL 的工具",是"把表的世界翻译成对象世界的常驻翻译官"。翻译官有自己的脾气(翻译规则、跟踪行为),5.2 与 5.3 节都在讲怎么跟它相处。