如何通过查询在 AWS AppSync 中的嵌套字段上进行筛选

Posted

技术标签:

【中文标题】如何通过查询在 AWS AppSync 中的嵌套字段上进行筛选【英文标题】:How to Filter by Query on Nested Fields in AWS AppSync 【发布时间】:2020-01-17 15:42:29 【问题描述】:

问题和预期结果

我正在使用概念验证架构和 DynamoDB 表设置来过滤嵌套字段值。我非常普遍地遵循了here 的想法以及$utils.transform.toDynamoDBFilterExpression (here) 的文档。

基本思想是这样的:使用相同的原则,我想按任意深度的嵌套字段(低于 DynamoDB 中的 32 个文档路径长度限制)进行过滤。相关设置如下所示:

AppSync 架构(为命名约定道歉;应该是一个快速而肮脏的 PoC):

query 
    listActiveListingsBySubAndFilter(
        filter: TableTestMasterDataTable_ImportV1FilterInput!,
        limit: Int,
        nextToken: String
    ): TestMasterDataTable_ImportV1Connection


input TableBooleanFilterInput 
    ne: Boolean
    eq: Boolean


input TableDataObjectFilterInput 
    beds: TableFloatFilterInput
    baths: TableFloatFilterInput


input TableFloatFilterInput 
    ne: Float
    eq: Float
    le: Float
    lt: Float
    ge: Float
    gt: Float
    contains: Float
    notContains: Float
    between: [Float]


input TableIDFilterInput 
    ne: ID
    eq: ID
    le: ID
    lt: ID
    ge: ID
    gt: ID
    contains: ID
    notContains: ID
    between: [ID]
    beginsWith: ID


input TableIntFilterInput 
    ne: Int
    eq: Int
    le: Int
    lt: Int
    ge: Int
    gt: Int
    contains: Int
    notContains: Int
    between: [Int]


input TableStringFilterInput 
    ne: String
    eq: String
    le: String
    lt: String
    ge: String
    gt: String
    contains: String
    notContains: String
    between: [String]
    beginsWith: String


input TableTestMasterDataTable_ImportV1FilterInput 
    id: TableStringFilterInput
    status: TableStringFilterInput
    sub: TableStringFilterInput
    data: TableDataObjectFilterInput


type TestMasterDataTable_ImportV1 
    id: String!
    status: String!
    sub: String!
    data: AWSJSON


type TestMasterDataTable_ImportV1Connection 
    items: [TestMasterDataTable_ImportV1]
    nextToken: String


input UpdateTestMasterDataTable_ImportV1Input 
    id: String!
    status: String
    sub: String!
    data: AWSJSON

VTL 请求和响应解析器:

## Request resolver

#set( $filter = $ctx.args.filter )
#set( $path = $filter.data )


    "version" : "2017-02-28",
    "operation" : "Query",
    "index" : "listings-index",  ## GSI on table with HASH: status, RANGE: sub
    "query" : 
        "expression": "#status = :status and #sub = :sub",
        "expressionNames" : 
            "#status" : "status",
            "#sub" : "sub"
        ,
        "expressionValues" : 
            ":status" : $util.dynamodb.toDynamoDBJson("Active"),
            ":sub" : $util.dynamodb.toDynamoDBJson($filter.sub.eq)
        
    ,
    "filter" : $util.transform.toDynamoDBFilterExpression($path),
    "limit": $util.defaultIfNull($ctx.args.limit, 20),
    "nextToken": $util.toJson($util.defaultIfNullOrEmpty($ctx.args.nextToken, null))



## Response resolver


    "items": $util.toJson($ctx.result.items),
    "nextToken": $util.toJson($util.defaultIfNullOrBlank($context.result.nextToken, null))

示例 DynamoDB 表元素:


  "_meta": 
    "exposure": 0.08,
    "lastActive": 1557800000,
    "lastUpdated": 1557878400,
    "lastView": 1557878500,
    "numViews": 63,
    "posted": 1557878400
  ,
  "buildingID": "325-5th-Ave,-New-York,-NY-10016,-USA",
  "data": 
    "agent": [
      
        "agentID": "daeo@gmail.com"
      ,
      
        "agentID": "ben@gmail.com"
      
    ],
    "amenities": [
      "hot tub",
      "time machine"
    ],
    "baths": 2,
    "beds": 2
  ,
  "id": "325-5th-Ave,-New-York,-NY-10016,-USA#37C:1557878400",
  "status": "Active",
  "sub": "new-york/manhattan/listings",
  "unitNum": "37C",
  "unitRefID": "325-5th-Ave,-New-York,-NY-10016,-USA#37C"

基于所有这些,如果我运行以下查询:

listActiveListingsBySubAndFilter(filter: 
    "sub" : 
      "eq" : "new-york/manhattan/listings"
    ,
    "data": 
      "beds": 
        "eq": 2.0
      
    ) 
    items 
      id
      status
    
    nextToken

我希望得到这样的回报:


  "data": 
    "listActiveListingsBySubAndFilter": 
      "items": [
          
              "id": "325-5th-Ave,-New-York,-NY-10016,-USA#37C:1557878400",
              "status": "Active"
          
      ],
      "nextToken": null
    
  

