GraphQL×Java入門 Query編

## はじめに

前回は基礎編ということでざっと用語紹介させてもらいました。

今回から、実際の実装例を紹介しますー。
ただ、JavaでGraphQLの実装してもらうときのポイントとなるところを紹介するのがメインなので、ソースコードを作り込んでいるわけではないので、実務でそのまま使用するのは避けてくださいmm
(時短で結構適当にロジックや例外処理書いてあります)

## 環境

・SpringBoot:3.3.2
・Java:17
・GraphQL:22.1

## ソースコード

[GraphQL Sample](https://github.com/kehuka11/graphql_sample)

## とりあえず、データ取得してみる

### スキーマ
~~~sql
type Query {
    searchBookById(id: ID!): Book
}

type Book {
    id: ID!
    title: String!
    author: Author
}

type Author {
    id: ID!
    firstName: String!
    lastName: String!
}
~~~

### モデル

#### Book

~~~java
public record Book(BookId id, String title, Author author) {}
~~~

#### BookId

~~~java
@Getter
public class BookId {

    private static final String UUID_REGEX = "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$";
    private static final Pattern UUID_PATTERN = Pattern.compile(UUID_REGEX);

    /** ID */
    private UUID id;

    public BookId(String id) {
        this.id = convertId(id);
    }

    private UUID convertId(String id) {
        if (isValidUUID(id)) {
            return UUID.fromString(id);
        } else {
            throw new RuntimeException("Invalid ID");
        }

    }

    private boolean isValidUUID(String id) {
        if (id == null) {
            return false;
        }
        return UUID_PATTERN.matcher(id).matches();
    }

}
~~~

#### Author

~~~java
public record Author(String id, String firstName, String lastName) {}
~~~

#### BookResponse

~~~java
public record BookResponse(String id, String title, Author author) {}
~~~


### コントローラ

~~~java
@Controller
public class BookController {


    private final BookService bookService;

    public BookController(BookService bookService) {
        this.bookService = bookService;
    }

    @QueryMapping
    public BookResponse searchBookById(@Argument("id") String id) {

        Book book = bookService.findById(id);

        return new BookResponse(book.getId().getId(), book.getTitle(), book.getAuthor());
    }

}
~~~

### ユースケース

~~~java
@Service
public class BookService {

    private final BookRepository bookRepository;

    public BookService(BookRepository bookRepository) {
        this.bookRepository = bookRepository;
    }

    public Book searchById(String id) {
        BookId bookId = new BookId(id);
        return bookRepository.findBookByID(bookId);
    }

}
~~~

### リポジトリ

~~~java
public interface BookRepository {
    Book searchByID(BookId id);
}

@Repository
public class BookRepositoryImpl implements BookRepository {
    @Autowired
    DSLContext dslContext;

    @Override
    public Book searchByID(BookId id) {

        Record record =
                dslContext
                    .select(
                            BOOK.ID,
                            BOOK.TITLE,
                            BOOK.AUTHORID,
                            AUTHOR.AUTHORID,
                            AUTHOR.FIRSTNAME,
                            AUTHOR.LASTNAME
                    )
                    .from(BOOK)
                    .leftJoin(AUTHOR)
                        .on(BOOK.AUTHORID
                            .eq(AUTHOR.AUTHORID))
                    .where(BOOK.ID.eq(id.getId().toString()))
                    .fetchOne();

        return new Book(
                new BookId(Objects.requireNonNull(record).get(BOOK.ID)),
                record.get(BOOK.TITLE),
                new Author(
                        record.get(AUTHOR.AUTHORID),
                        record.get(AUTHOR.FIRSTNAME),
                        record.get(AUTHOR.LASTNAME)
                )
        );

    }
}
~~~

### データ

![スクリーンショット 2024-10-04 20.13.32.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3900459/2f4e70a8-8c60-6bc2-8c6f-f8031a045221.png)


これでとりあえず動かすための最低限の準備整ったので、以下のクエリを実行してみる。

### query
~~~graphql
query {
  searchBookById(id:"bc9e6634-89c6-4e93-b2c7-049a1e53cc37") {
    id 
    title
    author {
      id
      firstName
      lastName
    }
  }
}
~~~

### 結果

~~~json
{
    "data": {
        "searchBookById": {
            "id": "bc9e6634-89c6-4e93-b2c7-049a1e53cc37",
            "title": "test",
            "author": {
                "id": "author-1",
                "firstName": "first-test-name",
                "lastName": "last-test-name"
            }
        }
    }
}
~~~

### 初めて実装するときのポイント

私は、最初気づかなかったが、公式でサンプル出ているので、それを参考にするといいかもしれない。

[公式サンプル](https://github.com/spring-guides/gs-graphql-server)

#### スキーマで定義したQueryとエンドポイント用メソッドマッピング

マッピングさせるときのポイントは、`@QueryMapping`を付与するのと、
例のようにスキーマで定義したQuery名とメソッド名を一致させる、もしくは、`@QueryMapping(name = "searchBookById")`にする必要がある。

引数を受け取るときは、`@Argument("id")`が必要。`@Argument`だけでもいけるはず。
これがないと、引数の受け渡しがうまくいかずエラーになる。
（最初気づかず、ハマった。。。）

#### クエリ

今回の例のように、Bookの中にAuthor型が定義されているような場合、

~~~graphql
query {
  searchBookById(id:"bc9e6634-89c6-4e93-b2c7-049a1e53cc37") {
    id 
    title
    author
  }
}

~~~

のようにしてしまうとエラーになる。

オブジェクトが持つフィールドまで指定しないといけないので注意。
ちなみに、取得したいデータだけ定義すればいい。
ここがREST APIと違っていい点だなと思った。（たまに、どえらい項目数のレスポンスとかあって、そこから必要な情報だけ取り出すのしんどいし、、）


## リストで取得してみる。

ページネーションを意識した機能になっている。

■スキーマ

~~~graphql
type Query {
    searchBooks(id: ID, count: Int!): [Book]
}
~~~

`[]`で囲むとリストで取得することを定義できる。


■コントローラ

~~~java
@Controller
public class BookController {


    private final BookService bookService;

    public BookController(BookService bookService) {
        this.bookService = bookService;
    }

    @QueryMapping
    public List<BookResponse> searchBooks(@Argument("id") String id, @Argument("count") int count) {

        return
                bookService.searchBooks(id, count)
                        .stream()
                        .map(item ->
                                new BookResponse(item.id().getId().toString(), item.title(), item.author()))
                        .collect(Collectors.toList());
    }

}
~~~


■ユースケース

~~~java
@Service
public class BookService {

    private final BookRepository bookRepository;

    public BookService(BookRepository bookRepository) {
        this.bookRepository = bookRepository;
    }

    public List<Book> searchBooks(String id, int count) {
       return bookRepository.SearchBooks(id != null ? new BookId(id) : null, count);
    }

}
~~~

■リポジトリ

~~~java
public interface BookRepository {
    List<Book> SearchBooks(BookId id, int count);
}


@Repository
public class BookRepositoryImpl implements BookRepository {
    @Autowired
    DSLContext dslContext;

    @Override
    public List<Book> SearchBooks(BookId id, int count) {

        // 動的に条件を組み立てる
        Condition condition = DSL.trueCondition();

        if (id != null) {
            condition = condition.and(BOOK.ID.gt(id.getId().toString()));
        }

        return
                dslContext.select(
                                BOOK.ID,
                                BOOK.TITLE,
                                BOOK.AUTHORID,
                                AUTHOR.AUTHORID,
                                AUTHOR.FIRSTNAME,
                                AUTHOR.LASTNAME
                        ).from(BOOK)
                        .leftJoin(AUTHOR)
                        .on(BOOK.AUTHORID.eq(AUTHOR.AUTHORID))
                        .where(condition)
                        .orderBy(BOOK.ID)
                        .limit(count)
                        .fetch()
                        .stream()
                        .map(record -> new Book(
                                new BookId(record.get(BOOK.ID)),
                                record.get(BOOK.TITLE),
                                new Author(
                                        record.get(AUTHOR.AUTHORID),
                                        record.get(AUTHOR.FIRSTNAME),
                                        record.get(AUTHOR.LASTNAME)
                                )
                                )
                        )
                        .collect(Collectors.toList());
    }
~~~

■クエリ

~~~graphql
query {
  searchBooks(id: "bc9e6634-89c6-4e93-b2c7-049a1e53cc36", count: 2) {
    id 
    title
    author {
      id
      firstName
      lastName
    }
  }
}
~~~

■結果

~~~json
{
    "data": {
        "searchBooks": [
            {
                "id": "bc9e6634-89c6-4e93-b2c7-049a1e53cc37",
                "title": "test",
                "author": {
                    "id": "author-1",
                    "firstName": "first-test-name",
                    "lastName": "last-test-name"
                }
            },
            {
                "id": "bc9e6634-89c6-4e93-b2c7-049a1e53cc38",
                "title": "hh",
                "author": {
                    "id": "author-1",
                    "firstName": "first-test-name",
                    "lastName": "last-test-name"
                }
            }
        ]
    }
}
~~~

`bc9e6634-89c6-4e93-b2c7-049a1e53cc36`以降のIDの本を2件とってくるという期待値通りになっている。
リストでの取得方法も分かったので、どのくらいのデータ量取るとパフォーマンスに影響出るのか検証してみる。

### 取得件数によるパフォーマンス調査

APIを実行して、レスポンスが帰ってくるまでの時間で計測する。
データは10000件程度入れている。

| 取得件数 | 1回目の取得時間(ms) | 2回目の取得時間(ms) | 3回目の取得時間(ms) | 
| --- | --- | --- | --- |
|100 | 391.36 | 285.84 | 155.77 |
| 1000 | 196.63 | 160.02 | 144.41 |
| 10000 | 330.63 | 198.06 | 163.92 |

件数増えてもパフォーマンスは対して変わらない。

問題となるのは、クエリの指定の仕方とバックエンドの作りの問題で意図せず大量のデータを取得してしまうことだろう。

パフォーマンス周りはまた、別途調査したいと思います。


## 参考文献

https://spring.pleiades.io/spring-graphql/reference

https://graphql.org/learn/schema/#union-types