MicroProfile GraphQL introduction

原文はこちら。
The original entry was written by Phillip Krüger (Red Hat).
https://www.phillip-kruger.com/post/microprofile_graphql_introduction/

MicroProfile GraphQL 1.0がリリースされました。このエントリではこのリリースで利用可能な機能を見ていきます(訳注:2020/04/06現在では既に1.0.1がリリースされています)。

Microprofile GraphQL Specification
https://github.com/eclipse/microprofile-graphql

サンプルアプリケーションを参照します。ここでThorntailをランタイムとして利用し、SmallRye GraphQLという実装を手作業で追加します。

Thorntail
https://thorntail.io/
SmallRye
https://github.com/smallrye/smallrye-graphql

What is GraphQL?

GraphQLはAPIのためのオープンソースのデータクエリおよび操作言語で、既存のデータでクエリを実行するためのランタイムです。GraphQLはクライアントからの文字列を解釈し、理解可能で予測できる、事前定義済みの方法でデータを返します。GraphQLはRESTの代替手段ではありますが、必ずしもRESTを置き換えるものではありません。

GraphQL仕様の全文は以下からどうぞ。

GraphQL
http://spec.graphql.org/draft/

What is MicroProfile GraphQL?

MicroProfile GraphQL仕様は、コードファースト(code first)なAPIを提供することを意図しています。これを使うと、ユーザーが素早く移植可能なGraphQLベースのアプリケーションをJavaで開発できます。この仕様の全ての実装には、主に2個の要件があります。

  • GraphQL Schemaを生成し、利用可能にすること。これを実現するユーザーコードの注釈を見てこれを実現する。そして全てのGraphQL QueryとMutationだけでなく、QueryやMutationのレスポンスタイプもしくは引数を使って暗黙のうちに定義された全てのエンティティを含める必要がある。
  • GraphQLリクエストを実行すること。これはQueryもしくはMutationのいずれかの形式で、最小限、仕様上はこれらのリクエストをHTTPでの実行をサポートしなければならない。

MicroProfile GraphQLの仕様全文は以下からどうぞ。

MicroProfile GraphQL
https://download.eclipse.org/microprofile/microprofile-graphql-1.0/microprofile-graphql.html

The Example

このエントリ全体で、あるアクティビティに基づいて個人にスコアを付けるシステムの例を参照します。

  • 体を動かす頻度、例えばジムに行く頻度の例
  • 例えば制限速度を超えないなどで、どれほど安全に運転するか。
  • 1日の歩数、など。

サンプルにはPersonオブジェクトがあり、これには個人に関する詳細情報(名前、名字、住所、電話番号、SNSなど)の全てが入っています。そして、あるアクションに対するスコアを持つScoreオブジェクトがあります。

Query data

GraphQLでは、QueryはRESTのGETに似ています。Queryはデータを変更せず、データを受け取るのみです。GraphQLを使うと、実際に必要なものを尋ねることができる点が(RESTと)異なります。

Using JAX-RS

例えば、個人に関する情報が必要だとします。JAX-RSでは、次のようなものを作成します。

@Path("/person")
@Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
@Tag(name = "Person service",description = "Person Services")
public class PersonRestApi {

    @Inject 
    private PersonDB personDB;

    @GET 
    @Path("/{id}")
    @Operation(description = "Get a person using the person's Id")
    public Response getPerson(@PathParam("id") int id){
        return Response.ok(personDB.getPerson(id)).build();
    }
}

