LoginSignup
1
3

More than 1 year has passed since last update.

GraphQL KotlinでグローバルにユニークなNode IDを取り扱う

Posted at

前置き

GraphQL APIを開発する場合、Webフロントエンド側でどのGraphQLクライアントライブラリを使うにしてもとりあえずRelayの仕様を満たすようにAPIを設計しておくことが多いと思います。そのためにやることは色々ありますが、まずはサーバー側のリソースをNode IDを指定するだけでいつでも再取得可能にするために、エンティティに対してグローバルにユニークなIDを付与する機構を用意する必要があります。
データベースにエンティティを記録する際に、プライマリキーとしてAuto incrementなIDではなくUUIDを採用している場合はその値を使い回せば良いですが、連番のIDを振ることも多いはずです。そして連番IDではエンティティを横断したユニーク性を満たすことは難しいので、ちょっと工夫が必要になります。Relayの公式ガイドで説明されているやり方は以下のようなものです。

The Node interface and node field assume globally unique IDs for this refetching. A system without globally unique IDs can usually synthesize them by combining the type with the type-specific ID, which is what was done in this example.
The IDs we got back were base64 strings. IDs are designed to be opaque (the only thing that should be passed to the id argument on node is the unaltered result of querying id on some object in the system), and base64ing a string is a useful convention in GraphQL to remind viewers that the string is an opaque identifier.

要するにエンティティ名を表す文字列と連番IDを結合した値をBase64エンコーディングした文字列をIDとして使えということです。ちなみにRelayが用意しているサンプルではFaction:2のようにエンティティ名と連番IDの間を:で区切ってBase64エンコーディングしていました。とりあえずこれを真似しておけば間違いないでしょう。

真似するにあたって、別にそんなに難しいことをしているわけではないのでID生成用のメソッドを自作するのでも全然良かったのですが、このレベルの処理ならみんな必要だろうし流石にライブラリ側に実装されているのでは?と思って色々調査してみたのが本記事になります。なおここで言うライブラリというのはもちろんタイトルにある通りGraphQL Kotlinを指します。GraphQL Kotlinはあまり国内で使っている事例を聞きませんが、KotlinでSpringBootを書いてGraphQL APIを作るならGraphQL Javaをそのまま使うよりその拡張版であるGraphQL Kotlinを使う方が断然便利なのでオススメです。

調査ログ

まずはGraphQL Kotlinの公式ドキュメントを読み漁ってみましたが、残念ながらそれらしい記述はありませんでした。しかしGraphQL KotlinのベースとなっているGraphQL Javaのドキュメントにまで遡ると、Relay Supportの章にこんな記述がありました。

Very basic support for Relay is included.
Please look at https://github.com/graphql-java/todomvc-relay-java for a full example project,

このサンプルコードになにかヒントがありそうです。ちょっと覗いてみたところ、TodoSchema.javaファイル内に何やらそれらしい実装を見つけました。

TodoSchema.java
private void createUserType() {
        userType = newObject()
                .name("User")
                .field(newFieldDefinition()
                        .name("id")
                        .type(new GraphQLNonNull(GraphQLID))
                        .dataFetcher(environment -> {
                                    User user = (User) environment.getSource();
                                    return relay.toGlobalId("User", user.getId());  // <- これ
                                }
                        )
                        .build())
                .field(newFieldDefinition()
                        .name("todos")
                        .type(connectionFromUserToTodos)
                        .argument(relay.getConnectionFieldArguments())
                        .dataFetcher(simpleConnection)
                        .build())
                .withInterface(nodeInterface)
                .build();
}

relay.toGlobalId("User", user.getId()); という部分が見るからにそれっぽいですね :sweat_smile:
一応GraphQL Javaのソースコードまで潜って定義を見てみるとこんな感じになっていました。

Relay.java
private static final java.util.Base64.Encoder encoder = java.util.Base64.getUrlEncoder().withoutPadding();
private static final java.util.Base64.Decoder decoder = java.util.Base64.getUrlDecoder();

public String toGlobalId(String type, String id) {
    return encoder.encodeToString((type + ":" + id).getBytes(StandardCharsets.UTF_8));
}

まさに求めていたような内容になってますね。ちなみに逆変換用(ユニークID → エンティティ名 & 連番ID)のメソッドもちゃんと用意されていました。

Relay.java
public static class ResolvedGlobalId {

        public ResolvedGlobalId(String type, String id) {
            this.type = type;
            this.id = id;
        }

        private final String type;
        private final String id;

        public String getType() {
            return type;
        }

        public String getId() {
            return id;
        }
    }

//(中略)

public ResolvedGlobalId fromGlobalId(String globalId) {
        String[] split = new String(decoder.decode(globalId), StandardCharsets.UTF_8).split(":", 2);
        if (split.length != 2) {
            throw new IllegalArgumentException(String.format("expecting a valid global id, got %s", globalId));
        }
        return new ResolvedGlobalId(split[0], split[1]);
    }

この2つのメソッドを使えばユニークID問題は解決しそうです。

実装例

データベースから取り出したエンティティのIDがInt型だったとして、それをユニークIDに変換してAPIレスポンスに含めたり、逆にAPIリクエストから受け取ったユニークIDを連番IDに戻してからデータベースを検索したりといったことがやりやすいよう、以下のような拡張関数を定義してみました。

import com.expediagroup.graphql.generator.scalars.ID
import graphql.relay.Relay

fun ID.toInt(): Int {
    val globalId = Relay().fromGlobalId(this.toString())

    return globalId.id.trim().toInt()
}

fun Int.toID(type: String): ID {
    val globalId = Relay().toGlobalId(type, this.toString())

    return ID(globalId)
}

これで手軽にRelayの仕様を満たせますね!

1
3
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
1
3