Slide 1

Slide 1 text

GraphQL Kotlin + Spring WebFluxで始める なるべくPure Kotlinな GraphQLサーバーの実装入門 Kotlin Fest 2022 山中 良太 / @yamachoo567

Slide 2

Slide 2 text

株式会社マネーフォワード 名古屋開発拠点 サーバーサイドエンジニア 山中 良太 Ryota Yamanaka 今年の1月の転職を機会に、サーバーサイド Kotlinを 本格的に始める。業務では Spring Boot / jOOQ / GraphQL Kotlin / Kotestを中心に触っている 👀 ● GitHub:@yamachoo ● Twitter:@yamachoo567 ● 最近のできごと ○ 親知らずレスになりました ○ 「水星の魔女」が面白い!

Slide 3

Slide 3 text

話すこと/話さないこと [ 話すこと ] ● GraphQL Kotlinの導入→実装→テストまでの流れを説明 ● GraphQL Kotlinを使用した開発のTipsの紹介 → KotlinでGraphQLサーバーの開発を予定している方の参考になる情報を提供 [ 話さないこと ] ● Kotlin / Spring Boot / GraphQLの基本的な仕様・機能についての説明

Slide 4

Slide 4 text

GraphQL Kotlin GraphQL Kotlinの簡単な紹介 GraphQL Kotlinの使い方 ①Setup ②GraphQLスキーマとの対応 ③テスト Production Ready GraphQLの実践 ①Code First vs Schema First ②Errors as Data

Slide 5

Slide 5 text

GraphQL Kotlin GraphQL Kotlinの簡単な紹介 GraphQL Kotlinの使い方 ①Setup ②GraphQLスキーマとの対応 ③テスト Production Ready GraphQLの実践 ①Code First vs Schema First ②Errors as Data

Slide 6

Slide 6 text

GraphQL Kotlinとは? ● コードファーストでGraphQLスキーマを生成できる ● Spring BootやKtorに組み込んで、GraphQLサーバーを簡単に起動できる GraphQL Kotlin GraphQL Kotlin is a collection of libraries, built on top of graphql-java, that simplify running GraphQL clients and servers in Kotlin.
 GraphQL Kotlinはgraphql-javaの上に構築されたライブラリのコレクションで、 Kotlinで
 GraphQLクライアントとサーバーの実行を簡素化するものです。( by DeepL翻訳)
 ~ v.6.3.0 ★Star : 1.5k

Slide 7

Slide 7 text

なぜ、GraphQL Kotlin?

Slide 8

Slide 8 text

なぜ、GraphQL Kotlinを使うのか? GraphQL Java / Spring for GraphQL ● Javaにおけるデファクトスタンダードの GraphQLライブラリ ● GraphQL KotlinもDGS Frameworkも内部的 に利用している DGS(Domain Graph Service) Framework ● Netflixにより開発されたSpring Boot向けの GprahQLサーバーフレームワーク ● スキーマファーストなライブラリ、スキーマから 自動でコードの雛形を生成できる

Slide 9

Slide 9 text

なぜ、GraphQL Kotlinを使うのか? ● Pros ○ Kotlin向けのライブラリとして設計されている ○ コードファーストのアプローチを取れる ○ GraphQLのベストプラクティスを実践しやすい機能を提供 ● Cons ● 公式のドキュメント以外の情報や実装例がほとんど皆無… → このセッションで少しでも解決したい…!

Slide 10

Slide 10 text

● JavaよりKotlin ● スキーマファーストよりコードファースト という方にはGraphQL Kotlinがオススメ😎

Slide 11

Slide 11 text

GraphQL Kotlin GraphQL Kotlinの簡単な紹介 GraphQL Kotlinの使い方 ①Setup ②GraphQLスキーマとの対応 ③テスト Production Ready GraphQLの実践 ①Code First vs Schema First ②Errors as Data

Slide 12

Slide 12 text

GraphQL Kotlin + Spring WebFluxのSetup Spring Initializr 👆Dependenciesに『Spring Reactive Web』を追加 1. Spring Initializrで雛形の作成

Slide 13

Slide 13 text

GraphQL Kotlin + Spring WebFluxのSetup 2. graphql-kotlin-spring-serverの追加 // build.gradle.kts dependencies { // 追加 implementation("com.expediagroup:graphql-kotlin-spring-server:6.3.0") } 参考:GraphQL Kotlin Spring Server

Slide 14

Slide 14 text