[注意]

  • エンドポイントを文書化するためには、MicroProfile OpenAPI(@Tagと@Operation)も必要です。
  • /openapiでスキーマを取得できます。これには利用可能なサービスやモデルが記述されています。
  • /person/1に対してGETを実行するとPersonがjson形式で返ります。
  • Personのサイズ次第でファイルが大きくなる可能性があります。
{
  "addresses": [
    {
      "code": "49512-5971",
      "lines": [
        "212 Neomi Ridges",
        "Lake Winfordview",
        "Wisconsin",
        "Myanmar"
      ]
    },
    {
      "code": "09444-8413",
      "lines": [
        "1443 Bogan Harbor",
        "East Jacinto",
        "New York",
        "Timor-Leste"
      ]
    }
  ],
  "biography": "Perferendis asperiores non. Cumque voluptas et nisi rerum tenetur quaerat. Doloribus nihil et. Autem autem sapiente et ut inventore ipsum sint. Aut error pariatur quidem itaque deserunt. Dolorum aspernatur exercitationem. In tenetur quia iste qui quia officiis.",
  "birthDate": "25/01/1967",
  "coverphotos": [
    "http://lorempixel.com/g/1920/1200/city/"
  ],
  "creditCards": [
    {
      "expiry": "2013-9-12",
      "number": "1234-2121-1221-1211",
      "type": "solo"
    }
  ],
  "emailAddresses": [
    "leo.lesch@gmail.com",
    "laurie.ankunding@gmail.com"
  ],
  "favColor": "lime",
  "gender": "Female",
  "id": 1,
  "idNumber": "334-58-1049",
  "imClients": [
    {
      "identifier": "Slack",
      "im": "cory.langworth"
    },
    {
      "identifier": "ICQ",
      "im": "booker.boyle"
    }
  ],
  "interests": [
    "PUBG",
    "meditation"
  ],
  "joinDate": "14/09/2019",
  "locale": "en-ZA",
  "maritalStatus": "Divorced",
  "names": [
    "Nicholas",
    "Edwin"
  ],
  "nicknames": [
    "Oscar Ruitt"
  ],
  "occupation": "Officer",
  "organization": "Hodkiewicz Group",
  "phoneNumbers": [
    {
      "number": "1-852-821-3630",
      "type": "Cell"
    },
    {
      "number": "783.874.6411",
      "type": "Home"
    },
    {
      "number": "(489) 031-5336 x8428",
      "type": "Work"
    }
  ],
  "profilePictures": [
    "https://s3.amazonaws.com/uifaces/faces/twitter/stan/128.jpg"
  ],
  "relations": [
    {
      "personURI": "/rest/person/46",
      "relationType": "Spouse"
    }
  ],
  "skills": [
    "Communication",
    "Confidence"
  ],
  "socialMedias": [
    {
      "name": "@jordan.hane",
      "username": "Twitter"
    },
    {
      "name": "annamarie.casper",
      "username": "Facebook"
    }
  ],
  "surname": "Zulauf",
  "taglines": [
    "You will find only what you bring in.",
    "Chuck Norris' keyboard doesn't have a F1 key, the computer asks him for help."
  ],
  "title": "Dr.",
  "userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 12_0_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/12.0 Mobile/15E148 Safari/604.1",
  "username": "trent.kub",
  "website": "http://www.kasey-nikolaus.io"
}

これでモバイルアプリを作成すれば、もしくは別のサービスからこのAPIを利用したとしても、例えば名前、名字、idNumberにしか関心がないかもしれませんが、多数のデータが返ってくるため、無関係なデータを全て除外する必要があります。

Using MicroProfile GraphQL

では、MicroProfile GraphQLを追加して、どのようにクエリが可能か見てみましょう。

(少なくともランタイムがこれを標準機能としてサポートするまでは)まず、以下の依存関係をpom.xmlに追加します。

<dependency>
    <groupId>io.smallrye</groupId>
    <artifactId>smallrye-graphql-servlet</artifactId>
    <version>1.0.0</version>
</dependency>

これで、GraphQL Endpointを追加できます(上記のRESTサービスに類似しています)。

@GraphQLApi
public class ProfileGraphQLApi {
    
    @Inject 
    private PersonDB personDB;

    @Query
    @Description("Get a person using the person's Id")
    public Person getPerson(@Name("personId") int personId){
        return personDB.getPerson(personId);
    }
}

ご覧になるとわかる通り、コードは全くRESTサービスと同じですが、新たな注釈があります。

注釈説明
@GraphQLApiランタイムに対し、GraphQL Endpointであること、@Queryと@Mutationで注釈がついた全てのメソッドが公開されることを指示する。
@Queryクエリ可能なメソッド

/graphqlに対してクエリをPOSTすることができるようになったので、上記サンプルを使って個人の名前、名字、idNumberを取得してみましょう。

{
  person(personId:1){
    names
    surname
    idNumber
  }
}

これで求めたものだけが返ってきます。

