Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Scala + Caliban で作るGraphQL バックエンド / Making GraphQL Backend with Scala + Caliban

Scala + Caliban で作るGraphQL バックエンド / Making GraphQL Backend with Scala + Caliban

De2cd22cd6242773153ee76de1c9ecdb?s=128

AGAWA Koji

October 01, 2021
Tweet

Transcript

  1. 阿川 耕司 Scala + Caliban で作る GraphQL バックエンド

  2. 小売向けサイネージのプロダクト「ミライネージ」 でテックリードを担当している。 阿川 耕司 / Koji AGAWA 2013年度 中途入社 AI事業本部

    DX本部 ミライネージ @atty303 @atty303
  3. Scala + Caliban で作る GraphQL バックエンド

  4. 1.What's GraphQL ? 2.Caliban 3.Authentication 4.Node Interface 5.Relay-based Pagination 6.Summary

  5. What’s GraphQL ?

  6. GraphQL •APIのクエリ言語 •サーバーは型付きスキーマを公開 •クライアントは欲しいデータだけをリクエスト •クライアントとサーバーは任意の言語を使える •サポートツールが充実

  7. GraphQL in Scala Sangria Caliban Since 2015 2019 Effect System

    Future ZIO Boilerplate 多い 少ない Resolver 密結合 疎結合
  8. Caliban

  9. Caliban の特徴 •最小限のボイラープレート •Purely Functional •強い型付け •明示的なエラー •スキーマとリゾルバーの分離

  10. case class User( id: UUID, name: String, age: Option[Int] )

    スキーマ定義 type User { id: ID! name: String! age: Int }
  11. case class UserArgs(id: UUID) case class Query( user: UserArgs =>

    Option[User], users: Seq[User] ) スキーマ定義 type Query { user(id: ID): User users: [User!]! }
  12. trait UserResolver { def getUser(id: UUID): Option[User] def getAllUsers(): Seq[User]

    } val query = Query( args => userResolver.getUser(args.id), userResolver.getAllUsers ) val api = graphQL(RootResolver(query)) リゾルバ定義
  13. query { user(id: 123) { name groups { name }

    } } N+1 問題
  14. for { user <- getUser(id) groups <- ZIO.foreachPar(user.groupIds)(getGroup) } yield

    User(user.name, groups) 素朴な実装 N+1 Query
  15. for { user <- getUser(id) groups <- ZQuery.foreachPar(user.groupIds)(getGroup) } yield

    User(user.name, groups) ZQuery 1+1 Query
  16. case class GetGroup(id: Int) extends Request[Throwable, Group] val GroupDataSource =

    DataSource.fromFunctionBatchedM("GroupDataSource")( requests => dao.getGroups(requests.map(_.id)) ) def getGroup(id: Int): ZQuery[Any, Throwable, Group] = ZQuery.fromRequest(GetGroup(id))(GroupDataSource) ZQuery
  17. ZQuery のメリット •並列クエリ •同一クエリのキャッシュ(重複排除) •バッチ化 DataLoader の FP アプローチ

  18. Authentication

  19. val routesM: IO[HttpRoutes[RIO[AppEnv, *]]] = for { a = Http4sAdapter.makeHttpService

    [AppEnv with Has[Auth.Service], CalibanError](interpreter) route = new Auth[AppEnv].apply(a) } yield Router[RIO[AppEnv, *]]( "/graphql" -> route ) http4s router
  20. class Auth[R <: Has[_] with Database]() { def apply(route: HttpRoutes[RIO[R

    with Has[Auth.Service, *]]): HttpRoutes[RIO[R, *]] = Http4sAdapter.provideSomeLayerFromRequest[R with Database, Has[Auth.Service]]( route, req => makeService(req).toLayer ) def makeService(req: Request[RIO[R, *]]): RIO[Database, Auth.Service] = ??? } object Auth { trait Service { def authUser: Option[AuthUser] } } Authentication service
  21. case class Query( getAllUsers: ZIO[Has[Auth.Service], Throwable, User] ) val resolver

    = Query( ZIO.access[Has[Auth.Service]] { _.get.authUser match { case Some(authUser) => ??? case None => ??? } ) Usage
  22. Authentication •ZLayerの理解が必須 •サンプルが解読できずDiscordで作者に聞いた •ZIO コミュニティDiscordは活発

  23. Node Interface

  24. # An object with a Globally Unique ID interface Node

    { # The ID of the object. id: ID! } type User implements Node { id: ID! # Full name name: String! } Node Interface
  25. sealed trait Node { def id: String } case class

    User(id: String, name: String) extends Node Node Interface in Caliban (?)
  26. sealed trait Node { def id: String } case class

    User(id: String, name: String) extends Node Not scalable ...
  27. def lookupUser(id: String): UQuery[Option[User]] = ZQuery.some(User(id)) def lookupGroup(id: String): UQuery[Option[Group]]

    = ZQuery.some(Group(id)) val node = Node.instance .withType(lookupUser) .withType(lookupGroup) implicit val nodeSchema: Schema[Any, Node] = node.build case class Query( node: NodeArg => IO[CalibanError, Node] ) Solution
  28. Solution https://gist.github.com/paulpdaniels/d8e932b9faee19812d2de8f56dd77a51

  29. Node Interface •Caliban標準のサポート無し •サンプル実装をベースに独自実装

  30. Relay-based Pagination

  31. object PaginatedQuery { def apply[A: Get]( fullTableQuery: Fragment )( cursor:

    Cursor, orderBy: Fragment = Fragment.empty ): TranzactIO[PageInfoDto[A]] = ??? } Relay-based Pagination の一般化
  32. case class Cursor( first: Option[Int], after: Option[String], last: Option[Int], before:

    Option[String] ) Cursor / PageInfo case class PageInfoDto[A]( totalCount: Long, hasPreviousPage: Boolean, hasNextPage: Boolean, value: Vector[A] )
  33. WITH _SOURCE AS ($table), _T AS ( SELECT ROW_NUMBER() OVER

    (ORDER BY ${orderBy}) AS n, ${idColumn} AS id FROM _SOURCE ORDER BY ${orderBy} ), _afterN AS (SELECT n FROM _T WHERE _T.id = ${after}), _beforeN AS (SELECT n FROM _T WHERE _T.id = ${before}), _totalN AS (SELECT COUNT(1) AS total FROM _T), _page AS ( SELECT n, _T.id FROM _T WHERE (${cursor.after} IS NULL OR n > COALESCE((TABLE _afterN), 0)) AND (${cursor.first} IS NULL OR n <= COALESCE((TABLE _afterN), 0) + ${cursor.first}) AND (${cursor.before} IS NULL OR n < COALESCE((TABLE _beforeN), (TABLE _totalN) + 1)) AND (${cursor.last} IS NULL OR n >= COALESCE((TABLE _beforeN), (TABLE _totalN) + 1) - ${cursor.last}) ), _minN AS (SELECT MIN(n) FROM _page), _maxN AS (SELECT MAX(n) FROM _page) SELECT _totalN.total, CASE WHEN (TABLE _minN) > 1 THEN true ELSE false END AS has_previous_page, CASE WHEN (TABLE _maxN) < (TABLE _totalN) THEN true ELSE false END AS has_next_page, _page.id FROM _page, _totalN Pagination SQL (!)
  34. WITH _SOURCE AS ($table), … [$table] SELECT id FROM signages

    ORDER BY name Pagination SQL
  35. val nodes = ZQuery.foreachPar( pageInfo.value.map(id => GetSignageRequest(GRI(id.asString))) )( ZQuery.fromRequest(_)(getSignageDataSource.dataSource) )

    SignageConnection( edges = nodes.map(_.map(n => SignageEdge(n.databaseId.asString, n))), nodes = nodes, pageInfo = PageInfo( hasNextPage = pageInfo.hasNextPage, hasPreviousPage = pageInfo.hasPreviousPage, startCursor = pageInfo.value.headOption.map(_.asString), endCursor = pageInfo.value.lastOption.map(_.asString) ), totalCount = pageInfo.totalCount ) Resolver
  36. ミライネージの Pagination •Pagination の一般化 •ページングしない、IDだけを返すSQLだけ書く •ZQueryによりIDを肉付けする

  37. Summary

  38. GraphQLで得られたもの •フロントエンドに依存しすぎないバックエンド (CQRSではフロントエンドにあわせて都度Queryを実装) •Vue Apollo でフロントエンドの爆速実装 •graphql-code-generator で型安全なクエリ •安定したモデル (≠

    ドメインモデル)
  39. Summary •型安全&ボイラープレート無しの GraphQL バックエンド •純粋関数による構成からのテスト容易性 •標準仕様への準拠からの言語間の相互運用性 •責務の分離による記述の容易性

  40. ご視聴ありがとうございました。