.NET Framework 4.5 老项目直连 PostgreSQL 实践

发布时间:2026/10/27 10:35:17

.NET Framework 4.5 老项目直连 PostgreSQL 实践 简介这份面向 .NET Framework 4.5 开发者的 PostgreSQL 连接组件包聚焦 Npgsql 数据提供程序及配套安全组件适合需要在传统 .NET 项目中接入 PostgreSQL、处理 ADO.NET 数据访问或 Entity Framework 映射的开发者使用。包内共 19 个文件以 10 个 dll 动态库为主体辅以 3 个 pdb 调试符号、3 个 xml 文档说明以及 config、md、txt 等配置与说明文件压缩包约 726KB体积轻便便于直接引用和排查问题。已有 895 人学习说明其在旧版 .NET 与 PostgreSQL 集成场景中具有一定参考价值。核心的 Npgsql 库提供 DbConnection、DbCommand、DbDataReader 等 ADO.NET 兼容接口并覆盖查询、事务、参数化命令与类型映射Mono.Security 组件则涉及 X.509 证书、PKCS #7 与 ASN.1可用于 SSL 连接和服务器身份验证。包内还包含 Entity Framework 及其 Legacy 适配器、README 使用说明、调试符号和许可协议能帮助读者快速完成数据库连接配置、ORM 接入与调试排错。1. 老项目连 PostgreSQL两个 DLL 就能把 .NET Framework 4.5 接上库接手一个跑了七八年的 .NET Framework 4.5 后台服务数据库要换成 PostgreSQL最省事的路径往往不是上 ORM而是把 Npgsql.dll 和 Mono.Security.dll 两个文件丢进 lib 目录改一段连接字符串第二天就能对账。这份资源给的就是这么一套东西面向 .NET Framework 4.5 的 PostgreSQL 连接 API核心是 Npgsql 驱动配套 Mono.Security 兜住 SSL/TLS 握手没有多余依赖也不用把整个解决方案升级到更高版本的框架。它适合谁还在维护 Framework 4.5/4.6 老系统、只想找一个稳定直连方案、不愿意为一次数据库迁移动全身的开发者。下面按依赖怎么放、连接怎么建、SQL 怎么走、坑在哪、怎么封装改造一层层拆开看。2. 依赖落位Npgsql.dll 与 Mono.Security.dll 在 4.5 工程里的正确放法很多人拿到这类资源的第一反应是「双击 DLL 看看」结果装了个 ILSpy 翻了半小时也没看出怎么用其实这类驱动包的价值不在反编译里而在「引用进去之后能不能通」。这一章先把两个文件的职责边界划清楚再走一遍从建工程到第一次查询成功的完整流程最后处理配置层面的两个小麻烦provider 注册和 bindingRedirect。2.1 两个 DLL 分别管什么谁在连库谁在兜加密Npgsql.dll 是真正干活的它实现了 PostgreSQL 的前后端协议v3 协议、类型映射、连接池、事务、COPY 批量导入这一整套。你在代码里 new 出来的NpgsqlConnection、NpgsqlCommand、NpgsqlDataReader全部来自它SQL 语法层面它不管它只负责把命令按协议打包发出去、把结果集按类型解回来。Mono.Security.dll 的职责要窄得多但对生产环境是决定性的它提供 TLS 实现。早期版本的 Npgsql 在 .NET Framework 上没有直接依赖System.Net.Security.SslStream而是自带一套基于 Mono.Security 的加密栈。也就是说当连接字符串里写了SSL ModeRequire或者服务端pg_hba.conf强制 hostssl握手这一段是由 Mono.Security 完成的不是系统自带的那套。这个分工带来的直接后果是只引 Npgsql.dll本地关掉 SSL 一切正常一上生产就抛异常把 Mono.Security.dll 补上但版本对不上又会抛另一种初始化异常。所以引用阶段的第一个原则是——两个文件成对出现版本要配套别去别处单独下一个 Mono.Security 替换掉版本错配是后面 SSL 握手失败的头号原因。提示判断当前连接到底走没走 SSL不用猜。连上之后执行SHOW ssl;看服务端是否开启再执行SELECT ssl, version FROM pg_stat_ssl WHERE pid pg_backend_pid();ssl列为 true 才说明这一条连接真的加密了。2.2 从零建工程到第一次引用成功操作路径很朴素但顺序错一步就得重来。建议按下面的次序走新建一个 Console Application 或 Class Library目标框架明确选.NET Framework 4.5不要选 4.5.1 以上再改回来项目文件里的 TargetFrameworkVersion 会残留。在解决方案根目录下建lib文件夹把 Npgsql.dll 和 Mono.Security.dll 一起放进去。放仓库里而不是放 GAC是为了让 CI 和其他同事拉下来就能编译。右键「引用」→「添加引用」→「浏览」两个 DLL 一起选中。加完之后逐个选中看属性面板确认复制到本地Copy Local True。写一个冒烟程序只做「连上、查版本、断开」三件事。冒烟代码不要图省事用字符串拼接直接上NpgsqlConnectionStringBuilderusing System; using Npgsql; class Program { static void Main() { // 用 Builder 而不是拼字符串避免密码里有分号/引号时把连接串搅碎 var builder new NpgsqlConnectionStringBuilder { Host 10.0.0.21, Port 5432, Database appdb, UserName app_user, Password pss;word, // 分号在 Builder 里会被正确转义 Pooling true, Timeout 15, // 建立连接的超时单位秒 CommandTimeout 60, // 单条命令执行超时 ApplicationName smoke-test // 在 pg_stat_activity 里能看到排查时极其有用 }; using (var conn new NpgsqlConnection(builder.ConnectionString)) { conn.Open(); using (var cmd new NpgsqlCommand(select version(), current_database(), conn)) using (var reader cmd.ExecuteReader()) { while (reader.Read()) { Console.WriteLine(reader.GetString(0)); // PostgreSQL 版本串 Console.WriteLine(reader.GetString(1)); // 当前库名 } } } } }这段代码里有三个参数值得单独说。ApplicationName是最容易被忽视但收益最高的一个设置之后在服务端select application_name, state, query from pg_stat_activity就能区分出哪条连接是你的服务连接泄漏时不用瞎猜。Timeout指的是建立连接的超时不是查询超时两者混用是新手最常见的误解。CommandTimeout才是单条 SQL 的执行上限报表类查询要单独放大。如果这段代码能打印出版本串说明引用层、协议层、认证层全通了。打印不出来先看异常类型NpgsqlException里带28P01是密码错3D000是库名不存在SocketException是网络或端口不通TypeInitializationException且内部指向 Mono.Security那就是加密栈没加载起来。2.3 App.config 里的 provider 注册与 bindingRedirect直连场景下这两件事可以跳过但只要工程里用了DbProviderFactories、DataSet设计器、或者某个老 ORM 靠配置文件找驱动就得在App.config里补一段。这是 .NET Framework 时代特有的仪式感漏了会报「未能找到提供程序」。?xml version1.0 encodingutf-8? configuration startup supportedRuntime versionv4.0 sku.NETFramework,Versionv4.5 / /startup system.data DbProviderFactories !-- invariant 名要和代码里写的完全一致大小写都不能差 -- add nameNpgsql Data Provider invariantNpgsql descriptionPostgreSQL Data Provider for .NET typeNpgsql.NpgsqlFactory, Npgsql / /DbProviderFactories /system.data runtime assemblyBinding xmlnsurn:schemas-microsoft-com:asm.v1 dependentAssembly assemblyIdentity nameNpgsql publicKeyToken5d8b90d52f46fda7 cultureneutral / !-- 如果工程里别的包引了不同版本在这里统一收敛 -- bindingRedirect oldVersion0.0.0.0-2.2.7.0 newVersion2.2.7.0 / /dependentAssembly /assemblyBinding /runtime /configurationtype属性那行是最容易写错的地方格式是「完整类型名, 程序集名」中间逗号后面不能带.dll后缀。publicKeyToken也不要凭记忆填正确的做法是从packages或lib目录里用sn -T Npgsql.dll打出来或者直接在引用的属性面板里看。bindingRedirect 的newVersion必须和实际拷贝到输出目录的那个版本一致否则运行时依然报加载失败——这类问题不会在编译期暴露只有跑到那行代码才炸属于典型的血泪经验。3. 连接层拆解连接字符串里每个参数都会在出问题时咬你一口驱动引对之后八成的问题会集中到「连接」这一层。不是连接失败就是池子被抽干或者连上了但查得极慢。这一章把连接串的构造、连接池参数的真实含义、SSL 与 search_path 的交互拆开讲目标是你拿到任何一个新环境能自己判断该调哪个参数。3.1 一个最小可用的连接工厂生产代码里不该到处散落new NpgsqlConnection(Host...)配置一变就得全局搜。比较务实的做法是抽一个静态工厂把配置读取、异常包装、日志埋点收在一处。public static class PgConnectionFactory { // 连接串只在启动时构造一次避免每次 Open 都重新解析 private static readonly string _connStr BuildConnectionString(); private static string BuildConnectionString() { var b new NpgsqlConnectionStringBuilder { Host ConfigUtil.Get(pg.host), Port int.Parse(ConfigUtil.Get(pg.port)), Database ConfigUtil.Get(pg.database), UserName ConfigUtil.Get(pg.user), Password ConfigUtil.Get(pg.password), Pooling true, MinPoolSize 2, // 预热两条避开首次请求的握手延迟 MaxPoolSize 50, // 上限要和 DB 的 max_connections 一起算 Timeout 15, CommandTimeout 30, SearchPath biz,public,// 多 schema 时决定不加前缀的表落在哪 ApplicationName ConfigUtil.Get(app.name) }; return b.ConnectionString; } public static NpgsqlConnection Open() { var conn new NpgsqlConnection(_connStr); try { conn.Open(); return conn; } catch (NpgsqlException ex) { conn.Dispose(); // 关键失败的连接对象也要释放否则池里留残骸 Logger.Error(pg open failed, sqlstate ex.SqlState, ex); throw; } } }代码里有两处容易忽略的细节。第一处是catch里那句conn.Dispose()Open()抛异常时连接对象已经分配了非托管资源不释放会慢慢形成句柄泄漏表现出来是「跑了三天之后突然连不上了」。第二处是SearchPath多 schema 场景下它决定了select * from orders到底查的是biz.orders还是public.orders很多「本地好好的、生产查错表」的事故就出在这。MaxPoolSize不能拍脑袋写 100。服务端max_connections默认值通常只有 100 出头还要扣掉维护连接、复制连接和 DBA 自己占的。如果你的服务部署 4 个实例每个池子 50那就是 200 条直接超过服务端上限报错是sorry, too many clients already。常见做法是按「服务端上限 ÷ 实例数 × 0.8」倒推。3.2 连接池参数Maximum Pool Size 与 Timeout 的真实关系连接池的行为用一句话概括Open()时先从池里找一个空闲连接有就复用没有且未达上限就新建已达上限就阻塞等待等到Timeout秒还没等到就抛NpgsqlException: The connection pool has been exhausted。这条链路意味着MaxPoolSize和Timeout是一对联动参数单独调没有意义。Timeout调大到 60 秒只是让请求多等 45 秒然后依然失败用户体验更差。真正的解法只有两条要么把连接还回去的速度提上来见第 5 章的释放问题要么把上限提上去前提是服务端扛得住。判断池子是否健康看两个指标就够了。一是pg_stat_activity里按application_name分组的连接数如果长时间贴着MaxPoolSize不降说明有地方没释放。二是业务侧的 P99 耗时如果它和Timeout的值接近基本可以确定请求在排队等连接而不是在执行 SQL。这两个信号一起看比看任何监控大盘都直接。另外提醒一点MinPoolSize设成大于 0 是有代价的池子会维持这些连接常驻服务端会一直看到它们处于 idle 状态某些按连接数计费的托管数据库会因此多花钱。小流量服务老老实实设 0 就行。3.3 SSL 与 Search PathMono.Security 介入的时机SSL Mode有四个常用取值行为差别很大值得逐个记住取值行为适用场景Disable完全不走加密仅限本机或完全隔离的内网Allow先明文服务端要求才加密过渡期兼容老服务端Prefer先尝试加密失败回落明文默认值最省心Require强制加密握手失败直接报错生产环境推荐选Require时握手环节就落到 Mono.Security 上了。如果此时 Mono.Security.dll 没有被正确加载会看到一个很奇怪的现象异常信息里提到的类型你根本没在代码里用过堆栈里也找不到自己写的任何方法。这不代表代码有问题纯粹是依赖没到位。Search Path则是另一个维度的事。它接受逗号分隔的 schema 列表按顺序查找。在 .NET 侧用NpgsqlConnectionStringBuilder.SearchPath设置等价于连接建立后立刻执行SET search_path TO biz, public。注意它只影响不加 schema 前缀的对象名显式写public.orders时不受影响。团队协作时建议统一约定要么全部显式前缀要么全部靠 search_path别混着来混着来一定会有人在 code review 时看漏。4. 数据访问主链路一条 SQL 从 NpgsqlCommand 到 DataReader 的完整走法依赖和连接都稳了剩下就是每天要写几十遍的增删改查。这一章不铺 API 大全只挑三个真正影响正确性和性能的点参数化怎么写才不出类型错、事务里怎么拿自增主键、批量写入为什么必须走 COPY。4.1 参数化查询与类型映射先看一段典型代码里面埋了几个容易翻车的点public Order GetOrder(long orderId, string status) { const string sql select id, order_no, amount, status, created_at from biz.orders where id id and status status; using (var conn PgConnectionFactory.Open()) using (var cmd new NpgsqlCommand(sql, conn)) { // 参数名带不带 都行Npgsql 会自动补但类型必须显式给 cmd.Parameters.Add(new NpgsqlParameter(id, NpgsqlDbType.Bigint) { Value orderId }); cmd.Parameters.Add(new NpgsqlParameter(status, NpgsqlDbType.Varchar) { Value status }); using (var reader cmd.ExecuteReader()) { if (!reader.Read()) return null; return new Order { Id reader.GetInt64(0), OrderNo reader.GetString(1), Amount reader.GetDecimal(2), // numeric 对应 decimal别用 double Status reader.GetString(3), CreatedAt reader.GetDateTime(4) }; } } }参数化这里最重要的是显式指定NpgsqlDbType。如果只写cmd.Parameters.AddWithValue(id, orderId)驱动要靠推断long推断成bigint没问题但DateTime推断成timestamp还是timestamptz就看运气了一旦和列类型不匹配服务端会走隐式转换索引直接失效查询从毫秒级掉到秒级。这个坑非常隐蔽因为结果是对的只是慢。还有一个高频误解Npgsql 的参数占位符支持name也支持:name两种都能用但同一个命令里不要混用混用会导致部分参数识别不到报「参数未提供」。团队里统一用一种别让下一个人猜。读取端推荐用序号GetInt64(0)而不是列名GetInt64(id)序号在编译期就固定列名每次Read()都要做一次字符串查找大结果集下这个开销不小。代价是可读性差用注释补上即可。4.2 事务与 RETURNING 拿自增主键PostgreSQL 拿自增主键不走IDENTITY而是用RETURNING。这个语法比 SQL Server 那套干净得多插入和取值是一条语句完成的public long CreateOrder(Order o) { const string sql insert into biz.orders (order_no, amount, status, created_at) values (no, amount, status, now()) returning id; // 关键一条语句同时完成插入和取回 using (var conn PgConnectionFactory.Open()) using (var tx conn.BeginTransaction()) // 显式事务后续还要插明细 using (var cmd new NpgsqlCommand(sql, conn, tx)) { cmd.Parameters.Add(new NpgsqlParameter(no, NpgsqlDbType.Varchar) { Value o.OrderNo }); cmd.Parameters.Add(new NpgsqlParameter(amount, NpgsqlDbType.Numeric) { Value o.Amount }); cmd.Parameters.Add(new NpgsqlParameter(status, NpgsqlDbType.Varchar) { Value NEW }); var newId Convert.ToInt64(cmd.ExecuteScalar()); // 明细行的插入省略失败时整体回滚 tx.Commit(); return newId; } }using包住BeginTransaction()的意义在于只要没走到Commit()Dispose()时会自动Rollback()。这比手写try/catch { tx.Rollback(); throw; }更不容易漏。这里如果翻车多数是忘了把tx传给NpgsqlCommand构造函数——命令没挂上事务就不在事务上下文里执行插入会立刻生效回滚变成空操作等发现数据不一致已经是几天后的事了。还有一点值得提醒RETURNING后面可以跟多个列甚至returning id, created_at这比插入后再select一次省一个往返在批量场景下差距很明显。4.3 批量写入COPY 与逐条 Insert 的差距这是整个数据访问链路里性能差距最大的一环。逐条Insert一万行即使包了事务也要一万次往返加上解析开销跑几十秒很正常。PostgreSQL 提供了COPY ... FROM STDIN协议驱动侧对应的是流式写入接口量级差距通常在一个数量级以上。// 注意COPY 的 API 在不同 Npgsql 大版本间改名过 // 2.x 里叫 NpgsqlCopyIn / NpgsqlCopySerializer新版是 BeginBinaryImport public void BulkInsert(string tableName, IEnumerablestring[] rows) { using (var conn PgConnectionFactory.Open()) { // 文本格式 COPY列顺序必须和表结构严格一致 var copy new NpgsqlCopyIn( string.Format(COPY {0} (order_no, amount, status) FROM STDIN, tableName), conn); copy.Start(); try { var stream copy.CopyStream; foreach (var row in rows) { // 字段用 \t 分隔行尾 \n空值必须写 \N不能写空字符串 var line string.Join(\t, row.Select(Escape)) \n; var bytes Encoding.UTF8.GetBytes(line); stream.Write(bytes, 0, bytes.Length); } copy.End(); } catch { copy.Cancel(bulk insert aborted); // 出错必须 Cancel否则连接状态残留 throw; } } }用 COPY 有三条硬约束必须记住。第一列顺序必须和COPY (col1, col2, col3)里写的顺序完全一致差一个就整批数据错位而且不报错这是最危险的一种失败。第二空值必须用\N表示写空字符串会被当成真的空串存进去varchar列上区分不出来numeric列上直接报类型错。第三字段内如果含制表符、换行、反斜杠必须转义否则整行会错位稳妥做法是先跑一遍清洗或者改用二进制格式——二进制格式没有转义问题但列类型必须精确匹配调试起来更麻烦。注意COPY 是整批原子性的中途报错整批不入库。如果数据源质量没把握先小批量试跑 1000 行确认没问题再放大。5. 避坑与排查连接池耗尽、SSL 握手失败和参数名大小写驱动用熟之后问题不再集中在「能不能连」而集中在「为什么跑一段时间就不行」。这一章把实际踩过的几类问题按「现象 → 原因 → 解决」列出来遇到类似症状可以先按这个顺序对号入座。5.1 连接池被抽干报 pool exhausted现象服务跑几小时后开始零星报错异常信息里带The connection pool has been exhausted重启立刻恢复过几小时再犯。原因有代码路径打开了连接或DataReader但没有释放。最常见的是ExecuteReader()之后只while(Read())完就return忘了Disposereader不关意味着它占用的那条连接回不到池子里。其次是把NpgsqlConnection存到了静态字段或长生命周期对象里池子的回收机制直接失效。解决所有连接一律using包裹reader也一律using哪怕只有一行读取。想确认是不是这个原因执行select application_name, count(*) from pg_stat_activity group by 1 order by 2 desc;如果连接数只增不减且大部分状态是idle in transaction那基本可以锁定是没提交事务或者没关连接。另外idle和idle in transaction要分开看后者更严重它会一直持有锁。5.2 SSL 握手时 Mono.Security 抛类型初始化异常现象本地开发机一切正常本地关着 SSL部署到测试环境后所有连接失败异常类型是TypeInitializationException内部异常的命名空间指向Mono.Security.Protocol.Tls之类的地方堆栈里没有任何业务代码。原因连接字符串里写了SSL ModeRequire驱动需要走加密握手但 Mono.Security.dll 没有被拷贝到输出目录或者拷贝进去的版本和 Npgsql.dll 编译时引用的版本对不上。后一种情况更隐蔽因为文件明明在。解决先确认输出目录bin下确实有这两个 DLL并且修改时间一致同批下载的。再用sn -T或引用的属性面板核对版本号。如果工程里有多个地方引用了不同版本的 Npgsql用bindingRedirect统一收敛到同一个版本。排查阶段可以先临时把连接串改成SSL ModePrefer对比一下如果能连上就说明确实是加密链路的问题把范围缩到 Mono.Security 上。5.3 参数名大小写与前缀导致的参数丢失现象报错「参数 Status 未提供」但cmd.Parameters.Add明明已经加了status。原因NpgsqlParameter的参数名匹配在某些版本里是大小写敏感的SQL 文本里写Status、代码里加的是status就对不上。另一种情况是同一个命令里混用了name和:name两种占位符驱动只识别先出现的那种风格。解决统一命名风格全小写或者驼峰选一个SQL 文本和Parameters.Add里的名字一字不差。团队里定个规矩参数名一律小写加下划线和列名保持一致这样复制粘贴都不容易错。改完之后如果还报错把cmd.CommandText和cmd.Parameters的ParameterName一起打日志一眼就能看出差在哪。5.4 时间戳字段返回的 Kind 是 Unspecified现象从timestamptz列读出来的DateTimeKind属性是Unspecified做时区转换时算错 8 小时日志时间对不上。原因老版本 Npgsql 对timestamp with time zone的默认行为是转换成服务器时区或 UTC然后返回一个KindUnspecified的DateTime。这属于驱动历史行为不是 bug但很容易误解成数据库存错了。解决连接串里显式设置时区相关参数老版本有Timezone选项或者在读取后统一自己做一次DateTime.SpecifyKind(dt, DateTimeKind.Utc)。更稳妥的做法是业务层全部按 UTC 存储和传输展示层再转本地时区别在数据访问层做转换。如果表设计还没定直接选timestamptz让它自己管时区比用timestamp省心。5.5 FileNotFoundException 与位数不匹配现象编译通过运行时抛Could not load file or assembly Npgsql或BadImageFormatException。原因前者通常是Copy Local被设成了 False或者引用的路径指向了解决方案外的一个临时目录别人拉代码后路径不存在。后者是位数问题——IIS 应用池如果开了 32 位模式而 DLL 是按 AnyCPU 编译、运行在 64 位下或者反过来就会撞上。解决引用属性里把Copy Local设为 True并把 DLL 放进lib目录随代码提交。位数问题先在应用池设置里确认「启用 32 位应用程序」的状态再确认进程实际位数。这类问题在本地 IIS Express 上往往复现不了只在正式 IIS 上出现属于典型的「环境玄学」但查清了就一句话的事。6. 二次开发把裸 API 包成能维护的仓储层直接用NpgsqlCommand写业务逻辑短期没问题写到第三十个方法就会开始重复每个方法都在拼using、都在写 try/catch、都在传连接。改造方向不是引入重型 ORM老项目上 ORM 的迁移成本往往比收益大而是在驱动之上加一层薄封装把「连接从哪来、异常怎么记、参数怎么传」这三件事统一收口。6.1 抽一层 NpgsqlHelper封装的目标是让业务代码只剩 SQL 和参数两样东西。下面这个版本够用且不复杂public static class PgHelper { // 查询返回映射后的对象列表 public static ListT QueryT(string sql, FuncNpgsqlDataReader, T map, params NpgsqlParameter[] ps) { var list new ListT(); using (var conn PgConnectionFactory.Open()) using (var cmd BuildCommand(sql, ps, conn)) using (var reader cmd.ExecuteReader()) { while (reader.Read()) list.Add(map(reader)); } return list; } // 执行返回受影响行数事务由调用方决定 public static int Execute(string sql, params NpgsqlParameter[] ps) { using (var conn PgConnectionFactory.Open()) using (var cmd BuildCommand(sql, ps, conn)) { return cmd.ExecuteNonQuery(); } } private static NpgsqlCommand BuildCommand(string sql, NpgsqlParameter[] ps, NpgsqlConnection conn) { var cmd new NpgsqlCommand(sql, conn); if (ps ! null ps.Length 0) cmd.Parameters.AddRange(ps); return cmd; } }这里刻意没做两件事。一是没有把事务塞进Execute因为事务边界属于业务语义封在 helper 里迟早会有人跨方法调用时搞错范围需要事务的地方老老实实using (var tx conn.BeginTransaction())。二是没有做自动对象映射用FuncNpgsqlDataReader, T让调用方自己决定怎么映射比反射映射快一个数量级也避免了「列名改了但代码不报错」的隐患。参数构造太啰嗦的问题可以再加一个小的糖public static NpgsqlParameter P(string name, NpgsqlDbType type, object value) { // DBNull 统一转换避免 null 被当成未赋值 return new NpgsqlParameter(name, type) { Value value ?? DBNull.Value }; }调用端就变成PgHelper.Query(select ... where id id, Map, P(id, NpgsqlDbType.Bigint, id))行数明显下来而且类型仍然显式没有丢NpgsqlDbType带来的索引失效隐患。6.2 验证方法从冒烟脚本到影子库比对改完之后怎么确认没改坏分三层验证比较踏实。第一层是连接冒烟就是第 2 章那段select version()每次改配置都跑一遍能挡住 80% 的环境问题。第二层是 SQL 回归把 helper 的每个方法对应一条最小 SQL用固定的测试库跑重点验三件事参数类型对不对、空值能不能正常读写、事务回滚是不是真的回滚了。第三层是影子库比对迁移期间让新旧两条路径同时写定时跑一次except双向差集差集为空才切流。-- 迁移期的双向比对两边差集都为空才算对齐 (select order_no, amount, status from biz.orders except select order_no, amount, status from legacy.orders) union all (select order_no, amount, status from legacy.orders except select order_no, amount, status from biz.orders);比对时numeric的精度最容易出问题一边是numeric(18,2)、另一边是numeric(18,4)值看着一样except却能比出来。这种差异要在建表阶段就对齐切流之后再发现就麻烦了。我现在的习惯是任何一次数据库迁移先写比对 SQL再写业务代码。比对 SQL 是那次迁移唯一可信的验收标准别指望人工抽样。从那以后我每次接这类迁移需求都强制先把双向差集跑通再动业务逻辑慢两天但心里踏实。这套 Npgsql Mono.Security 的组合放在 .NET Framework 4.5 的老工程里依然是最省事的直连方案希望帮到你。本文还有配套的精品资源点击获取
延伸阅读

