news 2026/9/25 5:32:57

使用 Scala 与 Sangria 实现 GraphQL Mutations:从输入类型到数据写入的完整实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
使用 Scala 与 Sangria 实现 GraphQL Mutations:从输入类型到数据写入的完整实战

【免费下载链接】howtographql

The Fullstack Tutorial for GraphQL

项目地址:https://gitcode.com/gh_mirrors/ho/howtographql
点击查看免费下载

导读

本文讲解如何在 Scala + Sangria + Slick 构建的 GraphQL 服务中实现写操作(mutation)。你将学会定义input输入类型与对应的 Scala case class、通过deriveInputObjectType宏与FromInput类型类完成 JSON 到领域模型的转换,并逐步实现createUser、createLink、createVote三个写接口,最终在 GraphiQL 控制台中验证数据能够写入 H2 内存数据库。读完本文,你将掌握 Sangria 中 mutation 从 Schema 定义到 DAO 持久化的完整调用链。

从 query 到 mutation:关键字背后的写语义

在之前的章节中(参见 2-preparing-first-query.md)你已经学会了如何用 GraphQL 读取数据。现在要做的是写入数据:语法几乎完全相同,区别仅在于服务器如何判断客户端是想读还是想写——答案是使用mutation关键字替代query。

语法上仅此而已,但 Sangria 在内部为 mutation 引入了几类 Query 中没有的新概念,本文会逐一展开:

  • input类型:用于承载复合参数的类型定义;
  • InputObjectType:input在 Sangria 中的对应物;
  • FromInput类型类:负责把传入的 JSON 结构转换为 case class;
  • DAO 中的写入方法:通过 Slick 将数据真正持久化到数据库。

目标 Schema:HowToGraphQL 公共 Mutation 定义

本教程系列均以 HowToGraphQL 的公共 schema 为蓝本,完整定义见仓库根目录的 meta/structure.graphql。其中Mutation类型定义了四个操作:

type Mutation { signinUser(email: AUTH_PROVIDER_EMAIL): SigninPayload! createUser(name: String!, authProvider: AuthProviderSignupData!): User createLink(description: String!, url: String!, postedById: ID): Link createVote(linkId: ID, userId: ID): Vote }

本章实现除signinUser(属于认证逻辑,将在下一章 9-authentication.md 中完成)之外的三个 mutation,对应的input类型为:

input AuthProviderSignupData { email: AUTH_PROVIDER_EMAIL } input AUTH_PROVIDER_EMAIL { email: String! password: String! }

createUser接收两个参数:String类型的name与AuthProviderSignupData类型的authProvider,并返回一个User。

type 与 input 的区别

此前我们一直使用type关键字,那么input是什么?input是一种专门用作参数的类型。它与type的关键差异在于:type描述的是可查询返回的对象形状,而input描述的是客户端提交给 mutation 的参数结构。因此你会在几乎所有 mutation 中频繁看到input的身影。

实现 createUser 的完整路径

按照下面的顺序逐步实现 mutation:

  1. 为输入定义 case classes;
  2. 为这些类定义InputObjectType;
  3. 定义负责所有 mutation 的ObjectType;
  4. 将 mutation 对象接入 Schema。

第一步:定义输入用的 case classes

输入数据在 Scala 侧也需要对应的领域模型。在models包的package.scala中添加:

case class AuthProviderEmail(email: String, password: String) case class AuthProviderSignupData(email: AuthProviderEmail)

注意这里不需要给AuthProviderEmail加Option包装:虽然 GraphQL schema 中email字段是可空的,但为简化示例,直接使用非空字符串即可。

第二步:定义 InputObjectType

InputObjectType之于input,正如ObjectType之于type关键字——它告诉 Sangria 如何理解传入的数据。事实上,同一个 case class 可以同时定义ObjectType和InputObjectType,甚至可以定义多个。一个典型的例子是User实体:注册新用户与登录时需要的字段不同,你可以为这两种场景分别创建不同的InputObjectType。

在GraphQLSchema.scala中添加如下定义:

implicit val AuthProviderEmailInputType: InputObjectType[AuthProviderEmail] = deriveInputObjectTypeAuthProviderEmail ) lazy val AuthProviderSignupDataInputType: InputObjectType[AuthProviderSignupData] = deriveInputObjectType[AuthProviderSignupData]()

这里有两个值得注意的细节:

  • lazy关键字:为了避免像上一章(7-relations.md)中那样遇到类型循环依赖,建议对每个类型都使用lazy。UserType与LinkType相互引用时正是通过lazy val解决的;
  • implicit关键字:AuthProviderEmail是AuthProviderSignupData中的嵌套对象,而后者由宏(deriveInputObjectType)构建。为了让宏在展开时能找到嵌套类型,必须让AuthProviderEmailInputType以implicit形式存在于作用域中,这就是为什么它不能是lazy的原因——它必须在宏执行的那一刻即可解析。

