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

GraphQL Kotlin + Spring WebFluxで始める、なるべくPure K...

yamachoo
December 10, 2022

GraphQL Kotlin + Spring WebFluxで始める、なるべくPure KotlinなGraphQLサーバーの実装入門

2022/12/10 Kotlin Fest 2022の登壇資料です。
https://fortee.jp/kotlin-fest-2022/proposal/5a5aa56c-2475-4339-b7fd-b5c0ce0ddc7e

yamachoo

December 10, 2022
Tweet

Other Decks in Programming

Transcript

  1. 株式会社マネーフォワード 名古屋開発拠点 サーバーサイドエンジニア 山中 良太 Ryota Yamanaka 今年の1月の転職を機会に、サーバーサイド Kotlinを 本格的に始める。業務では

    Spring Boot / jOOQ / GraphQL Kotlin / Kotestを中心に触っている 👀 • GitHub:@yamachoo • Twitter:@yamachoo567 • 最近のできごと ◦ 親知らずレスになりました ◦ 「水星の魔女」が面白い!
  2. 話すこと/話さないこと [ 話すこと ] • GraphQL Kotlinの導入→実装→テストまでの流れを説明 • GraphQL Kotlinを使用した開発のTipsの紹介

    → KotlinでGraphQLサーバーの開発を予定している方の参考になる情報を提供 [ 話さないこと ] • Kotlin / Spring Boot / GraphQLの基本的な仕様・機能についての説明
  3. 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
  4. なぜ、GraphQL Kotlinを使うのか? GraphQL Java / Spring for GraphQL • Javaにおけるデファクトスタンダードの

    GraphQLライブラリ • GraphQL KotlinもDGS Frameworkも内部的 に利用している DGS(Domain Graph Service) Framework • Netflixにより開発されたSpring Boot向けの GprahQLサーバーフレームワーク • スキーマファーストなライブラリ、スキーマから 自動でコードの雛形を生成できる
  5. 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
  6. 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
  7. 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クラスの作成を飛ばして サーバーを起動するとエラーで落ちる 😇
  8. 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のエンドポイント
  9. これから実装するモデル例の説明 • id • タイトル • 値段 • ジャンル •

    発売日 • 著者id 書籍(Book) 著者(Author) N 1 • id • 名前
  10. 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 }
  11. 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
  12. 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() }
  13. 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
  14. Queryの定義 @Component class BookQuery : Query { @GraphQLDescription("書籍の検索") fun book(id:

    ID): Book { // bookの検索処理 } } @Component class BooksQuery : Query { fun books( keyword: String? = null ): List<Book> { // booksの検索処理 } } type Query { book(id: ID!): BooK! books(keyword: String): [BooK!]! }
  15. 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<Book> { // booksの検索処理 } }
  16. 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! }
  17. 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
  18. ネストした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! }
  19. ネストした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 👈地味に一番良い機能
  20. その他 • Subscription • GraphQL Context • Apollo Federation •

    Automatic Persisted Queries • Data Loaders • GraphQL Client • Custom Instrumentation (GraphQL Javaを利用) なども使えるので、気になる方は公式ドキュメントへ!
  21. 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
  22. テストコードの書き方、Tipsなど GraphQL APIのテストコード webTestClient.postToGraphQLAPI(query = query) .jsonPath("$.data.book").isNotEmpty .jsonPath("$.data.book") .value<Map<String, Any>>

    { 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() ) }
  23. テストコードの書き方、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() }
  24. テストコードの書き方、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を使う } }
  25. 一年の開発を通して得たテストコードへの所感・課題感 所感 • Kotest + MockKでも必要なテストの機能は揃っている • GraphQLに関係のないテストは他で行い、シンプルに保つのが良い 課題感 •

    GraphQLのネストが深くなると全体的にテストコードが読みにくくなる • JsonPathから受け取るGprahQLの結果はJSONになり型が消えるので、 一発で正しいテストのアサーションを書ける気がしない
  26. Production Ready GraphQLとは? Production Ready GraphQL | The Book •

    GraphQLで有名なGitHubとShopifyに 勤務し、GraphQL APIを開発していた Marc-André Giroux氏が執筆 • GraphQLを本番利用するために役立つ ベストプラクティスや注意すべき事柄 が詰まった一冊
  27. Code First vs Schema First Q. スキーマファーストとコードファースト、どっちがいいの?? A. スキーマファーストとコードファーストをハイブリッドする •

    プログラミング言語の力を使ってより効率的にスキーマを定義する • かつ、SDLという信頼できる情報源を維持する 1. SDLでスキーマ設計について議論する 2. コードファーストアプローチで実装する 3. コード定義からSDLを生成する 👉両者の良いとこ取り
  28. コードファーストアプローチでスキーマファーストな設計 type Query { publishers: [Publisher!]! } type Publisher {

    id: ID! name: String! books: [Book!]! } 書籍 (Book) 著者 (Author) N 1 出版社 (Publisher) 1 N
  29. コードファーストアプローチでスキーマファーストな設計 interfaceやTODOを使用することで、 最小限の実装でKotlinのコンパイルを 通してSDLを生成する data class Publisher( val id: ID,

    val name: String ) { fun books(): List<Book> { TODO() } } @Component interface PublishersQuery : Query { fun publishers( keyword: String? = null ): List<Publisher> } type Query { publishers: [Publisher!]! } type Publisher { id: ID! name: String! books: [Book!]! }
  30. コードファーストアプローチでスキーマファーストな設計 生成した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) }
  31. コードファーストアプローチでスキーマファーストな設計 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で差分をとり、差分がある場合はエラーにする
  32. 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
  33. 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! } 👈👇複数のエラーを持てる 👈不可能な状態がない 👈クライアントが新しいエラーの追加にも対応しやすい 👈エラーの型を定義できる
  34. 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
  35. 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<Any> )
  36. 参考文献・バージョン • 参考文献 ◦ 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