За последние пару недель я потратил некоторое время на перенос существующего сервиса на Golang. Если вы читали мой блог раньше, то знаете, что я большой поклонник бессерверных решений. Неудивительно, что я основывал свой сервис на бессерверных технологиях, таких как Lambda и DynamoDB. Обучение написанию лямбда-кода на Go было самостоятельным вызовом, как и работа с DynamoDB. К счастью, AWS SDK для разных языков довольно похожи, но все языки работают немного по-разному. В этом посте я хотел рассказать, как я настроил DynamoDB и использовал его операции.
Конечно, весь код с открытым исходным кодом, так что не стесняйтесь смотреть на него.
Основы
Я начну с базовой инициализации. Мне нравится использовать гексагональную архитектуру, и я хотел попробовать использовать ту же архитектуру (или, по крайней мере, максимально приблизиться к чистой архитектуре) в этом рефакторинге. Чтобы реализовать свои надежды на архитектуру, я поместил все свои исходящие адаптеры в свою папку internal
и поделился как можно большим базовым и абстрактным кодом DynamoDB. Инициализация в Go выглядит иначе, чем то, что я обычно кодирую, но она включает возврат одноэлементного экземпляра клиента DynamoDB.
package adapters
import (
"context"
"sync"
"github.com/aws/aws-sdk-go-v2/aws"
awsConfigMod "github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
)
var awsConfig aws.Config
var onceAwsConfig sync.Once
var dynamodbClient *dynamodb.Client
var onceDdbClient sync.Once
func getAwsConfig() aws.Config {
onceAwsConfig.Do(func() {
var err error
awsConfig, err = awsConfigMod.LoadDefaultConfig(context.TODO())
if err != nil {
panic(err)
}
})
return awsConfig
}
func GetDynamodbClient() *dynamodb.Client {
onceDdbClient.Do(func() {
awsConfig = getAwsConfig()
region := config.Region
dynamodbClient = dynamodb.NewFromConfig(awsConfig, func(opt *dynamodb.Options) {
opt.Region = region
})
})
return dynamodbClient
}
Теперь всякий раз, когда моим исходящим адаптерам нужно было использовать клиент DynamoDB, им достаточно было вызвать GetDynamodbClient
.
Далее идут основные операции DynamoDB. К ним относятся GetItem
, PutItem
и т. д. Я быстро заметил повторение при вызове этих операций вокруг того, как я упорядочивал данные и обрабатывал ошибки, возвращаемые из SDK. По этой причине я создал базовые оболочки для каждой из операций, которые впоследствии вызывались моими исходящими адаптерами. Я думаю, что было бы лучше просто включить ссылку вместо того, чтобы копировать каждую из этих функций здесь. Вот ссылка на эти обертки.
Основная цель этих оберток — показать, сколько шаблонного кода задействовано в большинстве этих вызовов, и хотя 20 строк могут быть не такими уж большими для начала, они быстро складываются. Скорее всего, они станут для меня отправной точкой в любых последующих проектах, связанных с Go и DynamoDB, поэтому я постарался сделать их максимально пригодными для повторного использования.
Честно говоря, это относится к более низкоуровневым темам DynamoDB. Было легко встать и бежать. Однако есть еще две конкретные области, которые я хотел бы обсудить. Один включает в себя представление объектов, которые также прозрачны для пользователей. Другой — то, как DynamoDB обрабатывает Set
типов.
маршалинг
Приходя из Node, я в значительной степени полагался на Document Client для маршалинга типов DynamoDB. В прошлом я маршалировал вручную, и это не весело. Я волновался, что мне придется вручную маршалировать или писать пакет, чтобы сделать это на Go, но, к счастью, AWS предоставляет пакет, который справится с этим за нас. Он называется attributevalue
. Чаще всего я использовал функцию MarshalMap
. Идея заключается в том, что мы можем передать произвольную структуру, а attributevalue.MarshalMap
сможет указать типы членов структуры и создать соответствующую карту AttributeValue
, которую мы можем напрямую передать в операцию DynamoDB. Вот как это может выглядеть в действии.
func dynamodbPutWrapper(item interface{}) (*dynamodb.PutItemOutput, error) {
ddbClient := GetDynamodbClient()
av, marshalErr := attributevalue.MarshalMap(item)
if marshalErr != nil {
logger.Error("Failed to marshal item",
zap.Any("item", item),
zap.Error(marshalErr),
)
return &dynamodb.PutItemOutput{}, marshalErr
}
putItemRes, putItemErr := ddbClient.PutItem(context.TODO(), &dynamodb.PutItemInput{
TableName: aws.String(config.PrimaryTableName),
Item: av,
})
if putItemErr != nil {
logger.Error("Failed to put item", zap.Error(putItemErr))
return &dynamodb.PutItemOutput{}, putItemErr
}
return putItemRes, nil
}
Выражения
AWS также распространяет отличный пакет, который обрабатывает выражения, такие как выражения обновления. Этот пакет очень прост в использовании и поставляется с хорошими примерами. На данный момент я не использовал его широко, и у меня нет каких-либо уникальных отзывов о том, как я его использую. В любом случае, вот пример того, как его можно использовать для обновления чего-либо. (Этот пример неполон, потому что вокруг операции UpdateItem
много кода, но суть в том, чтобы сосредоточиться на использовании expression
.)
update := expression.Add(
expression.Name("userCount"),
expression.Value(value),
)
expr, builderErr := expression.NewBuilder().WithUpdate(update).Build()
if builderErr != nil {
logger.Error("Failed to build update expression",
zap.Error(builderErr),
)
return &dynamodb.UpdateItemOutput{}, builderErr
}
updateItemRes, updateItemErr := ddbClient.UpdateItem(context.TODO(), &dynamodb.UpdateItemInput{
TableName: aws.String(config.PrimaryTableName),
Key: av,
UpdateExpression: expr.Update(),
ExpressionAttributeNames: expr.Names(),
ExpressionAttributeValues: expr.Values(),
})
Представление юридических лиц
Один из шаблонов, который я реализовал в нескольких написанных мной службах, заключается в предоставлении того же объекта, который я записываю, в DynamoDB в результате чтения или того, что пользователь может обновить. Это упрощает работу с API и кодом. Я справился с этим в Node следующим образом: прочитал элемент из DynamoDB, использовал оператор расширения для удаления определенных полей, которые я не хотел раскрывать (например, разделы и ключи сортировки), а затем вернул полученный объект. В Go нет индивидуальной поддержки для чего-то подобного. Вместо этого я определил одну структуру для объекта, который можно просматривать извне, и другую структуру для операций DynamoDB, в которых первая использовалась как встроенная структура. Вот как это выглядит.
type UserItem struct {
MethodsUsed []string `json:"methodsUsed" dynamodbav:"methodsUsed,stringset,omitempty"`
LastSignIn string `json:"lastSignin" dynamodbav:"lastSignIn"`
Created string `json:"created" dynamodbav:"created"`
}
type DdbUserItem struct {
Id string `dynamodbav:"id"`
SecondaryId string `dynamodbav:"secondaryId"`
UserItem
}
Следующий фрагмент представляет собой просто синтаксис Go для создания и доступа к этой встроенной структуре, которую мне пришлось изучить.
func createUser(userId string) UserItem {
item := DdbUserItem{
Id: "partitionKey",
SecondaryId: userId,
UserItem: types.UserItem{
MethodsUsed: nil,
LastSignIn: time.Now().Format(time.RFC3339),
Created: time.Now().Format(time.RFC3339),
},
}
// PutItem to DDB
return item.UserItem
}
UserItem
— это то, что я возвращаю клиенту, а DdbUserItem
представляет тот же объект, но для DynamoDB. Это скрывает от клиентов любые подробности, относящиеся к реализации.
Наборы струн
У меня была одна проблема, которая, похоже, не получила широкого освещения в Интернете, была связана с типом DynamoDB SS
(набор строк). В Go нет встроенного типа Set. Так что там, где в Node я мог просто создать Set()
и позволить Document Client обрабатывать маршалинг, мне пришлось прыгать через дополнительные обручи с Go.
SDK позволяет использовать наборы строк двумя разными способами. Один из них был показан ранее в определении структуры UserItem
. Он включает в себя пометку структуры специальным тегом dynamodbav
. Это сообщает маршаллеру значений атрибутов, что []string
(встроенный тип Go для массива строк) должен маршалироваться как набор строк. Варианты тегов показаны в этой ссылке (которая работает на момент написания этой статьи). Так что вам не нужно прокручивать вверх, способ пометки показан ниже. Обратите внимание на тег omitempty
, который важен, поскольку DynamoDB не примет пустой набор строк.
type UserItem struct {
MethodsUsed []string `json:"methodsUsed" dynamodbav:"methodsUsed,stringset,omitempty"`
LastSignIn string `json:"lastSignin" dynamodbav:"lastSignIn"`
Created string `json:"created" dynamodbav:"created"`
}
Второй способ предполагает ручное добавление набора строк в операцию, которая не использует маршаллер значений атрибутов. Пример этого метода показан ниже.
update := expression.Add(
expression.Name("methodsUsed"),
expression.Value(
&ddbTypes.AttributeValueMemberSS{
Value: []string{signInMethod},
},
),
)
Первоначально опубликовано на https://thomasstep.com 1 сентября 2022 г.