NetflixのTech blogに書かれていたGraphQL FrameworkがOSSとして公開されていたので、さっそく使ってみました。
- How Netflix Scales its API with GraphQL Federation (Part 1)
- How Netflix Scales its API with GraphQL Federation (Part 2)
Netflix DGS
DGS(Domain Graph Service)は、Netflixが開発したGraphQLサーバーを構築するためのJava/Kotlin用のフレームワークです。
大規模GraphQLサービスの構築を想定していて、Apollo Federationを採用しています。
Spring Boot上で動作するので、非常に扱いやすいフレームワークになっています。Spring SecurityやスキーマからのCode generationもサポートしています。
詳しくは公式ドキュメントを見るのが良いです。
NetflixがGraphQLフレームワークを開発した背景
NetflixがGraphQLを採用した理由は、フロントエンドに公開するAPIの複雑さを解消するためです。
当初、各マイクロサービスが公開APIを用意していたので、下のような状態になっていました。
非常に複雑ですね。フロンドエンドの観点でみると、問い合わせ先のサーバエンドポイントがたくさんあるので、その管理が大変そうです。
サーバサイドの観点でも、認証など何か前処理が必要な場合に、各マイクロサービスでそれぞれ同じような処理を実装しなければならない等の問題が発生しそうです。
そこでNetflixではGraphQLを採用することに決めました。GraphQL採用後の図は以下の通りです。
フロンドエンドの問い合わせ先が紫色の部分、GraphQLに集約され、非常にスッキリした構成になりました。
フロンドエンドからのリクエストは、GraphQLを経由して各マイクロサービスにリクエストされます。
認証などの前処理もGraphQLの部分に実装すれば、同じ処理を各マイクロサービスで実装する必要はなさそうです。
しかし、ここで1つ問題がありました。GraphQLを運営するチームの負荷が大きい。
そこでApollo Federationを採用し、GraphQLを開発する負荷を分散させました。
Apollo Federation採用後の図は以下の通りです。
Apollo Federationを採用することで、前段にGraphQL Gatewayを配置し、そのバックエンドに各GraphQLサービスを配置する構成が可能になります。上の図だと、紫色の部分がGraphQL Gatewayで、その右の青色の部分が各マイクロサービス用のGraphQLです。
各マイクロサービス用のGraphQLは各サービスチームが運営し、GraphQL Gatewayは必要最小限のみの実装を行うことで、GraphQLの運営負荷の分散を実現しています。
実装する機能を決める
さて、実際にフレームワークを使って実装してみます。
以前書いた記事 GraphQLサーバーをJavaで実装してみると同じ機能(Query、Mutation、Subscription)を実装することにしました。機能のユースケースは以下の通りです。
- Query: 本の一覧を取得する。
- Query: 本のIDを指定して、特定の本を取得する。
- Mutation: 新しい本を登録する。
- Subscription: 新たに登録された本を通知する。
Federationを実装したかったのですが、現時点ではFederationに関するドキュメントが見つからず、特にGateway部分の実装がよくわかりません。ドキュメントが公開されたら実装してみることにします。
プロジェクトを構成する
プロジェクトはひな型はSpring Initializerで適当に作成します。必要なdependenciesは後から追加するので、ここではLombokのみ追加します。
アプリケーションを作成する
build.gradle
以下のように変更します。
plugins {
id 'org.springframework.boot' version '2.4.1'
id 'io.spring.dependency-management' version '1.0.10.RELEASE'
id 'java'
// Added for "api".
id 'java-library'
// Added for code generation from scheme.
id 'com.netflix.dgs.codegen' version '4.0.10'
}
sourceCompatibility = '11'
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
// Added to fix error "Could not find com.apollographql.federation:federation-graphql-java-support".
jcenter()
}
dependencies {
api 'com.netflix.graphql.dgs:graphql-dgs-spring-boot-starter:latest.release'
// Added for subscription.
implementation 'com.netflix.graphql.dgs:graphql-dgs-subscriptions-websockets-autoconfigure:latest.release'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
}
// Added for code generation from scheme.
generateJava {
schemaPaths = ["${projectDir}/src/main/resources/schema"]
// Set package name of generated code.
packageName = 'sandbox.dgs'
// Set "false" not to generate client code.
generateClient = false
}
schema.graphqls
GraphQLのスキーマを定義します。
schema.graphqls
はsrc/main/resources/schema
以下に登録します。
type Query {
bookById(id: ID): [Book]!
books: [Book]!
}
type Book {
id: ID
name: String
pageCount: Int
}
type Mutation {
registerBook (
id: ID
name: String
pageCount: Int
): Book
}
type Subscription {
subscribeBooks: Book
}
Java Model
Code generation plugin を使って、スキーマからJavaのクラスを作成します。
$ ./gradlew generateJava
$ ls -l build/generated/sandbox/dgs
total 8
-rw-r--r-- 1 xxxxxxx yyyyy 942 Jan 11 19:48 DgsConstants.java
drwxr-xr-x 4 xxxxxxx yyyyy 128 Jan 11 19:48 types
$ ls -l build/generated/sandbox/dgs/types
total 16
-rw-r--r-- 1 xxxxxxx yyyyy 2223 Jan 11 19:48 Book.java
-rw-r--r-- 1 xxxxxxx yyyyy 1487 Jan 11 19:48 Subscription.java
Bookクラスが作成できました。
DGS Component
次に、Query、Mutation、そしてSubscriptionを実装したServiceクラス(DGS component)を作成します。以下のことを行いました。
- クラスに
@DgsComponent
を付与します。 - メソッドに
@DgsData
を付与し、parentType
にはスキーマのtype name、field
にはfield nameを設定します。 - fieldの引数は、
@InputArgument
もしくはDataFetchingEnvironment
を使用して取得します。 - subscriptionのreturn値は
reactive-streams
のPublisher
を返します。
DataProvider
とIBookProcessor
は独自に作成したクラスです。以下の処理を行います。
- DataProvider : データ提供クラス。すべてのBookのList、または指定されたbookIdのBookを返す。
- IBookProcessor : 本の登録をイベントとして登録(emit)し、イベントのPublisherを発行(publish)する。
@AllArgsConstructor
@DgsComponent
public class BookService {
private final DataProvider dataProvider;
private final IBookProcessor bookProcessor;
@DgsData(parentType = "Query", field = "books")
public List<Book> books() {
return dataProvider.books();
}
@DgsData(parentType = "Query", field = "bookById")
public List<Book> books(@InputArgument("id") String id) {
if (id == null || id.isEmpty()) {
return dataProvider.books();
}
return List.of(dataProvider.bookById(id));
}
@DgsData(parentType = "Mutation", field = "registerBook")
public Book registerBook(DataFetchingEnvironment dataFetchingEnvironment) {
final String id = dataFetchingEnvironment.getArgument("id");
final String name = dataFetchingEnvironment.getArgument("name");
final int pageCount = dataFetchingEnvironment.getArgument("pageCount");
final Book book = new Book(id, name, pageCount);
dataProvider.books().add(book);
// Emit an event for subscription.
bookProcessor.emit(book);
return book;
}
@DgsData(parentType = "Subscription", field = "subscribeBooks")
public Publisher<Book> subscribeBooks() {
return bookProcessor.publish();
}
}
アプリケーションを実行する
作成したアプリケーションを実行します。GraphiQLが付属されているので、SpringBootのアプリケーションを起動し、エンドポイント(http://localhost:8080/graphiql
)にブラウザでアクセスします。
Subscriptionだけエラーになりました。debugログを見てみると、以下のエラーが出力されていました。
2021-01-11 21:05:52.305 DEBUG 70696 --- [nio-8080-exec-2] o.s.w.s.m.m.a.HttpEntityMethodProcessor : Writing ["Trying to execute subscription on /graphql. Use /subscriptions instead!"]
2021-01-11 21:05:52.305 DEBUG 70696 --- [nio-8080-exec-2] o.s.web.servlet.DispatcherServlet : Completed 400 BAD_REQUEST
Subscriptionの場合は、/subscriptions
にリクエストする必要があるようです。
GraphiQLのリクエスト先を変更する方法がわからなかったので、以下のようなクライアントコードを実装して試したところ、うまく動きました。
import { WebSocketLink } from "@apollo/client/link/ws";
import { SubscriptionClient } from 'subscriptions-transport-ws';
import { ApolloClient, InMemoryCache } from "@apollo/client";
import gql from 'graphql-tag';
import * as $ from 'jquery';
const GRAPHQL_ENDPOINT = 'ws://localhost:8080/subscriptions';
const client = new SubscriptionClient(GRAPHQL_ENDPOINT, {
reconnect: true,
});
const link = new WebSocketLink(client);
const apolloClient = new ApolloClient({
link: link,
cache: new InMemoryCache()
});
const asGql = gql`
subscription BookSubscription {
subscribeBooks {
id,
name
}
}
`
const s = apolloClient.subscribe({
query: asGql
})
s.subscribe({
next: ({ data }) => {
const result = document.getElementById("result");
$("#result").append(JSON.stringify(data));
$("#result").append("<br>");
}
});
まとめ
以上、Netflix DGSを使ってGraphQLサーバーを実装する方法について記述しました。
今回、試したのはあくまでGraphQLの基本機能ですが、使い勝手は悪くない印象でした。
Federationの部分を早く試してみたいですね。
作成したソースコードは、以下のGitHub上に登録しています。参考にしていただければ幸いです。