做上位机开发或者后端服务的C#程序员一定都经历过这种纠结数据访问层到底用什么直接用ADO.NET写SqlConnection、SqlCommand、DataTable代码又臭又长上EF Core全家桶配置、迁移、导航属性一套组合拳下来很多时候只是为了执行一条简单的SELECT。直到我遇到Dapper才发现原来两者之间还存在一个这么舒服的中间态。Dapper是一个轻量级微ORM类库GitHub上Star数量常年位居C#类库前列核心就是一个SqlMapper.cs文件打包出来才几十KB。它做的事情非常简单粗暴扩展IDbConnection接口让你用最少的代码执行SQL同时把查询结果映射成强类型对象。这篇文章我会从原理讲到实操结合上位机、工控、Web后端等实际场景把Dapper的常用姿势和踩坑经验一次说清楚。无论你是刚入门C#的新手还是用EF Core写了好几年想换个口味的老兵这篇都能帮你快速上手Dapper。1. 为什么偏偏是Dapper先搞懂它到底解决什么问题1.1 Dapper是什么以及它不是什么先给Dapper定个性。它是一个微ORM不是全功能ORM。这两个概念的区别很多人一开始没搞清楚。全功能ORM的代表是EF Core、NHibernate。它们替你管理实体映射、跟踪状态变化、生成SQL语句你写的代码看起来几乎不像在操作数据库更像在操作内存里的对象集合。但代价是学习曲线陡峭性能损耗不可忽略出了复杂查询时生成的SQL一言难尽排查问题还得去翻它生成的日志。而Dapper走的是另一条路。它不会帮你生成SQLSQL还是你自己写它只帮你做两件事执行SQL、映射结果。网上有人把Dapper叫作“ADO.NET的语法糖”这个说法很贴切。它不存在状态跟踪没有工作单元概念没有延迟加载一个对象管到底用完就丢。这样说吧EF Core像全自动挡汽车踩油门就走但你永远不知道发动机舱里发生了什么Dapper像手动挡每个换挡时机都由你把控操控感强开好了效率极高但前提是你得懂点SQL。1.2 Dapper的核心工作原理它快在哪里很多人测出来Dapper的性能接近纯ADO.NET甚至有时比DataTable方式还快原因在哪里简单说Dapper做了三件事第一它是IDbConnection的扩展方法。这意味着它没有自己的连接池、没有额外的会话管理一切连接生命周期都跟随你传入的connection对象本身跟ADO.NET完全一致所以没有额外开销。第二它用IL动态生成实体映射。普通的DataTable映射要经过反射逐列赋值而Dapper在为某个类型第一次做映射时会动态生成一个委托后续所有查询都直接调用这个委托绕开了反复反射的损耗。这个机制跟序列化库的动态代码生成思路是一样的首次调用稍慢之后就非常快。第三它内部做了大量的缓存。命令文本、参数信息、类型映射器、字段解析结果全部缓存在静态字典里。同一个查询反复执行时很多步骤直接命中缓存。所以Dapper的性能优势来源不是黑科技而是对细节的极致优化不搞多余的抽象层、不反复反射、不重复解析这三点做到位性能天花板自然就高。1.3 选型对比Dapper、EF Core、ADO.NET怎么权衡先上一个我自己整理的对比表维度ADO.NETDapperEF Core代码量多样板代码重复少一行搞定查询映射最少但配置学习成本高SQL控制权完全控制完全控制弱复杂SQL需要FromSql/RawSql性能最高基础之上接近ADO.NET有固定损耗批量写入自己写循环或SqlBulkCopy自己控制配合扩展原生支持但性能一般实体跟踪无无有可以关闭适合场景追求极致、底层封装性能敏感、SQL复杂、快速开发业务复杂、模型丰富、CRUD为主我的建议是如果你要写一个复杂管理系统实体关系错综复杂后台管理界面居多CRUD占90%以上那EF Core确实省心但如果你是做上位机、网关服务、数据处理中间件或者任何对SQL执行效率和代码可控性有要求的场景Dapper几乎是首选。我见过不少项目一开始图省事上EF Core结果后来出现“N1查询问题”找不到原因优化SQL又无从下手最后被迫把热点查询改成Dapper。早知如此直接用Dapper多好。2. 半小时上手Dapper的核心API与日常用法2.1 安装与第一个查询安装没什么好说的NuGet搜索Dapper直接装。如果你用的是.Net Framework老项目注意选择对应版本Dapper对老框架兼容性一直做得不错。先看一段最简单的查询代码using Dapper; using System.Data.SqlClient; using var conn new SqlConnection(Serverlocalhost;DatabaseTestDb;User Idsa;Password****;); var users conn.QueryUser(SELECT Id, Name, Age FROM Users WHERE Age age, new { age 18 }).ToList(); public class User { public int Id { get; set; } public string Name { get; set; } public int Age { get; set; } }就这么简单一条Query方法完成了连接打开、命令执行、DataReader读取、实体映射、连接关闭这一整套流程。注意我用的是using var语法这是C# 8.0的简化写法方法结束时自动释放连接。Dapper内部会自己判断如果连接是关闭状态打开执行完再关闭如果调用前就已经打开它就只负责执行不动连接状态。这个设计很贴心让你既可以用using块包着也可以在一个打开的连接上执行多条语句。这里有个细节query返回的是IEnumerable 惰性求值只有你开始遍历或调用ToList()时才真正执行SQL。所以如果你只调用Query却不消费结果SQL根本不会执行到数据库。排查问题要注意这一点别误以为Dapper没执行。2.2 参数化查询别再用字符串拼接了说实话我见过太多新人写SQL还是这样的写法var sql SELECT * FROM Users WHERE Name name ;这句话要是上线了轻则查不出来数据重则被SQL注入攻击别人一个; DROP TABLE Users; --就能把你数据库清零。字符串拼接SQL是C#开发中最不应该出现的低级错误之一。Dapper的参数化写法非常自然用匿名对象传参var sql SELECT * FROM Users WHERE Name name AND Age age; var users conn.QueryUser(sql, new { name 张三, age 18 }).ToList();你的匿名对象属性名和SQL里的参数名对应上就行Dapper会自动匹配。动态场景还能用DynamicParametersvar parameters new DynamicParameters(); parameters.Add(name, 张三); parameters.Add(age, 18); // 支持输出参数 parameters.Add(totalCount, dbType: DbType.Int32, direction: ParameterDirection.Output); conn.Execute(sp_GetUsers, parameters, commandType: CommandType.StoredProcedure); int total parameters.Getint(totalCount);这个类在存储过程场景下特别常用因为输出参数、返回值这些通过匿名对象没法表达。另外提醒一点即使你的参数值是空字符串或者null也请规规矩矩用参数化写法这不是写法问题是安全意识问题。2.3 结果映射从匿名类型到强类型Dapper的Query 支持几种映射目标我按使用频率排一下强类型对象QueryUser最常见的用法要求SQL返回的列名和实体属性名能对应上。默认情况下不区分大小写Id和id都能匹配。动态类型Querydynamic每行返回一个IDictionarystring, object风格的动态对象适合查出来的列还没有对应实体的场景。标量值ExecuteScalarT返回单个值比如SELECT COUNT(*)。ValueTupleQuery(int Id, string Name)适合轻量查询不想专门建一个实体的场景需要在列名和元组元素上做点匹配工作。关于列名和属性名的匹配问题我自己碰到过一个印象很深的坑数据库列名是下划线风格user_name实体属性是驼峰风格UserName。默认情况下这两者匹配不上结果就是属性一直是默认值。解决办法有两个在SQL里用别名SELECT user_name AS UserName FROM users一劳永逸但每列都要写。配置自定义映射规则写一个ITypeMap的实现或者用Dapper.FluentMap这类扩展库做全局命名规则转换。更常见的做法是在SQL层做映射因为Dapper本身就推荐你使用显式列名。这种“SQL与实体的边界由你控制”的特性恰恰是Dapper灵活的地方。3. 进阶实操事务、多结果集、批量写入与存储过程3.1 事务与工作单元很多操作不能只有一条SQL比如转账扣掉A的余额加上B的余额任意一步失败都要回滚。Dapper的事务用法很直接因为它的一切都构建在ADO.NET事务之上。直接上代码using var conn new SqlConnection(connString); conn.Open(); using var transaction conn.BeginTransaction(); try { conn.Execute(UPDATE Accounts SET Balance Balance - amount WHERE Id id, new { amount 100, id 1 }, transaction); conn.Execute(UPDATE Accounts SET Balance Balance amount WHERE Id id, new { amount 100, id 2 }, transaction); transaction.Commit(); } catch { transaction.Rollback(); throw; }注意看每个Execute方法都多了第三个参数transaction这是Dapper事务的核心你必须在每条语句上显式传入事务对象它不会自动感知当前线程有没有未完成的事务。这一点和EF Core很不一样EF Core的DbContext内部会跟踪事务状态而Dapper把这个责任还给了你。我个人的习惯是封装一个简单的工作单元类把连接、事务、以及所有仓储方法包在一起。这样业务层只管调用方法不用关心事务参数往哪传。但具体封装方式这里先不展开等以后专门写一篇工作单元设计再聊。3.2 QueryMultiple一次查询取多张表多次往返数据库会显著拖慢性能而QueryMultiple可以让你一条SQL返回多个结果集一次网络往返全拿回来。场景查用户详情页需要用户基本信息、最近订单列表、用户标签。传统做法三次查询用QueryMultiple一次搞定var sql SELECT * FROM Users WHERE Id id; SELECT * FROM Orders WHERE UserId id ORDER BY CreateTime DESC; SELECT * FROM Tags WHERE UserId id;; using var multi conn.QueryMultiple(sql, new { id userId }); var user multi.ReadUser().FirstOrDefault(); var orders multi.ReadOrder().ToList(); var tags multi.ReadTag().ToList();ReadT()是惰性读取按顺序从结果集里读取当前一个结果。顺序必须和SQL里SELECT的顺序一致读错位置后面就全乱了。这个方法在取“主表子表”类聚合数据时特别实用。不过要注意如果子表数据量很大一次取回来可能内存吃紧这种情况要评估一下是分页还是分批。凡事有度QueryMultiple不是万能药。3.3 批量写入三种方案与性能取舍先明确一点Dapper原生没有批量插入方法有人觉得不方便但这反而是件好事——它把性能优化空间完全留给了你。批量写入几百上千条数据时最直接的做法是for循环一条条Execute稳定但慢单条提交的往返消耗积少成多。我实测过一万条数据逐条插入MySQL耗时大约在几十秒到几分钟完全没法用。我推荐三个方案按场景选方案一多条SQL打包提交var sql INSERT INTO Orders (OrderNo, Amount) VALUES (OrderNo, Amount);; conn.Execute(sql, orderList);Dapper有个贴心行为当你传入一个IEnumerable对象作为参数时它会自动把SQL循环执行多次。这个用法本质上是逐条提交但省去了你写for循环的样板代码。数据量不大几百条以内的时候用这个没毛病。方案二MySqlBulkCopy / SqlBulkCopy这是真正的批量入库存方案。SqlBulkCopy是SqlClient自带的类把DataTable一键灌进SQL Server速度极快五万条数据几秒钟完成。Dapper并没有覆盖这个场景但你可以手动拼DataTable后交给SqlBulkCopy。我自己封装过一套通用的批量写入工具用反射获取实体属性集合塞进DataTable的列映射然后调用SqlBulkCopy。虽然写起来有点仪式感但性能提升是肉眼可见的。如果你用的是MySQL对应有MySqlBulkCopy用法大同小异。这个方法唯一的门槛是数据库表结构需要和DataTable列一一对应字段顺序和数据格式都对得上才行。方案三临时表拼接这是大数据量写入的进阶姿势先把数据批量写入临时表然后用一条INSERT INTO ... SELECT从临时表搬到目标表。这个方案在目标表上有复杂约束时特别好用比如需要先判断数据是否存在再做更新upsert可以先丢进临时表再统一处理。方案三我用的不算多但每次用都是因为在性能之外还需要业务逻辑处理。建议大家把方案二作为默认首选方案一再配合小数据量场景基本就能覆盖绝大多数需求。3.4 存储过程调用Dapper调用存储过程和执行SQL没什么区别关键在于CommandType的设置var result conn.QueryOrder(sp_GetOrdersByUser, new { userId 123 }, commandType: CommandType.StoredProcedure).ToList();存储过程里如果有输出参数用之前提到的DynamicParameters就行。另外还有一类场景存储过程最后SELECT返回结果集但过程中也设置了输出参数。这种情况下先取参数再取结果集都行但参数获取必须在查询执行完成后进行否则拿不到值。还有个小陷阱如果存储过程返回的结果集列名和实体属性名对不上一样映射失败。所以存储过程里的SELECT列名最好写成和目标实体一致的或者显式AS别名别指望自动匹配能帮你处理一切。4. 上位机与工控场景下的Dapper实战心得4.1 高频采集数据入库的正确打开方式有好几个读者私信问过我同一个问题上位机从PLC或OPC Server采集数据频率可能是一秒几十个点甚至更高用Dapper入库怎么做到不卡顿先说结论逐条插入在任何高频采集场景都是死路。一秒采50个点每个点一条INSERT数据库连接来回打开关闭光握手就占了大部分时间。正确思路是缓存批量写。我的做法是采集线程把原始数据丢进一个线程安全的队列同时启动一个后台写入线程每隔固定时间比如3秒或者队列积压到一定条数比如500条就统一取出这批数据用SqlBulkCopy或者Dapper的多值插入一次性写入。private ConcurrentQueueSensorData _buffer new(); // 采集线程 private void OnDataReceived(SensorData data) { _buffer.Enqueue(data); } // 写库线程 private async Task FlushLoop(CancellationToken token) { while (!token.IsCancellationRequested) { await Task.Delay(3000, token); var batch new ListSensorData(); while (_buffer.TryDequeue(out var item)) { batch.Add(item); if (batch.Count 500) break; } if (batch.Count 0) { await BulkInsertAsync(batch); } } }这样设计的好处是数据库压力平稳采集线程不阻塞就算写库失败数据也不会丢失队列缓冲了。要更稳的话还可以在写入前把日志写到本地文件做持久化数据库恢复后再补录。我实际做过的项目里这套方案扛过1秒1000个点位的数据量CPU和内存占用都很平稳。4.2 连接生命周期与断线自愈工控环境里数据库断连太常见了数据库重启、网络闪断、交换机抖动任何一个都能让你的程序“假死”。Dapper本身没有重连机制它就是一个连接之上的操作库所以你得自己处理连接可靠性问题。首先不要长期持有一个连接对象。有人图省事启动时new一个连接一直用到程序退出这是隐患最大的做法。SQL Server的Connection Pool默认启用连接字符串相同的情况下Dapper每次打开连接实际复用的是池里的连接所以不断开关连接并不影响性能反而能让连接池自动清理坏连接。其次要处理连接异常。我习惯封装一个安全Execute方法public T SafeQueryT(string sql, object param, int retryCount 3) { for (int i 0; i retryCount; i) { try { using var conn new SqlConnection(_connString); return conn.QueryT(sql, param).ToList(); } catch (SqlException ex) when (ex.Number -2 || ex.Number 53) { // 超时或无法连接等1秒再试 Thread.Sleep(1000 * (i 1)); } } throw new Exception(数据库连接失败); }这个封装在断线场景下非常实用。核心思路就是不要让数据库异常直接崩掉你的采集线程而是重试几次。如果数据库长时间不可用最好把数据落盘缓存等恢复后从本地文件补录。4.3 与OPC、串口数据联动时的注意事项上位机里最常见的组合就是OPC读数据、串口读仪表然后把数据入库。用Dapper处理这类数据时有几个跟一般Web开发不一样的坑要注意。第一个坑是时间精度。PLC内部的时间戳大多精确到毫秒甚至微秒而SQL Server datetime类型默认精度是3.33毫秒。如果你的表字段用的是datetime你会发现入库后时间数据被“舍入”了导致做趋势分析时数据对不齐。解决办法很简单表字段用datetime2(3)或者datetime2(7)实体里对应用DateTime类型别用字符串传值。第二个坑是NULL值的处理。OPC采集的原始值经常是NULL仪表断线时返回null是正常的。Dapper和实体类的值类型属性之间做映射时如果数据库值为NULL而属性是int、double这类非空类型会直接抛异常这是非常坑的。我的做法是给这些可能为空的字段定义成int?、double?这样的可空类型或者做好DBNull到默认值的转换。保险起见查询数据时也可以用COALESCE(Value, 0)把它兜底成0值但这个要看业务上0值有没有特殊含义别乱用一个会跟真实数据混淆的值去顶替。第三个坑是中文编码和乱码。连着西门子或国产PLC的设备经常会出现控制字符比如字符串里的换行、转义字符甚至半截的GBK编码数据。存入SQL Server用nvarchar基本能扛住但如果你用Dapper操作老旧的Sybase或某些国产数据库字符集没配对就是一堆问号。我的建议是写入前对字符串做一次清洗剔除非可见字符避免脏数据入库。上位机开发里“数据对不对”永远比“代码优雅不优雅”重要。5. 高频踩坑记录与排查速查5.1 Dapper实战中的6个典型坑No.1 列名映射失败属性静默为默认值这个一般不会抛异常最迷惑人。SQL返回的列名和实体属性对不上时Dapper默认保持沉默你拿到的对象里就是一串0、false、null。排查方法很直接把SQL放在数据库工具里跑一遍看实际返回的列名然后跟实体属性逐一对一下。用AS别名让列名跟属性完全一致是最省事的方式。No.2 Execute方法返回的是受影响行数不是查询结果新手很容易把Execute和Query混了。Execute用于增删改命令和非查询操作返回int型受影响行数。要查数据用Query系列如果拿数据用成了Execute看着返回的int一脸懵排查半天才发现是方法用错了。No.3 连接未打开事务直接抛异常Dapper的Query/Execute方法会自动打开连接但要注意BeginTransaction这个操作不会自动打开连接。你必须先显式调用conn.Open()再BeginTransaction否则会抛“连接未打开”的异常。这个其实文档里写了但太容易忽略了。No.4 参数名大小写问题在Oracle里特别坑SQL Server的参数名不区分大小写但Oracle对参数名大小写是敏感的。使用Oracle.ManagedDataAccess时匿名对象的属性名和SQL里的参数名必须完全一致包括大小写否则报参数缺失或绑定失败。跨数据库经验少的人很容易在这里卡住。No.5 SQLite与Dapper的兼容性坑SQLite的参数不像SQL Server那样用前缀而是用、$、:、? 多种风格混用。用Dapper操作SQLite时用的还是参数名但SQLite对参数名的处理比较特殊参数名匹配失败时会静默返回null而不是报错。我的建议是写SQLite语句时格外小心参数名写完之后做一次空值检查。No.6 批量Execute的“假成功”conn.Execute(sql, list)传入一个集合时Dapper循环执行了多次但返回的是所有行数影响的总和。如果你只是检查返回值判断“有一条没写入成功”这个总行数会骗你。批量操作最好还是走SqlBulkCopy或者每批数据执行后单独校验关键字段别只依赖Execute方法的返回值。5.2 性能排查的小技巧开启Dapper日志与会话统计查性能问题时很多人一上来就怀疑Dapper慢。但绝大多数情况下慢的不是Dapper而是SQL本身、索引缺失、锁等待或者查询返回了超量数据。我会用SQL Server自带的DMV看会话统计Dapper只是帮你发SQL的工具真正在数据库上干活的是SQL本身。比如排查“某个接口为什么慢”先在SQL Server Management Studio里开启SET STATISTICS IO ON; SET STATISTICS TIME ON;然后把Dapper最终生成的完整SQL拿到SSMS里手动执行一遍看耗时。如果SSMS里也慢那就是索引或SQL写法问题跟Dapper八竿子打不着。说到查看SQLDapper默认不打印日志。调试时可以自己包装一下IDbConnection把CommandText和参数输出到控制台方便观察实际执行的SQL。不用装什么第三方库自己封装几十行代码就够用了。5.3 关于异步API的使用建议Dapper的异步APIQueryAsync、ExecuteAsync在Web项目里几乎是必选要不线程池会被数据库阻塞搞垮。上位机里有多通道数据采集写库时异步也能减少阻塞。但要注意两点一是异步API要和取消令牌配合长时间无响应的查询能通过CancellationToken及时中止不然后台任务卡死很难排查。二是UI线程或测试代码里不要用同步方法包装异步方法.Result或.Wait()容易死锁。我见过一个同事在WPF按钮事件里用conn.QueryAsync(sql).Result界面直接假死查了半天才发现是同步阻塞和UI线程上下文互相等待的经典问题。只要环境允许统一走全异步路线别混着来。维护起来轻松太多。再分享一个经验排查N1问题。什么是N1就是你先查了主表N条数据然后循环里每条去查一次子表总共执行了N1次查询。表面看每一条SQL都快得不得了但累积下来就慢得吓人。Dapper本身不管这个它一门心思执行你写的SQL。解决方式也很简单用上面的QueryMultiple或者JOIN一次性查出全部数据把性能问题消灭在SQL设计阶段。6. 写给正在权衡技术选型的人如果说文章到这里还有什么值得再强调的那就是不要因为Dapper简单就低估它的价值也不要因为它“只是扩展方法”就觉得上不了台面。我把大量采集项目、中间件项目、报表服务统统改到Dapper之后最直观的感受是数据访问层大约80%的代码都被删掉了出了问题也能一眼定位到SQL不用再绕一大圈去猜框架做了什么手脚。Dapper也适合做EF Core的补充而不是替代。你完全可以在同一个项目里用EF Core做后台管理的CRUD用Dapper做报表查询和高频写入。这种“混合模式”在实践中比单纯押注某一种框架要舒服得多。它也是我敢在任何新项目里第一个引入的类库——小到几百行的工具脚本大到多服务分布式系统它都没有掉链子。希望这篇文章能从底层原理到实战场景让你对Dapper有了更完整的认识。如果后面你在项目里碰到了什么奇怪的Dapper问题欢迎按文章里提到的排查顺序先自查一遍很多时候答案就在SQL本身。
企业数字化 ERP 产品动态
相关推荐
无人机蜂群全套工程链开源:从零件到真机复现指南 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 14:03:53
YOLOv8n+PyQt5课堂行为检测系统:CPU实时部署与教师友好设计 简介:本资源是一套面向教育技术开发者与一线教师的课堂行为智能分析系统,基于YOLOv8目标检测算法与PyQt5图形界面框架构建,解决传统课堂依赖人工观察、效率低、覆盖不全等痛点,适用于线上教学监控、智慧教室部署及学生专注度研究等… · 2026/9/26 14:03:53
Traefik应用:配置容器多个网络时无法访问问题——用 TaoToken 统一 Key 排查路由与容器网络 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 14:03:53
【AI Agent 开发避坑】上下文越长,Agent越“傻”?一文讲清原因与优化策略 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 15:48:28
OpenClaw+LibTV视频生成实测(含安装+配置+分析):ai生成工作流很规范,但画面在“打架“ /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 15:48:21
Apex Amp 混合精度训练实战:从 opt_level 到统一 API 的完整指南 人工智能大模型音乐生成音频预训练 【免费下载链接】jukebox Code for the paper "Jukebox: A Generative Model for Music" 项目地址: https://gitcode.com/gh_mirrors/ju/jukebox 点击查看 免费下载 本文以 NVIDIA Apex 仓库中 amp.rst 文档为主线&… · 2026/9/26 15:48:01
给爸妈配吸附性义齿,做子女的要先弄清哪几件事?/钟祥小灰兔科普 咱们钟祥人讲孝心,都是实打实的。上回在阳春大街碰见老同学,他说给老爷子买了副新假牙,结果老爷子吃饭还是嫌松,打喷嚏的时候赶紧用手捂着嘴,生怕假牙“跑”出来。这场景,好多街坊家里是不是都见过… · 2026/9/26 15:47:55
数据库课后习题答案别硬背:当测试用例集刷,效率翻倍 简介:万常选版《数据库原理与设计》课后习题答案资源,覆盖第2至6章及第9章,适合正在学习关系模型、数据库建模、关系数据理论与模式求精的本科生、自学者作为复习与自测材料。压缩包共7个文件,含3个doc参考答案、2个sql示例脚本、… · 2026/9/26 0:00:21
OpenClaw 替代品?Hermes Agent 踩坑实录:macOS 飞书接入 TaoToken 配置 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/26 0:00:40
向下兼容与向上兼容:接口设计中的兼容性策略与工程实践 一次版本升级事故,是很多团队绕不过去的坎。线上环境里,服务端明明已经上线了新版接口,老的移动端还在照着旧文档传参数。请求一到网关,校验直接拒绝,用户操作失败,客服群炸了锅,开发群里开始互… · 2026/9/26 0:00:46