Как обрабатывать преобразования типов с помощью DynamoDB Go SDK
Учитесь на практических примерах кода
DynamoDB
предоставляет богатый набор типов данных, включая String
s, Number
s, Set
s, List
s, Map
s и т. д. В Go SDK для DynamoDB пакет types содержит Go-представления этих типов данных и модуль attributevalue. предоставляет функции для работы с типами Go и DynamoDB
.
В этом сообщении блога показано, как обрабатывать преобразования между типами Go в вашем приложении и DynamoDB
. Мы начнем с простых фрагментов кода, чтобы представить некоторые конструкции API, и завершим пример того, как использовать эти функции Go SDK в контексте полного приложения (включая пошаговое руководство по коду).
Вы можете посмотреть полный код на GitHub
Для начала пройдемся по нескольким примерам.
Обратите внимание, что обработка ошибок намеренно опущена в приведенных ниже фрагментах кода, чтобы сделать их краткими.
Преобразование типов Go в DynamoDB
Об этом позаботится семейство функций Маршал. Он работает с базовыми скалярами (int
, uint
, float
, bool
, string
), map
s, slice
s и struct
s.
Для работы со скалярными типами просто используйте (общую) функцию 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
При работе с slice
s и map
s лучше использовать специальные функции, такие как 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 struct
s. Итак, давайте рассмотрим несколько примеров этого.
Работа со структурами 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
, упрощают работу с несколькими slice
s 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, а также с struct
s, map
s, slice
s и т. д. Я рекомендую вам изучить некоторые другие нюансы, например, как настроить функции Marshal
и Unmarshal
с использованием MarshalWithOptions и UnmarshalWithOptions соответственно.