Как обрабатывать преобразования типов с помощью DynamoDB Go SDK

Учитесь на практических примерах кода

DynamoDB предоставляет богатый набор типов данных, включая Strings, Numbers, Sets, Lists, Maps и т. д. В Go SDK для DynamoDB пакет types содержит Go-представления этих типов данных и модуль attributevalue. предоставляет функции для работы с типами Go и DynamoDB.

В этом сообщении блога показано, как обрабатывать преобразования между типами Go в вашем приложении и DynamoDB. Мы начнем с простых фрагментов кода, чтобы представить некоторые конструкции API, и завершим пример того, как использовать эти функции Go SDK в контексте полного приложения (включая пошаговое руководство по коду).

Вы можете посмотреть полный код на GitHub

Для начала пройдемся по нескольким примерам.

Обратите внимание, что обработка ошибок намеренно опущена в приведенных ниже фрагментах кода, чтобы сделать их краткими.

Преобразование типов Go в DynamoDB

Об этом позаботится семейство функций Маршал. Он работает с базовыми скалярами (int, uint, float, bool, string), maps, slices и structs.

Для работы со скалярными типами просто используйте (общую) функцию Marshal:

func marshalScalars() {
 av, err := attributevalue.Marshal("foo")
 log.Println(av.(*types.AttributeValueMemberS).Value)

 av, err = attributevalue.Marshal(true)
 log.Println(av.(*types.AttributeValueMemberBOOL).Value)

 av, err = attributevalue.Marshal(42)
 log.Println(av.(*types.AttributeValueMemberN).Value)

 av, err = attributevalue.Marshal(42.42)
 log.Println(av.(*types.AttributeValueMemberN).Value)
}

Marshal преобразует тип данных Go в AttributeValue. Но AttributeValue сам по себе является просто interface и требует, чтобы вы привели его к конкретному типу, такому как AttributeValueMemberS (для string), AttributeValueMemberBOOL (для boolean) и т. д.

Если вы попытаетесь преобразовать несовместимые типы, SDK выдаст полезное сообщение об ошибке. Например, panic: interface conversion: types.AttributeValue is *types.AttributeValueMemberN, not *types.AttributeValueMemberS

При работе с slices и maps лучше использовать специальные функции, такие как MarshalList и MarshalMap:

func marshalSlicesAndMaps() {
 avl, err := attributevalue.MarshalList([]string{"foo", "bar"})

 for _, v := range avl {
  log.Println(v.(*types.AttributeValueMemberS).Value)
 }

 avm, err := attributevalue.MarshalMap(map[string]interface{}{"foo": "bar", "boo": "42"})

 for k, v := range avm {
  log.Println(k, "=", v.(*types.AttributeValueMemberS).Value)
 }
}

Приведенные выше примеры дали вам представление о том, как работать с простыми типами данных изолированно. В реальном приложении вы будете использовать составные типы данных для представления вашей модели предметной области — скорее всего, они будут в форме Go structs. Итак, давайте рассмотрим несколько примеров этого.

Работа со структурами Go

Вот простой:

type User struct {
 Name string
 Age  string
}

func marshalStruct() {
 user := User{Name: "foo", Age: "42"}

 av, err := attributevalue.Marshal(user)

 avm := av.(*types.AttributeValueMemberM).Value
 log.Println("name", avm["Name"].(*types.AttributeValueMemberS).Value)
 log.Println("age", avm["Age"].(*types.AttributeValueMemberS).Value)

 avMap, err := attributevalue.MarshalMap(user)

 for name, value := range avMap {
  log.Println(name, "=", value.(*types.AttributeValueMemberS).Value)
 }
}

Обратите внимание, как удобно использовать MarshalMap (вместо Marshal) при работе со структурами Go, особенно если ваше приложение не знает всех имен атрибутов.

Пока кажется, что мы можем справиться с простыми вариантами использования. Но мы можем сделать лучше. В этом примере был однородный тип данных, т. е. struct имел только string тип, что позволяло легко перебирать результат map и приводить значение к *types.AttributeValueMemberS - если бы это было не так, вам пришлось бы перебирать каждый тип значения атрибута. и приведите его к соответствующему типу Go. Это будет очевидно при работе с остальными DynamoDB API. Например, результат вызова GetItem (GetItemOutput) содержит map[string]types.AttributeValue.

