1. 概述

多年来,GraphQL 已经被广泛接受为Web服务通信模式之一。尽管它功能丰富且灵活,但在某些场景下可能会遇到挑战,其中之一是查询时返回一个Map。因为Map在GraphQL中不是一个类型,这构成了一种挑战。

本教程将学习如何从GraphQL查询中返回Map的技术。

2. 示例

以一个包含无限数量自定义属性的产品数据库为例。

作为数据库实体,每个产品可能有固定的字段,如namepricecategory等。但它们也可能有根据类别变化的属性。这些属性需要以保留标识键的方式返回给客户端。

为此,我们可以使用Map作为这些属性的类型。

3. 返回Map

为了返回Map,我们有三种选择:

  • 作为JSON字符串返回
  • 使用GraphQL的自定义标量类型
  • 作为键值对列表返回

接下来,我们将探讨所有三个选项。

3.1. JSON字符串

这是最简单的选择。在产品解析器中,我们将Map转换为JSON字符串格式:

query {
    product(id:1){ 
        id 
        name 
        description 
        attributes 
    }
}

GraphQL的schema如下:

type Product {
    id: ID
    name: String!
    description: String
    attributes:String
}

实施后查询的结果如下:

{
  "data": {
    "product": {
      "id": "1",
      "name": "Product 1",
      "description": "Product 1 description",
      "attributes": "{\"size\": {
                                     \"name\": \"Large\",
                                     \"description\": \"This is custom attribute description\",
                                     \"unit\": \"This is custom attribute unit\"
                                    },
                   \"attribute_1\": {
                                     \"name\": \"Attribute1 name\",
                                     \"description\": \"This is custom attribute description\",
                                     \"unit\": \"This is custom attribute unit\"
                                    }
                        }"
    }
  }
}

这个方法存在两个问题:首先,客户端需要处理JSON字符串才能得到可用格式;其次,无法对属性进行子查询。

为了解决第一个问题,我们可以借助GraphQL自定义标量类型。

3.2. GraphQL自定义标量类型

在Java中,我们将使用Extended Scalars库来实现。首先,在pom.xml中添加依赖:

<dependency>
    <groupId>com.graphql-java</groupId>
    <artifactId>graphql-java-extended-scalars</artifactId>
    <version>2022-04-06T00-10-27-a70541e</version>
</dependency>

然后,在GraphQL配置组件中注册所需的标量类型(这里选择JSON):

@Bean
public GraphQLScalarType json() {
    return ExtendedScalars.Json;
}

最后更新GraphQL schema:

type Product {
    id: ID
    name: String!
    description: String
    attributes: JSON
}
scalar JSON

实施后的结果如下:

{
  "data": {
    "product": {
      "id": "1",
      "name": "Product 1",
      "description": "Product 1 description",
      "attributes": {
        "size": {
          "name": "Large",
          "description": "This is custom attribute description",
          "unit": "This is a custom attribute unit"
        },
        "attribute_1": {
          "name": "Attribute1 name",
          "description": "This is custom attribute description",
          "unit": "This is a custom attribute unit"
        }
      }
    }
  }
}

这种方法无需在客户端处理属性Map,但标量类型有自己的限制。

在GraphQL中,标量类型是查询的叶子节点,这意味着它们不能进一步查询。

3.3. 键值对列表

如果需要进一步查询Map,这是最可行的选择。我们将Map对象转换为键值对对象的列表。

这是代表键值对的类:

public class AttributeKeyValueModel {
    private String key;
    private Attribute value;
    
    public AttributeKeyValueModel(String key, Attribute value) {
        this.key = key;
        this.value = value;
    }
}

在产品解析器中添加以下实现:

List<AttributeKeyValueModel> attributeModelList = new LinkedList<>();
product.getAttributes().forEach((key, val) -> attributeModelList.add(new AttributeKeyValueModel(key, val)));

最后更新schema:

type Product {
    id: ID
    name: String!
    description: String
    attributes:[AttributeKeyValuePair]
}
type AttributeKeyValuePair {
    key:String
    value:Attribute
}
type Attribute {
    name:String
    description:String
    unit:String
}

由于更新了schema,我们也需要更新查询:

query {
    product(id:1){ 
         id 
         name 
         description 
         attributes {
             key 
             value {
                 name 
                 description 
                 unit
             }
        } 
    }
}

现在来看结果:

{
  "data": {
    "product": {
      "id": "1",
      "name": "Product 1",
      "description": "Product 1 description",
      "attributes": [
        {
          "key": "size",
          "value": {
            "name": "Large",
            "description": "This is custom attribute description",
            "unit": "This is custom attribute unit"
          }
        },
        {
          "key": "attribute_1",
          "value": {
            "name": "Attribute1 name",
            "description": "This is custom attribute description",
            "unit": "This is custom attribute unit"
          }
        }
      ]
    }
  }
}

这个选项也有两个问题:查询变得更复杂,且对象结构需要硬编码。未知的Map对象在这种情况下无法工作。

4. 总结

在这篇文章中,我们探讨了从GraphQL查询中返回Map对象的三种不同方法及其局限性。根据需求,应选择最适合的方法。

如往常一样,本文的示例代码可在GitHub上找到。