0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

GraphQL×Java入門 Query編

Posted at

はじめに

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

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

環境

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

ソースコード

GraphQL Sample

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

スキーマ

type Query {
    searchBookById(id: ID!): Book
}

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

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

モデル

Book

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

BookId

@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

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

BookResponse

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

コントローラ

@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());
    }

}

ユースケース

@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);
    }

}

リポジトリ

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

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

query

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

結果

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

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

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

公式サンプル

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

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

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

クエリ

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

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

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

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

リストで取得してみる。

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

■スキーマ

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

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

■コントローラ

@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());
    }

}

■ユースケース

@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);
    }

}

■リポジトリ

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());
    }

■クエリ

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

■結果

{
    "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

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

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

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

参考文献

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?