如何将DynamoDB的Golang SDK中的`LastEvaluatedKey`序列化?

huangapple go评论65阅读模式
英文:

How to serialize `LastEvaluatedKey` from DynamoDB's Golang SDK?

问题

在使用Golang处理DynamoDB时,如果对query的调用有更多的结果,它会在QueryOutput上设置LastEvaluatedKey,然后您可以将其作为ExclusiveStartKey传递给下一次调用的query,以从上次离开的地方继续。

当值保留在Golang中时,这个方法非常有效。然而,我正在编写一个分页的API端点,所以我希望能够序列化这个键,以便我可以将它作为分页令牌返回给客户端。类似下面的代码,其中something是我想要的魔法包:

type GetDomainObjectsResponse struct {
  Items     []MyDomainObject `json:"items"`
  NextToken string           `json:"next_token"`
}
  
func GetDomainObjects(w http.ResponseWriter, req *http.Request) {
  // ... 解析查询参数,设置 dynamoIn ...

  dynamoIn.ExclusiveStartKey = something.Decode(params.NextToken)

  dynamoOut, _ := db.Query(dynamoIn)

  response := GetDomainObjectsResponse{}
  dynamodbattribute.UnmarshalListOfMaps(dynamoOut.Items, &response.Items)

  response.NextToken := something.Encode(dynamoOut.LastEvaluatedKey)
  
  // ... 编组和写入响应 ...
}

(请原谅上面的任何拼写错误,这是我快速编写的代码的玩具版本,用于隔离问题)

因为我需要支持几个具有不同搜索模式的端点,所以我希望有一种生成分页令牌的方法,不依赖于特定的搜索键。

问题是,我还没有找到一种干净且通用的方法来序列化LastEvaluatedKey。您可以直接将其编组为JSON(然后例如对其进行base64编码以获得令牌),但这样做是不可逆的。LastEvaluatedKey是一个map[string]types.AttributeValue,而types.AttributeValue是一个接口,因此虽然JSON编码器可以读取它,但无法写入它。

例如,以下代码会导致panic: json: cannot unmarshal object into Go value of type types.AttributeValue的恐慌。

lastEvaluatedKey := map[string]types.AttributeValue{
	"year":  &types.AttributeValueMemberN{Value: "1993"},
	"title": &types.AttributeValueMemberS{Value: "Benny & Joon"},
}

bytes, err := json.Marshal(lastEvaluatedKey)
if err != nil {
  panic(err)
}

decoded := map[string]types.AttributeValue{}
err = json.Unmarshal(bytes, &decoded)
if err != nil {
  panic(err)
}

我希望能够直接使用DynamoDB风格的JSON,就像在CLI上运行aws dynamodb query时得到的结果那样。不幸的是,Golang SDK不支持这个

我想我可以为AttributeValue类型编写自己的序列化器/反序列化器,但这比这个项目值得的工作要多。

有人找到了一种通用的方法吗?

英文:

When working with DynamoDB in Golang, if a call to query has more results, it will set LastEvaluatedKey on the QueryOutput, which you can then pass in to your next call to query as ExclusiveStartKey to pick up where you left off.

This works great when the values stay in Golang. However, I am writing a paginated API endpoint, so I would like to serialize this key so I can hand it back to the client as a pagination token. Something like this, where something is the magic package that does what I want:

type GetDomainObjectsResponse struct {
  Items     []MyDomainObject `json:"items"`
  NextToken string           `json:"next_token"`
}
  
func GetDomainObjects(w http.ResponseWriter, req *http.Request) {
  // ... parse query params, set up dynamoIn ...

  dynamoIn.ExclusiveStartKey = something.Decode(params.NextToken)

  dynamoOut, _ := db.Query(dynamoIn)

  response := GetDomainObjectsResponse{}
  dynamodbattribute.UnmarshalListOfMaps(dynamoOut.Items, &response.Items)

  response.NextToken := something.Encode(dynamoOut.LastEvaluatedKey)
  
  // ... marshal and write the response ...
}

