Slide 1

Slide 1 text

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

Slide 2

Slide 2 text

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

Slide 3

Slide 3 text

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

Slide 4

Slide 4 text

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

Slide 5

Slide 5 text

What’s GraphQL ?

Slide 6

Slide 6 text

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

Slide 7

Slide 7 text

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

Slide 8

Slide 8 text

Caliban

Slide 9

Slide 9 text

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

Slide 10

Slide 10 text

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

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

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

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

Authentication

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

Node Interface

Slide 24

Slide 24 text

# 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

Slide 25

Slide 25 text

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

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

Relay-based Pagination

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

WITH _SOURCE AS ($table), … [$table] SELECT id FROM signages ORDER BY name Pagination SQL

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

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

Slide 37

Slide 37 text

Summary

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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