【免费下载链接】howtographql
The Fullstack Tutorial for GraphQL
导读
本文讲解如何在 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:
- 为输入定义 case classes;
- 为这些类定义
InputObjectType; - 定义负责所有 mutation 的
ObjectType; - 将 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): VoteDAO.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
相关推荐
Sangria: Scala GraphQL 实现
Sangria: Scala GraphQL 实现 项目基础介绍和主要编程语言 Sangria 是一个基于 Scala 编程语言的 GraphQL 实现。它旨在
性能优化指南:如何为unicorn-worker-killer设置最优内存和请求阈值
性能优化指南:如何为unicorn worker killer设置最优内存和请求阈值 unicorn worker killer 是一个专为Unicorn服务器
无人机日志分析终极指南:5分钟掌握免费在线工具UAV Log Viewer
无人机日志分析终极指南:5分钟掌握免费在线工具UAV Log Viewer 你是否曾经面对复杂的无人机飞行日志文件感到无从下手?每次飞行后生成的数据文件包含了滚
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考