SDK позволяет нам сделать это намного проще!

Преобразование типов DynamoDB в Go

Об этом позаботится семейство функций Unmarshal. Вот еще один пример:

type AdvancedUser struct {
 Name         string
 Age          int
 IsOnline     bool
 Favourites   []string
 Contact      map[string]string
 RegisteredOn time.Time
}

func marshalUnmarshal() {
 user := AdvancedUser{
  Name:         "abhishek",
  Age:          35,
  IsOnline:     false,
  Favourites:   []string{"Lost In Translation, The Walking Dead"},
  Contact:      map[string]string{"mobile": "+919718861200", "email": "[email protected]"},
  RegisteredOn: time.Now(),
 }

 avMap, err := attributevalue.MarshalMap(user)

 var result AdvancedUser
 err = attributevalue.UnmarshalMap(avMap, &result)

 log.Println("\nname", result.Name, "\nage", result.Age, "\nfavs", result.Favourites)
}

С помощью MarshalMap мы преобразовали экземпляр структуры AdvancedUser в map[string]types.AttributeValue (представьте, что вы получаете это как ответ на вызов API GetItem). Теперь, вместо того, чтобы перебирать отдельные AttributeValue, мы просто используем UnmarshalMap, чтобы преобразовать его обратно в Go struct.

Есть еще! Вспомогательные функции, такие как UnmarshalListOfMaps, упрощают работу с несколькими slices Go struct.

type AdvancedUser struct {
 Name         string
 Age          int
 IsOnline     bool
 Favourites   []string
 Contact      map[string]string
 RegisteredOn time.Time
}

func marshalUnmarshal() {
 user := AdvancedUser{
  Name:         "abhishek",
  Age:          35,
  IsOnline:     false,
  Favourites:   []string{"Lost In Translation, The Walking Dead"},
  Contact:      map[string]string{"mobile": "+919718861200", "email": "[email protected]"},
  RegisteredOn: time.Now(),
 }

 avMap, err := attributevalue.MarshalMap(user)

 var result AdvancedUser
 err = attributevalue.UnmarshalMap(avMap, &result)

 log.Println("\nname", result.Name, "\nage", result.Age, "\nfavs", result.Favourites)
}

Использование структурных тегов для настройки

Функции Marshal и Unmarshal поддерживают тег структуры dynamodbav для управления преобразованием между типами Go и DynamoDB AttributeValue. Рассмотрим следующее struct:

type User struct {
 Email string `dynamodbav:"email" json:"user_email"`
 Age   int    `dynamodbav:"age,omitempty" json:"age,omitempty"`
 City  string `dynamodbav:"city" json:"city"`
}

Несколько распространенных сценариев, где пригодится dynamodbav.

Настроить имя атрибута

Скажем, у нас есть таблица с электронной почтой в качестве ключа раздела. Без тега dynamodbav:"email", когда мы упорядочиваем структуру User и пытаемся сохранить в таблице, она будет использовать Email (верхний регистр) в качестве имени атрибута — DynamoDB не примет это, поскольку имена атрибутов чувствительны к регистру«Все имена должны быть закодированы с использованием UTF-8 и чувствительны к регистру.'

Обратите внимание, что мы также объединили теги json (это совершенно верно) — они используются не DynamoDB, а библиотекой json при кодировании и декодировании данных

Обработка отсутствующих атрибутов

DynamoDB — это база данных NoSQL, и таблицы не имеют фиксированной схемы (за исключением ключа partition и необязательного ключа sort). Например, пользовательский элемент может не включать атрибут возраста. При использовании dynamodbav:"age,omitempty", если поле Age отсутствует, оно не будет отправлено на DynamoDB (оно будет проигнорировано).

При отсутствии этого тега наша запись DynamoDB будет иметь атрибут Age, установленный на 0 — в зависимости от вашего варианта использования это может быть или не быть

