PalORM.PostgreSql
6.8.0
dotnet add package PalORM.PostgreSql --version 6.8.0
NuGet\Install-Package PalORM.PostgreSql -Version 6.8.0
<PackageReference Include="PalORM.PostgreSql" Version="6.8.0" />
<PackageVersion Include="PalORM.PostgreSql" Version="6.8.0" />
<PackageReference Include="PalORM.PostgreSql" />
paket add PalORM.PostgreSql --version 6.8.0
#r "nuget: PalORM.PostgreSql, 6.8.0"
#:package PalORM.PostgreSql@6.8.0
#addin nuget:?package=PalORM.PostgreSql&version=6.8.0
#tool nuget:?package=PalORM.PostgreSql&version=6.8.0
<div align="center">
PalORM
面向 Native AOT 的 .NET 11 微 ORM:编译时生成一切,运行时零反射
简体中文 | English
AOT 原生、编译期可验证、SQL 可控的微 ORM
</div>
Roslyn 源生成器在编译期产出 SQL 构造、参数绑定、对象映射与迁移 DDL,运行时零反射、零 IL Emit;支持 PostgreSQL / MySQL / SQLite 三方言,多租户、乐观锁、软删除、审计、咨询锁等企业级特性开箱即用。
目录
| 本文档 | 深度文档(docs/) | ||
|---|---|---|---|
| ✨ 特性 | 🚀 快速开始 | 功能特性全景(源码级) | AOT 部署指南 |
| 🆚 与主流 ORM 对比 | 📖 使用 | API 参考 | 性能基准规范 |
| 📊 性能 | ✅ 最佳实践速查 | 架构设计 | 发布规范 |
| 📦 安装 | 🔧 配置 | 架构决策(ADR) | 编码规范 |
| 🔄 升级指南 | 🧰 开发 | 贡献指南 | 变更日志 |
| 💬 社区 | 📄 许可证 |
✨ 特性
- 编译期生成:Roslyn 源生成器为每个
[Table]实体生成 RowFactory、CommandFactory 与三方言 Migration DDL,为[Projection]DTO 生成物化委托,运行时零反射、零 IL Emit。 - 编译期安全网:45 条 Roslyn 诊断把缺主键、租户列可空绕过隔离、乐观锁基线为 0、循环内 N+1 等问题拦在构建期,错误可跳转声明位置。
- 全链路 Native AOT:SQLite / PostgreSQL / MySQL 三方言 AOT 发布与原生运行全部通过,消费方不需要 TrimmerRootAssembly。
- 地板级性能:单行 CRUD 与 ADO.NET 手写地板同量级,批量写按方言自动选择 PG Binary COPY、MySQL LOAD DATA、SQLite 行预编译与 json_each 批删,Web 一请求一会话形态内置会话池。
- 企业级横切:多租户列隔离、乐观锁、软删除、审计拦截器、读写分离、咨询锁、弹性重试与熔断开箱即用。
- SQL 资产编译期管理:
[SqlFile]嵌入并校验 .sql,[SqlTemplate]把插值 SQL 常量化,AutoTagging 自动注入“文件:行号 成员”定位注释。 - 运维与可观测:健康检查、跨方言统一异常族、OpenTelemetry tracing 与 metrics 内置。
- 方言专属能力:PG 的 UNNEST、咨询锁、LISTEN/NOTIFY、pgvector 与 JSONB 路径查询,MySQL 的 local_infile 降级与 UPDATE JOIN,SQLite 的 AES-256 加密与 PRAGMA 自动调优。
- Schema 与种子:
MigrateAsync按外键拓扑序执行三方言 DDL,SeedAsync事务内幂等种子,ValidateSchemaAsync对照实体返回缺失列。 - 零依赖核心:PalORM.Core 不引用第三方 NuGet 包,仅 BCL 与 ADO.NET 抽象。
🆚 与主流 ORM 对比
版本基准(2026-10-26 核对 NuGet 最新):PalORM 6.7.0 / Dapper 2.1.89 / RepoDb 1.17.0(仓库基准套件实际引用版本);EF Core 10.0.13(10.x LTS 最新补丁,对比所引)。
| 特性 | PalORM | Dapper | EF Core | RepoDb |
|---|---|---|---|---|
| Native AOT 全链路 | ✓ 源生成验证 | △ Dapper.Aot 可选(实验性拦截器) | ❌ 实验性,生产不推荐 | △ 全包 NativeAOT/trimming 兼容标注 + SQLite AOT 冒烟测试(反射路线,无源生成) |
| 编译时类型诊断 | ✓ 45 条(42 分析器 + 3 生成器),错误可跳转声明位置 | ❌ 运行时失败 | △ 迁移检查(设计时) | ❌ 运行时失败 |
| 编译时 SQL 预构建 | ✓ Roslyn 源生成 | ❌ 运行时拼接 | △ 预编译查询(实验性) | ❌ 运行时表达式树 |
| 运行时反射 | 零 | △ 首次反射 + IL Emit 缓存 | △ 表达式树编译 | △ 反射 + 表达式编译(IL Emit 已移除) |
| 查询书写范式 | ✓ 链式构建器(SQL 显式可见)+ FormattableString 原生 SQL |
△ 手写 SQL | ✓ LINQ 强类型翻译 | △ QueryBuilder |
| 模型构建成本 | ✓ 编译期生成,零运行时构建 | ✓ 无模型(首次反射 + 缓存) | △ 运行时构建(Compiled Model 需手动再生成) | ✓ 首次操作缓存 |
| 三方言批量策略 | ✓ COPY / BulkCopy / 行预编译·多值 / = ANY 数组 |
❌ 手写多值 SQL | △ Provider 各异 | △ Bulk 族扩展包覆盖 13+ 数据库(SQL Server/MySQL/Oracle 等,部分 preview) |
| 单语句多行 UPDATE | ✓ FROM VALUES / UPDATE JOIN VALUES ROW / CASE WHEN | ❌ | ❌ ExecuteUpdate 仅按 WHERE 单值 | ❌ |
| IN 集合参数翻译 | ✓ 500 键自动分批 + PG = ANY 数组形态 |
❌ 手写 | ✓ EF10 起多参数为默认,翻译模式可配(JSON/常量) | △ DeleteAll 内部按 MaxParameterCount 分批;查询侧 IN 手写 |
| 批量 UPSERT | ✓ BulkMergeAsync(ON CONFLICT / ON DUPLICATE KEY,冲突更新限定本租户) |
❌ 手写 | ❌ 需 raw SQL | △ Merge 经扩展包覆盖多 Provider(SQL Server/MySQL/Oracle 等) |
| 乐观锁 | ✓ [ConcurrencyCheck] 自动;批量单语句(PG/MySQL,能力不全回退 DbBatch) |
❌ 手写 | ✓ RowVersion 自动 |
❌ 手写 |
| 软删除 | ✓ [SoftDelete] 自动过滤(含批量删转 UPDATE) |
❌ | ✓ 全局查询过滤器(EF10 命名过滤器可按名禁用) | ❌ |
| 多租户列隔离 | ✓ [TenantAware] 编译时;批量写跨租户护栏 |
❌ | △ 需手动实现 | ❌ |
| 值转换 + 枚举存储 | ✓ [Converter] / 枚举三策略(int32/int64/文本)编译时发射 |
△ TypeHandler 手写 | ✓ ValueConverter / HasConversion(运行时) | △ TypeHandler |
| 读副本路由 | ✓ ParallelReadScope + 查询族 readFromReplica |
❌ | △ 需手动配置多上下文 | ❌ |
| Schema 自动迁移 | ✓ MigrateAsync + 并发竞态容错(重复对象即跳过) |
❌(无迁移系统) | ✓ Migrations(最全面) | ❌ |
| OwnedJson 编译时安全 | ✓ [OwnedJson] + 源生成 |
△ PropertyHandler 映射(运行时反射标注 RUC/DC;AOT 需传源生成 JsonTypeInfo) |
△ Owned Types(运行时) | ❌ |
| 审计拦截器 | ✓ | ❌ | ✓ Interceptors | ❌ |
| 咨询锁 | ✓ pg_advisory_xact_lock |
❌ | ❌ | ❌ |
| 会话级 SET | ✓ SessionSetupSql |
❌ | ❌ | ❌ |
| SQL 文件嵌入 | ✓ [SqlFile] 编译时校验 |
❌ | ❌ | ❌ |
| 断路器 + 重试 | ✓ 内置(HalfOpen 探针槽位并发安全) | ❌ 需 Polly | △ 执行策略 | ❌ |
| 查询缓存 | ✓ WithCache 有界缓存 + TTL + EvictQueryCache 收窄 |
❌ | ❌ 需第三方拦截器 | ✓ 原生 cacheKey 参数 |
| 流式消费 | ✓ QueryAsyncEnumerable / ForEachAsync |
△ 手写 CommandBehavior | ✓ AsAsyncEnumerable 原生 |
❌ |
| 可观测性(OTel) | ✓ WithMetrics / WithTracing 内置 |
❌ | △ 第三方 instrumentation 包 | △ 自建遥测扩展包(Telemetry.Core/Default,非 OTel) |
| SQL 预览 | ✓ AsDryRun(SQL + 参数) |
❌ | ✓ ToQueryString |
△ 构建器层 GetString |
| 悲观锁 | ✓ ForUpdate / ForShare 链式 |
❌ 手写 SQL | △ 本体无内置(PG provider 扩展) | ❌ |
| 导航装配(对象图) | △ Include 生成 JOIN 不装配(micro-ORM 取舍,按父分组自行配对) |
△ multi-mapping 手写 | ✓ Include 完整装配 + 变更追踪 |
❌ |
| 变更追踪 / 工作单元 | ❌ 无状态设计(零追踪开销) | ❌ | ✓ ChangeTracker(身份解析 + 状态机) | ❌ |
| JSON 列查询 | △ WhereJson JSONB 路径查询(PG 专有);映射见上方 [OwnedJson] |
△ 手写 SQL | ✓ EF7+ JSON 列映射 + 路径查询(EF10:SQL Server 2025 json 类型、ExecuteUpdate 支持) | △ PropertyHandler 映射,无路径查询 |
| CTE / 窗口函数 | ✓ 链式 API | △ 原生 SQL 字符串 | △ LINQ 翻译(部分) | △ 原生 SQL |
| 多结果集 | ✓ GridReader |
✓ QueryMultiple |
❌ | ✓ ExecuteQueryMultiple |
| Keyset 分页 | ✓ ToPageAsync |
❌ | ❌ | ❌ |
| Scaffold 工具 | ✓ 三 Provider | ❌ | ✓ dotnet ef dbContext scaffold |
❌ |
| 连接串自动调优 | ✓ PG 6 / MySQL 5 / SQLite 9 项 | ❌ | ❌ | ❌ |
| 会话池化 | ✓ DataSessionPool 内置(32 并发 P99 −18%) |
❌ | ✓ DbContextPool |
❌ |
| 存储过程 | ✓ StoredProc 强类型输入/输出参数 |
✓ DynamicParameters |
△ FromSqlRaw 间接 |
✓ ExecuteQuery |
| 整事务瞬时错误重试 | ✓ WithTransactionRetry(方言错误码重放,at-least-once) |
❌ | ✓ EnableRetryOnFailure 执行策略 |
❌ |
| 数据库健康检查 | ✓ HealthCheckAsync |
❌ 手写 SELECT 1 | ✓ CanConnect() |
❌ |
| SQL 文件 / 模板资产化 | ✓ [SqlFile] 方言条件段 + [SqlTemplate] 常量化,编译期校验 |
❌ | ❌ | ❌ |
| SQL 定位注释 | ✓ AutoTagging 自动注入「文件:行号 成员」(opt-in) | ❌ | △ TagWith() 手动 |
❌ |
| pgvector | ✓ 文本边界路线(::vector 写入,AOT 兼容) |
❌ | △ 第三方扩展包 | ❌ |
| 种子数据 | ✓ SeedAsync 事务内幂等 Upsert |
❌ | ✓ HasData(设计时) |
❌ |
| BulkInsert 内存 | SQLite ≈ Dapper 的 40%(PG / MySQL 5%~7%) | 基线 | 最高(ChangeTracker) | 中等 |
| 数据库覆盖广度 | 三方言(深度调优) | 任意 ADO.NET(方言自理) | 官方 Provider 矩阵(SQL Server / SQLite / PG / MySQL / Cosmos…) | 15+ Provider(1.16/1.17 大幅扩容) |
| 核心包 NuGet 依赖 | 零 | 零 | 高(多包拆分) | 中等 |
| 目标框架 | net11.0(单目标) | 多目标(netstandard2.0+) | 多目标(net8+) | 多目标(netstandard2.0+) |
| 许可证 | Apache-2.0 | Apache-2.0 | MIT | Apache-2.0 |
核心差异:编译时生成 + 全链路 AOT 兼容 + 三方言批量策略(COPY / BulkCopy / 行预编译 / 数组)。Dapper 快但运行时反射;EF Core 功能完整但运行时重、AOT 仍实验性;RepoDb 同为微 ORM 但无源生成,Bulk 族经扩展包扩展到 SQL Server/MySQL/Oracle 等 13+ 数据库。 RepoDb 1.16/1.17 开始已经移除掉了IL Emit实现,现在已经兼容AOT,使用表达式路径实现。(RepoDb早点实现AOT,我就不做这个ORM了,没想到用AI还要搞四个月 ☹)。
📊 性能
测试环境:AMD Ryzen 9 8945HX(32 逻辑核)· Windows 10 22H2 · .NET 11 RC2 · SQLite 临时文件库(WAL + 64 MB cache + mmap)· PG 18.6 / MySQL 8.4.11 本机容器。四臂同进程同轮采集、同数据同量具:ADO.NET(手写性能地板)/ Dapper 2.1.89 / FreeSql 3.5.311 / PalORM——每臂用其生态惯用的最优写法,每操作两遍反序臂序取中位(消除臂序相位)。
数据批次:2026-10-25 夜,代码
3affd97。CrossBench 四臂批crossbench-20261025-213235.json+PerfCli full三完整批(416 项/批);同批门禁 BDN 27/27、结果库 171 项 0 FAIL。数字由冻结模板脚本从批次 JSON 推导(scripts/crossbench-report.cs),完整报告见bench/reports/。
读表:每格 = 时间 [对 ADO 倍数] / 内存 [对 ADO 倍数];[1.00] = ±0.5% 持平带;≤0.67 或 ≥1.5 越线加粗;📌 = 越线待判读;🏆 = 该格最快臂。色标(正 % = 比基准慢/费):🟢 强优 ≤−30% · 🟩 中优 −29%~−10% · 🔹 微优 −9%~−1% · 🔸 微劣 +1%~+9% · 🟧 中劣 +10%~+29% · 🟥 强劣 ≥+30%。
总览(四臂 × 三方言 × 14 操作 = 69 格)
CRUD 单行与读(基准 = ADO [1.00])
- 🥇 GetByKey SQLite:PalORM 0.78(快 22%),FreeSql 4.70(慢 370%,全方言最差 5.00)
- QueryAll MySQL 20000 分配:PalORM 0.91(快 9%),Dapper 1.11(慢 11%)
- Insert SQLite:PalORM 1.00(持平),FreeSql 1.74(慢 74%)
批量处理(基准 = ADO [1.00])
- 🥇 BulkDelete PG 20000:PalORM 0.44(快 56%),分配 0.10(快 90%)
- 🥇 UpsertBatch PG 20000:PalORM 0.60(快 40%),分配 0.37(快 63%)
- BulkInsert SQLite 20000:PalORM 1.03(慢 3%),Dapper 1.68(慢 68%),FreeSql 3.99(慢 299%)
- BulkUpdate 三方言两档:PalORM 0.89~1.03,Dapper 最差 3.77(慢 277%)
事务(基准 = ADO [1.00])
- PalORM 全项 0.95~1.15,最快 TxHundredInserts MySQL 0.95
- FreeSql 最差 TxRollback SQLite 13.26,Dapper 最差 TxRollback SQLite 3.86
- EnumInserts SQLite 分配:PalORM 1.00(持平),FreeSql 8.42(慢 742%)
最快胜场
| 臂 | 最快 / 69 格 |
|---|---|
| ADO.NET(地板) | 33(48%) |
| PalORM | 31(45%) |
| Dapper | 5(7%) |
| FreeSql | 0(0%) |
P/ADO 中位 1.00(贴平手写 SQL 地板),全距 0.44~1.15。
表一:CRUD 单行与读(三方言,两档)
胜场:ADO 15 · PalORM 11 · Dapper 4 · FreeSql 0
| 操作 | 测什么 | 总评 |
|---|---|---|
| GetByKey | 主键单行点查 | PalORM 贴地板(SQLite 会话税被 SQL 常量缓存抵消);FreeSql 全方言最慢(表达式每次求值) |
| GetAllAsync | 全表直查 API(PalORM 专用 GetAllAsync,其余同 QueryAll) | 联网方言四臂接近;SQLite Dapper(反射物化)与 FreeSql(表达式管线)明显偏慢 |
| QueryAll | 全表物化查询(From<T>().ToListAsync / Select.ToList) | 同 GetAllAsync:PalORM ≈ ADO 地板,SQLite 两竞品明显偏慢 |
| Insert | 单行插入(自增主键) | 联网方言 fsync 主导四臂持平;SQLite PalORM 持平、FreeSql 明显偏慢(表达式管线) |
| Update | 单行整行更新 | 联网方言 fsync 主导持平;FreeSql 在 SQLite 与 MySQL 明显偏慢(表达式管线) |
SQLite
| 操作 | 档 | 🏆 | ADO.NET | PalORM | Dapper | FreeSql |
|---|---|---|---|---|---|---|
| GetByKey | 2000 | 🏆 PalORM | 7.4 µs [1.00] / 2.1KB [1.00] | 5.8 µs [0.78] / 2.1KB [1.02] | 9.7 µs [1.30] / 2.8KB [1.34] | 34.8 µs [4.70] / 10.2KB [4.86] |
| GetAllAsync | 2000 | 🏆 PalORM | 957.8 µs [1.00] / 308KB [1.00] | 949.0 µs [0.99] / 294KB [0.95] | 1.36 ms [1.42] / 513KB [1.66] | 1.38 ms [1.44] / 314KB [1.02] |
| QueryAll | 2000 | 🏆 PalORM | 956.6 µs [1.00] / 308KB [1.00] | 947.0 µs [0.99] / 292KB [0.95] | 1.34 ms [1.40] / 513KB [1.66] | 1.37 ms [1.43] / 314KB [1.02] |
| Insert | 2000 | ADO | 17.0 µs [1.00] / 2.8KB [1.00] | 17.0 µs [1.00] / 2.7KB [0.97] | 17.8 µs [1.05] / 3.5KB [1.28] | 29.6 µs [1.74] / 5.7KB [2.06] |
| Update | 2000 | ADO | 7.0 µs [1.00] / 2.5KB [1.00] | 7.2 µs [1.01] / 2.9KB [1.15] | 7.8 µs [1.11] / 3.3KB [1.30] | 19.1 µs [2.71] / 7.4KB [2.94] |
| GetByKey | 20000 | ADO | 6.0 µs [1.00] / 2.1KB [1.00] | 6.2 µs [1.04] / 2.2KB [1.02] | 7.1 µs [1.18] / 2.8KB [1.33] | 30.0 µs [5.00] / 10.4KB [4.93] |
| GetAllAsync | 20000 | ADO | 9.52 ms [1.00] / 3.3MB [1.00] | 9.55 ms [1.00] / 2.9MB [0.89] | 13.72 ms [1.44] / 5.3MB [1.61] | 13.82 ms [1.45] / 3.3MB [1.01] |
| QueryAll | 20000 | 🏆 PalORM | 9.54 ms [1.00] / 3.3MB [1.00] | 9.48 ms [0.99] / 2.9MB [0.89] | 13.66 ms [1.43] / 5.3MB [1.61] | 13.78 ms [1.44] / 3.3MB [1.01] |
| Insert | 20000 | 🏆 PalORM | 17.1 µs [1.00] / 2.8KB [1.00] | 16.9 µs [0.99] / 2.7KB [0.97] | 18.0 µs [1.06] / 3.6KB [1.31] | 29.1 µs [1.71] / 5.7KB [2.06] |
| Update | 20000 | ADO | 7.1 µs [1.00] / 2.5KB [1.00] | 7.1 µs [1.00] / 2.9KB [1.15] | 8.0 µs [1.12] / 3.3KB [1.30] | 21.4 µs [3.01] / 7.4KB [2.95] |
MySQL
| 操作 | 档 | 🏆 | ADO.NET | PalORM | Dapper | FreeSql |
|---|---|---|---|---|---|---|
| GetByKey | 2000 | ADO | 190.0 µs [1.00] / 4.5KB [1.00] | 190.8 µs [1.00] / 4.9KB [1.09] | 198.4 µs [1.04] / 5.8KB [1.30] | 414.6 µs [2.18] / 16.5KB [3.68] |
| GetAllAsync | 2000 | ADO | 1.09 ms [1.00] / 398KB [1.00] | 1.10 ms [1.00] / 384KB [0.96] | 1.13 ms [1.04] / 447KB [1.12] | 1.36 ms [1.24] / 407KB [1.02] |
| QueryAll | 2000 | 🏆 PalORM | 1.11 ms [1.00] / 398KB [1.00] | 1.10 ms [0.99] / 384KB [0.96] | 1.13 ms [1.02] / 447KB [1.12] | 1.36 ms [1.22] / 407KB [1.02] |
| Insert | 2000 | 🏆 PalORM | 3.84 ms [1.00] / 16.3KB [1.00] | 3.66 ms [0.95] / 16.3KB [1.00] | 3.79 ms [0.98] / 16.4KB [1.01] | 4.09 ms [1.06] / 22.1KB [1.35] |
| Update | 2000 | 🏆 PalORM | 193.4 µs [1.00] / 5.3KB [1.00] | 189.7 µs [0.98] / 5.8KB [1.09] | 198.6 µs [1.03] / 5.2KB [0.97] | 402.8 µs [2.08] / 13.2KB [2.47] |
| GetByKey | 20000 | ADO | 194.3 µs [1.00] / 4.5KB [1.00] | 194.7 µs [1.00] / 4.9KB [1.09] | 204.5 µs [1.05] / 5.9KB [1.30] | 421.9 µs [2.17] / 16.8KB [3.72] |
| GetAllAsync | 20000 | Dapper | 9.95 ms [1.00] / 4.0MB [1.00] | 9.84 ms [0.99] / 3.7MB [0.91] | 9.84 ms [0.99] / 4.5MB [1.11] | 10.50 ms [1.06] / 4.1MB [1.00] |
| QueryAll | 20000 | ADO | 9.47 ms [1.00] / 4.0MB [1.00] | 9.76 ms [1.03] / 3.7MB [0.91] | 9.82 ms [1.04] / 4.5MB [1.11] | 10.57 ms [1.12] / 4.1MB [1.00] |
| Insert | 20000 | 🏆 PalORM | 3.90 ms [1.00] / 16.7KB [1.00] | 3.85 ms [0.99] / 16.7KB [0.99] | 3.87 ms [0.99] / 16.4KB [0.98] | 4.27 ms [1.10] / 22.6KB [1.35] |
| Update | 20000 | ADO | 167.3 µs [1.00] / 5.1KB [1.00] | 176.8 µs [1.06] / 5.7KB [1.11] | 174.3 µs [1.04] / 5.1KB [1.00] | 400.3 µs [2.39] / 13.3KB [2.59] |
PostgreSQL
| 操作 | 档 | 🏆 | ADO.NET | PalORM | Dapper | FreeSql |
|---|---|---|---|---|---|---|
| GetByKey | 2000 | Dapper | 197.4 µs [1.00] / 3.0KB [1.00] | 198.2 µs [1.00] / 3.4KB [1.13] | 192.8 µs [0.98] / 3.2KB [1.09] | 263.5 µs [1.33] / 12.0KB [4.04] |
| GetAllAsync | 2000 | 🏆 PalORM | 665.7 µs [1.00] / 240KB [1.00] | 662.2 µs [0.99] / 225KB [0.94] | 725.4 µs [1.09] / 442KB [1.85] | 742.8 µs [1.12] / 244KB [1.02] |
| QueryAll | 2000 | ADO | 637.3 µs [1.00] / 239KB [1.00] | 664.5 µs [1.04] / 225KB [0.94] | 668.5 µs [1.05] / 442KB [1.85] | 735.0 µs [1.15] / 244KB [1.02] |
| Insert | 2000 | ADO | 1.36 ms [1.00] / 7.5KB [1.00] | 1.40 ms [1.03] / 7.5KB [1.01] | 1.41 ms [1.03] / 7.3KB [0.98] | 1.42 ms [1.04] / 11.0KB [1.47] |
| Update | 2000 | Dapper | 1.39 ms [1.00] / 7.3KB [1.00] | 1.40 ms [1.01] / 7.8KB [1.06] | 1.38 ms [1.00] / 7.2KB [0.97] | 1.43 ms [1.03] / 13.0KB [1.77] |
| GetByKey | 20000 | ADO | 199.5 µs [1.00] / 3.0KB [1.00] | 199.6 µs [1.00] / 3.4KB [1.13] | 200.6 µs [1.01] / 3.3KB [1.09] | 271.3 µs [1.36] / 12.1KB [4.06] |
| GetAllAsync | 20000 | ADO | 5.14 ms [1.00] / 2.5MB [1.00] | 5.15 ms [1.00] / 2.2MB [0.86] | 5.34 ms [1.04] / 4.5MB [1.79] | 5.81 ms [1.13] / 2.5MB [1.00] |
| QueryAll | 20000 | ADO | 5.12 ms [1.00] / 2.5MB [1.00] | 5.20 ms [1.02] / 2.2MB [0.86] | 5.37 ms [1.05] / 4.5MB [1.79] | 5.92 ms [1.16] / 2.5MB [1.00] |
| Insert | 20000 | 🏆 PalORM | 1.37 ms [1.00] / 7.5KB [1.00] | 1.36 ms [1.00] / 7.5KB [1.00] | 1.38 ms [1.01] / 7.1KB [0.96] | 1.44 ms [1.05] / 11.0KB [1.47] |
| Update | 20000 | Dapper | 1.44 ms [1.00] / 7.7KB [1.00] | 1.39 ms [0.96] / 7.8KB [1.01] | 1.39 ms [0.96] / 7.0KB [0.91] | 1.48 ms [1.03] / 13.1KB [1.70] |
表二:批量处理(三方言,两档)
胜场:PalORM 14 · ADO 10 · Dapper 0 · FreeSql 0
| 操作 | 测什么 | 总评 |
|---|---|---|
| BulkInsert | 批量插入(SQLite 行预编译 / PG COPY / MySQL BulkCopy) | PalORM 与 ADO 同形态贴平;Dapper/FreeSql 批量路径全方言偏慢,PG/MySQL 大档差距最大 |
| BulkUpdate | 批量更新(SQLite 逐条裹事务 / PG UPDATE FROM VALUES / MySQL CASE WHEN) | PalORM 三方言与 ADO 同形态贴平;Dapper/FreeSql 无集合式封装,明显偏慢 |
| BulkDelete | 批量删除(SQLite json_each 单参数 / PG = ANY 数组 / MySQL IN 分批) | PG 数组形态快于 ADO 的 IN 占位符;SQLite json_each 与 MySQL 分批接近地板 |
| UpsertBatch | 批量 UPSERT(ON CONFLICT / ON DUPLICATE KEY) | PG 数组形态快于 ADO 与其余臂;SQLite 四臂同形态贴平;MySQL Dapper/FreeSql 偏慢 |
SQLite
| 操作 | 档 | 🏆 | ADO.NET | PalORM | Dapper | FreeSql |
|---|---|---|---|---|---|---|
| BulkInsert | 2000 | ADO | 2.52 ms [1.00] / 1.7MB [1.00] | 2.54 ms [1.01] / 1.7MB [1.00] | 4.55 ms [1.81] / 4.3MB [2.48] | 10.61 ms [4.21] / 6.5MB [3.79] |
| BulkUpdate | 2000 | 🏆 PalORM | 2.31 ms [1.00] / 1.7MB [1.00] | 2.30 ms [1.00] / 1.8MB [1.04] | 8.69 ms [3.77] / 6.3MB [3.75] | 13.38 ms [5.80] / 7.0MB [4.15] |
| BulkDelete | 2000 | 🏆 PalORM | 803.4 µs [1.00] / 197KB [1.00] | 797.3 µs [0.99] / 198KB [1.01] | 21.65 ms [26.95] / 1.6MB [8.12] | 1.53 ms [1.91] / 568KB [2.88] |
| UpsertBatch | 2000 | ADO | 2.38 ms [1.00] / 1.7MB [1.00] | 2.41 ms [1.01] / 1.7MB [1.01] | 4.81 ms [2.02] / 4.2MB [2.50] | 3.59 ms [1.51] / 2.5MB [1.47] |
| BulkInsert | 20000 | ADO | 28.19 ms [1.00] / 17.2MB [1.00] | 29.04 ms [1.03] / 17.2MB [1.00] | 47.26 ms [1.68] / 42.5MB [2.48] | 112.42 ms [3.99] / 66.1MB [3.85] |
| BulkUpdate | 20000 | 🏆 PalORM | 25.82 ms [1.00] / 17.0MB [1.00] | 25.52 ms [0.99] / 17.6MB [1.04] | 90.97 ms [3.52] / 63.5MB [3.74] | 134.45 ms [5.21] / 73.4MB [4.32] |
| BulkDelete | 20000 | 🏆 PalORM | 8.44 ms [1.00] / 2.0MB [1.00] | 8.18 ms [0.97] / 2.0MB [1.00] | 218.58 ms [25.89] / 15.6MB [7.71] | 15.87 ms [1.88] / 5.9MB [2.94] |
| UpsertBatch | 20000 | ADO | 25.54 ms [1.00] / 17.0MB [1.00] | 26.19 ms [1.03] / 17.1MB [1.01] | 59.79 ms [2.34] / 42.4MB [2.50] | 40.46 ms [1.58] / 25.8MB [1.52] |
MySQL
| 操作 | 档 | 🏆 | ADO.NET | PalORM | Dapper | FreeSql |
|---|---|---|---|---|---|---|
| BulkInsert | 2000 | 🏆 PalORM | 11.60 ms [1.00] / 476KB [1.00] | 11.41 ms [0.98] / 480KB [1.01] | 24.56 ms [2.12] / 6.7MB [14.30] | 17.33 ms [1.49] / 3.2MB [6.95] |
| BulkUpdate | 2000 | 🏆 PalORM | 10.87 ms [1.00] / 4.6MB [1.00] | 9.68 ms [0.89] / 3.7MB [0.80] | 11.05 ms [1.02] / 6.5MB [1.41] | 9.83 ms [0.90] / 2.1MB [0.45] |
| BulkDelete | 2000 | ADO | 12.07 ms [1.00] / 882KB [1.00] | 12.10 ms [1.00] / 808KB [0.92] | 12.10 ms [1.00] / 1.3MB [1.56] | 17.46 ms [1.45] / 608KB [0.69] |
| UpsertBatch | 2000 | 🏆 PalORM | 8.28 ms [1.00] / 2.9MB [1.00] | 8.03 ms [0.97] / 2.5MB [0.85] | 10.62 ms [1.28] / 6.6MB [2.28] | 9.06 ms [1.09] / 2.5MB [0.86] |
| BulkInsert | 20000 | ADO | 58.38 ms [1.00] / 4.4MB [1.00] | 59.30 ms [1.02] / 4.4MB [1.00] | 257.38 ms [4.41] / 66.5MB [14.95] | 116.99 ms [2.00] / 33.2MB [7.47] |
| BulkUpdate | 20000 | 🏆 PalORM | 88.98 ms [1.00] / 14.6MB [1.00] | 88.76 ms [1.00] / 13.7MB [0.94] | 139.83 ms [1.57] / 64.5MB [4.42] | 92.33 ms [1.04] / 21.8MB [1.49] |
| BulkDelete | 20000 | 🏆 PalORM | 75.24 ms [1.00] / 8.4MB [1.00] | 70.44 ms [0.94] / 3.5MB [0.42] | 76.69 ms [1.02] / 13.2MB [1.57] | 182.86 ms [2.43] / 6.2MB [0.74] |
| UpsertBatch | 20000 | ADO | 75.36 ms [1.00] / 12.9MB [1.00] | 79.85 ms [1.06] / 12.6MB [0.98] | 131.34 ms [1.74] / 66.1MB [5.15] | 124.68 ms [1.65] / 25.8MB [2.01] |
PostgreSQL
| 操作 | 档 | 🏆 | ADO.NET | PalORM | Dapper | FreeSql |
|---|---|---|---|---|---|---|
| BulkInsert | 2000 | ADO | 4.72 ms [1.00] / 341KB [1.00] | 4.90 ms [1.04] / 343KB [1.01] | 16.82 ms [3.56] / 6.7MB [20.10] | 12.95 ms [2.74] / 3.8MB [11.56] |
| BulkUpdate | 2000 | ADO | 5.28 ms [1.00] / 418KB [1.00] | 5.45 ms [1.03] / 422KB [1.01] | 19.04 ms [3.61] / 6.5MB [15.99] | 14.07 ms [2.67] / 2.7MB [6.64] |
| 📌BulkDelete | 2000 | 🏆 PalORM | 3.49 ms [1.00] / 784KB [1.00] | 2.27 ms [0.65] / 89.2KB [0.11] | 3.97 ms [1.14] / 1.3MB [1.71] | 5.88 ms [1.68] / 571KB [0.73] |
| 📌UpsertBatch | 2000 | 🏆 PalORM | 14.90 ms [1.00] / 2.7MB [1.00] | 7.53 ms [0.51] / 445KB [0.16] | 20.29 ms [1.36] / 6.7MB [2.47] | 16.17 ms [1.09] / 2.7MB [1.01] |
| BulkInsert | 20000 | ADO | 19.89 ms [1.00] / 3.3MB [1.00] | 20.29 ms [1.02] / 3.3MB [1.00] | 197.63 ms [9.94] / 66.9MB [20.55] | 110.04 ms [5.53] / 39.6MB [12.15] |
| BulkUpdate | 20000 | 🏆 PalORM | 68.68 ms [1.00] / 4.1MB [1.00] | 61.05 ms [0.89] / 4.1MB [0.99] | 224.10 ms [3.26] / 65.4MB [16.00] | 124.58 ms [1.81] / 28.3MB [6.92] |
| 📌BulkDelete | 20000 | 🏆 PalORM | 25.03 ms [1.00] / 7.6MB [1.00] | 10.99 ms [0.44] / 815KB [0.10] | 27.89 ms [1.11] / 13.0MB [1.71] | 59.11 ms [2.36] / 6.0MB [0.78] |
| 📌UpsertBatch | 20000 | 🏆 PalORM | 137.91 ms [1.00] / 11.6MB [1.00] | 83.33 ms [0.60] / 4.3MB [0.37] | 219.95 ms [1.59] / 66.9MB [5.79] | 151.51 ms [1.10] / 28.3MB [2.45] |
表三:事务(三方言,仅最小档)
胜场:ADO 8 · PalORM 6 · Dapper 1 · FreeSql 0
| 操作 | 测什么 | 总评 |
|---|---|---|
| TxSingleInsert | 事务内单行插入 | 四臂持平(事务开销主导) |
| TxHundredInserts | 事务内 100 行逐条插入 | 联网方言接近;FreeSql SQLite 明显偏慢(逐行表达式开销放大) |
| TxBulkInsert | 事务内批量插入 | PalORM 贴地板;Dapper(multi-exec 无语句缓存复用)与 FreeSql 明显偏慢 |
| TxRollback | 事务回滚(更新后显式 Rollback) | 联网方言接近;SQLite FreeSql 明显偏慢 |
| EnumInserts | 枚举列事务内逐条插入(Status TEXT 列) | PalORM 贴平(SQLite 略高为每调用会话契约成本,已探针定案);FreeSql 全方言明显偏慢,Dapper SQLite 偏慢 |
SQLite
| 操作 | 档 | 🏆 | ADO.NET | PalORM | Dapper | FreeSql |
|---|---|---|---|---|---|---|
| TxSingleInsert | 2000 | ADO | 18.9 µs [1.00] / 3.9KB [1.00] | 19.6 µs [1.03] / 4.3KB [1.09] | 19.6 µs [1.04] / 4.7KB [1.20] | 32.4 µs [1.71] / 7.0KB [1.78] |
| TxHundredInserts | 2000 | 🏆 PalORM | 185.7 µs [1.00] / 137KB [1.00] | 185.4 µs [1.00] / 92.7KB [0.68] | 254.3 µs [1.37] / 220KB [1.61] | 1.51 ms [8.11] / 541KB [3.96] |
| TxBulkInsert | 2000 | ADO | 2.54 ms [1.00] / 1.7MB [1.00] | 2.56 ms [1.01] / 1.7MB [1.00] | 4.50 ms [1.77] / 4.3MB [2.48] | 7.51 ms [2.96] / 3.5MB [2.02] |
| TxRollback | 2000 | ADO | 569.8 µs [1.00] / 435KB [1.00] | 631.5 µs [1.11] / 437KB [1.01] | 2.20 ms [3.86] / 1.6MB [3.74] | 7.55 ms [13.26] / 3.5MB [8.16] |
| EnumInserts | 2000 | ADO | 129.9 µs [1.00] / 59.0KB [1.00] | 149.2 µs [1.15] / 59.1KB [1.00] | 365.1 µs [2.81] / 223KB [3.77] | 1.25 ms [9.58] / 497KB [8.42] |
MySQL
| 操作 | 档 | 🏆 | ADO.NET | PalORM | Dapper | FreeSql |
|---|---|---|---|---|---|---|
| TxSingleInsert | 2000 | Dapper | 4.48 ms [1.00] / 21.1KB [1.00] | 4.43 ms [0.99] / 22.2KB [1.05] | 4.42 ms [0.99] / 25.6KB [1.21] | 4.54 ms [1.01] / 26.9KB [1.27] |
| TxHundredInserts | 2000 | 🏆 PalORM | 25.70 ms [1.00] / 368KB [1.00] | 24.46 ms [0.95] / 301KB [0.82] | 25.16 ms [0.98] / 348KB [0.94] | 27.74 ms [1.08] / 811KB [2.20] |
| TxBulkInsert | 2000 | 🏆 PalORM | 11.64 ms [1.00] / 485KB [1.00] | 11.56 ms [0.99] / 490KB [1.01] | 27.22 ms [2.34] / 6.7MB [14.07] | 20.97 ms [1.80] / 3.4MB [7.22] |
| TxRollback | 2000 | ADO | 93.37 ms [1.00] / 1.3MB [1.00] | 93.93 ms [1.01] / 1.4MB [1.02] | 99.58 ms [1.07] / 2.4MB [1.78] | 114.81 ms [1.23] / 4.9MB [3.62] |
| EnumInserts | 2000 | 🏆 PalORM | 24.93 ms [1.00] / 286KB [1.00] | 24.48 ms [0.98] / 285KB [1.00] | 25.80 ms [1.03] / 434KB [1.52] | 29.01 ms [1.16] / 772KB [2.70] |
PostgreSQL
| 操作 | 档 | 🏆 | ADO.NET | PalORM | Dapper | FreeSql |
|---|---|---|---|---|---|---|
| TxSingleInsert | 2000 | ADO | 1.52 ms [1.00] / 8.4KB [1.00] | 1.53 ms [1.01] / 9.3KB [1.11] | 1.56 ms [1.03] / 8.2KB [0.98] | 1.60 ms [1.06] / 12.3KB [1.47] |
| TxHundredInserts | 2000 | 🏆 PalORM | 20.60 ms [1.00] / 314KB [1.00] | 20.46 ms [0.99] / 212KB [0.67] | 20.91 ms [1.01] / 283KB [0.90] | 23.98 ms [1.16] / 703KB [2.24] |
| TxBulkInsert | 2000 | 🏆 PalORM | 4.77 ms [1.00] / 340KB [1.00] | 4.75 ms [1.00] / 341KB [1.00] | 17.79 ms [3.73] / 6.7MB [20.13] | 21.38 ms [4.48] / 3.4MB [10.24] |
| TxRollback | 2000 | ADO | 102.80 ms [1.00] / 934KB [1.00] | 102.91 ms [1.00] / 1.0MB [1.11] | 105.76 ms [1.03] / 1.7MB [1.88] | 122.72 ms [1.19] / 4.4MB [4.85] |
| EnumInserts | 2000 | ADO | 19.86 ms [1.00] / 163KB [1.00] | 20.26 ms [1.02] / 184KB [1.13] | 20.51 ms [1.03] / 280KB [1.72] | 23.45 ms [1.18] / 660KB [4.05] |
表四:跨方言 PalORM/ADO 比值(批量族 + GetAllAsync;裸比值,越线 📌)
| 操作 | 档 | SQLite | PostgreSQL | MySQL |
|---|---|---|---|---|
| BulkInsert | 2000 | 1.01 | 1.04 | 0.98 |
| BulkInsert | 20000 | 1.03 | 1.02 | 1.02 |
| BulkUpdate | 2000 | 1.00 | 1.03 | 0.89 |
| BulkUpdate | 20000 | 0.99 | 0.89 | 1.00 |
| BulkDelete | 2000 | 0.99 | 📌0.65 | 1.00 |
| BulkDelete | 20000 | 0.97 | 📌0.44 | 0.94 |
| UpsertBatch | 2000 | 1.01 | 📌0.51 | 0.97 |
| UpsertBatch | 20000 | 1.03 | 📌0.60 | 1.06 |
| GetAllAsync | 2000 | 0.99 | 0.99 | 1.00 |
| GetAllAsync | 20000 | 1.00 | 1.00 | 0.99 |
📦 安装
三个 Provider 包任选其一,每个都自动携带
PalORM.Core(运行时)与PalORM.SourceGen(编译时源生成器)。 环境要求:.NET SDK11.0.100-rc.2或更高(global.json已锁定rollForward: disable);IDE 需支持 Roslyn 源生成器(SourceGen 以 Microsoft.CodeAnalysis 5.9.0 构建)。Native AOT 发布另需约 4 GB 内存(ILC 编译器),详见开发。
<PackageReference Include="PalORM.PostgreSql" Version="6.8.0" />
<PackageReference Include="PalORM.MySql" Version="6.8.0" />
<PackageReference Include="PalORM.Sqlite" Version="6.8.0" />
每个 Provider 包含 PalORM.Core(运行时)和 PalORM.SourceGen(编译时源生成器)。安装后用下方快速开始的最小示例验证:能创建会话并完成一次插入即安装成功。
数据库兼容性
| 数据库 | 版本 | 驱动 | 加密 |
|---|---|---|---|
| PostgreSQL | 14+(推荐 18) | Npgsql 10.0.3 | SSL/TLS |
| MySQL | 8.0+(推荐 8.4 LTS) | MySqlConnector 2.6.2 | SSL/TLS |
| SQLite | 3.54+(via SQLite3MC 2.6.1) | Microsoft.Data.Sqlite.Core 11.0.0-rc.2 | AES-256(驱动层,Password=) |
🚀 快速开始
五步跑通:定义实体 → 创建会话 → 建表种子 → CRUD → 批量操作。所有片段可直接组合成最小可运行程序。
定义实体
using PalORM;
[Table("users")]
public partial class User
{
[Key] public long Id { get; set; }
[Column("email")] public string Email { get; set; } = "";
[Column("created_at")] public DateTime CreatedAt { get; set; }
[Column("metadata")]
[OwnedJson(typeof(UserJsonContext))]
public UserMetadata? Metadata { get; set; }
}
可空约定:引用类型属性声明为非可空(如
string Email)而库里是 NULL 时,读取抛SqlNullValueException(响亮失败,不返回 null、不出静默错数据)。可能存 NULL 的列请声明为可空(string?)。替代方案评估留档于docs/性能优化方案-step7-pg-master.mdT13。
创建会话
using var db = await DataSession<PostgreSqlProvider>.CreateAsync(new DbOptions
{
ConnectionString = "Host=localhost;Username=user;Password=xxx;Database=mydb"
});
可选:启动期预热连接池(v5.6.0),让首批突发查询命中暖连接而非各付一次建连(远程建连实测约 8.5 ms/条):
// 打开 N 条连接随即归还池;SQLite 无池直接返回。建议配合 MinPoolSize 保持暖态
await DataSession<PostgreSqlProvider>.PreWarmAsync(options, count: 10);
会话复用优先:同一作用域(请求、批处理循环)内请复用一个 db 会话跑多次操作,而不是每操作新建。会话内同形状操作自动晋升命令复用槽(第 3 次起复用命令对象与参数槽),且避免每次重新 prepare——Microsoft.Data.Sqlite 的预备语句缓存按命令实例生效(官方源码确认:换命令即重新 sqlite3_prepare_v2 并额外分配一段 SQL 字节缓冲)。复用形态下每操作省下会话构造(约 632 B,2026-10-14 W3 后探针)与重复准备成本;单次即弃的写法(每操作 CreateAsync)适合低频调用,高频热路径建议复用。
建表与种子
// 执行编译期生成的三方言建表 DDL(幂等:已存在对象自动跳过;FK 按依赖拓扑序建表)
await db.MigrateAsync();
// 种子数据:稳定主键事务内幂等 Upsert,重复执行不重复插入
await db.SeedAsync(new[] { new User { Id = 1, Email = "admin@example.com", CreatedAt = DateTime.UtcNow } });
// 对照实体声明检查数据库缺列(返回缺失列清单,升级/对账用)
List<string> missing = await db.ValidateSchemaAsync<User>();
CRUD
// 插入(PG/SQLite RETURNING / MySQL LAST_INSERT_ID 自增回填)
var user = await db.InsertAsync(new User { Email = "alice@example.com", CreatedAt = DateTime.UtcNow });
// 查询
var alice = await db.GetAsync<User>(user.Id); // 单键直查:首选专用 API
var all = await db.From<User>().Where($"email LIKE {"%@example.com%"}").ToListAsync();
// 更新(乐观锁实体自动校验版本,冲突抛 ConcurrencyConflictException)
alice.Email = "new@example.com";
await db.UpdateAsync(alice);
// 插入或更新(默认主键走 Insert,非默认主键单行 UPSERT)
await db.SaveAsync(alice);
// 删除(软删实体自动转 UPDATE deleted_at,否则物理 DELETE)
await db.DeleteAsync<User>(alice.Id);
单键直查用 GetAsync,不要用链式等价写法:GetAsync 为单键设计(SQL 常量缓存、单行直读、不经查询构建器与列表物化),同口径实测比 From<T>().Where(Id==x).FirstOrDefaultAsync() 省约 880 B/操作且快 25~30%(2026-10-01 探针,3000 次摊销)。带过滤条件的查询才用 From<T>() 链式。
批量操作
// 批量插入(PG: Binary COPY / MySQL: BulkCopy 或多值 INSERT / SQLite: 行预编译批量)
await db.BulkInsertAsync(users);
// 批量更新(乐观锁实体:PG/MySQL 自动走版本化单语句,能力不全回退 DbBatch)
await db.BulkUpdateAsync(users);
// 批量更新(v5.0 单语句批量)
await db.BulkUpdateBatchAsync(users);
// 批量删除(PG: = ANY(数组) 单语句 / SQLite: json_each 单参数服务端展开 / MySQL: IN 分批单语句,满批命令与参数池化复用)
await db.BulkDeleteAsync<User>(keyList);
// 批量 UPSERT(多行 UPSERT 单语句;默认键新行走逐条 INSERT 回填自增 ID)
await db.BulkMergeAsync(users);
各方法按方言的 SQL 策略:
| 方法 | PG | MySQL | SQLite |
|---|---|---|---|
BulkInsertAsync |
Binary COPY | BulkCopy(local_infile)或多值 INSERT | 行预编译批量 |
BulkUpdateAsync |
版本化单语句 + 乐观锁(能力不全回退 DbBatch) | 版本化单语句 + 乐观锁(能力不全回退 DbBatch) | 逐条 + 乐观锁 |
BulkUpdateBatchAsync |
FROM VALUES |
UPDATE JOIN VALUES ROW(8.0.19+,低版本回退 CASE WHEN) |
自动回退逐条 |
BulkDeleteAsync |
= ANY(数组) 单语句 |
IN 分批单语句 |
json_each 单参数服务端展开 |
BulkMergeAsync |
多行 UPSERT(ON CONFLICT DO UPDATE) |
多行 UPSERT(ON DUPLICATE KEY UPDATE) |
行预编译 UPSERT(ON CONFLICT DO UPDATE,单行语句缓存复用) |
BulkMergeAsync(v5.6.0 集合化)按键状态分区:默认键(新行)走逐条 INSERT 保留 ID 回填契约,非默认键行 PG/MySQL 按方言参数上限分批走多行 UPSERT(PG 与 MySQL 65535 参数上限,MySQL ODKU 单批 1000 行),SQLite 走行预编译 UPSERT(单行语句缓存复用,免参数上限分批);[ConcurrencyCheck] 实体维持逐条(UPSERT 无法尊重乐观锁)。
📖 使用
查询与分页、聚合与流式、事务与弹性、横切关注点、原生 SQL 与存储过程、Scaffold 工具、批量选型与形态选型。
查询与分页
From<T>() 返回 struct QueryBuilder<T>,链式 .Where() / .OrderBy() / .Take() / .Skip() / .Select() / .GroupBy() / .Having() / .Include() / .ThenInclude()。支持 InnerJoin / LeftJoin / RightJoin、WhereIn / WhereNotIn(自动分批)、CTE(.With())、窗口函数、悲观锁(ForUpdate / ForShare)、SQL 预览(AsDryRun)、查询缓存(WithCache)、可观测性(WithMetrics / WithTracing,OTel 指标与 tracing)。
同会话并发执行多个查询前先声明并行读租约:
await using (db.ForParallelReads())
{
await Task.WhenAll(db.From<User>().ToListAsync(), db.From<Order>().ToListAsync());
}
并行读作用域:
From<T>()查询族与GetAsync单行直查族在ForParallelReads()内都走作用域局部读连接池(各自独立连接,上限 8,查询结束归还);与在飞只读操作并发的写/事务/Bulk 会被单活动门禁拒绝。流式消费大结果集用QueryAsyncEnumerable<T>(await foreach),避免全量物化的内存峰值;Include只生成 JOIN 不装配导航,需按父分组时自行配对或直接流式处理。
内存与 GC 选型
- 结果集形态:
ToListAsync全量物化(List<T>初始容量按会话内上次行数自适应,重复查询同表零扩容拷贝);行数不可控的大表用QueryAsyncEnumerable<T>流式(每行即时消费,峰值内存 O(1))。 - 查询缓存:
WithCache按查询 opt-in(默认关);未注入DbOptions.QueryCache时使用进程级共享默认实例(1024 条,多租户靠__t:{tenantIdLength}:{tenantId}:前缀隔离,IgnoreFilters 走独立__all__:),需要按租户控制容量/TTL 时注入独立实例;TTL 最终一致,写后读一致用db.EvictQueryCache()显式收窄。 - GC 模式:高吞吐服务端(多核 + 大堆)建议
<ServerGarbageCollection>true</ServerGarbageCollection>(Gen0 分配缓冲更大、GC 频率降);容器配GarbageCollectionHeapHardLimit(如 75c00000=2GB)防OOM;客户端/小内存场景保持 Workstation 默认。基准口径为 Workstation(docs/性能基准规范.md§4.1),Server GC 下分配数字不变、GC 频率与暂停分布不同。
Keyset 游标分页
大偏移量场景比 OFFSET 稳定,返回行列表与总数:
// 第一页:按 CreatedAt 降序取 20 行
var (rows, total) = await db.From<Order>().ToPageAsync(20, o => o.CreatedAt);
// 下一页:上一页末行的排序值作游标
var next = await db.From<Order>().ToPageAsync(20, o => o.CreatedAt, lastValue: rows[^1].CreatedAt);
聚合五函数
软删除与租户过滤自动生效:
long count = await db.CountAsync<Order>($"status = {OrderStatus.Paid}");
decimal sum = await db.SumAsync<Order>(o => o.Amount);
double avg = await db.AvgAsync<Order>(o => o.Amount);
DateTime? latest = await db.MaxAsync<Order, DateTime>(o => o.CreatedAt);
流式消费大结果集
两种形态都不物化整表 List<T>,峰值内存 O(1):
// 回调式:逐行消费,不建列表
await db.From<Order>().ForEachAsync(order => sink.Write(order));
// 异步枚举式:await foreach 消费原生 SQL 结果
await foreach (var order in db.QueryAsyncEnumerable<Order>($"SELECT * FROM orders WHERE status = {status}"))
sink.Write(order);
多结果集(GridReader):
await using var grid = await db.From<User>()
.QueryMultipleAsync($"SELECT * FROM users WHERE id = {userId}; SELECT * FROM orders WHERE user_id = {userId}");
var user = await grid.ReadFirstAsync<User>();
var orders = await grid.ReadAsync<Order>().ToListAsync();
事务与弹性
函数式事务 WithTransaction(callback) 自动 commit/rollback,支持保存点:
await db.WithTransaction(async ct =>
{
await db.InsertAsync(order, ct);
await db.BulkInsertAsync(order.Items, ct);
await db.ExecuteAsync($"UPDATE inventory SET stock = stock - {order.Items.Count} WHERE product_id = {productId}", ct);
});
事务级瞬时错误重试
(整事务按方言错误码重放,at-least-once 契约:PG 40001/40P01、MySQL 1213/1205、SQLite BUSY/LOCKED);保存点与外部事务接入:
// 事务重试:死锁/锁超时整事务重放(回调须幂等)
await db.WithTransactionRetry(async ct => await db.BulkInsertAsync(items, ct: ct));
// 保存点:长事务内部分回滚
await using var tran = await db.BeginTransactionAsync();
await db.InsertAsync(order);
await db.SavepointAsync(tran, "sp1");
await db.ExecuteAsync($"UPDATE ..."); // 失败则只回滚到 sp1
await db.RollbackToAsync(tran, "sp1");
await tran.CommitAsync();
// 外部事务接入(外部 Dispose 后本会话续用会响亮失败,不静默自动提交)
db.UseTransaction(externalTran);
// 隔离级别:会话级设定,后续 BeginTransactionAsync 默认取用
db.WithIsolationLevel(IsolationLevel.ReadCommitted);
await using var tran = await db.BeginTransactionAsync();
弹性策略(WithRetry 指数退避 + WithCircuitBreaker 熔断)自 v5.4 起自动覆盖只读查询管线:
// Production 预设已含弹性(MaxRetries=5 / 熔断阈值 10 / 半开 60s / 作用域 Process)
await using var db = await DataSession<PostgreSqlProvider>.CreateAsync(
DbOptions.Production(connectionString));
// 需要自定义阈值时在会话上链式覆盖(方法在 DataSession 上,返回同一会话)
db.WithRetry(maxRetries: 3)
.WithCircuitBreaker(failureThreshold: 5, resetAfter: TimeSpan.FromSeconds(30));
// SELECT 瞬时故障(死锁/超时/连接闪断)自动重试;非幂等写入不自动重试,显式声明弹性意图:
long affected = await db.ExecuteWithResilience(
token => db.From<Order>()
.Set(o => o.Status, OrderStatus.Paid)
.Where($"id = {orderId}")
.ExecuteNonQueryAsync(token), ct);
| 路径 | 弹性覆盖 | 说明 |
|---|---|---|
From<T>() SELECT 族、GetAsync/GetAllAsync、聚合 |
自动 | 瞬时故障按配置重试并计入熔断 |
| 连接建立 | 自动 | CreateAsync 自有重试循环 |
| 写入路径(Insert/Update/Delete/Save/Bulk/存储过程) | 直连 | 非幂等写自动重试有重复执行风险;显式需求用 ExecuteWithResilience |
事务内查询 / ToPageAsync / 原始 SQL 家族 |
直连 | 事务内重试以次生异常掩盖根因 |
只读查询走弹性策略的每查询常数开销约 272 B 分配,与结果行数无关(单行 +8%,千行 +0.2%);MaxRetries=0 + CircuitBreakerThreshold=0 可完全直通,代价是失去带 PalORM.InfrastructureTimeout 标记的超时异常。
PG 事务级咨询锁(TryAcquireXactLockAsync 为非阻塞变体;必须在事务内调用,事务外获取会立即释放并显式报错):
await db.WithTransaction(async ct =>
{
await db.AcquireXactLockAsync(resourceKey, ct);
// 临界区操作...
});
// 事务结束自动释放锁
横切关注点
// 审计拦截器(v5.0):logParameters:true 时 [SensitiveData] 列参数值自动掩码
var db = await DataSession<PostgreSqlProvider>.CreateAsync(new DbOptions
{
ConnectionString = connectionString,
Interceptors = [new AuditInterceptor(loggerFactory.CreateLogger("Audit"))],
SessionSetupSql = "SET TIME ZONE 'UTC'; SET search_path TO 'app, public'"
});
⚠️ AuditInterceptor 覆盖面:实体 SELECT 管线、QueryBuilder UPDATE 与 ExecuteAsync(v5.6.0 接入);InsertAsync/DeleteAsync/SaveAsync/Bulk 家族/存储过程/迁移不产生审计记录,完整写入审计请用数据库层审计或 OpenTelemetry。
其余横切能力:[SoftDelete] 自动 WHERE 过滤、[TenantAware] + WithTenant(id) 单库列隔离(缓存 key 自动加 __t:{tenantIdLength}:{tenantId}: 前缀,IgnoreFilters() 走独立 __all__: 命名空间)、[ConcurrencyCheck] 乐观锁、ForRead 读写分离。全部注解(23 个)与执行方法明细见 docs/API参考.md。
原生 SQL 与 SQL 文件
var count = await db.ScalarAsync<long>($"SELECT COUNT(*) FROM users WHERE email LIKE {"%@example.com%"}");
SQL 文件编译时嵌入、校验存在性并按需提取方言段:
public static partial class Reports
{
[SqlFile("Reports/MonthlySales.sql")]
public static partial string MonthlySales();
// Provider = "pg" 只提取文件内 -- @pg 方言段
[SqlFile("Reports/Sales.sql", Provider = "pg")]
public static partial string SalesPg();
}
// MonthlySales() 返回编译期嵌入的 SQL 文本(生成器校验文件存在),交由 QueryAsync / ScalarAsync 等执行
[SqlTemplate] 把方法体插值串提升为模板常量(方法须无参非泛型,插值洞只引用静态可解析符号),重复执行的 SQL 免每次字符串构造,配合查询构建器 .AsPrepared() 走 DbCommand.PrepareAsync 预编译:
public static class UserQueries
{
[SqlTemplate("ActiveUsers")]
public static FormattableString ActiveUsers()
=> $"SELECT * FROM users WHERE active = 1 AND deleted_at IS NULL";
}
// 模板方法返回 FormattableString,直接交给原生 SQL 族执行
var users = await db.QueryAsync<User>(UserQueries.ActiveUsers());
PG 专有:WhereJson(JSONB 路径查询)、PgNotificationListener(NOTIFY/LISTEN 异步监听,自动重连)、NotifyAsync(参数化 pg_notify() 发送)。
存储过程与健康检查
// 存储过程:强类型输入/输出参数,过程名白名单校验,single-use 契约
var sp = db.StoredProc("sp_transfer_stock")
.WithParam("from_sku", fromSku)
.WithParam("qty", qty)
.WithOutputParam<int>("rows_affected");
int affected = await sp.ExecuteAsync(ct); // 返回受影响行数
int? moved = sp.GetOutputValue<int>("rows_affected"); // 输出参数强类型取回
// 存储过程结果集直接物化
List<StockRow> rows = await sp.QueryAsync<StockRow>(ct);
// 健康检查:SELECT 1 探活(错误消息只透出异常类型名,防数据库拓扑泄露)
HealthResult health = await db.HealthCheckAsync();
// 唯一键冲突跨方言统一翻译(不用记各驱动错误码)
try { await db.InsertAsync(user); }
catch (UniqueConstraintViolationException) { /* 邮箱已注册 */ }
// 乐观锁冲突([ConcurrencyCheck] 实体 UpdateAsync 版本不匹配时)
try { await db.UpdateAsync(order); }
catch (ConcurrencyConflictException) { /* 重新读取合并 */ }
需要驱动原生能力时 db.GetRawConnection() 返回底层 DbConnection 逃生舱。
Scaffold 工具
从既有数据库反向生成实体:
dotnet run --project tools/PalORM.Scaffold -- <connection-string> --dialect sqlite|pg|mysql [--namespace NS] [--output DIR]
三 Provider schema → C# 实体,40+ 类型映射(uuid → Guid、jsonb → string、bytea → byte[]、date → DateOnly、time → TimeOnly)。
批量 API 选型速查
| 场景 | 用 | 不用 | 理由 |
|---|---|---|---|
| 批量插入,不需要 ID 回填 | BulkInsertAsync |
逐条 / SessionBatch |
方言最快路径(COPY / LOAD DATA / 行预编译),实测事务内 100 行比逐条快 8.4× |
| 批量插入且需逐行回填自增 ID | BulkMergeAsync(默认键走逐条回填)或 SessionBatch |
BulkInsertAsync |
COPY / LOAD DATA 不回填 ID |
| 混合语句或需逐条语义控制 | SessionBatch(DbBatch 打包) |
逐条往返 | 单往返打包多语句,SQLite 20 条/批 |
| 按非主键列批量更新(每行不同值) | BulkUpdateBatchAsync |
逐条 | 单语句多行 SET(PG FROM VALUES / MySQL JOIN VALUES ROW) |
| 乐观锁实体批量更新 | BulkUpdateAsync |
BulkUpdateBatchAsync |
版本化单语句(PG UNNEST / MySQL JOIN VALUES ROW)逐行校验版本;能力不全回退 DbBatch |
| 插入或更新(键存在则更新) | BulkMergeAsync |
手写 ON CONFLICT | 租户实体自动限定本租户(跨租户命中 0 行) |
| 大键集删除(万级以上) | BulkDeleteAsync |
逐条 Delete | SQLite json_each 单参数免参数上限分批 / PG = ANY 数组 / MySQL IN 按上限分批,整批单事务,参数对象批间复用 |
按形态选 API:会话池与批量路径
同一 API 在不同使用形态下的表观开销构成不同,按形态选对入口的收益大于任何库内微优化。
| 场景 | 入口 | 关键收益 |
|---|---|---|
| 一请求一会话(Web/API) | DataSessionPool.RentAsync |
复用槽跨租期激活;租还约 1 µs |
| 高吞吐逐行写 | BulkInsertAsync / BulkUpdateBatchAsync |
会话/命令成本摊到 1/N |
| 大结果集 / 无界流 | QueryAsyncEnumerable<T> |
恒定内存,每行分配更低 |
| 单键查询 | GetAsync |
直查槽 + 单行物化 |
| 分页 | ToPageAsync |
单往返分页(PG COUNT+页 SELECT 打包) |
下面按场景展开;批量选型见上方“批量 API 选型速查”。
一请求一会话(Web/API 服务)用会话池(v6.5 DataSessionPool,API 细节见API 参考 · 会话池):
// 启动时创建一次(maxIdleSize 按峰值并发调整)
var pool = DataSessionPool<PostgreSqlProvider>.Create(options, maxIdleSize: 16);
// 每请求:租出即用,DisposeAsync 即归还
await using var db = await pool.RentAsync();
var user = await db.GetAsync<User>(id);
归还时瞬态状态(租户/超时/重试等 With* 覆写、拦截器加项)全部复位(租期加项一并释放;池基准拦截器由调用方持有);租出时对脏会话复位连接级会话态(PG RESET ALL; RESET ROLE / MySQL COM_RESET_CONNECTION)并重放 SessionSetupSql;空闲超过 PoolConnectionValidationInterval(默认 30s)的会话先 SELECT 1 探活、失败换新,命令复用槽、读连接、物化容量跨租期保留——复用槽在"一请求一会话"形态下不经池化永不激活(同形状第 3 次操作才晋升)。32 并发压测实测:P99 −18%、Gen0 回收 −58%、每操作分配 −2576 B;池化形态单键直查 P/ADO 比值 MySQL 0.99 / PG 0.87(台账批),租还自身约 1 µs。
DI 容器场景把 IDataSessionFactory<TProvider> 注册为单例即可,不依赖额外容器包:
var factory = new DataSessionFactory<PostgreSqlProvider>(options, maxIdleSize: 16);
services.AddSingleton<IDataSessionFactory<PostgreSqlProvider>>(factory);
// 每请求(scoped 服务的构造/方法内)
await using var db = await factory.RentAsync(ct);
高吞吐逐行写换批量 API,不要在逐条循环里找损耗。逐条 UpdateAsync 每行各付一次会话门禁与命令往返;合成单语句多行(BulkUpdateBatchAsync)或走批量路径(见上方速查表)后,这部分成本摊薄到 1/N。逐条循环里的每行开销是形态决定的,不是可挤的库内水分。
长会话顺序循环的对比数字要读连接态。基准中"每操作新建会话"与"长会话复用"的表观差距主要来自连接/命令冷态与驱动侧成本:库侧单键执行成本已与 ADO 地板重合(见性能),评估自身业务收益时以同形态对照为准。
✅ 最佳实践速查
按使用频率收拢,详细依据见各链接位置:
会话与连接
- 同一作用域复用一个
DataSession,不要每操作CreateAsync(会话内同形状操作第 3 次起自动复用命令与参数槽) - Web/API"一请求一会话"部署用
DataSessionPool(启动建池、RentAsync租用、DisposeAsync归还):复用槽跨租期保留是池化的核心收益,不池化则该形态下永不激活 - 启动期预热连接池
PreWarmAsync(远程建连实测约 8.5 ms/条),配合MinPoolSize保持暖态 - 高频单键查询用
GetAsync(比链式等价写法快 25~30%、省 880 B);带过滤条件才用From<T>() - 只读副本配置
ReadConnectionString后,报表/列表查询显式.ForRead(),主从延迟敏感的强读留在主库 - 并发查询先声明
await using (db.ForParallelReads());作用域内GetAsync与查询族走作用域局部读连接池(上限 8);与在飞读并发的写操作会被门禁拒绝 - 跨程序集模型报 "not registered":各模型程序集经
ModuleInitializer在首次被触达时注册,确保实体类型被真实引用(类型实例化/成员调用均算触达)
查询与结果集
- 行数不可控的大表用
QueryAsyncEnumerable<T>流式消费(峰值内存 O(1)),不要ToListAsync全量物化 - 深翻页用
ToPageAsync游标(Keyset),不要大 OFFSET(扫描量随偏移线性增长) - 大键集过滤用
WhereIn/WhereNotIn(自动按 500 键分批、空集安全恒假;总绑定参数仍受方言上限约束,SQLite 999 / 通用 65535,超限会提前抛ArgumentException,更大的键集用临时表 JOIN) - 热路径的
OrderBy/Select表达式提到静态字段(每棵树 512 B + 0.5~1.6 µs 的调用点成本可消除) - 排查 SQL 用
AsDryRun()/ToSql预览(SQL + 参数,不执行);生产定位慢查询可开 AutoTagging(PalORMAutoTagging编译开关,SQL 自动注入「文件:行号 成员」注释) Include/ThenInclude只生成 JOIN 不装配导航(按父分组自行配对);列裁剪物化用[Projection]DTO +QueryAsync<T>(大结果集用 struct 投影 +QueryStructsAsync<T>;Select投影仅 DryRun/ToSql 支持)WithCache默认关;启用后写后读一致用db.EvictQueryCache()显式收窄
写入与批量
- 批量 API 按上方选型速查表选择;不要在循环里逐条
InsertAsync(连接往返是数量级差异) - 唯一键冲突捕获
UniqueConstraintViolationException(跨方言统一翻译,不用记驱动错误码);乐观锁冲突捕获ConcurrencyConflictException(重读数据合并后重试) [ConcurrencyCheck]实体不要调BulkUpdateBatchAsync(显式单语句 API 不承载逐行版本谓词,PALORM031 编译期已拦),批量更新走BulkUpdateAsync(自动路由到版本化单语句,能力不全回退 DbBatch)BulkInsertAsync不回填自增 ID(COPY / LOAD DATA 契约);需逐行回填用BulkMergeAsync(默认键走逐条)或SessionBatch- 可能存 NULL 的列声明可空属性(
string?),非可空遇 NULL 会响亮抛SqlNullValueException(不出静默 null) - 敏感列标
[SensitiveData](Set()写入该列的参数值在审计日志中掩码;Where 手写 SQL 的洞值无列映射,不掩码);非幂等写不要指望自动重试(写入路径直连语义),确需弹性用ExecuteWithResilience包裹并自行评估重复执行风险
事务
- 事务内批量优先
BulkInsertAsync而非SessionBatch(同事务 100 行 5.4ms vs 16.8ms) - 事务内查询 / 原始 SQL 不自动重试(重试会以次生异常掩盖根因);确需事务级容错用
WithTransactionRetry(方言瞬时错误码整事务重放),回调必须幂等(at-least-once 契约) - 不支持嵌套事务(重复
BeginTransactionAsync响亮抛异常);长事务内部分回滚用保存点SavepointAsync/RollbackToAsync - 外部事务经
UseTransaction接入后,外部已 Dispose 再用本会话会响亮失败(不静默自动提交)
多租户与横切
[TenantAware]实体会话创建后立即WithTenant(id),不要延后到首个查询(缓存 key 前缀随租户切换失效)InsertAsync/BulkInsertAsync/BulkMergeAsync不附加租户谓词,tenant_id必须由调用方赋值(PALORM040 只拦可空列);租户列在 Update/BulkUpdate 与 UPSERT 冲突更新中不可写(防跨租户搬移),确需迁移用原生 SQL- 需要跨租户管理查询时用
IgnoreFilters();该调用把会话切到全量视图且当前无公开复位 API,管理查询结束请换一个会话(池化会话归还后重租即可),不要在同一个会话里继续做租户过滤查询 AuditInterceptor覆盖面 = 实体 SELECT 管线 / QueryBuilder UPDATE /ExecuteAsync;InsertAsync/DeleteAsync/Bulk 族/存储过程不产生审计记录,完整写入审计用数据库层审计或 OpenTelemetry- PG 连接串调优
NoResetOnClose=true下 raw SQL 的SET/临时表跨池租客可见(ITM-652):需要隔离时用独立连接(Max Pool Size=1)或显式DISCARD
AOT 与部署
[OwnedJson]对象形态必须配 System.Text.Json 源生成JsonSerializerContext(AOT 安全的硬要求,缺失编译期报 PALORM008-010)- 高吞吐服务端开
<ServerGarbageCollection>true</ServerGarbageCollection>;容器配GarbageCollectionHeapHardLimit防 OOM(Server GC 下分配数字不变、GC 频率与暂停分布不同) - 只部署单方言时可设 MSBuild 属性
PalORMTargetDialects裁剪 CRUD SQL 发射面(DDL 恒全方言);AOT 发布需约 4 GB 内存(ILC 编译器)
运维与排查
- 生产用
DbOptions.Production(已含弹性/连接池预设),只覆盖确需自定义的项 - 一请求一个
DataSession的短会话部署:Production预设已默认CircuitBreakerScope.Process(熔断跨会话累计,数据库持续故障时快速失败);自建DbOptions的等价部署请显式设CircuitBreakerScope.Process,Session 级计数随会话销毁、短会话下阈值几乎不可能达到 - 健康检查端点接
HealthCheckAsync(SELECT 1 探活,错误消息只透出异常类型名,防数据库拓扑泄露) - 可观测性接 OTel:
WithTracing+WithMetrics(指标族palorm.query.executions/palorm.query.duration,查询缓存另有palorm.cache.*四指标) - 超时异常带
PalORM.InfrastructureTimeout标记的属基础设施侧(网络/服务器),先查环境再查代码;瞬时故障判定用Data["PalORM.IsTransient"]标记 - MySQL 批量写报
ER_NET_PACKET_TOO_LARGE先查服务端max_allowed_packet;LOAD DATA 回退多值 INSERT 是local_infile关闭时的预期行为(非错误)
🔧 配置
三预设起步(Development / Production / Testing),24 项配置按需覆盖;三方言连接串自动调优仅补默认、不覆盖显式设置。
预设配置
var dev = DbOptions.Development(connectionString); // 开发环境
var prod = DbOptions.Production(connectionString, readConn); // 生产(含连接池 + 读写分离)
var test = DbOptions.Testing(connectionString); // 测试(零重试 + 短超时)
var env = DbOptions.FromEnvironment("PALORM_CONNECTION"); // 环境变量(Docker/K8s 友好)
FromEnvironment 支持的环境变量:PALORM_CONNECTION(必需)、PALORM_READ_CONNECTION、PALORM_COMMAND_TIMEOUT、PALORM_CONNECTION_TIMEOUT、PALORM_MAX_RETRIES、PALORM_CIRCUIT_BREAKER_THRESHOLD、PALORM_MAX_POOL_SIZE;组装后自动 Validate(),非法值在启动期报错。
配置项
连接与池
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
ConnectionString |
string(必需) |
— | 主库连接串,支持 $ENV:VAR 环境变量引用 |
ReadConnectionString |
string? |
null | 只读副本连接串,配置后 ForRead() 自动路由 |
MaxPoolSize |
int |
100 | 连接池上限(SQLite 忽略) |
MinPoolSize |
int |
0 | 池内保底暖连接数(0=驱动默认) |
PoolIdleTimeoutSeconds |
int |
0 | 空闲连接超时秒数(0=驱动默认) |
PoolLifetimeMinutes |
int |
60 | 连接最大生命周期,到期强制重建 |
PoolConnectionValidationInterval |
TimeSpan |
30s | 池化租出探活间隔(0=关闭) |
超时 · 重试 · 熔断
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
ConnectionTimeout |
TimeSpan |
15s | 连接建立超时(含重试) |
CommandTimeout |
TimeSpan |
30s | 命令执行超时;亚秒值向上取整为 1 秒 |
MaxRetries |
int |
3 | 瞬时故障最大重试次数(0=禁用,上限 10;只覆盖读管线) |
RetryBackoff |
Func<int, TimeSpan>? |
100→200→400ms | 自定义重试退避(参数=重试次数) |
OverallDeadline |
TimeSpan |
Zero(不设) | 整次操作总预算,到期停止重试并抛超时 |
CircuitBreakerThreshold |
int |
5 | 连续失败阈值(0=禁用熔断) |
CircuitBreakerResetAfter |
TimeSpan |
30s | 熔断后半开等待时间 |
CircuitBreakerScope |
CircuitBreakerScope |
Session | 熔断作用域(Session/Process,见注) |
会话与行为
| 属性 | 类型 | 默认值 | 说明 |
|---|---|---|---|
SessionSetupSql |
string? |
null | 主连接首次激活后执行的 SQL(分号分隔) |
ReadSessionSetupSql |
string? |
null | 读副本连接的同类设置(每会话一次) |
Interceptors |
IReadOnlyList<IQueryInterceptor>? |
null | 查询拦截器,按 Priority 升序执行 |
ValidateQueryColumnOrder |
bool |
true | QueryAsync 首行列序校验(别名列需关闭,见注) |
UseSynchronousRowRead |
bool |
false | 物化读走同步 Read()(MySQL 建议开启,见注) |
PrepareCacheSize |
int |
100 | PG AutoPrepare 缓存容量(0=禁用;其余方言忽略) |
PrepareThreshold |
int |
2 | PG 同 SQL 自动预编译阈值(最小 1;其余方言忽略) |
QueryCache |
IQueryCache? |
1024 条 | 查询缓存(默认进程级共享) |
LoggerFactory |
ILoggerFactory? |
null | 设置后 DataSession 创建 ILogger |
注(机制细节):
OverallDeadline:不设时最坏墙钟 ≈ (MaxRetries+1)×CommandTimeout+Σ退避(默认配置约 120 秒);取值应大于单次CommandTimeout。CircuitBreakerScope:Session 的失败计数随会话销毁(长会话可用);Process 按 (Provider, 连接串, 阈值, 冷却) 跨会话累计,一请求一会话的短会话部署必选 Process,否则熔断形同虚设(Production预设已默认 Process)。MinPoolSize/PoolIdleTimeoutSeconds:0=保留驱动默认(Npgsql 300s / MySqlConnector 180s),正数才覆盖;消除突发查询重建连接的延迟尖峰,代价是空闲超时后首个查询需重建物理连接(跨网段实测SELECT 1池内 0.3ms vs 新建 13.5ms;IdleTimeout 自 v5.6.0 起默认 0)。RetryBackoff:默认退避含 50~100% 抖动;负值抛异常。UseSynchronousRowRead:MySQL 实测约 998 vs 1846~2043 ns/行;SQLite 因进程内引擎恒同步;流式 API 始终异步;高并发服务端请先做线程池压力验证。ValidateQueryColumnOrder:用列别名/表达式列时需关闭。QueryCache:注入独立实例实现会话/租户级隔离(需线程安全)。
SQLite 并发写扩展性(2026-09-21 实测):80/20 读写混合负载下,SQLite 从 1 到 8 线程吞吐下降 69%(WAL 单写者模型,裸 ADO.NET 对照同为 −69%,非 PalORM 引入的锁);同期 PostgreSQL +7.2×、MySQL +4.6×。高并发写场景选 PG/MySQL,SQLite 适合读密集或低并发写的嵌入式场景。详见 docs/架构设计.md。
连接串自动调优
CreateConnection 时自动调优,仅当用户未显式设置时覆盖默认值。
PostgreSQL(6 项):
| 参数 | 默认 → 调优值 | 收益 |
|---|---|---|
MaxAutoPrepare |
0 → 100 | 自动预编译,查询延迟 −30~50% |
AutoPrepareMinUsages |
5 → 2 | 第 2 次执行起 Prepare |
NoResetOnClose |
false → true | 归还连接跳过 DISCARD ALL,+30% localhost 吞吐 |
ReadBufferSize |
8192 → 16384 | 大结果集吞吐 |
WriteBufferSize |
8192 → 16384 | 大值写入吞吐 |
Enlist |
true → false | 跳过 TransactionScope 检查 |
MySQL(5 项):
| 参数 | 默认 → 调优值 | 收益 |
|---|---|---|
AutoEnlist |
true → false | 跳过 TransactionScope |
ConnectionReset |
true → false | 跳过 COM_RESET_CONNECTION |
CancellationTimeout |
2 → 5 | 防连接泄漏 |
AllowLoadLocalInfile |
false → true | MySqlBulkCopy 前提 |
ServerRedirectionMode |
Disabled → Preferred | Azure MySQL 直连 |
SQLite PRAGMA(9 项):
| PRAGMA | 默认 → 调优值 | 收益 |
|---|---|---|
busy_timeout |
0 → 5000ms | 并发写 BUSY 在引擎内等待 |
synchronous |
FULL → NORMAL | WAL 下安全,减少 fsync |
cache_size |
2MB → 64MB | 读密集型提升 |
temp_store |
DEFAULT → MEMORY | 临时表走内存 |
wal_autocheckpoint |
1000 → 1000 | 显式固定防漂移 |
journal_size_limit |
-1 → 64MB | 防 WAL 无界膨胀拖慢检查点 |
mmap_size |
0 → 256MB | 文件库 I/O 加速(:memory: 跳过) |
analysis_limit |
1000 → 400 | 约束 optimize/ANALYZE 采样成本 |
optimize |
0 → 0x10002 | 连接打开时自动统计优化(官方对长生命周期连接的推荐形态,采样成本由 analysis_limit 约束);仅文件库宽分支——内存库无持久统计面,只读连接写统计面会 SQLITE_READONLY |
NoResetOnClose=true 的会话状态泄漏取舍(raw SQL 的 SET/临时表跨池租客可见)见 ITM-652:需要隔离时用独立连接(连接串 Max Pool Size=1)或显式 DISCARD。
进阶配方
SQLite(经 DbOptions.SessionSetupSql 执行,可覆盖上述默认):
| 场景 | 配方 | 说明 |
|---|---|---|
| 批量/大行负载建库 | PRAGMA page_size=16384 |
须在库首次创建前生效;既有库静默 no-op |
| 读密集 | PRAGMA mmap_size=1073741824 |
提至 1GB(引擎上限 2GB−64KB) |
| 删除密集 | PRAGMA secure_delete=OFF |
消除删页覆写写放大;已删内容不再清零,加密库上有 forensic 残留 |
| 本地弹性直通 | MaxRetries = 0 |
本地 BUSY 已由 busy_timeout 吸收,弹性机构在 RTT≈0 的本地库是净开销;远程库维持默认 |
PostgreSQL(连接串层显式启用,各有触发条件与取舍):
| 场景 | 配方 | 说明 |
|---|---|---|
| 本机/同容器 PG | Host=/var/run/postgresql |
Unix domain socket 替代 TCP(Npgsql 官方性能文档);Host 以斜杠或盘符开头即按 socket 目录解析 |
| GSS 协商长尾削峰 | GssEncryptionMode=Disable |
实测(2026-09-26,Npgsql 10.0.3 × PG 18.4)中位建连不变,长尾 149ms → ≤21ms;安全策略变更,仅服务端不要求 GSS 加密时使用 |
| 非关键表批量写 | 事务内首条 SET LOCAL synchronous_commit TO off |
PG 官方 28.4:短事务吞吐的最大应用层杠杆;崩溃丢最近约 600ms 提交(不损数据)。事件日志/缓存类可接受,账务类不可;须显式 opt-in,PalORM 不默认开启 |
MySQL(服务端/DBA 决策或显式操作):
| 场景 | 配方 | 说明 |
|---|---|---|
| 计划器统计刷新 | ANALYZE TABLE 表名(经 ExecuteAsync) |
InnoDB 默认 10% 行数变化自动重算,多数场景无需手工;MigrateAsync 不自动跑 |
| 批量写提交削峰 | innodb_flush_log_at_trx_commit=2(服务端全局变量) |
提交只写 OS 缓存不 fsync;崩溃丢最近约 1 秒提交,判据同 PG synchronous_commit=off |
| 锁等待 | 无需配置 | innodb_lock_wait_timeout(默认 50s)+ PalORM 弹性重试(死锁 1213 / 锁超时 1205)两层兜底 |
| 批量写报文上限 | 服务端 max_allowed_packet ≥ 16MB |
LOAD DATA 与大参数批单包可达数 MB,超限报 ER_NET_PACKET_TOO_LARGE 或静默断连;报此错先查服务端值 |
| 批量写 LOAD DATA 开关 | 服务端 local_infile=ON |
缺一即回退多值 INSERT(检测按连接缓存 60s);本机实测 LOAD DATA 比多值 INSERT 快 1.39×,远程库差距更大(官方基准 4~5× 量级) |
| 事务内多行插入选型 | 优先 BulkInsertAsync |
同一事务 100 行:逐条 45.7ms → BulkInsert 5.4ms(8.44×)→ SessionBatch 16.8ms;需逐条回填自增 ID 或混合语句时才用 SessionBatch |
| 排序/连接内存缓冲 | sort_buffer_size / join_buffer_size ≥ 256KB(默认即此值) |
远程大结果集 / 百万行级排序 join 先 EXPLAIN 确认无 Using filesort 再按需调(DBA 决策) |
| 隔离级别 RR→RC | WithIsolationLevel(IsolationLevel.ReadCommitted) |
引擎默认 RR(间隙锁)放大热点行锁竞争;取舍为 MVCC 快照语义变化,属业务决策 |
🔄 升级指南
按版本倒序排列;数据正确性修复优先阅读对应小节。
从 6.4.x 升级到 6.5.0:查询语义两处修复(结果可能变化)
两项用户可见行为修复,升级前请核对(详见 CHANGELOG):
- WHERE/OR 链式优先级(C16):
Where(A).OrWhere(B).Where(C)此前生成(A) OR (B) AND (C)(SQL 求值为A OR (B AND C),与方法文档承诺的链式形态不符),本版修复为((A) OR (B)) AND (C)。混合 Where/OrWhere 链的查询结果可能变化:纯 AND 链、纯 OR 链与 OR 收尾链的 SQL 文本逐字不变,无需动作。WhereIn/WhereNotIn同受此修复覆盖。升级动作:检索代码中混合Where/OrWhere链,确认查询结果符合链式直觉预期。 - FormattableString 引号内花括号(C17):引号内
{N}/}此前被替换或误抛FormatException(JSON 文本'{"a":1}'无法书写),修复后引号内一律原样透传。此前为绕过该缺陷手写拆串的代码可还原为直书 JSON 字面量。
其余为纯新增(会话池 DataSessionPool)与内部性能/修复,无迁移动作。
从 6.3.x 升级到 6.4.0:Production 预设熔断作用域变更
DbOptions.Production 预设自 6.4.0 起显式设 CircuitBreakerScope = Process(此前为 Session)。影响与动因:
- 变化:使用
Production预设时,熔断失败计数按 (Provider, 连接串, 阈值, 冷却) 跨会话累计。一请求一会话的部署下,数据库持续故障时请求快速失败(熔断开闸),而非此前每个请求各自重试 5 次再失败、熔断从不触发的虚设形态。 - 不变:裸
new DbOptions { ... }默认仍为Session;长生命周期会话与测试场景零变化。 - 多租户注意:多租户共用一个连接串时熔断器为租户间共享粒度,需要租户级隔离的部署用独立连接串,或
with { CircuitBreakerScope = CircuitBreakerScope.Session }显式回退。
从 6.2.x 及更早升级到 6.3.x(数据正确性修复,强烈建议)
6.3.0 修复了 R-UNNESTB:PG auto-prepare × 命令复用槽交互下的批量操作静默错数(影响 5.7.0 ~ 6.2.0 共 7 个版本);6.3.1 在此之上恢复了 BulkDelete IN 形态的池化性能(20K 键 37 ms 快态带,0.55× 地板,见性能)。R-UNNESTB 触发条件:连接串启用 MaxAutoPrepare(PalORM 默认调优会设为 100,见连接串自动调优)且同一会话执行第 3 个及后续等长批量批次;后果是第 3 批起静默重发第 2 批的参数(批量删除少删行、批量 UPSERT 写错键),无异常无告警。
- 升级到 6.3.1 即彻底修复(参数对象从建立到释放全程稳定,auto-prepare 缓存引用不再错配)
- 暂不能升级的止血:连接串显式加
MaxAutoPrepare=0(代价是失去自动预编译的查询延迟收益,Npgsql 默认即 0) - 风险面自查:若历史负载满足触发条件,建议对受影响表的批量操作结果做一次对账(行数/键集合核对)
6.3.0 同时包含 PG 三条批量写走数组形态(UPDATE FROM VALUES(...) 的 UNNEST 化与 DELETE ... = ANY(数组),批量族时延 −40~74%),行为面无破坏。
从 5.x 升级到 6.0
<details> <summary><b>四项破坏性变更(详见 ADR-N)</b></summary>
| 删除/变更 | 迁移动作 |
|---|---|
IRowFactory<T> 接口 |
无——零实现零消费,若外部代码引用了它(不存在官方用法),删除引用即可 |
DataSession.DiffAsync<T>() |
换用 ValidateSchemaAsync<T>(),需要 [DIFF] 前缀时自行拼接 |
DbOptions.NamingConvention(含 ApplyNaming) |
删除该设置即可——行为从未生效过(列名映射在编译期由 [Table]/[Column] 注解决定);要改列名用 [Column("...")] |
DbOptions.PoolExplicitlyConfigured |
无——零读方内部标记;WithPool/PALORM_MAX_POOL_SIZE 行为不变 |
[Column] 的 Length/Precision/Scale:int? → int(0=未设置) |
无——[Column(Length = 64)] 语法在 5.x 从未可编译过(CS0655);v6.0 起该语法首次真正可用并参与 DDL |
破坏性变更全表见 ADR-N:v6.0 破坏性变更汇总。
</details>
完整版本历史见 CHANGELOG.md。
🧰 开发
构建环境、项目结构与注册契约;完整构建路径与提交防线见 CONTRIBUTING.md。
环境要求
| 维度 | JIT 运行时 | Native AOT 编译 |
|---|---|---|
| CPU 架构 | x64 / ARM64 | x64 已验证(ARM64 未实测),需指定 RID(-r win-x64 / linux-x64) |
| 内存 | 256 MB | 4 GB(ILC 编译器;运行时仅需约 100 MB) |
| 磁盘 | 50 MB | 100 MB,SSD 推荐(ILC 大量临时文件,HDD 编译慢 5-10x) |
软件:.NET SDK 11.0.100-rc.2+(global.json 锁定)· C# latest · Windows 10+ / Linux / macOS · Visual Studio 2026 / Rider / VS Code(需支持 Roslyn 源生成器)。
项目结构
PalORM.Core 运行时核心(DataSession / QueryBuilder / Resilience / IQueryInterceptor)
PalORM.PostgreSql Npgsql 适配 + JSONB / NOTIFY / Binary COPY / AdvisoryXactLock
PalORM.MySql MySqlConnector 适配 + MySqlBulkCopy
PalORM.Sqlite MDS + SQLite3MC 适配 + PRAGMA 调优
PalORM.SourceGen Roslyn IIncrementalGenerator(netstandard2.0 编译器插件)
PalORM.Testing 测试辅助(TestEnvironment / TestDb)
跨程序集注册契约:每个模型程序集的生成物通过 ModuleInitializer 在该模块首次被触达(任一成员被调用、类型被实例化、静态字段被访问)时向 PalORM_Runtime 注册。引用了库程序集但从未触达其中任何类型时,该程序集的实体不会注册,运行期表现为 "not registered":跨程序集消费方请确保实体类型被真实引用。
构建、测试与基准
SQLite 测试开箱即跑(无需外部数据库);PG/MySQL 集成测试需外部数据库。构建路径、本地提交防线与基准环境(BenchmarkDotNet 本地 fork)见 CONTRIBUTING.md,架构决策见 docs/架构设计.md 与 docs/adr/。
🤝 贡献
欢迎 issue 与 PR。提交前请跑通相关测试套件并保持 0 警告(TreatWarningsAsErrors);涉及性能的改动请附基准前后对比。详细流程见 CONTRIBUTING.md。
💬 社区
<div align="center">
<img src="docs/qq-group.jpg" width="260" alt="QQ 群二维码:C#/.NET 新技术交流群(群号 1125599744)">
C#/.NET 新技术交流群(群号 1125599744)
QQ 内搜索群号或扫码加入。
</div>
📄 许可证
v6.7.0 及以前的已发布版本仍为 AGPL-3.0-only;<br> v6.8.0 及以后的新版本统一变更为 Apache-2.0 发布。
<div align="right">
</div>
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net11.0 is compatible. |
-
net11.0
- Npgsql (>= 10.0.3)
- PalORM.Core (>= 6.8.0)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on PalORM.PostgreSql:
| Package | Downloads |
|---|---|
|
PalDDD.PalORM.PostgreSql
Pal.DDD PalORM PostgreSQL 适配包(AOT + 源生成 + RETURNING/COPY) |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated | |
|---|---|---|---|
| 6.8.0 | 165 | 10/25/2026 | |
| 6.7.0 | 110 | 10/23/2026 | |
| 6.6.0 | 129 | 10/20/2026 | |
| 6.5.0 | 125 | 10/14/2026 | |
| 6.4.1 | 167 | 10/11/2026 | |
| 6.4.0 | 111 | 10/10/2026 | |
| 6.3.1 | 139 | 10/7/2026 | |
| 6.3.0 | 112 | 10/6/2026 | |
| 6.2.0 | 135 | 9/30/2026 | |
| 6.1.0 | 123 | 9/29/2026 | |
| 6.0.1 | 123 | 9/28/2026 | |
| 6.0.0 | 122 | 9/28/2026 | |
| 5.9.0 | 128 | 9/27/2026 | |
| 5.8.0 | 121 | 9/27/2026 | |
| 5.7.0 | 120 | 9/26/2026 | |
| 5.6.0 | 199 | 9/24/2026 | |
| 5.5.1 | 328 | 9/11/2026 | |
| 5.5.0 | 128 | 9/11/2026 | |
| 5.4.0 | 167 | 9/2/2026 | |
| 5.3.0 | 169 | 8/21/2026 |