Crystal 中 `groupBy` 步骤(Step)的深入解析:将列表按分组键聚合为 Map
Crystal 中groupBy步骤Step的深入解析将列表按分组键聚合为 Map【免费下载链接】crystal Graphiles Crystal Monorepo; home to Grafast, PostGraphile, pg-introspection, pg-sql2 and much more!项目地址: https://gitcode.com/gh_mirrors/cry/crystal开头导读groupBy是 Grafast 提供的内置标准步骤standard step它接受一个一维列表步骤与一个计划期plan-time映射回调返回一个新的步骤其运行结果是一个Map——键为分组键值为与原列表中匹配该分组键的原始条目组成的子列表。本文基于该文档 grafast/website/grafast/standard-steps/groupBy.md 为核心骨架结合仓库中 groupBy.ts、listTransform.ts 等源码实现与 exampleSchema.ts 中的真实用例展开讲解其 API 形态、底层实现机制、典型应用场景与注意事项帮助读者在 plan resolver 中正确、高效地使用groupBy。groupBy 是什么在 Grafast 中步骤Step是描述数据流的最小单元。标准步骤是内置于grafast模块的一组常用步骤用于加载数据、构造对象与列表、对列表进行操作等。groupBy属于“对列表进行操作”Operating on lists这一类与其并列的还有first、last、reverse、filter、each等详见 standard-steps 索引。groupBy的功能定义如下接受一个单维列表计划plan和一个产生分组键的映射器mapper返回一个计划其结果是Map键为分组键值为与原列表条目中匹配这些分组键的列表。其核心特点在于映射器是计划期plan-time的即该回调在查询规划阶段运行接收列表项步骤并返回一个用于产生分组键的步骤而非在运行时直接操作 JS 值。这与lambda执行期回调形成鲜明对比。基本用法文档给出的最小用法示例// Runtime input: Post[] // Runtime output: RecordID, Post[] const $groupedByAuthorId groupBy($posts, ($post) $post.get(author_id));要点解读$posts是一个代表Post[]的列表步骤例如由$pgSelect或loadMany产生第二个参数($post) $post.get(author_id)是映射器它为列表中的每个条目构造一个“分组键步骤”这里取每个 post 的author_id返回的$groupedByAuthorId步骤在运行期产生一个Map键为author_id的值值为该作者名下的Post[]子列表分组后子列表中的元素仍是原始条目entire item value不是映射后的键值。注意虽然示例注释中写的是RecordID, Post[]但实际的运行时结构是 JavaScript 的MapMapID, Post[]键为任意类型而非仅限于字符串键。函数签名与类型定义groupBy的完整函数签名位于 src/steps/groupBy.tsexport function groupBy TListStep extends StepRepresentingListany, TItemStep extends Stepnumber, ( listStep: TListStep, mapper: ListTransformItemPlanCallbackItemsStepTListStep, TItemStep, ): __ListTransformStepTListStep, TItemStep, GroupByPlanMemo, any参数与返回值的类型解释TListStep列表步骤的泛型约束要求其实现了StepRepresentingList即一个可表示列表的步骤mapper类型为ListTransformItemPlanCallbackItemsStepTListStep, TItemStep定义在 listTransform.ts。它接收列表项步骤listItemPlan或__ItemStep返回一个“依赖步骤”TDepsStep该步骤的运行值即分组键返回值__ListTransformStep即__ListTransformStepTListStep, TItemStep, GroupByPlanMemo, any其中GroupByPlanMemo定义为Mapunknown, unknown[]见 groupBy.ts。groupBy在grafast模块中被统一导出可从src/steps/index.ts第 31-32 行导出GroupByPlanMemo类型与groupBy函数以及 src/index.ts 中导入。底层实现基于listTransform的 reduce 机制groupBy本身并不包含复杂的逻辑它是建立在listTransform列表变换机制之上的一个特例。listTransform是 Grafast 内部的一种“特殊”步骤具有在 Crystal 引擎中的自定义处理逻辑用于“把列表变成别的东西或更多的列表”详见 listTransform.ts 与文档 listTransform.md。groupBy的完整实现const reduceCallback: ListTransformReduceGroupByPlanMemo, any ( memo, entireItemValue, idx, ) { let list memo.get(idx); if (!list) { list []; memo.set(idx, list); } list.push(entireItemValue); return memo; }; const initialState (): GroupByPlanMemo new Map(); export function groupByTListStep extends StepRepresentingListany, TItemStep extends Stepnumber( listStep: TListStep, mapper: ListTransformItemPlanCallbackItemsStepTListStep, TItemStep, ): __ListTransformStepTListStep, TItemStep, GroupByPlanMemo, any { return listTransformTListStep, TItemStep, GroupByPlanMemo, any({ listStep, itemPlanCallback: mapper, initialState, reduceCallback: reduceCallback, listItem: isListCapableStep(listStep) ? (itemPlan) each(itemPlan as any, ($item) listStep.listItem($item as any)) : undefined, meta: groupBy:${chalk.yellow(listStep.id)}${mapper.name ? /${mapper.name} : }, }); }关键点分析itemPlanCallback直接就是用户传入的mapper规划期__ListTransformStep会为每个列表项创建__ItemStep特殊的条目步骤并通过listStep.listItem(...)展开为真实的条目步骤然后调用mapper产生分组键步骤。这个过程发生在构造函数中的子程序层subroutine layer内参见 listTransform.ts。initialState为() new Map()每个“父条目”即原列表中的每个元素的分组结果从空Map开始。reduceCallback这是分组逻辑的核心——它把每个条目值entireItemValue注意是整个条目而非仅键追加到memo.get(分组键)对应的数组末尾若该键尚未出现则先初始化空数组。这与文档中“值为原列表中匹配分组键的条目列表”的描述完全一致。listItem当输入列表步骤是“列表能力步骤”isListCapableStep时需要借助each把每个分组键映射回条目计划以保证分组后的子列表仍能进行列选择、子查询等操作。这保证了groupBy的结果可以继续参与后续的列表遍历与字段展开见下文的示例 Schema 用例。meta用于调试标识格式为groupBy:listStepId[/mapperName]例如groupBy:5/authorIdCallback。若映射器是具名函数会附带其函数名便于在计划plan调试输出中定位。__ListTransformStep的执行过程在运行期__ListTransformStep.executelistTransform.ts做了以下事情将父 bucket 中的每个“父条目”按其列表展开为每个列表项创建新的子 bucket 索引indexForEach循环中“multiply up”的过程在子程序层执行所有列表项步骤与分组键步骤得到depResults对每个父条目取出其对应的子索引集合indexes用list.reduce将reduceCallback应用到初始状态上最终产出Map。这也解释了为何groupBy被称为“列表变换”它把一维列表变换为“键 → 子列表”的二维结构。listTransform.md中曾用 PostgreSQL 的random_user_array_set()返回setof users[]的场景说明此类需求把 1 维数组还原为 2 维数组而“按索引分组”正是groupBy的通用化。与其它列表操作步骤的关系filter同样是listTransform之上的特例但reduceCallback仅把满足条件的条目 push 进数组产生一维列表each对每个条目做映射reduceCallback是(memo, item) (memo.push(item), memo)保持一维结构且带有优化eachOptimize在可跳过时替换为原列表步骤groupBy则把条目按分组键归入不同的子数组产出Map。三者共享ListTransformOptions结构listStep、itemPlanCallback、initialState、reduceCallback、meta等这也是为什么文档将其归类为“对列表进行操作”系列。运行结果的结构groupBy产生的是一个Map而非普通对象这一点对使用方非常重要键分组键步骤在运行期的值例如某个author_id、featured布尔值键类型不限值由原始列表条目组成的数组顺序保持原列表中的相对顺序空输入若输入列表为空或为null结果Map为空initialState为new Map()子列表中的条目是“整个条目值”entire item value也就是输入列表步骤的原始条目而不是分组键或映射产物。因此若你需要把Map转回数组例如按分组顺序输出可以像示例 Schema 中那样用lambda取[...map.values()]。真实用例PostGraphile 示例 Schema 中的按字段分组仓库中的 exampleSchema.ts 提供了一个groupBy的完整实战范例第 2812-2827 行。该场景是把一个论坛消息列表按featured属性分组再转成“分组的列表的列表”以嵌套输出// Group messages by the featured property const $grouped groupBy($messagesFromOtherForums, ($message) ($message as unknown as MessageStep).get(featured), ); // Since groupBy results in a Map, turn it into an array by just getting the values const $entries lambda( $grouped, (map) [...map.values()], true, ); // Now map over the resulting list of list of values and wrap with the message list item plan. return each($entries, ($group) each($group, ($item) $messages.listItem($item)), );这段代码展示了三个关键实践分组键来自条目步骤的字段访问($message) $message.get(featured)与文档示例$post.get(author_id)完全同构Map→ 数组的转换groupBy返回Map若下游需要数组用lambda取.values()与each组合还原列表能力由于groupBy产生的是“列表的列表”用嵌套each逐层展开并通过$messages.listItem($item)恢复消息条目的列选择能力使结果能够参与后续的 GraphQL 字段解析。与pgSelect的groupBy的区别请勿混淆需要注意dataplan-pg 的$pgSelect上还有一个同名方法groupBy(spec)见 pgSelect.ts它用于在数据库端生成 SQL 的GROUP BY子句groupBy(spec) { info.groups.push(runtimeScopedSQL(spec)); }两者的分工是$pgSelect.groupBy(...)在 PostgreSQL 侧执行聚合分组属于 SQL 语义标准步骤groupBy($list, mapper)在 Grafast 的 JavaScript 运行时对已取回的列表进行分组属于 JS 语义。什么时候用哪个若列表数据已经在内存中例如由loadMany或其它步骤产生用标准步骤groupBy即可若数据量巨大且希望把分组下推到数据库则应优先考虑$pgSelect.groupBy(...)等数据库端方案。两者的使用场景在 pgSelect.md 的列表操作一节也有并列说明其中提到filter时建议优先在数据库端过滤groupBy同理。使用注意事项1. 映射器是计划期回调mapper在查询规划阶段运行它返回的是步骤而非值。这意味着不能在 mapper 中直接读取数据库值做 JS 判断——那时值尚未产生若需要基于运行期值做逻辑应组合lambda等执行期步骤在 TypeScript 中若条目步骤是自定义类可能需要类型断言如示例中的($message as unknown as MessageStep)。2. 分组后仍是“步骤”可继续参与计划groupBy返回的__ListTransformStep可以继续作为其它步骤的依赖例如嵌套each、lambda、filter等因为它本身就是一个步骤。但其变换未必立即发生——某些列表变换步骤会延迟到“被分页遍历”时才应用。3. 依赖变换结果时的陷阱根据 pgSelect.md 的说明如果执行了此类列表变换变换并不总是立即发生——有时只有当步骤被分页遍历paginated over时才应用。如果你把变换后的步骤作为另一个步骤的依赖可能会拿到未变换的原始值造成困惑与 bug。此时应使用applyTransforms强制变换在当前层级立即生效再安全地依赖变换后的值。4. 调试标识每个groupBy步骤在计划输出中的标识为groupBy:listStepId[/mapperName]。如果希望调试信息更可读可以给 mapper 命名const byAuthorId ($post: PostStep) $post.get(author_id); const $groupedByAuthorId groupBy($posts, byAuthorId); // meta: groupBy:3/byAuthorId小结groupBy($list, mapper)是 Grafast 标准步骤之一用于把一维列表按计划期映射的分组键聚合成MapKey, Item[]它建立在listTransform的 reduce 机制之上核心是initialState () new Map()与“按键收集整个条目值”的reduceCallback结果Map的值是原始条目且可借助listItem/each继续保留列选择能力注意与$pgSelect.groupBySQL 端分组区分并留意列表变换的延迟应用问题必要时使用applyTransforms。延伸阅读listTransform.md列表变换机制的设计背景与通用 reduce 模型groupBy.ts 源码 与 listTransform.ts 源码其它列表操作步骤filter.md、each.md、first.md、last.mdpgSelect.md 中关于数据库端分组与列表变换注意事项的说明完整实战示例见 exampleSchema.ts【免费下载链接】crystal Graphiles Crystal Monorepo; home to Grafast, PostGraphile, pg-introspection, pg-sql2 and much more!项目地址: https://gitcode.com/gh_mirrors/cry/crystal创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