deriveInputObjectType是sangria.macros.derive包提供的宏,可基于 case class 自动推导InputObjectType的字段与类型,极大减少样板代码。

第三步:定义 Mutation 对象

Mutation 对象的定义方式与 Query 类型类似,使用ObjectType:

val NameArg = Argument("name", StringType) val AuthProviderArg = Argument("authProvider", AuthProviderSignupDataInputType) val Mutation = ObjectType( "Mutation", fieldsMyContext, Unit, c.arg(AuthProviderArg)) ) ) )

解析要点:

  • Argument("name", StringType)声明了一个名为name、类型为标量String的参数;AuthProviderArg则将authProvider参数绑定到上一步定义的输入类型;
  • Field的第一个参数是字段名createUser,第二个参数UserType是返回类型,arguments声明参数列表,resolve是解析函数;
  • 解析函数通过c.arg(NameArg)取出参数值,调用c.ctx.dao.createUser(...)——c.ctx即MyContext(其中封装了DAO),这与此前 query 的 resolver 模式完全一致。

第四步:DAO 中的 createUser

现在DAO还缺少createUser函数,需要补充:

//add to imports import com.howtographql.scala.sangria.models.AuthProviderSignupData //add in body def createUser(name: String, authProvider: AuthProviderSignupData): Future[User] = { val newUser = User(0, name, authProvider.email.email, authProvider.email.password) val insertAndReturnUserQuery = (Users returning Users.map(_.id)) into { (user, id) => user.copy(id = id) } db.run { insertAndReturnUserQuery += newUser } }

这段代码展示了 Slick 的插入并返回自增主键惯用法:

  • User(0, ...)中的id = 0是占位值,真正的 id 由数据库自增生成;
  • (Users returning Users.map(_.id)) into { (user, id) => user.copy(id = id) }表示插入后把数据库生成的id回填到 case class 中;
  • db.run(...)提交整个数据库动作,返回Future[User]。

第五步:把 Mutation 接入 Schema

在GraphQLSchema.scala中替换schemaDefinition:

val SchemaDefinition = Schema(QueryType, Some(Mutation))

注意Some包装:所有 mutation 都是可选的,Sangria 要求用Option显式声明其存在。

如果你此时尝试运行服务器,会得到关于未实现FromInput的错误——这是下一步要解决的问题。

第六步:为输入类提供 FromInput

Sangria 需要读取 JSON 形式的结构并转换为 case class,这正是FromInput类型类的作用。实现它有两种途径:

  • 手写自己的 mapper;
  • 使用任意 JSON 库辅助完成转换。

项目在第一步(1-getting-started.md)中已引入sangria-spray-json依赖,Sangria 会借助该库把 JSON 自动转换为合适的FromInput类型。因此我们只需为 case class 定义相应的 JSONReader,并导入转换函数即可。

在GraphQLSchema文件中、InputObjectType定义之前添加:

import sangria.marshalling.sprayJson._ import spray.json.DefaultJsonProtocol._ implicit val authProviderEmailFormat = jsonFormat2(AuthProviderEmail) implicit val authProviderSignupDataFormat = jsonFormat1(AuthProviderSignupData)

jsonFormatN是 spray-json 根据 case class 字段数量生成JsonFormat的辅助函数,implicit修饰确保 Sangria 能在隐式解析时找到它们。完成之后,一切应当如预期工作。

验证:在 GraphiQL 控制台执行 createUser