更多相关文章

2026/10/27 10:35:17

深入理解 Radix Tree:原理、优势、劣势与 C 语言实现

一、引言在操作系统、网络协议栈、数据库和高性能存储系统中,经常需要根据某个键快速查找对应的数据。例如:根据字符串查找对应的配置项。根据 IP 地址查找路由表中的匹配项。根据页索引查找 Linux 内核中的 page cache。根据整数索引查找内核对象。根据…

2026/10/27 10:35:17

Unity心电图波形参数化:振幅、波速、颜色实时可调

简介:面向Unity开发者与图形交互方向学习者的心电图线波模拟演示工程,围绕ECG波形上下振幅、波线滚动速度与线条颜色等参数的可调实现,适合从事游戏UI动效、医疗可视化演示及互动装置原型的中初级开发者参考。包内共6688个文件,以…

2026/10/27 11:40:45

2026国产大模型核心技术解析与开发实战指南

1. 国产大模型发展现状与技术特征 2026年的国产大模型已经形成了独特的技术路线和产业生态。与三年前相比,当前主流模型的参数量级普遍达到万亿级别,但在架构设计上更注重实用性和性价比。典型代表如华为的"盘古3.0"、百度的"文心5.0&quo…

2026/10/27 11:40:45

SSE流式输出实战:协议、网关缓冲与断线重连排坑