注意:这是唯一的预期回报,因为此时数据库中只有一项符合这些要求。

实际结果

综上所述,我得到的(或缺乏的)结果没有多大意义。无论查询(data.bedsdata.baths),如果字段嵌套在data中,返回都是一样的:


  "data": 
    "listActiveListingsBySubAndFilter": 
      "items": [],
      "nextToken": null
    
  

我已验证查询按预期工作,并且过滤器表达式的格式正确(它适用于其他非嵌套字段,如 id)。令人困惑的是,过滤器似乎没有被应用(或者可能以某种非直观的方式应用?)。作为参考,以下是上述典型 CloudWatch 日志的 sn-p:


    "context": 
        "arguments": 
            "filter": 
                "sub": 
                    "eq": "new-york/manhattan/listings"
                ,
                "data": 
                    "beds": 
                        "eq": 2
                    
                
            ,
            "limit": 200
        ,
        "stash": ,
        "outErrors": []
    ,
    "fieldInError": false,
    "errors": [],
    "parentType": "Query",
    "graphQLAPIId": "q7ueubhsorehbjpr5e6ymj7uua",
    "transformedTemplate": "\n\n\n    \"version\" : \"2017-02-28\",\n    \"operation\" : \"Query\",\n    \"index\" : \"listings-index\",\n    \"query\" : \n        \"expression\": \"#status = :status and #sub = :sub\",\n        \"expressionNames\" : \n        \t\"#status\" : \"status\",\n            \"#sub\" : \"sub\"\n    \t,\n        \"expressionValues\" : \n            \":status\" : \"S\":\"Active\",\n            \":sub\" : \"S\":\"new-york/manhattan/listings\"\n        \n    ,\n    \"filter\" : \"expression\":\"(#beds = :beds_eq)\",\"expressionNames\":\"#beds\":\"beds\",\"expressionValues\":\":beds_eq\":\"N\":2.0,\n    \"limit\": 200,\n    \"nextToken\": null\n"

注意transformedTemplate: "N" : 2.0 (无$util.toDynamoDBJson 格式)中的过滤器expressionValues 值,并将其与DynamoDB 中该字段的对象中的值进行比较。

我已经尝试了所有方法,包括将字段本身更改为字符串并执行各种过滤操作,例如 eqcontains 以查看这是否是一些奇怪的类型不一致,但没有运气。

到目前为止,我有两个备份解决方案,它们涉及“拉起”我可能想要过滤的所有相关字段(将我的记录与我宁愿保持嵌套的属性弄乱)或创建一个新的嵌套类型,其中仅包含用于过滤的高级字段——即,有效地将记录拆分为记录引用和记录过滤器引用。在这种情况下,我们会得到一些“Listing”记录,其data 字段值类似于ListingFilterData——例如:

type Listing 
    id: String!
    sub: String!
    status: String!
    data: ListingFilterData!


type ListingFilterData 
    beds: Float!
    baths: Float!

两者都是可行的,但我宁愿尝试解决当前问题,而不是向我的表中添加一堆额外数据。

有什么想法吗?

2019 年 9 月 17 日更新

经过一番折腾,我偶然发现了隐含的解决方案here。根据我对解决方案的了解,我使用以下 VTL 请求解析器成功实现了一个硬编码的嵌套查询过滤器(并更改了过滤器表达式键名以避免data 上的保留字冲突):

#set( $filter = $ctx.args.filter )
#set( $path = $filter.filterData ) ## currently, unused


    "version" : "2017-02-28",
    "operation" : "Query",
    "index" : "listings-index",
    "query" : 
        "expression": "#status = :status and #sub = :sub",
        "expressionNames" : 
            "#status" : "status",
            "#sub" : "sub"
        ,
        "expressionValues" : 
            ":status" : $util.dynamodb.toDynamoDBJson("Active"),
            ":sub" : $util.dynamodb.toDynamoDBJson($filter.sub.eq)
        
    ,
    "filter" : 
        "expression" : "#filterData.beds = :beds",
        "expressionValues" : 
            ":beds" : $util.dynamodb.toDynamoDBJson(2.0)
        
    ,
    "limit": $util.defaultIfNull($ctx.args.limit, 20),
    "nextToken": $util.toJson($util.defaultIfNullOrEmpty($ctx.args.nextToken, null))

这将返回我的预期结果:


  "data": 
    "listActiveListingsBySubAndFilter": 
      "items": [
        
          "id": "325-5th-Ave,-New-York,-NY-10016,-USA#37C:1557878400",
          "status": "Active"
        
      ],
      "nextToken": null
    
  

看起来像是进步,但是关于如何动态创建 docpath 并为嵌套属性创建表达式名称的任何想法?运行更多的想法,如果有任何有趣的东西出现,我们会报告......

2019 年 9 月 17 日更新 #2