GraphQL Kotlin + Spring WebFluxのSetup 3. application.ymlの編集 // application.yml graphql: packages: - "com.example.graphqlKotlinDemo.graphql" endpoint: "/hoge/graphql" introspection: enabled: ${INTROSPECTION_ENABLED} sdl: enabled: false 参考:Configuration Properties | GraphQL Kotlin

Slide 15

Slide 15 text

GraphQL Kotlin + Spring WebFluxのSetup type Query { hello: String! } 4. Queryクラスの作成 // 指定したpackagesの配下に作成 @Component class HelloQuery : Query { fun hello() = "Hello, world!" } GraphQL Kotlin Spring Serverの提供する interfaceを継承し、SpringのDIコンテナに 登録されたクラス内のメソッドが ● メソッド名 = フィールド名 ● 返り値の型 = GraphQLの型 に自動でマッピングされる Queryクラスの作成を飛ばして サーバーを起動するとエラーで落ちる 😇

Slide 16

Slide 16 text

GraphQL Kotlin + Spring WebFluxのSetup 5. サーバーの起動 $ ./gradlew bootRun # -> http://localhost:8080/playground などでアクセスできる Default Routes ● /graphql - QueryとMutationのGraphQLサーバーのエンドポイント ● /subscriptions - SubscriptionのGraphQLサーバーのエンドポイント ● /sdl - 現在のスキーマをスキーマ定義言語形式( SDL)で返すエンドポイント ● /playground - PrismaのGraphQL Playground IDEのエンドポイント

Slide 17

Slide 17 text

No content

Slide 18

Slide 18 text

GraphQL Kotlin GraphQL Kotlinの簡単な紹介 GraphQL Kotlinの使い方 ①Setup ②GraphQLスキーマとの対応 ③テスト Production Ready GraphQLの実践 ①Code First vs Schema First ②Errors as Data

Slide 19

Slide 19 text

これから実装するモデル例の説明 ● id ● タイトル ● 値段 ● ジャンル ● 発売日 ● 著者id 書籍(Book) 著者(Author) N 1 ● id ● 名前

Slide 20

Slide 20 text

TypeとFieldの定義 data class Book( val id: ID, val title: String, val price: Int, val genre: BookGenre, val releasedAt: OffsetDateTime ) enum class BookGenre { TECHNICAL, BUSINESS, NOVEL, COMIC, MAGAZINE } type Book { id: ID! title: String! price: Int! genre: BookGenre! releasedAt: DateTime! } enum BookGenre { TECHNICAL BUSINESS NOVEL COMIC MAGAZINE }

Slide 21

Slide 21 text

TypeとFieldの定義 data class Book( val id: ID, val title: String, val price: Int, val genre: BookGenre, val releasedAt: OffsetDateTime ) enum class BookGenre { TECHNICAL, BUSINESS, NOVEL, COMIC, MAGAZINE } 関連するGprahQL Kotlinのキーワード ● Types and Fields ● Primitive Types ● GraphQL ID ● Enums ● Custom Scalars ○ Schema Generator Hooks ● Extended Scalars

Slide 22

Slide 22 text

TypeとFieldの定義 Relay形式のIDを生成、またはIntに変換する拡張関数 fun Int.toID(type: String): ID { val globalId: String = Relay().toGlobalId(type, this.toString()) return ID(globalId) } fun ID.toInt(): Int { val globalId: Relay.ResolvedGlobalId = Relay().fromGlobalId(this.toString()) return globalId.id.trim().toInt() }

Slide 23

Slide 23 text

TypeとFieldの定義 Schema Generator HooksとExtended ScalarsでCustom Scalarsを定義する方法 class CustomSchemaGeneratorHooks : SchemaGeneratorHooks { override fun willGenerateGraphQLType(type: KType): GraphQLType? = when (type.classifier as? KClass<*>) { OffsetDateTime::class -> ExtendedScalars.DateTime else -> null } } class CustomSchemaGeneratorHooksProvider : SchemaGeneratorHooksProvider { override fun hooks(): SchemaGeneratorHooks = CustomSchemaGeneratorHooks() } 参考:Scalars | GraphQL Kotlin

Slide 24

Slide 24 text