{
  "data": {
    "person": {
      "names": [
        "Nicholas",
        "Edwin"
      ],
      "surname": "Zulauf",
      "idNumber": "334-58-1049"
    }
  }
}

ご覧になった通り、ペイロードはRESTの例と比べて圧倒的に小さくなっています。

これでオーバーフェッチの問題を解決しました(オーバーフェッチとは、大量のデータをフェッチしすぎることであり、結果としてレスポンス中に使わないデータがあります)。

次に、この人物のスコアも取得したいとします。RESTの例では、元の巨大なjsonからidNumberを取得し、その後scoreサービスを呼び出す必要があります。

@ApplicationScoped
@Path("/score")
@Consumes(MediaType.APPLICATION_JSON) @Produces(MediaType.APPLICATION_JSON)
@Tag(name = "Score service",description = "Score Services")
public class ScoreRestApi {
    
    @Inject 
    private ScoreDB scoreDB;
    
    @GET 
    @Path("/{idNumber}")
    @Operation(description = "Get a person's scores using the person's Id Number")
    public Response getScores(@PathParam("idNumber") String idNumber){
        return Response.ok(scoreDB.getScores(idNumber)).build();
    }
}

curl -X GET “http://localhost:8080/rest/score/334-58-1049&#8221; で呼び出すと、以下のようなレスポンスがあります。

[
  {
    "id": "4873aa28-cc7e-49b0-841f-ea4df4db0fa2",
    "name": "Driving",
    "value": 31
  },
  {
    "id": "8645363d-fa46-4cb9-8bca-e14435dd5d17",
    "name": "Fitness",
    "value": 85
  },
  {
    "id": "4b2af0b2-2d8b-4210-96e9-2692769c5ef7",
    "name": "Activity",
    "value": 91
  },
  {
    "id": "36d4585f-7061-4ad4-b5cc-5561eba983e7",
    "name": "Financial",
    "value": 22
  }
]

(idには関心がないかもしれません)

MicroProfile GraphQLの場合、JavaのPOJOモデルにない場合でもGraphQLでフィールドを追加できる特殊なクエリを追加します。これが可能なのは、GraphQLのすべてのフィールドが単なる別のクエリであるためです。通常の場合、オブジェクトからプロパティをフェッチするだけですが、別のクエリを使用してフィールドをフェッチすることもできます。以下のメソッドを既存のEndpointに追加して、personにscoreフィールドを追加できます。

public List<Score> getScores(@Source Person person) {
    return scoreDB.getScores(person.getIdNumber());
}

元のクエリにフィールドを追加し、関心のあるサブフィールドも指定します。

{
  person(personId:1){
    names
    surname
    idNumber
    scores {
      name
      value
    }
  }
}

上記の機能は、元のクエリのscoreの名前と値を使ってscoresに含めています。

{
  "data": {
    "person": {
      "names": [
        "Nicholas",
        "Edwin"
      ],
      "surname": "Zulauf",
      "idNumber": "334-58-1049",
      "scores": [
        {
          "name": "Driving",
          "value": 27
        },
        {
          "name": "Fitness",
          "value": 36
        },
        {
          "name": "Activity",
          "value": 88
        },
        {
          "name": "Financial",
          "value": 88
        }
      ]
    }
  }
}

アンダーフェッチの問題も解決しました(アンダーフェッチはエンドポイント呼び出しで十分なデータがないため、結果として2個目のエンドポイントへの呼び出しが必要になること)。

RESTの場合、大量のデータが返る呼び出し後、返ってきたものから必要なものを取得するためにデータをフィルタリングしなければならず(オーバーフェッチ)、そして必要なデータの残りを取得するために別のサービスへの呼び出しをしなければなりませんでした(アンダーフェッチ)。

GraphQLでは、1回の呼び出しで必要なデータ全てを返すことができました。

そして、スコアデータが不要な場合、そのメソッドは呼ばれることはないため、バックエンドが効率的になります。

Mutations

GraphQLでは、データの取得(GET)以外は全てmutation(PUT、POST、DELETE)と呼びます。

全てのmutationはデータを変更して結果を返します。

Personを作成もしくは更新する必要があるとした場合、以下のメソッドを既存のEndpointに追加できます。