(please forgive any typos in the above, it's a toy version of the code I whipped up quickly to isolate the issue)

Because I'll need to support several endpoints with different search patterns, I would love a way to generate pagination tokens that doesn't depend on the specific search key.

The trouble is, I haven't found a clean and generic way to serialize the LastEvaluatedKey. You can marshal it directly to JSON (and then e.g. base64 encode it to get a token), but doing so is not reversible. LastEvaluatedKey is a map[string]types.AttributeValue, and types.AttributeValue is an interface, so while the json encoder can read it, it can't write it.

For example, the following code panics with panic: json: cannot unmarshal object into Go value of type types.AttributeValue.

lastEvaluatedKey := map[string]types.AttributeValue{
	"year":  &types.AttributeValueMemberN{Value: "1993"},
	"title": &types.AttributeValueMemberS{Value: "Benny & Joon"},
}

bytes, err := json.Marshal(lastEvaluatedKey)
if err != nil {
  panic(err)
}

decoded := map[string]types.AttributeValue{}
err = json.Unmarshal(bytes, &decoded)
if err != nil {
  panic(err)
}

What I would love would be a way to use the DynamoDB-flavored JSON directly, like what you get when you run aws dynamodb query on the CLI. Unfortunately the golang SDK doesn't support this.

I suppose I could write my own serializer / deserializer for the AttributeValue types, but that's more effort than this project deserves.

Has anyone found a generic way to do this?

答案1

得分: 6

好的,下面是翻译好的内容:

type GetDomainObjectsResponse struct {
  Items     []MyDomainObject `json:"items"`
  NextToken string           `json:"next_token"`
}
  
func GetDomainObjects(w http.ResponseWriter, req *http.Request) {
  // ... 解析查询参数,设置 dynamoIn ...

  eskMap := map[string]string{}
  json.Unmarshal(params.NextToken, &eskMap)
  esk, _ = dynamodbattribute.MarshalMap(eskMap)
  dynamoIn.ExclusiveStartKey = esk

  dynamoOut, _ := db.Query(dynamoIn)

  response := GetDomainObjectsResponse{}
  dynamodbattribute.UnmarshalListOfMaps(dynamoOut.Items, &response.Items)

  lek := map[string]string{}
  dynamodbattribute.UnmarshalMap(dynamoOut.LastEvaluatedKey, &lek)
  response.NextToken = json.Marshal(lek)
  
  // ... 序列化并写入响应 ...
}

正如 @buraksurdar 指出的那样,attributevalue.Unmarshal 接受一个 interface{}。事实证明,除了具体类型之外,你还可以传入一个 map[string]string,它也能正常工作。

我相信,如果 AttributeValue 不是扁平的,这种方法是行不通的,所以这不是一个通用的解决方案 [需要引用]。但我理解的是,从 Query 调用返回的 LastEvaluatedKey 总是扁平的,所以对于这个用例来说是有效的。

英文:

OK, I figured something out.

type GetDomainObjectsResponse struct {
  Items     []MyDomainObject `json:"items"`
  NextToken string           `json:"next_token"`
}
  
func GetDomainObjects(w http.ResponseWriter, req *http.Request) {
  // ... parse query params, set up dynamoIn ...

  eskMap := map[string]string{}
  json.Unmarshal(params.NextToken, &eskMap)
  esk, _ = dynamodbattribute.MarshalMap(eskMap)
  dynamoIn.ExclusiveStartKey = esk

  dynamoOut, _ := db.Query(dynamoIn)

  response := GetDomainObjectsResponse{}
  dynamodbattribute.UnmarshalListOfMaps(dynamoOut.Items, &response.Items)

  lek := map[string]string{}
  dynamodbattribute.UnmarshalMap(dynamoOut.LastEvaluatedKey, &lek)
  response.NextToken := json.Marshal(lek)
  
  // ... marshal and write the response ...
}

(again this is my real solution hastily transferred back to the toy problem, so please forgive any typos)

As @buraksurdar pointed out, attributevalue.Unmarshal takes an inteface{}. Turns out in addition to a concrete type, you can pass in a map[string]string, and it just works.

I believe this will NOT work if the AttributeValue is not flat, so this isn't a general solution [citation needed]. But my understanding is the LastEvaluatedKey returned from a call to Query will always be flat, so it works for this usecase.

答案2

得分: 3

受到Dan的启发,这是一个将数据序列化和反序列化为base64的解决方案。

package dynamodb_helpers

import (
	"encoding/base64"
	"encoding/json"

	"github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue"
	"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
)

func Serialize(input map[string]types.AttributeValue) (*string, error) {
	var inputMap map[string]interface{}
	err := attributevalue.UnmarshalMap(input, &inputMap)
	if err != nil {
		return nil, err
	}
	bytesJSON, err := json.Marshal(inputMap)
	if err != nil {
		return nil, err
	}
	output := base64.StdEncoding.EncodeToString(bytesJSON)
	return &output, nil
}

func Deserialize(input string) (map[string]types.AttributeValue, error) {
	bytesJSON, err := base64.StdEncoding.DecodeString(input)
	if err != nil {
		return nil, err
	}
	outputJSON := map[string]interface{}{}
	err = json.Unmarshal(bytesJSON, &outputJSON)
	if err != nil {
		return nil, err
	}

	return attributevalue.MarshalMap(outputJSON)
}

希望对你有帮助!

英文:

Inspired by Dan, here is a solution to serialize and deserialize to/from base64

package dynamodb_helpers

import (
	"encoding/base64"
	"encoding/json"

	"github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue"
	"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
)

func Serialize(input map[string]types.AttributeValue) (*string, error) {
	var inputMap map[string]interface{}
	err := attributevalue.UnmarshalMap(input, &inputMap)
	if err != nil {
		return nil, err
	}
	bytesJSON, err := json.Marshal(inputMap)
	if err != nil {
		return nil, err
	}
	output := base64.StdEncoding.EncodeToString(bytesJSON)
	return &output, nil
}

func Deserialize(input string) (map[string]types.AttributeValue, error) {
	bytesJSON, err := base64.StdEncoding.DecodeString(input)
	if err != nil {
		return nil, err
	}
	outputJSON := map[string]interface{}{}
	err = json.Unmarshal(bytesJSON, &outputJSON)
	if err != nil {
		return nil, err
	}

	return attributevalue.MarshalMap(outputJSON)
}

答案3

得分: 1

受到Dan和ThomasP1988的启发,我想通过合并上述输入来给出一个快速的解决方案。以下是如何在GoLang和DynamoDb中使用LastEvaluatedKey和ExclusiveStartKey实现API分页的方法:

// 从base64字符串获取lsk映射
var esk map[string]*dynamodb.AttributeValue
if lastEvaluatedKey != "" {
    bytesJSON, err := base64.StdEncoding.DecodeString(lastEvaluatedKey)
    if err != nil {
        return fmt.Errorf("failed to decode lek: %w", err)
    }
    var outputJSON map[string]interface{}
    err = json.Unmarshal(bytesJSON, &outputJSON)
    if err != nil {
        return fmt.Errorf("failed to unmarshal lek: %w", err)
    }
    esk, err = dynamodbattribute.MarshalMap(outputJSON)
    if err != nil {
        return fmt.Errorf("failed to marshal lek: %w", err)
    }
}

// 在查询输入中传递此参数和限制条件
result, err := s.db.Query(
    &dynamodb.QueryInput{
        TableName:                 aws.String("TableName"),
        IndexName:                 aws.String("IndexName"),
        ExpressionAttributeNames:  expr.Names(),
        ExpressionAttributeValues: expr.Values(),
        KeyConditionExpression:    expr.KeyCondition(),
        ScanIndexForward:          aws.Bool(false),
        ExclusiveStartKey:         esk,
        Limit:                     aws.Int64(10),
    },
)

// 将LastEvaluatedKey作为base64编码的字符串返回
lekOutPut := ""
if result.LastEvaluatedKey != nil {
    lek := map[string]interface{}{}
    dynamodbattribute.UnmarshalMap(result.LastEvaluatedKey, &lek) // 将最后评估的键放入映射中
    lastKey, err := json.Marshal(lek)                             // 转换为字节数组
    if err != nil {
        return fmt.Errorf("failed to marshal lek: %w", err)
    }
    lekOutPut = base64.StdEncoding.EncodeToString(lastKey) // 转换为字符串以作为参数返回
}

// 将lekOutPut作为响应返回给客户端,以便客户端可以在另一个API请求中将其发送回给您
return lekOutPut

以上是使用LastEvaluatedKey和ExclusiveStartKey在GoLang和DynamoDb中实现API分页的示例代码。

英文:

Inspired by Dan and ThomasP1988 I would like to give a quick solution by merging above inputs. Heres how you can implement pagination in your API using LastEvaluatedKey and ExclusiveStartKey in GoLang and DynamoDb

// getting lsk map from base64 string
var esk map[string]*dynamodb.AttributeValue
if lastEvaluatedKey != "" {
bytesJSON, err := base64.StdEncoding.DecodeString(lastEvaluatedKey)
if err != nil {
return fmt.Errorf("failed to decode lek : %w", err)
}
var outputJSON map[string]interface{}
err = json.Unmarshal(bytesJSON, &outputJSON)
if err != nil {
return fmt.Errorf("failed to unmarshal lek : %w", err)
}
esk, err = dynamodbattribute.MarshalMap(outputJSON)
if err != nil {
return fmt.Errorf("failed to marshal lek : %w", err)
}
}
// you need to pass this in your Query input with limit
result, err := s.db.Query(
&dynamodb.QueryInput{
TableName:                 aws.String("TableName"),
IndexName:                 aws.String("IndexName"),
ExpressionAttributeNames:  expr.Names(),
ExpressionAttributeValues: expr.Values(),
KeyConditionExpression:    expr.KeyCondition(),
ScanIndexForward:          aws.Bool(false),
ExclusiveStartKey:         esk,
Limit:                     aws.Int64(10),
},
)
// returning LastEvaluatedKey as base 64 encoded string
lekOutPut := ""
if result.LastEvaluatedKey != nil {
lek := map[string]interface{}{}
dynamodbattribute.UnmarshalMap(result.LastEvaluatedKey, &lek) //put last evaluated key in a map
lastKey, err := json.Marshal(lek)                             // convert to byte array
if err != nil {
return fmt.Errorf("failed to marshal lek : %w", err)
}
lekOutPut = base64.StdEncoding.EncodeToString(lastKey) //convert to string to return as a param
}

return lekOutPut in response to client so that client can send it back to you in another API req

huangapple
  • 本文由 发表于 2021年7月9日 04:17:28
  • 转载请务必保留本文链接:https://go.coder-hub.com/68308139.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定