Queryの定義 @Component class BookQuery : Query { @GraphQLDescription("書籍の検索") fun book(id: ID): Book { // bookの検索処理 } } @Component class BooksQuery : Query { fun books( keyword: String? = null ): List { // booksの検索処理 } } type Query { book(id: ID!): BooK! books(keyword: String): [BooK!]! }

Slide 25

Slide 25 text

Queryの定義 @Component class BookQuery : Query { @GraphQLDescription("書籍の検索") fun book(id: ID): Book { // bookの検索処理 } } 関連するGprahQL Kotlinのキーワード ● Writing Schemas with Spring ● Documenting Schema ● Lists ● Nullability ● Arguments ○ Optional fields @Component class BooksQuery : Query { fun books( keyword: String? = null ): List { // booksの検索処理 } }

Slide 26

Slide 26 text

Mutationの定義 @Component class RegisterBookMutation : Mutation { fun registerBook( input: RegisterBookInput ): Book { // 新規のbookの登録処理 } } @GraphQLValidObjectLocations( [Locations.INPUT_OBJECT] ) data class RegisterBookInput( val title: String, val price: Int, val genre: BookGenre, val releasedAt: OffsetDateTime, val authorId: ID ) type Mutation { registerBook( input: RegisterBookInput! ): Book! } input RegisterBookInput { title: String! price: Int! genre: BookGenre! releasedAt: DateTime! authorId: ID! }

Slide 27

Slide 27 text

Mutationの定義 @Component class RegisterBookMutation : Mutation { fun registerBook( input: RegisterBookInput ): Book { // 新規のbookの登録処理 } } @GraphQLValidObjectLocations( [Locations.INPUT_OBJECT] ) data class RegisterBookInput( val title: String, val price: Int, val genre: BookGenre, val releasedAt: OffsetDateTime, val authorId: ID ) 関連するGprahQL Kotlinのキーワード ● Writing Schemas with Spring ● Restricting Input and Output Types ○ @GraphQLValidObjectLocations

Slide 28

Slide 28 text

ネストしたResolverの定義 data class Author( val id: ID, val name: String ) data class Book( // 省略 private val authorId: Int ) { fun author( @GraphQLIgnore @Autowired usecase: FetchAuthorByIdUseCase ): Author { // authorIdでauthorを検索する処理 } } Book Author N 1 type Author { id: ID! name: String! } type Book { // 省略 author: Author! }

Slide 29

Slide 29 text

ネストしたResolverの定義 data class Author( val id: ID, val name: String ) 関連するGprahQL Kotlinのキーワード ● Nested Resolvers and Shared Arguments ● Excluding Fields ○ @GraphQLIgnore data class Book( // 省略 private val authorId: Int ) { fun author( @GraphQLIgnore @Autowired usecase: FetchAuthorByIdUseCase ): Author { // authorIdでauthorを検索する処理 } } Book Author N 1 👈地味に一番良い機能

Slide 30

Slide 30 text

その他 ● Subscription ● GraphQL Context ● Apollo Federation ● Automatic Persisted Queries ● Data Loaders ● GraphQL Client ● Custom Instrumentation (GraphQL Javaを利用) なども使えるので、気になる方は公式ドキュメントへ!

Slide 31

Slide 31 text

GraphQL Kotlin GraphQL Kotlinの簡単な紹介 GraphQL Kotlinの使い方 ①Setup ②GraphQLスキーマとの対応 ③テスト Production Ready GraphQLの実践 ①Code First vs Schema First ②Errors as Data

Slide 32

Slide 32 text

Kotest・MockK・spring-boot-starter-test spring-boot-starter-test

Slide 33

Slide 33 text

Kotest・MockK・spring-boot-starter-testの導入方法 // build.gradle.kts val kotestVersion = "5.5.4" dependencies { // 修正 testImplementation("org.springframework.boot:spring-boot-starter-test") { exclude(module = "mockito-core") // 使用しないmockitoを除外 } // 追加 testImplementation("io.kotest:kotest-runner-junit5:$kotestVersion") testImplementation("io.kotest:kotest-assertions-core:$kotestVersion") testImplementation("io.kotest.extensions:kotest-extensions-spring:1.1.2") testImplementation("com.ninja-squad:springmockk:3.1.1") } 参考:Quick Start | Kotest、Spring | Kotest、SpringMockK

Slide 34

Slide 34 text

Kotest・MockK・spring-boot-starter-testの導入方法 ● dependenciesに追加しただけでは、Kotestが提供している Spring extensionは有効にならない ● 各テストファイルでextensionsを上書きする方法も提供されているが、設定 ファイルでextensionsの上書きした方が便利なのでオススメ // src/test/kotlin直下にProjectConfig.ktを作成する class ProjectConfig : AbstractProjectConfig() { override fun extensions() = listOf(SpringExtension) } 参考:Spring | Kotest

Slide 35

Slide 35 text

テストコードの書き方、Tipsなど GraphQL APIのテストコード webTestClient.postToGraphQLAPI(query = query) .jsonPath("$.data.book").isNotEmpty .jsonPath("$.data.book") .value> { actualBook -> actualBook shouldBe mapOf( "id" to expectedBook.id.toID(Book::class.java.simpleName).toString(), "title" to expectedBook.title, "price" to expectedBook.price, "genre" to expectedBook.genre.name, "releasedAt" to expectedBook.releasedAt.toString() ) }

Slide 36

Slide 36 text

テストコードの書き方、Tipsなど GraphQL APIのテスト用の拡張関数 fun WebTestClient.postToGraphQLAPI(query: String): BodyContentSpec { return this .post() .uri(GRAPHQL_ENDPOINT) .accept(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON) .bodyValue(GraphQLRequest(query)) .exchange() .expectStatus().isOk .expectBody() }

Slide 37

Slide 37 text

テストコードの書き方、Tipsなど GraphQLのクエリ作成用の関数 companion object { private fun generateQueryAsString(queryName: String, input: InputData): String { return """ query { $queryName(id: "${input.id}") { id title price genre releasedAt } } """.trimIndent() // 条件が分岐するようなものでは StringBuilderを使う } }

Slide 38

Slide 38 text

一年の開発を通して得たテストコードへの所感・課題感 所感 ● Kotest + MockKでも必要なテストの機能は揃っている ● GraphQLに関係のないテストは他で行い、シンプルに保つのが良い 課題感 ● GraphQLのネストが深くなると全体的にテストコードが読みにくくなる ● JsonPathから受け取るGprahQLの結果はJSONになり型が消えるので、 一発で正しいテストのアサーションを書ける気がしない

Slide 39

Slide 39 text

GraphQL Kotlin GraphQL Kotlinの簡単な紹介 GraphQL Kotlinの使い方 ①Setup ②GraphQLスキーマとの対応 ③テスト Production Ready GraphQLの実践 ①Code First vs Schema First ②Errors as Data

Slide 40

Slide 40 text

Production Ready GraphQLとは? Production Ready GraphQL | The Book ● GraphQLで有名なGitHubとShopifyに 勤務し、GraphQL APIを開発していた Marc-André Giroux氏が執筆 ● GraphQLを本番利用するために役立つ ベストプラクティスや注意すべき事柄 が詰まった一冊

Slide 41

Slide 41 text

● Code First vs Schema First ● Errors as Data

Slide 42

Slide 42 text

GraphQL Kotlin GraphQL Kotlinの簡単な紹介 GraphQL Kotlinの使い方 ①Setup ②GraphQLスキーマとの対応 ③テスト Production Ready GraphQLの実践 ①Code First vs Schema First ②Errors as Data

Slide 43

Slide 43 text

Code First vs Schema First Q. スキーマファーストとコードファースト、どっちがいいの?? A. スキーマファーストとコードファーストをハイブリッドする ● プログラミング言語の力を使ってより効率的にスキーマを定義する ● かつ、SDLという信頼できる情報源を維持する 1. SDLでスキーマ設計について議論する 2. コードファーストアプローチで実装する 3. コード定義からSDLを生成する 👉両者の良いとこ取り

Slide 44

Slide 44 text

コードファーストアプローチでスキーマファーストな設計 Front-end Back-end Front-end & Back-end Front-end & Back-end ① Back-end ② ③ ⑤ ④

Slide 45

Slide 45 text

コードファーストアプローチでスキーマファーストな設計 type Query { publishers: [Publisher!]! } type Publisher { id: ID! name: String! books: [Book!]! } 書籍 (Book) 著者 (Author) N 1 出版社 (Publisher) 1 N

Slide 46

Slide 46 text

コードファーストアプローチでスキーマファーストな設計 interfaceやTODOを使用することで、 最小限の実装でKotlinのコンパイルを 通してSDLを生成する data class Publisher( val id: ID, val name: String ) { fun books(): List { TODO() } } @Component interface PublishersQuery : Query { fun publishers( keyword: String? = null ): List } type Query { publishers: [Publisher!]! } type Publisher { id: ID! name: String! books: [Book!]! }

Slide 47

Slide 47 text

コードファーストアプローチでスキーマファーストな設計 生成したSDLをGit管理するため、スキーマジェネレータを設定 // build.gradle.kts plugins { // 追加 id("com.expediagroup.graphql") version "6.3.0" } val graphqlGenerateSDL by tasks.getting(GraphQLGenerateSDLTask::class) { packages.set(listOf("com.example.graphqlKotlinDemo.graphql")) schemaFile.set(file("生成するschema.graphqlの出力先を指定する ")) } tasks.build { dependsOn(tasks.graphqlGenerateSDL) }

Slide 48

Slide 48 text

コードファーストアプローチでスキーマファーストな設計 CIにKotlinコードとGraphQLスキーマの差分チェックの機構を組み込む name: Verify Schema command: | ./gradlew graphqlGenerateSDL if [ -n "$(git status <生成したschema.graphqlのパスを指定> --porcelain)" ]; then echo -e "\nGenerated schema file does not match the commit content\n" exit 1 fi 1. CI上で現在のKotlinコードをもとに、GraphQLスキーマを生成する 2. Gitのstatusで差分をとり、差分がある場合はエラーにする

Slide 49

Slide 49 text

コードファーストアプローチのデメリット(トレードオフ) https://github.com/ExpediaGroup/graphql-kotlin/issues/53 GraphQL Kotlinでは、現在(2022年11月)もコード上でResolverの引数にデフォルト値を設定し ても、SDLの引数にデフォルト値が反映されないという問題がある

Slide 50

Slide 50 text

GraphQL Kotlin GraphQL Kotlinの簡単な紹介 GraphQL Kotlinの使い方 ①Setup ②GraphQLスキーマとの対応 ③テスト Production Ready GraphQLの実践 ①Code First vs Schema First ②Errors as Data

Slide 51

Slide 51 text

Errors as Data ● 開発者向けエラー :GraphQLレスポンスのerrorsキーに含める ● ユーザー向けエラー:GraphQLスキーマの一部として設計する ○ Errors as Data ■ Ad hoc error fields ■ Error Array ■ Error Interface ■ Result Types ■ Error Union List ■ Error Union List + Interface

Slide 52

Slide 52 text

GraphQL KotlinでErrors as Dataの導入 type Mutation { registerBook(input: RegisterBookInput!): RegisterBookResult! } union RegisterBookResult = Book | RegisterBookErrors type RegisterBookErrors { errors: [RegisterBookError!]! } union RegisterBookError = InputValueError | … interface UserError { message: String! } type InputValueError implements UserError { message: String! } 👈👇複数のエラーを持てる 👈不可能な状態がない 👈クライアントが新しいエラーの追加にも対応しやすい 👈エラーの型を定義できる

Slide 53

Slide 53 text

GraphQL KotlinでErrors as Dataの導入 @Component class RegisterBookMutation : Mutation { @GraphQLDescription("書籍を登録する") @GraphQLUnion( name = "RegisterBookResult", possibleTypes = [ Book::class, RegisterBookErrors::class ], description = "書籍を登録した結果" ) fun registerBook( input: RegisterBookInput ): Any { // 新規のbookの登録処理 } } 関連するGprahQL Kotlinのキーワード ● Unions ○ @GraphQLUnion ■ name ■ possibleTypes ■ description

Slide 54

Slide 54 text

GraphQL KotlinでErrors as Dataの導入 関連するGprahQL Kotlinのキーワード ● Unions ○ @GraphQLUnion ● Interfaces interface UserError { val message: String } data class InputValueError( override val message: String ): UserError @GraphQLUnion( name = "RegisterBookError", possibleTypes = [ InputValueError::class // 発生する可能性があるエラーを追加 ], description = "書籍を登録で発生するエラー " ) annotation class RegisterBookError data class RegisterBookErrors( @RegisterBookError val errors: List )

Slide 55

Slide 55 text

GraphQL Kotlin GraphQL Kotlinの簡単な紹介 GraphQL Kotlinの使い方 ①Setup ②GraphQLスキーマとの対応 ③テスト Production Ready GraphQLの実践 ①Code First vs Schema First ②Errors as Data

Slide 56

Slide 56 text

● JavaよりKotlin ● スキーマファーストよりコードファースト という方にはGraphQL Kotlinがオススメ😎

Slide 57

Slide 57 text

End.

Slide 58

Slide 58 text

参考文献・バージョン ● 参考文献 ○ GraphQL Kotlin:https://opensource.expediagroup.com/graphql-kotlin/docs ○ GraphQL:https://graphql.org/learn/ ○ Kotest:https://kotest.io/ ○ SpringMockK:https://github.com/Ninja-Squad/springmockk ○ Spring Boot:https://spring.pleiades.io/spring-boot/docs/current/reference/html/ ○ Production Ready GraphQL:https://book.productionreadygraphql.com/ ● バージョン ○ Kotlin :v 1.6.21 ○ Spring Boot :v 2.7.5 ○ GraphQL Kotlin:v 6.3.0 ● ○ Kotest :v 5.5.4 ○ SpringMockK :v 3.1.1