Чтобы просмотреть все шаблоны использования этого структурного тега, обратитесь к Документации по API Marshal.

Как и было обещано ранее, давайте рассмотрим, как использовать все эти API в рамках…

… Сквозной пример

Мы рассмотрим приложение Go, которое предоставляет REST API с несколькими конечными точками. Он объединяет API-интерфейсы CRUD (PutItem, GetItem и т. д.) вместе со всеми функциями/API, упомянутыми выше.

Попробуйте приложение

Прежде чем мы увидим код, давайте быстро просмотрим и протестируем конечные точки, предоставляемые приложением. Вам нужно будет установить Go, клонировать приложение и перейти в нужный каталог.

git clone https://github.com/abhirockzz/dynamodb-go-sdk-type-conversion
cd dynamodb-go-sdk-type-conversion

Во-первых, создайте таблицу DynamoDB (вы можете назвать ее users). Используйте город в качестве ключа Partition, адрес электронной почты в качестве ключа Sort.

Вам нужны тестовые данные. Вы можете сделать это вручную, но я включил простую утилиту для заполнения некоторых тестовых данных во время запуска приложения. Чтобы использовать его, просто установите переменную SEED_TEST_DATA при запуске приложения:

export SEED_TEST_DATA=true

go run main.go
# output
started http server...

Это создаст 100 элементов. Проверьте таблицу DynamoDB, чтобы подтвердить:

Ваше приложение должно быть доступно на порту 8080. Вы можете использовать curl или любой другой клиент HTTP для вызова конечных точек:

# to get all users
curl -i http://localhost:8080/users/

# to get all users in a particular city
curl -i http://localhost:8080/users/London

# to get a specific user
curl -i "http://localhost:8080/user?city=London&[email protected]"

Чтобы лучше понять, как используются вышеуказанные API, давайте кратко рассмотрим ключевые части кода:

Прохождение кода

Добавить новый элемент в таблицу DynamoDB

Начиная с обработчика HTTP для добавления User:

