GraphQL + Javaでアップロードされたファイルを処理する

# 概要

- JavaでGraphQLサーバーを構築した際のファイルアップロード処理の実装例
- GraphQLクライアント側の説明は割愛

# 構成

- Java 11
- SpringBoot 2.3.4
- gradle 7.4.1
- GraphQL For Java
    - graphql-spring-boot-starter 7.1.0 [ref. 1]
    - io.github.kobylynskyi.graphql.codegen 5.5.0

※. 最新のSpringBootの構成ではないため注意！

# 実装方法

## ポイント

- schema定義について
    - GraphQLのschema定義ではアップロードするファイルをUploadで定義する
    - Uploadは、Java側で任意のクラスにマッピングするため、scalarに記載する
- Java側について
    - schema定義のUploadをjavax.servlet.http.Partで受け取る

## 詳細

- 全ソースは[ここから](https://github.com/keiji-ueno/demographqlfileupload)どうぞ

### ① 各種設定ファイルを準備

1. [Spring Initializr](https://start.spring.io/)のベースになるプロジェクトを作成
1. build.gradleを以下のように記載
    - ``` gradle: build.gradle
      plugins {
      	id 'org.springframework.boot' version '2.3.4.RELEASE'
      	id 'io.spring.dependency-management' version '1.0.15.RELEASE'
          id 'io.github.kobylynskyi.graphql.codegen' version '5.5.0'
      	id 'java'
      }
  
      group = 'com.example'
      version = '0.0.1-SNAPSHOT'
      sourceCompatibility = '11'
      
      repositories {
      	mavenCentral()
      }
  
      dependencies {
          implementation 'org.springframework.boot:spring-boot-starter'
      	testImplementation 'org.springframework.boot:spring-boot-starter-test'
          implementation 'com.graphql-java-kickstart:graphql-spring-boot-starter:7.1.0'
          testImplementation 'com.graphql-java-kickstart:graphql-spring-boot-starter:7.1.0'
      }
  
      tasks.named('test') {
      	useJUnitPlatform()
      }
      ```
3. URLなどはデフォルト設定のまま利用するので、application.propertiesは変更しなくてOK [ref. 2]

### ② ファイルアップロードのschema定義を作成

1. 以下のようにする [ref. 3]
    - ``` graphql: graphql/schema.graphqls
      """
      アップロードファイル
      """
      scalar Upload
  
      # 今回、Queryは使用しないが、書かないとアプリケーション起動時に怒られるため記載
      type Query {}
  
      type Mutation {
        """ 
        ファイルをアップロードする
        """
        upload(
          """ ファイル """
          file: Upload
        ): Boolean
      }
      ```

### ③ schema定義からJavaのインターフェース・クラスを生成する

1. 手で作成してもよいですが今回はgraphql-java-codegenで生成します。[ref. 4]
1. build.gradleに以下のように設定を追加
    - ``` gradle: build.gradle
      // 末尾に追記
      // GraphQL schema定義からのJavaコード生成
      apply from: 'gradle/graphql-java-codegen.gradle'
      ```
1. 設定ファイルgraphql-java-codegen.gradleを作成し、schema.graphqlsにおいてscalarに定義したUploadをMappingする型などを設定 [ref. 3],[ref. 5]
    - ``` gradle: gradle/graphql-java-codegen.gradle
      apply plugin: "io.github.kobylynskyi.graphql.codegen"
  
      compileJava.options.encoding = 'UTF-8'
  
      graphqlCodegen {
          // all config options:
          // https://github.com/kobylynskyi/graphql-java-codegen/blob/master/docs/codegen-options.md
          outputDir = new File("$buildDir/generated")
          graphqlSchemas.includePattern = "schema\\.graphqls"
          apiPackageName = "com.example.demographqlfileupload.graphql.resolvers"
          // 通常、modelが存在しないことはほぼあり得ないが、本サンプルでは存在しないため指定しない
          //modelPackageName = "com.example.demographqlfileupload.graphql.model"
          modelNameSuffix = "TO"
          // scalarのマッピング対象クラス
          customTypesMapping = [
              Upload: "javax.servlet.http.Part"
          ]
          parentInterfaces {
              queryResolver = "graphql.kickstart.tools.GraphQLQueryResolver"
              mutationResolver = "graphql.kickstart.tools.GraphQLMutationResolver"
          }
      }
      ```
1. 設定ファイルを元に以下のgradleコマンドを実行しファイルを生成する。
    - `gradle graphqlCodegen -Dfile.encoding=utf-8` 
1. `$buildDir/generated`配下にファイルが生成されているのでプロジェクトの任意のフォルダに移動。今回は以下に移動した。
    - `/demographqlfileupload/src/main/java/com/example/demographqlfileupload/graphql/resolvers`
1. 以下のようなファイルが生成されるので、プロジェクトにコピー
    - ``` java: UploadMutationResolver.java
      package com.example.demographqlfileupload.graphql.resolvers;
  
      /**
       * ファイルをアップロードする
       */
      @javax.annotation.processing.Generated(
          value = "com.kobylynskyi.graphql.codegen.GraphQLCodegen",
          date = "2022-10-25T19:12:40+0900"
      )
      public interface UploadMutationResolver extends graphql.kickstart.tools.GraphQLMutationResolver {
  
          /**
           * ファイルをアップロードする
           */
          Boolean upload(javax.servlet.http.Part file) throws Exception;
  
      }
      ```

### ④ scalarからの変換を明示

1. 以下のようなクラスを作成 [ref. 6]
    - ``` java: ExtendedScalarConfiguration.java
      package com.example.demographqlfileupload.config;
      
      import org.springframework.context.annotation.Bean;
      import org.springframework.context.annotation.Configuration;
      
      import graphql.kickstart.servlet.apollo.ApolloScalars;
      import graphql.schema.GraphQLScalarType;
      
      @Configuration
      public class ExtendedScalarConfiguration {
      
        @Bean
        public GraphQLScalarType uploadScalar() {
            return ApolloScalars.Upload;
        }
      }
      ```

### ⑤ ③で生成したinterfaceを使ってResolverクラスを作成

1. 以下のようなクラスを作成
    - ``` java: UploadResolver
      package com.example.demographqlfileupload.resolver;
      
      import javax.servlet.http.Part;
      
      import org.springframework.stereotype.Component;
      
      import com.example.demographqlfileupload.graphql.resolvers.QueryResolver;
      import com.example.demographqlfileupload.graphql.resolvers.UploadMutationResolver;
      
      @Component
      public class UploadResolver
          implements UploadMutationResolver, QueryResolver {
      
      	@Override
      	public Boolean upload(Part file) throws Exception {
            if (Objects.isNull(file)) {
                return false;
            }
      		System.out.println(file.getSubmittedFileName());
      		return null;
      	}
      }
      ```

### ⑥ 適宜ちゃんとファイル処理できるようにして完成！

## 動作確認

- いろいろGraphQLクライアントはあると思いますが、私はPostmanで統一したいのでPostmanを使っています。
- ファイルアップロードについては癖があるので注意

### Postmanでの手順 [ref. 7]

1. GraphQLではなくform-dataを選択
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384027/755b461b-cd01-2618-fab8-248c5f1eb41b.png)
1. KEY/VALUEに以下のように設定
    - `operations` / `{"query":"mutation upload($file: Upload){upload(file: $file)}","variables":{"file":null}}`
    - `map` / `{"0":["variables.file"]}`
    - `0` / 任意のファイル ※. ここは形式をTextではなくFileに変更
1. 実行！ファイル情報も取得できているしよさそう
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384027/d40f0a99-7a5d-b867-8198-a5c16e029fd3.png)
    ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2384027/131bccc1-99c0-ce4e-2227-90a2f7612558.png)

