За последние пару недель я потратил некоторое время на перенос существующего сервиса на 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 г.