GraphQL Scala 与 Sangria 实战:用 Relation 与 Fetcher 打通 User、Link、Vote 模型关联查询

发布时间:2026/9/25 11:38:04

GraphQL Scala 与 Sangria 实战:用 Relation 与 Fetcher 打通 User、Link、Vote 模型关联查询 【免费下载链接】howtographqlThe Fullstack Tutorial for GraphQL项目地址https://gitcode.com/gh_mirrors/ho/howtographql点击查看免费下载本文基于 HowToGraphQL 的 Scala/Sangria 后端教程系统讲解如何在 Sangria 中借助Relation与Fetcher.rel为User、Link、Vote三个模型建立一对多关联并实现按关联 ID 批量预取数据的查询优化。读完本文你将掌握SimpleRelation的定义方式、Fetcher.rel双函数签名、RelationIds的用法、ReplaceField/AddFields宏配置以及如何用模式匹配在 DAO 层按不同关系统一取数最终写出可直接在 GraphiQL 控制台运行的嵌套关联查询。关联Relations在 Sangria 中的角色在传统数据库中关系Relations定义了实体之间如何连接。在 GraphQL 与 Sangria 中Relation与延迟解析器Deferred Resolver紧密相关承担着类似职责当你需要查找相关联的实体时查询可以被优化——所有需要的数据一次性批量取回而不是为每个实体单独发一次数据库请求。从 4-deferred-resolvers.md 可以知道Fetcher是Deferred Resolver的高层 API它按 ID 或关系优化实体解析、对结果去重并缓存。而Relation正是把 Fetcher 的能力从仅按实体自身的 id 字段查找扩展为按另一个实体中存储的外键 ID 查找。换句话说Relations 扩展了 Fetcher使我们不仅能按实体的 id 字段查找实体还能按常存储于另一实体字段中的 id 进行查找。在动手前先梳理当前 schema 中到底有多少条关系User拥有links与votes字段Link拥有postedBy与votes字段Vote拥有user与link字段。这些关系如何运作Link是核心实体由一个唯一的用户发布postedBy反过来一个用户可以有多个链接。用户还可以给链接投票一个用户对同一个链接最多投一次但一个链接可以拥有多个投票。因此本应用中一共有3 条一对多one-to-many关系User→Link、User→Vote、Link→Vote。这与教程目标 schema见 meta/structure.graphql 中UsersLinks、UsersVotes、VotesOnLink三个 relation 注解一一对应。数据库层准备给 Link 加上 postedBy 并补全外键领域模型发生了变化数据库层要先行跟上。扩展 Link 模型首先修改Linkcase class新增postedBy: Int字段表示发布该链接的用户 ID同时保留默认的createdAtcase class Link(id: Int, url: String, description: String, postedBy: Int, createdAt: DateTime DateTime.now) extends Identifiable这里Link仍实现Identifiabletrait含val id: Int沿用第 6 章 Interfaces 中抽取的公共接口与配套的HasId隐式转换相关背景可回看 6-interfaces.md。更新 LinksTable 表结构在DBSchema中修改LinksTable增加postedBy列并把它纳入*投影使 Slick 能完成整行到 case class 的映射class LinksTable(tag: Tag) extends TableLink{ // ... def postedBy columnInt def * (id, url, description, postedBy, createdAt).mapTo[Link] }添加外键约束在LinksTable中添加指向Users主键的外键def postedByFK foreignKey(postedBy_FK, postedBy, Users)(_.id)Votes模型本身已经带有存储外部 ID 的字段userId、linkId因此只需在数据库设置中补上外键。在VotesTable中添加def userFK foreignKey(user_FK, userId, Users)(_.id) def linkFK foreignKey(link_FK, linkId, Links)(_.id)重新定义种子数据由于领域模型发生变化databaseSetup也需要随之更新——建表顺序必须符合外键依赖先Users再Links最后Votes并填充带关联的示例数据/** * Load schema and populate sample data within this Sequence od DBActions */ val databaseSetup DBIO.seq( Users.schema.create, Links.schema.create, Votes.schema.create, Users forceInsertAll Seq( User(1, mario, marioexample.com, s3cr3t), User(2, Fred, fredflinstones.com, wilmalove) ), Links forceInsertAll Seq( Link(1, http://howtographql.com, Awesome community driven GraphQL tutorial,1, DateTime(2017,9,12)), Link(2, http://graphql.org, Official GraphQL web page,1, DateTime(2017,10,1)), Link(3, https://graphql.org/, GraphQL specification,2, DateTime(2017,10,2)) ), Votes forceInsertAll Seq( Vote(1, 1, 1), Vote(2, 1, 2), Vote(3, 1, 3), Vote(4, 2, 2), ) )从数据可以看出用户marioid1发布了链接 1、2用户Fredid2发布了链接 3四条投票全部由两个用户对三个链接投出。这部分完成后数据库层改动即告一段落接下来进入 GraphQL 层。定义 User→Link 关系Relation Fetcher.rel先从 User-Link 关系入手。第一个实体User需要新增links字段第二个实体Link需要新增postedBy字段两个字段使用同一个 Relation 模型。实际上Link实体需要定义两条关系第一条是按 id 查找链接现有 Fetcher 已经支持第二条是按postedBy列中存储的用户 id 过滤链接。后者需要显式定义。定义 SimpleRelation在GraphQLSchema中、fetcher 定义之前添加//add to imports: import sangria.execution.deferred.Relation //place before fetchers definition val linkByUserRel RelationLink, Int)这条关系是SimpleRelation类型只有两个参数第一个是关系名称byUser第二个是一个函数从链接实体中提取出一组用户 id。本例非常简单——postedBy本身就是该 id只需把它包进一个Seq。需要注意Relation的提取函数始终要返回一个序列即使只有一个值。把关系挂到 Fetcher 上要让 fetcher 支持按关系取数必须用Fetcher.rel而不是之前的Fetcher.apply后者在 4-deferred-resolvers.md 中介绍过//add to imports: import sangria.execution.deferred.RelationIds //replace the current linksFetcher declaration val linksFetcher Fetcher.rel( (ctx: MyContext, ids: Seq[Int]) ctx.dao.getLinks(ids), (ctx: MyContext, ids: RelationIds[Link]) ctx.dao.getLinksByUserIds(ids(linkByUserRel)) )Fetcher.rel需要两个函数第一个函数按 id 集合取实体与普通Fetcher一致第二个函数负责从数据源按关系取相关数据。这里调用的是getLinksByUserIds而ids(linkByUserRel)会按关系定义的方式提取出用户 id 并传给 DAO 函数。在 DAO 中实现批量取数函数在DAO类中添加def getLinksByUserIds(ids: Seq[Int]): Future[Seq[Link]] { db.run { Links.filter(_.postedBy inSet ids).result } }即用 Slick 的inSet一次性按postedBy集合过滤出所有相关链接。你可能会想如果 Link 同时拥有两条关系呢getLinksByUserIds还能复用吗先耐心往下读——本章稍后会专门讨论这种一个 Fetcher 对应多条关系的情况。目前只有一条关系所以直接调用ids(linkByUserRel)就能取出全部userId。为 GraphQL Object 添加与替换字段数据库和 fetcher 就绪后要把关系暴露到 GraphQL schema 中。LinkType用 ReplaceField 把 postedBy 从 Int 升级为 UserLink已经有postedBy字段但目前它只是Int而我们需要返回整个User对象。做法是替换整个字段定义并让解析器复用已定义好的usersFetcherReplaceField(postedBy, Field(postedBy, UserType, resolve c usersFetcher.defer(c.value.postedBy)) )usersFetcher.defer(...)返回一个延迟解析的值Sangria 会把所有需要解析的postedBy收集起来调用DeferredResolver见 4-deferred-resolvers.md 中的DeferredResolver.fetchers(linksFetcher, usersFetcher, votesFetcher)统一批量取回用户避免 N1 查询。UserType用 AddFields 手动新增 links 字段User实体没有links属性所以需要手动在ObjectType上添加该字段。这正是AddFieldstype class 的用途AddFields( Field(links, ListType(LinkType), resolve c linksFetcher.deferRelSeq(linkByUserRel, c.value.id)) )这里调用的是另一类 fetcher 方法——所有.deferRel...系列函数都需要两个参数而不是一个第一个参数是关系对象第二个参数是一个函数此处为c.value.id用于从当前实体取得映射值。deferRelSeq会把用户 id 按linkByUserRel关系批量预取链接列表。用 lazy val 解决循环引用为User和Link两个对象类型同时添加字段后如果直接运行你会遇到问题对象类型声明中出现了循环引用UserType引用LinkTypeLinkType又引用UserType。要规避它必须做两件事把Link与User的类型声明改为lazy val并给所有类型显式标注类型参数lazy val UserType: ObjectType[Unit, User] deriveObjectType[Unit, User]//... lazy val LinkType: ObjectType[Unit, Link] deriveObjectType[Unit, Link]//...这里为什么是Unit而不是MyContext因为在字段声明中如果没有显式使用 context例如从 context 读取数据就可以这样声明使对象字段适配任意 context而不只是某一个。lazy则保证循环引用在运行时才被解析。验证 User→Link 关系打开浏览器中的 GraphiQL 控制台执行以下查询提示如果新字段没有出现在自动补全中刷新页面即可query { link(id: 1){ id url createdAt postedBy { name links { id url } } } }这条查询先取 id1 的链接再通过postedBy取到发布者mario再通过其links取回他发布的所有链接——两条关系都正常工作了。添加 Vote→User 关系接下来添加第二条关系。事实上一半工作已经完成Vote模型本来就有userId字段数据库也准备好了剩下的只是补一个 DAO 函数和几处 schema 修改。DAO 函数在DAO类中添加def getVotesByUserIds(ids: Seq[Int]): Future[Seq[Vote]] { db.run { Votes.filter(_.userId inSet ids).result } }定义关系并改造 votesFetcher其余改动都在GraphQLSchema文件中。先添加Vote与User之间的关系val voteByUserRel RelationVote, Int)别忘了Relation的提取函数始终要返回序列然后修改votesFetcher定义同样换成Fetcher.relval votesFetcher Fetcher.rel( (ctx: MyContext, ids: Seq[Int]) ctx.dao.getVotes(ids), (ctx: MyContext, ids: RelationIds[Vote]) ctx.dao.getVotesByUserIds(ids(voteByUserRel)) )修改 UserType 与 VoteType在UserType的AddFieldstype class 中新增votes字段Field(votes, ListType(VoteType), resolve c votesFetcher.deferRelSeq(voteByUserRel, c.value.id))同时替换VoteType——用ExcludeFields(userId)隐藏原始的外键字段用AddFields暴露完整的user对象lazy val VoteType: ObjectType[Unit, Vote] deriveObjectTypeUnit, Vote, ExcludeFields(userId), AddFields(Field(user, UserType, resolve c usersFetcher.defer(c.value.userId))) )完成后即可执行嵌套更深的查询query { link(id: 1){ id url createdAt postedBy { name links { id url } votes { id user { name } } } } }可以看到我们甚至能查询为当前链接作者所发链接投过票的用户——链路为 link → postedBy(作者) → votes(作者收到的投票) → user(投票人)。就这么简单。添加 Vote→Link 关系与多关系 Fetcher 重构还有最后一条关系Vote→Link。case class 与数据库设置早已支持它无需再改直接进入 GraphQL 层。定义 voteByLinkRel 并给 LinkType 添加 votes 字段在GraphQLSchema中添加val voteByLinkRel RelationVote, Int)然后在LinkType中、已有ReplaceField之后追加AddFields( Field(votes, ListType(VoteType), resolve c votesFetcher.deferRelSeq(voteByLinkRel, c.value.id)) )对比一下两个votes字段你会发现惊人的相似//UserType Field(votes, ListType(VoteType), resolve c votesFetcher.deferRelSeq(voteByUserRel, c.value.id)) //LinkType Field(votes, ListType(VoteType), resolve c votesFetcher.deferRelSeq(voteByLinkRel, c.value.id))两者几乎一模一样唯一区别是作为第一个参数传入的Relation类型不同。事实上用这种方式你可以添加任意关系。让一个 Fetcher 支持多条关系接下来是本章较难的部分。请看现有votesFetcher定义val votesFetcher Fetcher.rel( (ctx: MyContext, ids: Seq[Int]) ctx.dao.getVotes(ids), (ctx: MyContext, ids: RelationIds[Vote]) ctx.dao.getVotesByUserIds(ids(voteByUserRel)) )第一个函数按 vote 的 id 取实体无需多言第二个函数按关系取实体——目前被硬编码为voteByUserRel关系。Fetcher API 不支持传入多个关系函数因此需要小幅重构。现在的诉求是无论按User还是按Link关系都能取到 votes。ids(voteByUserRel)提取出用户 id 并传给数据库函数我们要改掉这一写法。更好的思路是把整个ids即RelationIds[Vote]直接下传给 DAO 函数由 DAO 根据具体是哪种关系来决定用哪个字段过滤。替换votesFetcher的第二个函数(ctx: MyContext, ids: RelationIds[Vote]) ctx.dao.getVotesByRelationIds(ids)DAO 中的模式匹配取数现在补上缺失的DAO.getVotesByRelationIds函数。它应当识别请求的是哪种关系并根据关系选择对应的过滤字段//add to imports: import sangria.execution.deferred.{RelationIds, SimpleRelation} //add in body def getVotesByRelationIds(rel: RelationIds[Vote]): Future[Seq[Vote]] db.run( Votes.filter { vote rel.rawIds.collect({ case (SimpleRelation(byUser), ids: Seq[Int]) vote.userId inSet ids case (SimpleRelation(byLink), ids: Seq[Int]) vote.linkId inSet ids }).foldLeft(true: Rep[Boolean])(_ || _) } result )这个函数用模式匹配识别关系类型rel.rawIds中每一项都是(关系对象, id 序列)二元组SimpleRelation(byUser)匹配用户关系、SimpleRelation(byLink)匹配链接关系各自对应vote.userId inSet ids与vote.linkId inSet ids的过滤条件。由于一次查询可能同时包含多条关系最后用foldLeft(true: Rep[Boolean])(_ || _)把这些条件用 OR 合并生成最终 SQL。更新 VoteType 并移除冗余函数最后一步是修改VoteType移除linkId属性改为返回完整Link对象的link字段lazy val VoteType: ObjectType[Unit, Vote] deriveObjectTypeUnit, Vote, ExcludeFields(userId, linkId), AddFields(Field(user, UserType, resolve c usersFetcher.defer(c.value.userId))), AddFields(Field(link, LinkType, resolve c linksFetcher.defer(c.value.linkId))) )至此VoteType同时隐藏了userId与linkId两个外键字段对外只暴露user与link对象。你还可以顺手删除DAO.getVotesByUserIds函数——它已被getVotesByRelationIds取代不再需要。现在可以执行下面的查询验证query { links(ids :[1,2]){ url votes { user{ name } } } }它会取回链接 1、2对每个链接列出所有投票及其投票人——第三条关系Link→Vote正常工作。本章小结本章目标已达成三个模型现在拥有了完整的关系字段User拥有links与votes字段Link拥有postedBy与votes字段Vote拥有user与link字段。现在你可以自由地取回关联数据且所有关联查询都经由Fetcher批量预取不会出现逐条回查数据库的性能问题。回顾整章核心要点可以归纳为Relation 定义RelationEntity, Id)只做一件事——从实体中提取外键 id 序列Fetcher.rel 双函数第一个函数按主键 id 取实体第二个函数按RelationIds取相关实体ids(relation)负责按关系提取 id 集合Schema 字段注入ReplaceField用于替换已有标量字段为对象字段AddFields用于新增字段.deferRelSeq(rel, value)按关系延迟批量解析循环引用处理互相引用的对象类型必须声明为lazy val并显式标注类型参数多关系统一取数把RelationIds整体下传给 DAO用SimpleRelation模式匹配 inSet条件 OR 合并让一个 Fetcher 同时支持多条关系。下一章将在此基础上学习如何用 GraphQL mutations 新增并保存实体见 8-mutations.md。赞分享【免费下载链接】howtographqlThe Fullstack Tutorial for GraphQL项目地址https://gitcode.com/gh_mirrors/ho/howtographql点击查看免费下载相关推荐foo2zjsLinux打印机驱动的终极解决方案让100多款打印机完美运行foo2zjsLinux打印机驱动的终极解决方案让100多款打印机完美运行 你是否曾为Linux系统找不到合适的打印机驱动而烦恼面对市面上琳琅满目的打印机Sangria: Scala GraphQL 实现Sangria: Scala GraphQL 实现 项目基础介绍和主要编程语言 Sangria 是一个基于 Scala 编程语言的 GraphQL 实现。它旨在GraphQL Scala 与 Sangria 实战Query 参数Arguments的定义、解析与 DRY 重构GraphQL Scala 与 Sangria 实战Query 参数Arguments的定义、解析与 DRY 重构 导读 本文围绕 HowToGraphQ上一篇免费AI视频放大神器Video2X3步轻松将低清视频变4K高清下一篇如何快速上手OBS Spout2插件3步实现4K视频流无损传输创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
延伸阅读