1. 先把这件被说烂了的事讲透:SSE到底在流式对话里扮演什么角色 前阵子帮一个团队看他们的对话产品,现象很典型:后端日志显示模型已经在吐字了,前端界面上那个“思考中”的转圈图标还转足了三秒,然后一大段文字“啪”地…

2026/10/27 11:40:45

大模型与知识图谱融合架构设计与实践

1. 大模型与知识图谱的融合趋势 最近两年,大模型和知识图谱这两个技术方向都获得了突破性进展。作为一名长期跟踪AI技术发展的从业者,我观察到这两者的结合正在催生新一代智能系统。大模型以其强大的语言理解和生成能力著称,而知识图谱则以结…

2026/10/27 11:40:45

AI代理视频渲染如何实现确定性?HyperFrames框架原理与实践

1. 为什么“确定性渲染”是 AI 代理做视频的第一道生死线如果你最近在折腾 AI 代理自动生成视频,大概率遇到过这种让人抓狂的情况:同一个提示词、同一份素材、同一个脚本,跑第一遍出来的视频没问题,跑第二遍字幕错位了&#xff0c…

2026/10/27 11:40:45

基于改进Mask R-CNN的自卸车部件识别与优化实践

1. 项目背景与核心挑战 自卸车作为工程机械领域的重要运输工具,其关键部件(如货箱、液压缸、车架等)的实时识别与状态监测对智能运维和自动化作业至关重要。传统人工检测方式存在效率低、成本高、主观性强等问题,而基于深度学习的…

