はじめに
前回は基礎編ということでざっと用語紹介させてもらいました。
今回から、実際の実装例を紹介しますー。
ただ、JavaでGraphQLの実装してもらうときのポイントとなるところを紹介するのがメインなので、ソースコードを作り込んでいるわけではないので、実務でそのまま使用するのは避けてくださいmm
(時短で結構適当にロジックや例外処理書いてあります)
環境
・SpringBoot:3.3.2
・Java:17
・GraphQL:22.1
ソースコード
とりあえず、データ取得してみる
スキーマ
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)
)
);
}
}
データ
これでとりあえず動かすための最低限の準備整ったので、以下のクエリを実行してみる。
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 |
件数増えてもパフォーマンスは対して変わらない。
問題となるのは、クエリの指定の仕方とバックエンドの作りの問題で意図せず大量のデータを取得してしまうことだろう。
パフォーマンス周りはまた、別途調査したいと思います。
参考文献