# まとめ

- GraphQLのschema定義scalarでUploadを指定する
- Java側ではschema定義のUploadをjavax.servlet.http.Partで受け取れるようマッピングしてやる
- SpringBootのバージョンによって取得するGraphQLのDependenciesが変わっているので注意！！

# その他

- GraphQLはアップロードはサポートしていますがダウンロードに使うのは想定されていないようです。自前で実装しましょう。
    - Springならコントローラーを追加して通常のHttpRequestとして処理すればOK
- Spring Boot 2.7 以降は[Spring Initializr](https://start.spring.io/)でSpring for GraphQLというDependenciesを利用することができます。提供されるパッケージが変わるため本稿の実装はそのままは使えないです。
    - 例えば本稿ではgraphql.kickstartのパッケージを利用していますが、バージョンによってはこのパッケージは提供されていないため利用できなくなります。[ref. 8], [ref. 9], [ref. 10]

# 参考情報

## 引用部

- [ref. 1 GraphQL Java Kickstart][ref. 1]
- [ref. 2 Enable GraphQL Servlet][ref. 2]
- [ref. 3 How to upload files with graphql-java][ref. 3]
- [ref. 4 kobylynskyi/graphql-java-codegen][ref. 4]
- [ref. 5 graphql-java-codegen/docs/codegen-options.md][ref. 5]
- [ref. 6 Custom Scalar実装（GraphQL-SpringBoot）][ref. 6]
- [ref. 7 Is there any way to upload files via postman into a GraphQL API?][ref. 7]
- [ref. 8 GraphQL with spring-boot-starter-graphql][ref. 8]
- [ref. 9 Spring for GraphQL Documentation][ref. 9]
- [ref. 10 Getting started with Spring for GraphQL][ref. 10]

[ref. 1]: https://www.graphql-java-kickstart.com/spring-boot/getting-started "GraphQL Java Kickstart"
[ref. 2]: https://github.com/graphql-java-kickstart/graphql-spring-boot#enable-graphql-servlet "Enable GraphQL Servlet"
[ref. 3]: https://stackoverflow.com/questions/57372259/how-to-upload-files-with-graphql-java "How to upload files with graphql-java?"
[ref. 4]: https://github.com/kobylynskyi/graphql-java-codegen "kobylynskyi/graphql-java-codegen"
[ref. 5]: https://github.com/kobylynskyi/graphql-java-codegen/blob/master/docs/codegen-options.md "graphql-java-codegen/docs/codegen-options.md"
[ref. 6]: https://qiita.com/kurukuruz/items/7c3a1c0e9be954665c52 "Custom Scalar実装（GraphQL-SpringBoot）"
[ref. 7]: https://stackoverflow.com/questions/61892306/is-there-any-way-to-upload-files-via-postman-into-a-graphql-api "Is there any way to upload files via postman into a GraphQL API?"
[ref. 8]: https://blog.devgenius.io/graphql-with-spring-boot-starter-graphql-7b406998c0b5 "GraphQL with spring-boot-starter-graphql"
[ref. 9]: https://docs.spring.io/spring-graphql/docs/current/reference/html/ "Spring for GraphQL Documentation"
[ref. 10]: https://www.graphql-java.com/tutorials/getting-started-with-spring-boot/ "Getting started with Spring for GraphQL"

## 全体的なとこ

- [Spring BootでGraphQLを試した](https://qiita.com/kurukuruz/items/b948e77a56b946864a86)
- [Uploading files with Spring and GraphQL](https://dimitr.im/uploading-files-spring-graphql)