func (h Handler) CreateUser(rw http.ResponseWriter, req *http.Request) {
 var user model.User

 err := json.NewDecoder(req.Body).Decode(&user)
 if err != nil {// handle error}

 err = h.d.Save(user)
 if err != nil {// handle error}

 err = json.NewEncoder(rw).Encode(user.Email)
 if err != nil {// handle error}
}

Сначала мы преобразуем полезную нагрузку JSON в структуру User, которую затем передаем функции Save.

func (d DB) Save(user model.User) error {

 item, err := attributevalue.MarshalMap(user)

 if err != nil {// handle error}

 _, err = d.client.PutItem(context.Background(), &dynamodb.PutItemInput{
  TableName: aws.String(d.table),
  Item:      item})

 if err != nil {// handle error}

 return nil
}

Обратите внимание, как MarshalMap используется для преобразования структуры User в map[string]types.AttributeValue, которую может принять PutItem API:

Получить один элемент из DynamoDB

Поскольку наша таблица имеет составной первичный ключ (city — это ключ partition, а email — это ключ sort), нам нужно будет предоставить их оба, чтобы найти определенный пользовательский элемент:

func (h Handler) FetchUser(rw http.ResponseWriter, req *http.Request) {

 email := req.URL.Query().Get("email")
 city := req.URL.Query().Get("city")

 log.Println("getting user with email", email, "in city", city)

 user, err := h.d.GetOne(email, city)
 if err != nil {// handle error}


 err = json.NewEncoder(rw).Encode(user)
 if err != nil {// handle error}
}

Мы извлекаем email и city из параметров запроса в запросе HTTP и передаем их на уровень базы данных (функция GetOne).

func (d DB) GetOne(email, city string) (model.User, error) {

 result, err := d.client.GetItem(context.Background(),
  &dynamodb.GetItemInput{
   TableName: aws.String(d.table),
   Key: map[string]types.AttributeValue{
    "email": &types.AttributeValueMemberS{Value: email},
    "city":  &types.AttributeValueMemberS{Value: city}},
  })

 if err != nil {// handle error}

 if result.Item == nil {
  return model.User{}, ErrNotFound
 }

 var user model.User

 err = attributevalue.UnmarshalMap(result.Item, &user)
 if err != nil {// handle error}

 return user, nil
}

Мы вызываем GetItem API и возвращаем результат в виде map[string]types.AttributeValue (через атрибут Item в GetItemOutput). Это преобразуется обратно в структуру Go (User) с использованием UnmarshalMap.

Обратите внимание, что атрибут Key в GetItemInput также принимает map[string]types.AttributeValue, но мы не используем MarshalMap для его создания

Выбрать несколько элементов

Мы можем запросить всех пользователей в конкретном городе — это вполне допустимый шаблон доступа, поскольку city — это ключ partition.

Функция обработчика HTTP принимает город в качестве параметра пути, который передается на уровень базы данных.

func (h Handler) FetchUsers(rw http.ResponseWriter, req *http.Request) {
 city := mux.Vars(req)["city"]
 log.Println("city", city)

 log.Println("getting users in city", city)

 users, err := h.d.GetMany(city)

 if err != nil {
  http.Error(rw, err.Error(), http.StatusInternalServerError)
  return
 }

 err = json.NewEncoder(rw).Encode(users)
 if err != nil {
  http.Error(rw, err.Error(), http.StatusInternalServerError)
  return
 }
}

С этого момента всю работу выполняет функция GetMany:

func (d DB) GetMany(city string) ([]model.User, error) {

 kcb := expression.Key("city").Equal(expression.Value(city))
 kce, _ := expression.NewBuilder().WithKeyCondition(kcb).Build()

 result, err := d.client.Query(context.Background(), &dynamodb.QueryInput{
  TableName:                 aws.String(d.table),
  KeyConditionExpression:    kce.KeyCondition(),
  ExpressionAttributeNames:  kce.Names(),
  ExpressionAttributeValues: kce.Values(),
 })

 if err != nil {
  log.Println("Query failed with error", err)
  return []model.User{}, err
 }

 users := []model.User{}

 if len(result.Items) == 0 {
  return users, nil
 }

 err = attributevalue.UnmarshalListOfMaps(result.Items, &users)
 if err != nil {
  log.Println("UnmarshalMap failed with error", err)
  return []model.User{}, err
 }

 return users, nil
}

Обратите внимание на две вещи:

  • Как используется KeyConditionExpression (это из пакета expressions)
  • И что еще более интересно, использование функции UnmarshalListOfMaps для прямого преобразования []map[string]types.AttributeValue (slice элементов из DynamoDB) в структуру slice из User. Если бы не эта функция, нам пришлось бы извлекать каждый элемент из результата, то есть map[string]types.AttributeValue, и вызывать UnmarshalMap для каждого из них. Так что это очень удобно!

Наконец-то — просто получите все!

Функция GetAll использует операцию Scan для получения всех записей в таблице DynamoDB.

Операция Scan проходит по всей таблице (или вторичному индексу), и весьма вероятно, что она в конечном итоге будет потреблять большую часть выделенной пропускной способности, особенно если это большая таблица. Это должно быть вашим последним средством — проверьте, подходит ли Query API (или BatchGetItem) для вашего варианта использования.

func (d DB) GetAll() ([]model.User, error) {

 result, err := d.client.Scan(context.Background(), &dynamodb.ScanInput{
  TableName: aws.String(d.table),
 })

 if err != nil {
  log.Println("Scan failed with error", err)
  return []model.User{}, err
 }

 users := []model.User{}

 err = attributevalue.UnmarshalListOfMaps(result.Items, &users)

 if err != nil {
  log.Println("UnmarshalMap failed with error", err)
  return []model.User{}, err
 }

 return users, nil
}

Заворачивать

Я надеюсь, что вы нашли это полезным, и теперь вы знаете об API в DynamoDB Go SDK для работы с простыми типами Go, а также с structs, maps, slices и т. д. Я рекомендую вам изучить некоторые другие нюансы, например, как настроить функции Marshal и Unmarshal с использованием MarshalWithOptions и UnmarshalWithOptions соответственно.