2026/10/27 11:35:37

DB2异机恢复实战:NetBackup跨机还原避坑指南

简介:本资源是一份面向DB2数据库管理员与企业级灾备工程师的DB2异机恢复技术实践指南,聚焦基于Veritas NetBackup(NBU)实现跨服务器环境的DB2数据库可靠恢复,解决生产环境中因硬件故障、机房迁移等场景下的业务连续性保…

2026/10/26 0:20:20

全国计算机二级C语言真题PDF高效刷题指南

简介:这份PDF是全国计算机二级C语言考试的试题与答案解析,专为备考二级C语言的考生准备。内容涵盖C语言特点与设计理念、程序基本组成单位、main函数执行流程、函数嵌套调用规则、标识符命名规范、整型实型字符型数据类型、八进制十六进制数合法形式、运…

2026/10/27 9:34:54

requests+BeautifulSoup:爬虫抓取h1标题的入门实战指南

简介:一份以实战为导向的Python爬虫入门资源,面向具备基本编程基础、希望快速掌握网络数据抓取的初学者,可解决从零编写简单爬虫的常见困惑。实例以requests库发送HTTP请求、BeautifulSoup解析HTML文档,完整演示从网页中提取 标题…

2026/10/27 0:59:21

递推最小二乘参数辨识MATLAB编程:二阶系统与渐消记忆实现

简介:这是一篇面向控制系统、信号处理与机器学习等领域学习者的最小二乘参数辨识程序说明文档。资源共包含一个docx格式文档,压缩包大小七十八KB,内容围绕递推最小二乘算法和渐消记忆最小二乘算法展开:先构造一个极点位于单位圆内…

2026/10/27 0:02:28

WorkBuddy_新建任务与工作模式19_“模式选错”的典型代价

“模式选错”的典型代价 摘要 在软件工程中,设计模式、架构模式、并发模式乃至团队协作模式的选择,往往在项目早期就决定了后续的开发效率和维护成本。本文通过多个真实场景,剖析“模式选错”带来的典型代价——包括返工成本、技术债务累积、…

2026/10/27 0:02:28

WorkBuddy_新建任务与工作模式20_新手推荐路径

新手推荐路径 摘要 在 AI 辅助开发工具快速普及的今天,新手面对最多的困惑往往不是"工具够不够强",而是"我该从哪一步开始用"。本文提出一条经过实践验证的三阶段学习路径:先从 Ask(问答式交互)起…

还想了解更多?直接咨询顾问

免费诊断 + 免费方案 + 透明报价。

全国咨询热线400-8866-253
免费获取方案
☎咨询二维码 ☎ ↑