相关新闻

Python Tkinter开发简约桌面时钟:从原理到实践

Python Tkinter开发简约桌面时钟:从原理到实践

1. 项目概述与核心价值去年在整理工作台时,发现一个有趣的现象:虽然手机和电脑都自带时钟功能,但真正需要专注工作时,频繁切换窗口查看时间反而会打断思路。于是萌生了用Python开发一个简约桌面时钟的想法,既能满足基础…

2026/9/23 5:36:11 阅读更多 →
程序员节1024图解密:一张图里的编码逻辑与职业认同

程序员节1024图解密:一张图里的编码逻辑与职业认同

1. 为什么一张1024节日图,值得程序员花十分钟细看?你有没有在10月24日早上刷到过这样一张图:深蓝底色上浮着一行等宽字体的0x3FF,右下角嵌着一个微缩的二进制时钟,背景隐约透出十六进制网格线,而最醒目的—…

2026/9/23 5:36:11 阅读更多 →
2026最新lol每日一笑实战:3步搞定版本升级API全变痛点

2026最新lol每日一笑实战:3步搞定版本升级API全变痛点

2026最新lol每日一笑实战:3步搞定版本升级API全变痛点 版本升级后 API 全变了,代码一跑就报错,这种崩溃感谁懂? 别再手动一个个改接口了,效率低还容易漏。 今天带你用 Python 从零搭建一个 lol每日一笑…

