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

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

> 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](https://opensource.expediagroup.com/graphql-kotlin/docs/)の公式ドキュメントを読み漁ってみましたが、残念ながらそれらしい記述はありませんでした。しかしGraphQL KotlinのベースとなっているGraphQL Javaのドキュメントにまで遡ると、[Relay Support](https://www.graphql-java.com/documentation/relay)の章にこんな記述がありました。

> 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ファイル](https://github.com/graphql-java/todomvc-relay-java/blob/5c37d571a77bf88e02598f916515374fdceb5b45/src/main/java/todomvc/TodoSchema.java)内に何やらそれらしい実装を見つけました。

```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のソースコードまで潜って定義を見てみるとこんな感じになっていました。

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

https://github.com/graphql-java/graphql-java/blob/62ccef6301c935b876e98d57960fa8f11b496bc1/src/main/java/graphql/relay/Relay.java#L244

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

```java: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に戻してからデータベースを検索したりといったことがやりやすいよう、以下のような拡張関数を定義してみました。

```kotlin
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の仕様を満たせますね！