在进一步使用请求解析器之后,我想我找到了一种快速而肮脏的方法来动态获取路径和目标变量,以便为我的嵌套属性创建过滤器表达式。 注意:整个事情仍然返回一个空结果集,并且假设只有一个过滤键(目前),但保留关键字位似乎已经解决。仍然想知道为什么结果没有按预期显示。

#set( $filter = $ctx.args.filter )
#foreach( $parent in $filter.keySet() )
    #set( $path = $parent )
#end
#set( $target = $filter[$path] )
#foreach( $ff in $target.keySet() ) ## should only contain one Map key-value pair
    #set( $fp = $ff )
#end
#set( $fv = $target[$fp] )


    "version" : "2017-02-28",
    "operation" : "Query",
    "index" : "listings-index",
    "query" : 
        "expression": "#status = :status and #sub = :sub",
        "expressionNames" : 
            "#status" : "status",
            "#sub" : "sub"
        ,
        "expressionValues" : 
            ":status" : $util.dynamodb.toDynamoDBJson("Active"),
            ":sub" : $util.dynamodb.toDynamoDBJson($filter.sub.eq)
        
    ,
    "filter" : 
        "expression" : "#ffp = :$fp",  ## filter path parent.target = :target
        "expressionNames" : 
            "#ffp" : "$path.$fp"
        ,
        "expressionValues" : 
            ":$fp" : $util.dynamodb.toDynamoDBJson($fv.eq), ## :target : value to filter for
        
    ,
    "limit": $util.defaultIfNull($ctx.args.limit, 200),
    "nextToken": $util.toJson($util.defaultIfNullOrEmpty($ctx.args.nextToken, null))

检查 CloudWatch 日志 transformedTemplate 显示表达式名称和值已被适当替换:

"filter" : 
    "expression\" : "#ffp = :beds",
    "expressionNames" : 
        "#ffp" : "filterData.beds"
    ,
    "expressionValues" : 
        ":beds" :  "N": 2.0 
    

2019 年 9 月 18 日更新

我可能终于发现了问题的根源:似乎expressionNames 的评估方式不允许密钥成为文档路径。如果我运行以下任一过滤器(注意使用非保留的 DynamoDB 关键字来说明问题在于表达式名称替换),我将得到我正在寻找的结果:

"filter" : 
    "expression" : "filterData.beds = :beds",  ## filter path parent.target = :target
        "expressionValues" : 
            ":beds" : $util.dynamodb.toDynamoDBJson($fv.eq) ## :target : value to filter for
        
    

"filter" : 
    "expression" : "filterData.beds = :$fp",  ## filter path parent.target = :target
        "expressionValues" : 
            ":fp" : $util.dynamodb.toDynamoDBJson($fv.eq) ## :target : value to filter for
        
    

现在,如果我做一个小改动,只尝试用表达式名称值替换

"filter" : 
    "expression" : "#filterData.beds = :$fp",  ## filter path parent.target = :target
        "expressionNames": 
            "#filterData.beds" : "filterData.beds"
        ,
        "expressionValues" : 
            ":fp" : $util.dynamodb.toDynamoDBJson($fv.eq) ## :target : value to filter for
        
    

我收到以下错误消息:

"ExpressionAttributeNames contains invalid key: Syntax error; key: \"#filterData.beds\" (Service: AmazonDynamoDBv2; Status Code: 400; Error Code: ValidationException"

即使使用硬编码的路径替换,VTL 似乎也将路径读取为单个键名。动态交换表达式的值时同样的问题,因此没有硬编码的字符串。

【问题讨论】:

【参考方案1】:

已解决

我偶然发现了这个gem,它给了我一些额外的东西,我需要找到一个具有动态键名的可行解决方案!

这是过滤器表达式现在的样子:

"filter" : 
        "expression" : "#path.#filter = :$fp",  ## filter path parent.target = :target
        "expressionNames": 
            "#path" : "$path",
            "#filter" : "$fp"
        ,
        "expressionValues" : 
            ":$fp" : $util.dynamodb.toDynamoDBJson($fv.eq) ## :target : value to filter for
        
    

这里的问题是,虽然表达式属性名称通常被解释为文档路径,但随着替代名称的引入,解释器将键名称视为标量属性而不是文档路径。您需要单独识别路径元素并替换每个元素。

【讨论】:

作为参考,这是 AWS Amplify 存储库中的一个未解决问题。来源:github.com/aws-amplify/amplify-cli/issues/2311 你有一个完整的工作 VTL 吗?我仍在尝试这样做,但没有成功

以上是关于如何通过查询在 AWS AppSync 中的嵌套字段上进行筛选的主要内容,如果未能解决你的问题,请参考以下文章

使用 appsync 解析器、aws dymaodb 的嵌套查询

AWS AppSync:如何通过 DynamoDB 返回有效的 JSON

如何使用 Aws AppSync 将 JWT 令牌(或任何其他变量)从父解析器传递给子解析器

如何通过 AWS AppSync 客户端将保存字符串的变量传递给 GraphQL 查询?

如何从命令行向 AWS AppSync 发送 GraphQL 查询?

如何使用 AWS appsync (GraphQL) 禁用自省查询?