在 GraphiQL 控制台(http://localhost:8080/graphiql,通过sbt run启动服务器)执行:

mutation addMe { createUser( name: "Mario", authProvider:{ email:{ email:"mario@example.com", password:"p4ssw0rd" } }){ id name } }

当然你可以换用任意测试数据。如果一切正常,就可以继续实现另外两个 mutation 了。

实现 createLink:纯标量参数的 mutation

createLink的目标 schema 定义为:

createLink(description: String!, url: String!, postedById: ID): Link

这里有一个提示:可以跳过创建 case class 的阶段,因为三个参数description、url、postedById都是String与Int这类开箱即用的简单标量,不需要自定义输入类型。建议先自行尝试,再对照下面的解法。

DAO.createLink

在DAO中添加函数:

def createLink(url: String, description: String, postedBy: Int): Future[Link] = { val insertAndReturnLinkQuery = (Links returning Links.map(_.id)) into { (link, id) => link.copy(id = id) } db.run { insertAndReturnLinkQuery += Link(0, url, description, postedBy) } }

模式与createUser完全一致:Links returning Links.map(_.id)回填自增 id,Link(0, ...)的postedBy字段是上一章(7-relations.md)中加入的用户外键。

Mutation 中的 createLink 字段

在GraphQLSchema文件的Mutation定义内追加字段:

Field("createLink", LinkType, arguments = UrlArg :: DescArg :: PostedByArg :: Nil, resolve = c => c.ctx.dao.createLink(c.arg(UrlArg), c.arg(DescArg), c.arg(PostedByArg)))

并在Mutation定义之前补充参数定义:

val UrlArg = Argument("url", StringType) val DescArg = Argument("description", StringType) val PostedByArg = Argument("postedById", IntType)

postedById虽在公共 schema 中声明为ID,但在 Slick 的表映射里对应整型外键,因此这里使用IntType。

验证 createLink

现在可以执行如下查询:

mutation addLink { createLink( url: "howtographql.com", description: "Great tutorial page", postedById: 1 ){ url description postedBy{ name } } }

得益于上一章实现的postedBy关系(fetcher + deferred resolver),返回结果中可以直接嵌套查询发布者User的name。

实现 createVote:投票的写操作

最后一个 mutation 用于投票,目标 schema:

createVote(linkId: ID, userId: ID): Vote

DAO.createVote

在DAO中添加保存新投票的函数:

def createVote(linkId: Int, userId: Int): Future[Vote] = { val insertAndReturnVoteQuery = (Votes returning Votes.map(_.id)) into { (vote, id) => vote.copy(id = id) } db.run { insertAndReturnVoteQuery += Vote(0, userId, linkId) } }

Vote(0, userId, linkId)对应 7-relations.md 中定义的Vote模型,userId与linkId分别是到Users和Links表的外键。

参数定义与 mutation 字段

添加参数定义:

val LinkIdArg = Argument("linkId", IntType) val UserIdArg = Argument("userId", IntType)

在MutationobjectType 中再添加一个字段:

Field("createVote", VoteType, arguments = LinkIdArg :: UserIdArg :: Nil, resolve = c => c.ctx.dao.createVote(c.arg(LinkIdArg), c.arg(UserIdArg)))

至此三个 mutation 全部完成,可以在 GraphiQL 控制台中逐一测试了。

本章文件变更清单

本章涉及的文件最终状态如下(对应源码文件在 giter8 模板项目marioosh/howtographql-scala-sangria.g8中,结构可参见 1-getting-started.md 中的项目树):

  • models/package.scala:新增AuthProviderEmail、AuthProviderSignupData两个输入 case class;
  • DAO.scala:新增createUser、createLink、createVote三个写入函数;
  • GraphQLSchema.scala:新增两个InputObjectType、FromInput支持、Mutation对象及其三个字段,并将SchemaDefinition更新为Schema(QueryType, Some(Mutation))。

小结与下一步

现在你已经掌握了如何通过 mutation 向服务器发送数据:定义输入 case class → 用宏推导InputObjectType→ 提供FromInput完成 JSON 转换 → 在Mutation对象中声明Field与Argument→ 在 DAO 中用 Slick 完成带主键回填的插入。这套模式对后续任何写接口都适用。

下一章 9-authentication.md 将实现登录(signinUser)与鉴权逻辑,届时会用FieldTag与 Sangria Middleware 保护createLink等写操作,你在这里学到的 mutation 知识将直接派上用场。

【免费下载链接】howtographql

The Fullstack Tutorial for GraphQL

项目地址:https://gitcode.com/gh_mirrors/ho/howtographql
点击查看免费下载
上一篇:【实测免费】V编程语言:让C代码秒变安全,编译速度提升10倍的新选择
下一篇:Morphic后端API版本控制:兼容性策略与路由设计

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/25 5:32:50

2026企业SD-WAN组网怎么选?12个选型要点与5种主流组网模式

本文面向负责多分支网络的 IT 负责人与网络架构师。厂商与产品信息为公开资料整理;文中案例均已脱敏,数字为参考值;技术估算基于公开模型,以实测为准。SD-WAN 已经过了"要不要上"的阶段。十年前它以"用互联网替代昂…

作者头像 李华
网站建设 2026/9/25 5:27:21

终极指南:Windows环境下ASP.NET应用的零停机更新与回滚实践

终极指南:Windows环境下ASP.NET应用的零停机更新与回滚实践 在当今数字化时代,用户对应用程序的可用性要求越来越高。任何因更新或维护导致的停机都可能造成业务损失和用户不满。Docker技术的出现为解决这一问题提供了全新的方案,特别是在Wi…

作者头像 李华
网站建设 2026/9/25 5:25:43

AI Agent + MCP 首次接入过程 简单记录

关键词:AI Agent 、MCP(Model Context Protocol)、 Function Calling 入门 vscode 插件 cline,配合deepseek free... mcp server MCP:GitHub - modelcontextprotocol/servers: Model Context Protocol Servers 官方…

作者头像 李华