Hasura GraphQL引擎调研

2023年 7月 14日 52.2k 0

因为工作需要,需要使用 GraphQL 作为数据处理层,Apollo GQL与Hasura都是可选方案。本文将深入调研Hasura功能,并在此场景下测试其实现 GraphQL Request -> Elastic Search -> GraphQL Response 的数据处理功能。

Hasura 简介

Hasura是一个GraphQL的引擎,其核心思想是避免手工编写枯燥的CRUD API,而只关注于业务逻辑部分与Supabase类似提供 BaaS 功能。它位于数据库实例之前,接受来自客户端应用程序的GraphQL请求。通过使用 Actions 来扩展、处理用例,例如来自外部源的数据验证、数据扩充和任何其他复杂的业务逻辑等。它可以是 REST 处理程序,也可以是无服务器函数等。

私有化搭建 Hasura

由于用于测试的 PGQL 在本地环境,所以Hasura也同样需要在本地搭建以实现数据通讯。
Hasura 提供了多种的部署方式,这里仅例出 docker compose 的方式。

Docker Compose

Hasura 支持使用 docker compose 构建本地的docker containers。

  • 获取 hasura 的 docker-compose.yml文件:

    curl https://raw.githubusercontent.com/hasura/graphql-engine/stable/install-manifests/docker-compose/docker-compose.yaml -o docker-compose.yml
    
  • 运行docker compose up命令:

    docker compose up -d  
    
  • 运行成功后即可从 http://localhost:8080 访问 hasura web ui了,我们需要在这里完成后续的步骤。

  • 添加 Local Database

    Hasura GraphQL引擎可以连接到各种各样的数据库,并自动生成一个全功能的数据API,而无需编写处理程序、模式或解析器。

  • 打开 http://localhost:8080/console/data/manage 页面并点击 Connect Database 按钮进入添加 Database 页面:

  • 选择对应的数据库类型,我们在这里添加 PGQL 以添加本地 PGQL 数据库:

  • 在Connect 页面输入Database nameDatabase URL 完成数据库的连接。

  • 创建完成之后即可在左侧看见对应的数据库,并可以添加 table 至 gql api里了

  • 使用 GraphQL 查询 table

    访问 http://localhost:8080/console/api/api-explorer 查看添加的表,这里选择 product_releases_basic_info 尝试搜索:

    如图可见 hasura 基本实现了 PGQL 的字段选择、过滤、排序等功能,十分的方便。

    添加 Elastic Search Action

    Action 是直接从GraphQL API连接到RESTful APIs以实现需要的任何业务逻辑的一种方便且安全的方式。我们需要利用它来访问 Elastic 的 Search RESTful API 实现数据检索的功能。

    进入 Action Configuration 配置页

    打开 http://localhost:8080/console/actions/manage/actions 页面,并点击 create 按钮进入 Action Configuration 页:

    Comment / Description 配置项

    填写 Action Configuration 的 Comment / Description 配置项,此字段为 GraphQL Query 中 Field 的注释,是一个可选项。

    定义 Action GraphQL SD(Schema definition language)与 Type

    这里是比较麻烦的一步,我们先跳过 Action Definition ,先定义 Type 以供 Definition 使用。而定义 Type 需要了解不少 GraphQL 的基础语法,如 type 语法:

    type Products {
        uid: String!
        name: String!
        # ...otherFields
    }
    

    如果需要使用自定义的类型则需要使用 GraphQL 的 scalar 语法,如定义 JSON 类型:

    # 自定义 JSON 类型
    scalar JSON
    
    # 使用 JSON 类型
    type Test {
        config: JSON!
    } 
    

    field 的值为对象时,GraphQL 并不支持嵌套对象的方式,需要先定义这个对象的类型才能使用,如下所示:

    type Company {
        id: Number!
        name: String!
    }
    
    type Products {
        # 必须使用 type 定义 Company 对象
        company: Company!
    }
    

    我们需要返回的测试数据的 type 大致如下:

    scalar JSON
    
    type SearchProductsByES {
      uid: String!
      name: String!
      publisher_id: String!
      channel: String!
      logo_url: String
      category: String
      bundle_id: String!
      first_release_date: String
      publisher_company: PublisherCompany
    }
    
    type PublisherCompany {
      id: String!
      title: String!
    }
    

    到这里我们就可以回去定义 Action Definition 了,它是由一个特殊的 Query type 定义的:

    type Query {
      # 返回 SearchProductsByES 类型的数组, 同比 typescript 中的 SearchProductsByES[]
      products(offset: Int, limit: Int, query: JSON): [SearchProductsByES!]!
    }
    

    这里 products 对象就是最终我们可以查询到的 GraphQL Schema

    定义 Elasitc 的 Webhook Handle

    这也是比较麻烦的一步,我们需要先获取到 Elasitc 的 token,或者使用Elasitc的账号密码 + BA (Basic Auth) 认证,这里选用后者实现(因为暂时没有 https 的 Elasitc 环境,而创建RESTful API token 需要开启 HTTPS)。

    1. 添加 Webhook Handle URL

    获取到这些必要的信息之后添加 Elasitc Index Search的地址至 Webhook handle URL:

    而 Elasitc Index Search 的地址为 Elasitc 的地址 + index的名称 + "_search":

    http://es.domain.com:9200/indexName/_search
    

    2. 添加 Authorization BA 鉴权 Header

    最原始的 BA 即为 Authorization + 账号密码的 Base64 编码,编码的原则为 username:password。例如账号是 elastic,密码为 123456,那么编码前为 elastic:123456,编码后即为 'ZWxhc3RpYzoxMjM0NTY=',利用 base64 工具可以测试编码:

    # 编码
    # echo -n 为不打印尾随换行符(n)
    echo -n "elastic:123456" | base64
    # 输出 ZWxhc3RpYzoxMjM0NTY=
    
    # 解码
    echo -n "ZWxhc3RpYzoxMjM0NTY=" | base64 -D
    # 输出 elastic:123456
    

    下一步就可以将 BA 添加至 header:

    这里的 Forward client headers to webhook 为是否转发请求头,是否勾选都可以。

    3. 添加 Payload format

    Payload format为格式化 GraphQL 的 input 输入,如上我们定义了三个参数:

    type Query {
      # 定义 offset、limit、query 三个参数
      products(offset: Int, limit: Int, query: JSON): [SearchProductsByES!]!
    }
    

    在 Elastic Search 中 offset 对应 fromlimit对应sizequery就是需要查询的 dls,那么就需要一一对应的格式化为相对的字段,而我们访问传入的参数可以使用 {{$body}},实现如下:

    {
        "from": {{$body.input.offset}},
        "size": {{$body.input.limit}},
        "query": {{$body.input.query}}
    }
    

    {{$body.input.offset}}即为输入的offset,我们可以在Sample Input 中测试输入:

    {
      "action": {
        "name": "products"
      },
      "input": {
        "offset": 0,
        "limit": 10,
        "query": {}
      }
    }
    

    Configure Request Body中填入我们的 format,并选择 application/json类型:

    Transformed Request Body中我们可以看见最终的转换结果,记住要保持与REST API相同的输入格式:

    4. 添加 Response transform

    Elastic 的返回类型为: hits: { hits: [ { _source: {...fields} } ] },而在这个简单的场景中我们只需要最终的 fields,不需要外层的内容,怎么去转换response呢?这就需要用到 Hasura 提供的简易 json template:kriti-lang。因为最终的实现并没有多么深入,这里就不展开它的语法介绍了,transform 如下:

    {{ range _, item := $body.hits.hits }}
      {{ item["_source"] }}
    {{ end }}
    

    到这里我们就完成了基本的Elastic查询的输入format与输出transform,可以保存 action 并在 GraphQL API 中使用了。

    使用 Elastic Search Response 关联 PGQL 的 Table

    Elastic Search Response 获取的数据比较基础,需要与其他的 PGQL table 数据关联。那么我们就需要在刚刚添加的 action 中添加对应的关联项。这里演示关联的 table 为之前添加的product_release_basic_info

    添加关联项在修改 action 页面中的Relationships项,也就是说需要添加关联必须要先创建好 action:

    首先,我们需要定义关系的类型与展示的字段名称,它们分别为 Relationship Type 与 Relationship Name:

    Relationship Type 有 Object 类型与 Array 类型,即为SQL 关系中的一对一与一对多(1:1, 1:N) ,由于我们需要的关系为一对一,所以这里选择 Object 类型:

    Relationship Name 为 GraphQL 的搜索字段,这里填写 baseInfo 即可。

    接下来我们需要添加的 Database 、Reference Schama、Reference Table与From填入相对应的 table 与 对应 field 即可,最终的实现如下:

    到此就实现了 Hasura GraphQL 引擎提供的大部分功能的实现,现在我们使用 GraphQL API 测试一下效果。

    测试 GraphQL API

    基本功能测试

    Hasura 提供了 GraphQL API的可视化界面,如上相同地址为 http://localhost:8080/console/api/api-explorer 。我们添加需要测试基础的queryRelationship,如下:

    query GetProducts($query: JSON!, $offset: Int = 0, $limit: Int = 2) {
      products(query: $query, offset: $offset, limit: $limit) {
        name
        # 测试 Object Field
        publisher_company {
          title
        }
        # 测试 Relationship
        baseInfo {
          size
        }
      }
    }
    

    Query Variables 即为 Elastic Search 的QueryBodyoffsetlimit都有默认值可以跳过:

    {
        "query": {
            "bool": {
                "should": [
                    {
                        "match": { "name": "小红书" }
                    }
                ]
            }
        }
    }
    

    查询结果如下:

    可以看见基本的查询功能是OK的,并且查询时间(598ms)能够接受,那么我们测试一下数量比较大的情况,看看查询的速度是否还是比较理想。

    压力测试与字段调整

    修改/添加Query Variables中的limit即可实现数据量的测试:

    {
        "query": {},
        "limit": 1000
    }
    

    结果:

    Limit 1K 就出现了查询 long time 的警告,这不是一个理想的情况,我们需要排除一下变量,看看是什么原因导致的。

    那么我们先用 Elastic RESTful API 查询一下,看看是不是 Elastic 导致的:

    果然,搜索出来的response数据太大,花费了 1055.65ms 的下载时间,那么我们需要限制一下返回的 "_source"

    添加 "_source" 字段限制 Elastic Search Action 返回字段

    返回 es 的 acion,把 "_source" 限制添加至 "Payload format" 中,对应的字段即位 GraphQL Type 中定义的字段,即:

    {
        "from": {{$body.input.offset}},
        "size": {{$body.input.limit}},
        "query": {{$body.input.query}},
        "_source": [
            "uid",
            "name",
            "publisher_id",
            "channel",
            "logo_url",
            "category",
            "product_id",
            "bundle_id",
            "first_release_date",
            "publisher_company.title",
            "publisher_company.industry",
            "publisher_company.financing_round"
        ]
    }
    

    保存后再测试一下:

    可以看到最终的查询时间压缩到了 746ms,这里也可以看出它内部的 Relationship Select Query 聚合了 Elastic Response 的结果,只使用了一次查询完成了数据的关联。

    总结

    Hasura GraphQL 引擎提供了一种方便且安全的方式,可以直接从 GraphQL API 连接到 RESTful APIs 以实现需要的任何业务逻辑。但配置 action 的过程也比较繁琐,并且需要有非常专业的知识(意思是产品来配置的话很吃力)。如果使用其作为数据处理层优势在于减少了 FieldResolver 的编写时间与放弃独立的 Express Service 的自由,转而仅需攻略 Hasura 的 Actions 即可。

    相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论