2026/9/23 5:36:11 阅读更多 →

最新新闻

FreeMaster Recorder嵌入式运行时数据采集原理与实战

FreeMaster Recorder嵌入式运行时数据采集原理与实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/24 10:04:04 阅读更多 →
2026 企业 AI 办公工具选型指南:从场景匹配到权限评估

2026 企业 AI 办公工具选型指南:从场景匹配到权限评估

企业调研AI办公工具的过程中,很容易陷入几个典型的认知误区:不少团队一开始直接拉一张公开的功能清单挨个打勾,以功能点的数量多少作为核心判断标准,也有不少采购方只盯着预算阈值选报价最低的产品,还有部分决策者直接…

2026/9/24 10:04:04 阅读更多 →
AI 搜索获客与传统 SEO 获客 ROI 对比|B 端中小企业 GEO 落地实战分析

AI 搜索获客与传统 SEO 获客 ROI 对比|B 端中小企业 GEO 落地实战分析

随着生成式大模型普及,豆包、DeepSeek、Kimi 等 AI 工具成为 B 端采购调研供应商的重要入口。传统 SEO 以网页排名为核心,而 GEO(生成式引擎优化)以 AI 模型引用、品牌推荐为目标。本文对比两套获客模式的 ROI 差异,结…

2026/9/24 10:04:04 阅读更多 →
氨水净化除铁装置设计全解析:工艺选型与运行避坑指南