@Mutation
public Person updatePerson(Person person){
    return personDB.updatePerson(person);    
}

Personを削除するためには

@Mutation
public Person deletePerson(int id){
    return personDB.deletePerson(id);    
}

このmutationを使ってPersonを追加できます(idフィールドがないことに注意してください)。

mutation CreatePerson{
  updatePerson(person : 
    {
      names: "Phillip"
    }
  ){
    id
    names
    surname
    profilePictures
    website
  }
}

この結果、新規作成されたPersonが返ります。

{
  "data": {
    "updatePerson": {
      "id": 101,
      "names": [
        "Phillip"
      ],
      "surname": null,
      "profilePictures": null,
      "website": null
    }
  }
}

ではmutationを使って存在しないフィールドを更新しましょう(idが含まれていることに注意してください)。

mutation UpdatePerson{
  updatePerson(person : 
    {
      id: 101, 
      names:"Phillip",
      surname: "Kruger", 
      profilePictures: [
        "https://pbs.twimg.com/profile_images/1170690050524405762/I8KJ_hF4_400x400.jpg"
      ],
      website: "http://www.phillip-kruger.com"
    }){
    id
    names
    surname
    profilePictures
    website
  }
}

更新されたPersonが返ってきます。

{
  "data": {
    "updatePerson": {
      "id": 101,
      "names": [
        "Phillip"
      ],
      "surname": "Kruger",
      "profilePictures": [
        "https://pbs.twimg.com/profile_images/1170690050524405762/I8KJ_hF4_400x400.jpg"
      ],
      "website": "http://www.phillip-kruger.com"
    }
  }
}

最後に、このPersonを削除できます。

mutation DeletePerson{
  deletePerson(id :101){
    id
    names
    surname
    profilePictures
    website
  }
}

上記の機能で、削除される直前のデータを返します。

{
  "data": {
    "deletePerson": {
      "id": 101,
      "names": [
        "Phillip"
      ],
      "surname": "Kruger",
      "profilePictures": [
        "https://pbs.twimg.com/profile_images/1170690050524405762/I8KJ_hF4_400x400.jpg"
      ],
      "website": "http://www.phillip-kruger.com"
    }
  }
}

101というidを持つPersonをクエリする場合、

{
  person(personId:101){
    id
    names
    surname
  }
}

すでに削除済みなのでpersonは返ってきません。

  "data": {
    "person": null
  }
}

Partial results

上で示した @Source という注釈のように、フィールドを「つなぎ合わせる」ことができるので、クエリの中には失敗するものがあるかもしれません。例えば、スコアデータが個人データとは全く別のシステムにあるとします。そしてスコアシステムがダウンしているものの、個人データとスコアを要求するクエリが到着したとしましょう。この場合、クエリ全体が失敗するのではなく、部分的に結果を返すことができるため、必要な個人データの全てを返すことができますが、スコアデータのエラーを含めて返すことができます。

{
  person(personId:1){
    names
    surname
    idNumber
    scores {
      name
      value
    }
  }
}

以下のような結果が返ります。

{
  "data": {
    "person": {
      "names": [
        "Nicholas",
        "Edwin"
      ],
      "surname": "Zulauf",
      "idNumber": "334-58-1049",
      "scores": null
    }
  },
  "errors": [
    {
      "message": "Scores for person [334-58-1049] is not available",
      "locations": [
        {
          "line": 6,
          "column": 5
        }
      ],
      "path": [
        "person",
        "scores"
      ]
    }
  ]
}

Interfaces

スコアは人を測る上での方法の一つですが、他にも個人を測定する上で利用できるもの(年齢や体重など)があります。

スコア(と年齢、体重)が実装するインターフェースを定義できます。インターフェースを持つこのモデルはエンドポイントのスキーマで利用できます。

interface Measurable {
  value: BigInteger
}

type Age implements Measurable {
  value: BigInteger
}

type Weight implements Measurable {
  value: BigInteger
}

type Score implements Measurable {
  events: [Event]
  id: String
  name: ScoreType
  value: BigInteger
}

Introspection

GraphQLにはシステム組み込みの型があるため、スキーマの一部としてモデルを定義できます。REST(JAX-RS)を使う場合、MicroProfile OpenAPIのようなものを追加してエンドポイントを表現する必要があります。

