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

Spring for GraphQLって実際どうなの?〜小規模スタートアップの事例紹介〜

Spring for GraphQLって実際どうなの?〜小規模スタートアップの事例紹介〜

JJUG CCC Spring 2025登壇資料です。

Avatar for koga yushi

koga yushi

June 06, 2025
Tweet

Other Decks in Technology

Transcript

  1. Article ├ id ├ title
 ├ content └ authorUserId User

    ├ id └ name Ϧιʔεࢦ޲ͳREST APIͱ͸ʁ GET /articles/{articleId} GET /users/{userId} ˢผ్ɺऔಘ͢Δ ྫ͑͹ɺهࣄͱͦͷ౤ߘऀ͕औΓ͍ͨͱ͖ ˢ"SUJDMFͷଐੑͷΈฦ͢ ɹ"VUIPS 6TFS ͸ؚΊͳ͍
  2. Article ├ id ├ title
 ├ content └ User ├

    id └ name GET /articles/{articleId} Ϣʔεέʔεࢦ޲ͳREST APIͱ͸ʁ ˡର৅Ϧιʔεͱؔ࿈ͷ͋ΔϦιʔε΋ฦ͢ ྫ͑͹ɺهࣄͱͦͷ౤ߘऀ͕औΓ͍ͨͱ͖
  3. query articlesWithComments { articles { title comments { content author

    { name } } } } 1.৽ணهࣄʢλΠτϧʣͱ౤ߘͨ͠Ϣʔβʔ໊͕΄͍͠ 2.هࣄͷλΠτϧҰཡͱɺ͍ͭͨίϝϯτͱͦͷ౤ߘऀ΋஌Γ͍ͨ ϢʔεέʔεʹԠͨ͡ΫΤϦΛॻ͚ͩ͘ query fetchNewBlogForTopPage { articles { title author { name } } }
  4. query articlesWithComments { articles { title comments { content author

    { name } } } } 1.৽ணهࣄʢλΠτϧʣͱ౤ߘͨ͠Ϣʔβʔ໊͕΄͍͠ 2.هࣄͷλΠτϧҰཡͱɺ͍ͭͨίϝϯτͱͦͷ౤ߘऀ΋஌Γ͍ͨ ϢʔεέʔεʹԠͨ͡ΫΤϦΛॻ͚ͩ͘ query fetchNewBlogForTopPage { articles { title author { name } } } ˡهࣄʢλΠτϧʣͷҰཡ
  5. query articlesWithComments { articles { title comments { content author

    { name } } } } 1.৽ணهࣄʢλΠτϧʣͱ౤ߘͨ͠Ϣʔβʔ໊͕΄͍͠ 2.هࣄͷλΠτϧҰཡͱɺ͍ͭͨίϝϯτͱͦͷ౤ߘऀ΋஌Γ͍ͨ ϢʔεέʔεʹԠͨ͡ΫΤϦΛॻ͚ͩ͘ query fetchNewBlogForTopPage { articles { title author { name } } } ˡهࣄʢλΠτϧʣͷҰཡ ˡ౤ߘͨ͠Ϣʔβʔ໊
  6. query articlesWithComments { articles { title comments { content author

    { name } } } } 1.৽ணهࣄʢλΠτϧʣͱ౤ߘͨ͠Ϣʔβʔ໊͕΄͍͠ 2.هࣄͷλΠτϧҰཡͱɺ͍ͭͨίϝϯτͱͦͷ౤ߘऀ΋஌Γ͍ͨ ϢʔεέʔεʹԠͨ͡ΫΤϦΛॻ͚ͩ͘ query fetchNewBlogForTopPage { articles { title author { name } } } ˡهࣄͷλΠτϧͷҰཡ
  7. query articlesWithComments { articles { title comments { content author

    { name } } } } 1.৽ணهࣄʢλΠτϧʣͱ౤ߘͨ͠Ϣʔβʔ໊͕΄͍͠ 2.هࣄͷλΠτϧҰཡͱɺ͍ͭͨίϝϯτͱͦͷ౤ߘऀ΋஌Γ͍ͨ ϢʔεέʔεʹԠͨ͡ΫΤϦΛॻ͚ͩ͘ query fetchNewBlogForTopPage { articles { title author { name } } } ˡهࣄͷλΠτϧͷҰཡ ˡ͍ͭͨίϝϯτ
  8. query articlesWithComments { articles { title comments { content author

    { name } } } } 1.৽ணهࣄʢλΠτϧʣͱ౤ߘͨ͠Ϣʔβʔ໊͕΄͍͠ 2.هࣄͷλΠτϧҰཡͱɺ͍ͭͨίϝϯτͱͦͷ౤ߘऀ΋஌Γ͍ͨ ϢʔεέʔεʹԠͨ͡ΫΤϦΛॻ͚ͩ͘ query fetchNewBlogForTopPage { articles { title author { name } } } ˡهࣄͷλΠτϧͷҰཡ ˡ͍ͭͨίϝϯτ ˡίϝϯτͷ౤ߘऀ
  9. εΩʔϚͷॻ͖ํ UZQF2VFSZ\ BSUJDMFT<"SUJDMF> BSUJDMF BSUJDMF*E*% "SUJDMF ^ UZQF.VUBUJPO\ QPTU"SUJDMF JOQVU"SUJDMF*OQVU

    "SUJDMF ^ UZQF4VCTDSJQUJPO\ VQEBUFE"SUJDMFT BSUJDMF*ET<*%> "SUJDMF ^ JOQVU"SUJDMF*OQVU\ DPOUFOU4USJOH UJUMF4USJOH ^ UZQF"SUJDMF\ BVUIPS6TFS DPNNFOUT<$PNNFOU> DPOUFOU4USJOH JE*% MJLFE#Z<6TFS> UJUMF4USJOH ^
  10. UZQF2VFSZ\ BSUJDMFT<"SUJDMF> BSUJDMF BSUJDMF*E*% "SUJDMF ^ UZQF.VUBUJPO\ QPTU"SUJDMF JOQVU"SUJDMF*OQVU "SUJDMF

    ^ UZQF4VCTDSJQUJPO\ VQEBUFE"SUJDMFT BSUJDMF*ET<*%> "SUJDMF ^ JOQVU"SUJDMF*OQVU\ DPOUFOU4USJOH UJUMF4USJOH ^ UZQF"SUJDMF\ BVUIPS6TFS DPNNFOUT<$PNNFOU> DPOUFOU4USJOH JE*% MJLFE#Z<6TFS> UJUMF4USJOH ^ ˡJOQVU͸ೖྗ εΩʔϚͷॻ͖ํ
  11. UZQF2VFSZ\ BSUJDMFT<"SUJDMF> BSUJDMF BSUJDMF*E*% "SUJDMF ^ UZQF.VUBUJPO\ QPTU"SUJDMF JOQVU"SUJDMF*OQVU "SUJDMF

    ^ UZQF4VCTDSJQUJPO\ VQEBUFE"SUJDMFT BSUJDMF*ET<*%> "SUJDMF ^ JOQVU"SUJDMF*OQVU\ DPOUFOU4USJOH UJUMF4USJOH ^ UZQF"SUJDMF\ BVUIPS6TFS DPNNFOUT<$PNNFOU> DPOUFOU4USJOH JE*% MJLFE#Z<6TFS> UJUMF4USJOH ^ ˡJOQVU͸ೖྗ ˡzz͸ඞਢ εΩʔϚͷॻ͖ํ
  12. UZQF2VFSZ\ BSUJDMFT<"SUJDMF> BSUJDMF BSUJDMF*E*% "SUJDMF ^ UZQF.VUBUJPO\ QPTU"SUJDMF JOQVU"SUJDMF*OQVU "SUJDMF

    ^ UZQF4VCTDSJQUJPO\ VQEBUFE"SUJDMFT BSUJDMF*ET<*%> "SUJDMF ^ JOQVU"SUJDMF*OQVU\ DPOUFOU4USJOH UJUMF4USJOH ^ UZQF"SUJDMF\ BVUIPS6TFS DPNNFOUT<$PNNFOU> DPOUFOU4USJOH JE*% MJLFE#Z<6TFS> UJUMF4USJOH ^ ˡUZQF͸ग़ྗ εΩʔϚͷॻ͖ํ
  13. UZQF2VFSZ\ BSUJDMFT<"SUJDMF> BSUJDMF BSUJDMF*E*% "SUJDMF ^ UZQF.VUBUJPO\ QPTU"SUJDMF JOQVU"SUJDMF*OQVU "SUJDMF

    ^ UZQF4VCTDSJQUJPO\ VQEBUFE"SUJDMFT BSUJDMF*ET<*%> "SUJDMF ^ JOQVU"SUJDMF*OQVU\ DPOUFOU4USJOH UJUMF4USJOH ^ UZQF"SUJDMF\ BVUIPS6TFS DPNNFOUT<$PNNFOU> DPOUFOU4USJOH JE*% MJLFE#Z<6TFS> UJUMF4USJOH ^ ˡUZQF͸ग़ྗ ˢz<>z͸഑ྻ εΩʔϚͷॻ͖ํ
  14. UZQF2VFSZ\ BSUJDMFT<"SUJDMF> BSUJDMF BSUJDMF*E*% "SUJDMF ^ UZQF.VUBUJPO\ QPTU"SUJDMF JOQVU"SUJDMF*OQVU "SUJDMF

    ^ UZQF4VCTDSJQUJPO\ VQEBUFE"SUJDMFT BSUJDMF*ET<*%> "SUJDMF ^ JOQVU"SUJDMF*OQVU\ DPOUFOU4USJOH UJUMF4USJOH ^ UZQF"SUJDMF\ BVUIPS6TFS DPNNFOUT<$PNNFOU> DPOUFOU4USJOH JE*% MJLFE#Z<6TFS> UJUMF4USJOH ^ ˡUZQF͸ग़ྗ ˢz<>z͸഑ྻ ˡzz͸/PU/VMM εΩʔϚͷॻ͖ํ
  15. UZQF2VFSZ\ BSUJDMFT<"SUJDMF> BSUJDMF BSUJDMF*E*% "SUJDMF ^ UZQF.VUBUJPO\ QPTU"SUJDMF JOQVU"SUJDMF*OQVU "SUJDMF

    ^ UZQF4VCTDSJQUJPO\ VQEBUFE"SUJDMFT BSUJDMF*ET<*%> "SUJDMF ^ JOQVU"SUJDMF*OQVU\ DPOUFOU4USJOH UJUMF4USJOH ^ UZQF"SUJDMF\ BVUIPS6TFS DPNNFOUT<$PNNFOU> DPOUFOU4USJOH JE*% MJLFE#Z<6TFS> UJUMF4USJOH ^ ˣUZQF2VFSZͱͯ͠ఆٛ εΩʔϚͷॻ͖ํ
  16. UZQF2VFSZ\ BSUJDMFT<"SUJDMF> BSUJDMF BSUJDMF*E*% "SUJDMF ^ UZQF.VUBUJPO\ QPTU"SUJDMF JOQVU"SUJDMF*OQVU "SUJDMF

    ^ UZQF4VCTDSJQUJPO\ VQEBUFE"SUJDMFT BSUJDMF*ET<*%> "SUJDMF ^ JOQVU"SUJDMF*OQVU\ DPOUFOU4USJOH UJUMF4USJOH ^ UZQF"SUJDMF\ BVUIPS6TFS DPNNFOUT<$PNNFOU> DPOUFOU4USJOH JE*% MJLFE#Z<6TFS> UJUMF4USJOH ^ ˣUZQF2VFSZͱͯ͠ఆٛ ˡҾ਺ΛࢦఆՄ εΩʔϚͷॻ͖ํ
  17. type Query { article( articleId: ID! ): Article } type

    Article { id: ID! title: String! content: String! author: User! comments: [Comment!]! } @Controller class ArticleGraphQLController( private val fetchArticles: FetchArticles, ) { @QueryMapping fun article( @Argument articleId : UUID ): Article? { val articles = fetchArticleByArticleId.handle(articleId) return article?.toArticleDto() } } data class Article( val id: UUID, val title: String, val content: String, val authorId: UUID, ) Query࣮૷ྫɿQueryͷϚοϐϯάϧʔϧ
  18. type Query { article( articleId: ID! ): Article } type

    Article { id: ID! title: String! content: String! author: User! comments: [Comment!]! } @Controller class ArticleGraphQLController( private val fetchArticles: FetchArticles, ) { @QueryMapping fun article( @Argument articleId : UUID ): Article? { val articles = fetchArticleByArticleId.handle(articleId) return article?.toArticleDto() } } data class Article( val id: UUID, val title: String, val content: String, val authorId: UUID, ) Query࣮૷ྫɿQueryͷϚοϐϯάϧʔϧ ˢ!2VFSZ.BQQJOH͕͍͍ͭͯΔ ಉ໊ͷϝιουʹϚοϐϯά
  19. @Controller class ArticleGraphQLController( private val fetchArticles: FetchArticles, ) { @QueryMapping

    fun article( @Argument articleId : UUID ): Article? { val articles = fetchArticleByArticleId.handle(articleId) return article?.toArticleDto() } } data class Article( val id: UUID, val title: String, val content: String, val authorId: UUID, ) type Query { article( articleId: ID! ): Article } type Article { id: ID! title: String! content: String! author: User! comments: [Comment!]! } Query࣮૷ྫɿҾ਺ͷϚοϐϯάϧʔϧ ˢ!"SHVNFOU͕͍͍ͭͯΔ ɹಉ໊ͷҾ਺ʹϚοϐϯά
  20. @Controller class ArticleGraphQLController( private val fetchArticles: FetchArticles, ) { @QueryMapping

    fun article( @Argument articleId : UUID ): Article? { val articles = fetchArticleByArticleId.handle(articleId) return article?.toArticleDto() } } data class Article( val id: UUID, val title: String, val content: String, val authorId: UUID, ) type Query { article( articleId: ID! ): Article } type Article { id: ID! title: String! content: String! author: User! comments: [Comment!]! } Query࣮૷ྫɿฦΓ஋ͷϚοϐϯάϧʔϧ ฦΓ஋ͷܕఆٛ
  21. @Controller class ArticleGraphQLController( private val fetchArticles: FetchArticles, ) { @QueryMapping

    fun article( @Argument articleId : UUID ): Article? { val articles = fetchArticleByArticleId.handle(articleId) return article?.toArticleDto() } } data class Article( val id: UUID, val title: String, val content: String, val authorId: UUID, ) type Query { article( articleId: ID! ): Article } type Article { id: ID! title: String! content: String! author: User! comments: [Comment!]! } Query࣮૷ྫɿฦΓ஋ͷϚοϐϯάϧʔϧ εΩʔϚͰఆٛͨ͠ϑΟʔϧυ໊ͱ %50ಉ໊ͷϓϩύςΟ͕Ϛοϐϯά͞ΕΔ
  22. @Controller class ArticleGraphQLController( private val fetchArticles: FetchArticles, ) { @QueryMapping

    fun article( @Argument articleId : UUID ): Article? { val articles = fetchArticleByArticleId.handle(articleId) return article?.toArticleDto() } } data class Article( val id: UUID, val title: String, val content: String, val authorId: UUID, ) type Query { article( articleId: ID! ): Article } type Article { id: ID! title: String! content: String! author: User! comments: [Comment!]! } Query࣮૷ྫɿฦΓ஋ͷϚοϐϯάϧʔϧ ˢϚοϐϯά͞Εͯͳ͍ɺ͜ͷϑΟʔϧυ͸&EHF
  23. Edge࣮૷ྫɿEdgeͷϚοϐϯάϧʔϧ @Controller class CommentGraphQLController( private val fetchComments: FetchComments, ) {

    @BatchMapping(field = "comments") fun commentsOfArticle(articles: List<Article>): Map<Article, List<Comment>> { val articleIds = articles.map { it.id } val comments = fetchComments.handle( FetchCommentsInputData(articleIds) ) return articles.associateWith { val articleComments = comments .filter { comment -> comment.articleId == it.id } articleComments.map { it.toCommentDto() } } } } type Article { id: ID! title: String! content: String! author: User! comments: [Comment!]! } type User { id: ID! name: String! articles: [Article!]! comments: [Comment!]! }
  24. Edge࣮૷ྫɿEdgeͷϚοϐϯάϧʔϧ @Controller class CommentGraphQLController( private val fetchComments: FetchComments, ) {

    @BatchMapping(field = "comments") fun commentsOfArticle(articles: List<Article>): Map<Article, List<Comment>> { val articleIds = articles.map { it.id } val comments = fetchComments.handle( FetchCommentsInputData(articleIds) ) return articles.associateWith { val articleComments = comments .filter { comment -> comment.articleId == it.id } articleComments.map { it.toCommentDto() } } } } type Article { id: ID! title: String! content: String! author: User! comments: [Comment!]! } type User { id: ID! name: String! articles: [Article!]! comments: [Comment!]! }
  25. Edge࣮૷ྫɿEdgeͷϚοϐϯάϧʔϧ @Controller class CommentGraphQLController( private val fetchComments: FetchComments, ) {

    @BatchMapping(field = "comments") fun commentsOfArticle(articles: List<Article>): Map<Article, List<Comment>> { val articleIds = articles.map { it.id } val comments = fetchComments.handle( FetchCommentsInputData(articleIds) ) return articles.associateWith { val articleComments = comments .filter { comment -> comment.articleId == it.id } articleComments.map { it.toCommentDto() } } } } type Article { id: ID! title: String! content: String! author: User! comments: [Comment!]! } type User { id: ID! name: String! articles: [Article!]! comments: [Comment!]! } ɹɹɹɹɹɹɹɹɹˢ ΤοδͷϑΟʔϧυ໊ͱ Ξϊςʔγϣϯʹࢦఆͨ͠ϑΟʔϧυ໊ͱ Ҿ਺ͷํ͕Ұக͢ΔϝιουʹϚοϐϯά
  26. Edge࣮૷ྫɿEdgeͷϚοϐϯάϧʔϧ @Controller class CommentGraphQLController( private val fetchComments: FetchComments, ) {

    @BatchMapping(field = "comments") fun commentsOfArticle(articles: List<Article>): Map<Article, List<Comment>> { val articleIds = articles.map { it.id } val comments = fetchComments.handle( FetchCommentsInputData(articleIds) ) return articles.associateWith { val articleComments = comments .filter { comment -> comment.articleId == it.id } articleComments.map { it.toCommentDto() } } } } type Article { id: ID! title: String! content: String! author: User! comments: [Comment!]! } ϝιουͷҾ਺ʹɺؔ࿈ݩϊʔυͷ-JTU͕Θͨͬͯ͘Δˢ
  27. Edge࣮૷ྫɿEdgeͷϚοϐϯάϧʔϧ @Controller class CommentGraphQLController( private val fetchComments: FetchComments, ) {

    @BatchMapping(field = "comments") fun commentsOfArticle(articles: List<Article>): Map<Article, List<Comment>> { val articleIds = articles.map { it.id } val comments = fetchComments.handle( FetchCommentsInputData(articleIds) ) return articles.associateWith { val articleComments = comments .filter { comment -> comment.articleId == it.id } articleComments.map { it.toCommentDto() } } } } type Article { id: ID! title: String! content: String! author: User! comments: [Comment!]! } ίϝϯτΛऔಘͯ͠ɺ औಘϊʔυͱಥ͖߹Θͤͯˠ .BQʹͨ͠΋ͷΛฦ͢
  28. FluxΛ࢖͏લఏͩͱͳͥ೉͍͠ͷ͔ʁ • Spring Web fl uxͰ͸ͳ͘Spring MVCͰಈ͔ͤ͸͢Δ΋ͷͷ… • FluxͱSinksͷษڧ͕ඞཁ •

    Sinksͷ࢖͍ํ͕೉͍͠ • ಉ࣌ʹemit͸Τϥʔ → εϨουηʔϑతͳ޻෉͕ඞཁ͔΋ʁ • BlockingQueueΛ࢖ͬͯɺඇಉظॲཧΛಉظॲཧʹมߋ • ϫʔΧʔεϨουͰQueue͔ΒऔΓग़ͯ̍݅ͣͭ͠emit • ಈ͘αϯϓϧίʔυʹ࣮૷ྫΛ༻ҙͨ͠ • https://github.com/kogayushi/spring-for-graphql-tips-by-small-startup
  29. σϝϦοτ͸ʁ • Subscriptionͷ࣮૷೉қ౓͕ߴ͍🙄 • ͜ͷ͋ͱ঺հ͢Δɺಈ͘αϯϓϧίʔυ͕ࢀߟʹͳΔ͔΋ʁ • ϝϯόʔͷܦݧ͕ઙ͍͏ͪ͸ɺEdgeͷݺͼग़͠ͰN+1໰୊සൃ͕ͪ͠🐢 • BatchMappingͷར༻ΛపఈΛపఈ͢Ε͹͋Δఔ౓ղܾ͢Δ •

    ϑΝΠϧΞοϓϩʔυํ๏ͷਖ਼ղ͕ʢࠓͷͱ͜ΖʣΘ͔Βͳ͍🤷 • GraphQLʹ͸ϑΝΠϧΞοϓϩʔυͷ࢓༷͕ͳ͍ • ैདྷͷ΍ΓํͰ΍Δ͔͠ͳ͍ʁ໛ࡧத • εΩʔϚఆٛͷߋ৽࣌ɺࠩ෼Λ఻͑Δํ๏ʹ೰Ή • ·͍͍ͩ΍Γํ͕ࢥ͍͍ͭͯͳ͍ɺ໛ࡧத • ࠓ͸slackͰ౎౓ڞ༗ɺਓྗରॲ💪