氨水净化除铁装置设计全解析:工艺选型与运行避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/24 10:04:04 阅读更多 →
FX5U与汇川伺服Modbus-RTU实战接线调试指南

FX5U与汇川伺服Modbus-RTU实战接线调试指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/9/24 10:04:04 阅读更多 →
Robot Framework 4.1 版本特性详解:continue-on-failure 标签控制与参数转换增强

Robot Framework 4.1 版本特性详解:continue-on-failure 标签控制与参数转换增强

测试RPA接口测试 【免费下载链接】robotframework Generic automation framework for acceptance testing and RPA 项目地址: https://gitcode.com/gh_mirrors/ro/robotframework 点击查看 免费下载 导读 Robot Framework 4.1 是继 4.0 之后的一个特性版本&#x…

2026/9/24 10:03:04 阅读更多 →

日新闻

基于YOLOv8的渔船作业监控系统:从环境搭建到边缘部署全流程

基于YOLOv8的渔船作业监控系统:从环境搭建到边缘部署全流程

简介:这是一套面向计算机、人工智能、自动化等专业学生与教师的毕业设计级项目资源,围绕YOLOv8实现渔船作业监控系统,可用于毕设、课程设计、大作业或项目立项演示。压缩包共97个文件,约24.21MB,以70个Python源码文件为…