MicroProfile GraphQLでは、/graphql/schema.graphqlに対してGETすることで生成されたスキーマを取得できます。

GraphQL Queryを使ってスキーマを検査することもできます。これはイントロスペクションと呼ばれます。以下は利用可能な全ての型を取得したい場合の例です。

{
  __schema{
    types {
      name
      kind
    }
  }
}

この結果は以下の通りです。

{
  "data": {
    "__schema": {
      "types": [
        {
          "name": "Action",
          "kind": "ENUM"
        },
        {
          "name": "Address",
          "kind": "OBJECT"
        },
        {
          "name": "AddressInput",
          "kind": "INPUT_OBJECT"
        },
        {
          "name": "BigDecimal",
          "kind": "SCALAR"
        },
        ... abbreviated
      ]
    }
  }
}

Other annotations

注釈を使いモデルメタデータを定義できます。GraphQLは全てのオプションに対する組み込みの注釈を有していますが、JsonBの注釈もサポートします。ほとんどの注釈をフィールドやメソッドで利用できます。

  • 注釈がフィールドに付いている場合、出力型(query/mutationの結果)と入力型(query/mutationに対する入力パラメータ)の両方に適用される。
  • 注釈がgetterメソッドにのみ付いている場合、出力型(query/mutationの結果)のみに適用される。
  • 注釈がsetterメソッドにのみ付いている場合、入力型(query/mutationに対する入力パラメータ)にのみ適用される。

以下は利用可能なオプションの一例です。

注釈
@Name
@JsonbProperty
フィールドに名前を付けるために利用。注釈が存在しない場合、フィールド名(もしくはget/set/isを持たないメソッド名)を使う。
@Descriptionエンティティタイプやフィールドの生成されたスキーマ内での説明を記述するために利用。
@DefaultValue入力型の範囲で値を指定するために利用可能。クライアントが値を指定しなかった場合に利用する。
@Ignore
@JsonbTransient
スキーマ作成時にフィールドを省略するために利用
@NonNullスキーマで非nullとしてフィールドをマークするために利用

数値や日付のフォーマットを定義することもできます。

@NumberFormat
@JsonbNumberFormat
数値型のフォーマット定義に利用
@DateFormat
@JsonbDateFormat
日付や時間の型のフォーマット定義に利用

In the pipeline

それでは、MicroProfile GraphQLの今後はどうなっているのでしょうか。いくつかの機能が次期リリースで計画されています。

機能
コンテキストリクエストの情報を持つ、requestスコープのオブジェクト。これを使うと開発者はさらにダウンストリームでコードを最適化できます。例えば、リクエストされたフィールドに基づいて、よりよいクエリを作成することができます(訳注:原文はSQLですが、この例だとクエリが妥当なので書き換えています)。
ページネーション技術的には、最初のパラメータとオフセットのパラメータを手動で追加することでページネーションを追加することができるとはいえ、この機能を使うと、開発者はメソッドに @Paginate という注釈を付けるだけで、ページネーションフィールドを利用できるようになります。これにより、コードがよりすっきりします。
他のMicroProfile APIのサポート他の MicroProfile API を GraphQL と組み合わせて使用する方法を定義します。例えば、GraphQLリクエストがMicroProfile Metricsを使ってメトリクスをレポートできるようにします。
カスタムのScalar現時点では、開発者が使用できるように事前定義済みのScalarのセットがあります。開発者が独自のScalarを作成して定義できるようにする方法を調査します。

現在オープン中のIssueは以下から確認できます。

eclipse/microprofile-graphql
https://github.com/eclipse/microprofile-graphql/issues?q=is%3Aopen+is%3Aissue+milestone%3A1.1

コメントを残す

以下に詳細を記入するか、アイコンをクリックしてログインしてください。

WordPress.com ロゴ

WordPress.com アカウントを使ってコメントしています。 ログアウト /  変更 )

Google フォト

Google アカウントを使ってコメントしています。 ログアウト /  変更 )

Twitter 画像

Twitter アカウントを使ってコメントしています。 ログアウト /  変更 )

Facebook の写真

Facebook アカウントを使ってコメントしています。 ログアウト /  変更 )

%s と連携中