更多相关文章

2026/9/25 11:38:04

实验室检测报告管理程序:全流程管控与CNAS/CMA合规要点

1. 报告管理程序最容易被忽略,却又是不符合项的重灾区先说个真实场景。我参与过不少实验室的CNAS现场评审和CMA资质认定复查,几乎每一次,评审组长都会翻报告,而且是专门挑那些"看起来没问题"的报告翻。翻完之后的结果很…

2026/9/25 12:43:06

Atlas 300V Pro推理卡部署YOLO:从环境搭建到性能调优实战

这块卡到底是干嘛的?先说结论,免得大家看半天还在猜。Atlas 300V Pro(也就是很多人说的“300V 24G”)是一张推理加速卡,不是训练卡,更不是普通显卡。它的核心价值是把训练好的模型(比如YOLO、Re…

2026/9/25 12:43:06

Linux USB协议栈四层架构与枚举流程深度解析

1. 为什么看懂USB协议栈比会写一个驱动更重要在Linux设备开发一线干了十多年,我带过的新人里,八成以上卡在同一个地方:能照着例程改出一个能用的USB设备驱动,但一旦遇到枚举失败、数据错乱、热插拔异常,就只能靠重启、…

2026/9/24 20:24:47

GAMP 5 基于风险的计算机化系统验证:软件分类与审计追踪实践

简介:《A Risk-Based Approach to Compliant GxP Computerized Systems》即业内熟知的GAMP 5指南,面向制药企业质量与IT合规人员、验证工程师及计算机化系统管理者,用于解决GxP法规环境下系统合规性难以科学落地的问题。文档以风险管理为主线…

2026/9/23 12:06:55

安全托管MSSP实战:从静态防御到人机协同的攻防运营与应急响应

简介:这份PPT围绕互联网业务安全托管服务展开,面向企业安全负责人、IT运维人员及关注MSSP/MSS选型的读者,重点回应传统安全过度依赖人工、碎片化静态防御难以对抗产业化攻击等痛点。资源共1个pptx文件,包体约30.63MB,以…

2026/9/25 0:02:35

AI元人文:从工具使用到思维重构的深度探索

最近半年我一直在琢磨一件事:AI元人文到底是什么?说白了,就是“用元视角重新审视人与AI的关系”,也在“探索AI如何反向逼着我们发现自己的思考边界”。标题里的“元探索”,在我看就是一层套一层的追问——当你用AI解决…

2026/9/25 0:02:35

Python+CNN车牌识别实战:从数据预处理到模型训练与部署

简介:基于Python与卷积神经网络的车牌识别项目,面向计算机视觉初学者及智能交通开发者,目标是帮助用户掌握从数据预处理、模型构建到实际部署的完整流程。压缩包共25个文件,包含jpg/png图像样本、py训练脚本、md说明文档、dat数据…

2026/9/25 0:02:35

Vim基础操作全攻略:保存退出、模式切换与高频命令实战

1. 项目概述1.1 核心需求解析今天聊聊Vim。写这个题目的原因是:几乎每个后端开发者、运维人员、数据工程师某天都会遇到一个场景——深夜加班,服务器登录界面只有黑底白字,编辑器只有vi/vim,你必须在五分钟内完成一次配置修改并保…

2026/9/22 16:34:32

USB Type-C PCB布局分区设计:电源、高速信号与PD协议全攻略

做硬件这行,Type-C接口算是典型的“看着简单,做起来全坑”的东西。光引脚就24个,高低速信号、电源、控制线全部塞在一个小小的连接器里,如果PCB布局不做规划,打样回来基本就是“插上没反应”、“高速掉线”、“静电一打…

2026/9/22 20:01:30

系统编程学习原型如何补齐稳定性边界

系统编程学习原型如何补齐稳定性边界预算有限时&#xff0c;我先优化明显多余的复制&#xff0c;而不是猜测性地换容器。用借用传递只读数据通常就能减少分配&#xff1a; fn parse(line: &str) -> Result<Item, Error> { /* ... */ }用基准确认热点确实在分配&am…

2026/9/22 13:25:41

雨花区哪家财务公司代理记账比较好?

在雨花区&#xff0c;企业处理财税事务常常面临诸多挑战&#xff0c;选择一家靠谱的财务公司至关重要。湖南巨勤财务管理咨询有限公司就是本地正规实体财税服务机构&#xff0c;深耕本地工商财税行业多年&#xff0c;熟悉当地工商局、税务局最新政策与申报流程。主营公司注册、…

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

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

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