$30 off During Our Annual Pro Sale. View Details »

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

AGAWA Koji
October 01, 2021

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

AGAWA Koji

October 01, 2021
Tweet

More Decks by AGAWA Koji

Other Decks in Programming

Transcript

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

    View Slide

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

    View Slide

  3. Scala + Caliban で作る
    GraphQL バックエンド

    View Slide

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

    View Slide

  5. What’s GraphQL ?

    View Slide

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

    View Slide

  7. GraphQL in Scala
    Sangria Caliban
    Since 2015 2019
    Effect System Future ZIO
    Boilerplate 多い 少ない
    Resolver 密結合 疎結合

    View Slide

  8. Caliban

    View Slide

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

    View Slide

  10. case class User(
    id: UUID,
    name: String,
    age: Option[Int]
    )
    スキーマ定義
    type User {
    id: ID!
    name: String!
    age: Int
    }

    View Slide

  11. case class UserArgs(id: UUID)
    case class Query(
    user: UserArgs => Option[User],
    users: Seq[User]
    )
    スキーマ定義
    type Query {
    user(id: ID): User
    users: [User!]!
    }

    View Slide

  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))
    リゾルバ定義

    View Slide

  13. query {
    user(id: 123) {
    name
    groups {
    name
    }
    }
    }
    N+1 問題

    View Slide

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

    View Slide

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

    View Slide

  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

    View Slide

  17. ZQuery のメリット
    •並列クエリ
    •同一クエリのキャッシュ(重複排除)
    •バッチ化
    DataLoader の FP アプローチ

    View Slide

  18. Authentication

    View Slide

  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

    View Slide

  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

    View Slide

  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

    View Slide

  22. Authentication
    •ZLayerの理解が必須
    •サンプルが解読できずDiscordで作者に聞いた
    •ZIO コミュニティDiscordは活発

    View Slide

  23. Node Interface

    View Slide

  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

    View Slide

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

    View Slide

  26. sealed trait Node {
    def id: String
    }
    case class User(id: String, name: String) extends Node
    Not scalable ...

    View Slide

  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

    View Slide

  28. Solution
    https://gist.github.com/paulpdaniels/d8e932b9faee19812d2de8f56dd77a51

    View Slide

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

    View Slide

  30. Relay-based Pagination

    View Slide

  31. object PaginatedQuery {
    def apply[A: Get](
    fullTableQuery: Fragment
    )(
    cursor: Cursor,
    orderBy: Fragment = Fragment.empty
    ): TranzactIO[PageInfoDto[A]] = ???
    }
    Relay-based Pagination の一般化

    View Slide

  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]
    )

    View Slide

  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 (!)

    View Slide

  34. WITH _SOURCE AS ($table),

    [$table]
    SELECT id FROM signages ORDER BY name
    Pagination SQL

    View Slide

  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

    View Slide

  36. ミライネージの Pagination
    •Pagination の一般化
    •ページングしない、IDだけを返すSQLだけ書く
    •ZQueryによりIDを肉付けする

    View Slide

  37. Summary

    View Slide

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

    View Slide

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

    View Slide

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

    View Slide