在Kotlin中使用GraphQL处理全局唯一的Node ID

开始摆放

如果要开发GraphQL API,在Web前端无论使用哪个GraphQL客户端库,通常会首先设计API以满足Relay的规范。为了能够通过仅指定节点ID来重新获取任何时间的服务器资源,需要为实体提供一个全局唯一的ID机制。要做到这一点,有很多事情要做,但首先需要在服务器端为实体提供一个全局唯一的ID。

如果在将实体记录到数据库时采用UUID而不是自增ID作为主键,那么可以重用该值。但是,通常会分配连续编号的ID。由于连续编号的ID很难满足实体的跨越性唯一性,所以需要稍加设计。在Relay的官方指南中,它们解释了一种方法,大致如下。

Node 接口和 node 字段假定此 refetch 需要全局唯一标识符。如果没有全局唯一标识符的系统通常可以通过将类型与类型相关的 ID 组合来合成它们,这就是此示例中所做的。
我们得到的 ID 是 base64 字符串。ID 被设计为不透明(在 node 的 id 参数中应该传递的是系统中某个对象查询 id 的结果),而对字符串进行 base64 编码是 GraphQL 中的一种有用惯例,用于提醒查看者该字符串是一个不透明的标识符。

基本上就是將表示實體名稱的字串和連續編號ID結合起來的值,進行Base64編碼,然後將編碼後的字串作為ID使用。順便提一下,Relay提供的範例中,他們是使用冒號(:)將實體名稱和連續編號ID分隔,再進行Base64編碼。 遵照這個方法,基本上不會出錯。

在决定尝试模仿之前,并没有做什么特别困难的事情,所以即使自己编写了一个用于生成ID的方法也完全可以。但考虑到这种处理的水平可能是每个人都需要的,而且很可能已经在库中实施了,所以我进行了一些调查,本篇文章就是关于这些调查的内容。这里提到的库当然指的是标题中的GraphQL Kotlin。虽然很少听说国内使用GraphQL Kotlin的案例,但如果用Kotlin编写SpringBoot并创建GraphQL API,使用GraphQL Kotlin作为其扩展版本而不是直接使用GraphQL Java会更加方便实用,所以我推荐使用它。

调查日志

首先,我浏览了GraphQL Kotlin的官方文档,但可惜并没有找到相关描述。然而,如果回溯至GraphQL Kotlin所基于的GraphQL Java的文档,我们可以在Relay Support一章中找到如下描述。

基本的Relay支持已经包含在内。
请查看https://github.com/graphql-java/todomvc-relay-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())这部分是相对合理的。

当我查看GraphQL 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)也被正确地准备好了。

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

如果使用这两种方法,唯一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的规格要求了!

广告
将在 10 秒后关闭
bannerAds