2026/9/24 0:00:19 阅读更多 →
单细胞注释实战:基于Scanpy的标记基因与参考映射流程解析

单细胞注释实战:基于Scanpy的标记基因与参考映射流程解析

简介:一份基于单细胞RNA测序数据的细胞类型注释算法研究Python毕业设计源码,针对计算机相关专业正在做毕设或需要项目实战的学习者,可用于课程设计与期末大作业。项目代码完整、经导师指导评审通过,可直接运行,覆盖数据…

2026/9/24 0:00:19 阅读更多 →
C#源生成器实战:用增量生成器替代反射,告别AOT崩溃

C#源生成器实战:用增量生成器替代反射,告别AOT崩溃

第一次在项目里被反射卡住,是在一个老旧的WinForms模块里:几十个类依赖PropertyChanged通知,运行时反射读属性、发通知,每次启动慢半拍不说,一上.NET Native/AOT裁剪模式几乎全面崩盘。后来我把这段逻辑全部改成C#源生…

2026/9/24 0:00:19 阅读更多 →

周新闻

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

Flutter for OpenHarmony游戏卡片渐变背景实战:从原理到性能优化

直接铺开项目本身吧。这几个月我一直在折腾一件事:用Flutter给OpenHarmony做一款游戏集合类的App,说白了就是把若干小游戏塞进一个壳里,用统一入口分发。这个方向本身不算新鲜,真正让我花了不少心思的,是首页那堆游戏卡…

2026/9/23 4:55:02 阅读更多 →
Word表格编号全攻略:从列表编号到题注交叉引用

Word表格编号全攻略:从列表编号到题注交叉引用

写Word文档,最让人头疼的往往是那些“看起来不起眼”的小问题。比如表格编号这事:今天在表后面多加了两个空白行,明天给客户交稿前发现整个章节的编号全部错位,光是挨个改序号就能耗掉大半个下午。我前阵子帮人整理一份上百页的技…

2026/9/24 9:10:42 阅读更多 →
从第一个站到第二个站:独立开发者的静态网站选型与落地实践

从第一个站到第二个站:独立开发者的静态网站选型与落地实践

1. 项目概述1.1 核心需求解析做独立开发者这几年,说实话,第一个网站上线的那天晚上我兴奋得没睡着。但等它跑了半年,流量惨淡、功能臃肿、代码自己都懒得看第二遍之后,我才慢慢琢磨明白一个道理:第一个网站是练手&…

2026/9/23 9:53:41 阅读更多 →

月新闻

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能

持续集成 流水线自动化与 声明式交付 实践:原型怎样变成可用功能分类:[AI/大模型]细分主题:AI 增强型 CI/CD 流水线自动化与 GitOps 实践:Agent 工作流、工具调用与任务拆解:从原型到生产的验收清单很多团队在尝试用大…

2026/9/23 9:53:40 阅读更多 →
容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场

容器编排 生产环境运维与排障实战:复盘记录怎样真正派上用场分类:[工程技术]细分主题:Kubernetes 生产环境运维与排障实战:可复制的项目复盘模板与决策记录大部分团队的事故复盘报告,最后都变成了躺在 Confluence 或钉…

2026/9/23 9:53:40 阅读更多 →
容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步

容器 容器化技术与镜像安全管理:核心链路应该先拆哪一步分类:[工程技术]细分主题:Docker 容器化技术与镜像安全管理:核心链路的逐步实现与关键代码取舍面对一个积累了五六年历史包袱的单体架构应用(包含 Web 接口、后台…

2026/9/23 9:53:40 阅读更多 →