はじめに
最近、DynamoDB のデータモデリングを勉強しているが、これに従ってみようとすると、あれ?なんか Golang での実装が難しくないか?と思って調べてみた。
※Python だったらディクショナリ型を使えばお手軽に扱える気がするのだけど。
データモデリングのベストプラクティスについては、以下の記事を参考にすると良い。
何が難しいか?
いずれの記事でも、同じハッシュキーにできるものは1つのテーブルに寄せて、属性が異なるものはソートキーで種別を分ける、と書かれている。
例えば、あるハッシュキーに紐づく情報をqueryで一発で引くと、以下のように personal
と company
といった異なる属性を持ったアイテムが取得されることになる。
{
"Items": [
{
"department": {
"S": "sales"
},
"grade": {
"S": "A"
},
"id": {
"S": "00001"
},
"type": {
"S": "company"
}
},
{
"birthdt": {
"S": "19800101"
},
"postalcode": {
"S": "1740001"
},
"id": {
"S": "00001"
},
"telno": {
"S": "09000000000"
},
"name": {
"S": "Taro"
},
"type": {
"S": "personal"
}
}
],
"Count": 2,
"ScannedCount": 2,
"ConsumedCapacity": null
}
これを Golang で実装しようとした場合、もちろん GetItem()
を2回呼ぶという方法もあるが、それだとコードが冗長になってしまうケースがある。Query()
で一発で取得しようとしたらどうしたら良いだろうか?
考えてみる
上記のJSONを、JSON-to-Go に食わせてみると、以下のように出力される。
type AutoGenerated struct {
Items []struct {
Department struct {
S string `json:"S"`
} `json:"department,omitempty"`
Grade struct {
S string `json:"S"`
} `json:"grade,omitempty"`
ID struct {
S string `json:"S"`
} `json:"id"`
Type struct {
S string `json:"S"`
} `json:"type"`
Birthdt struct {
S string `json:"S"`
} `json:"birthdt,omitempty"`
Postalcode struct {
S string `json:"S"`
} `json:"postalcode,omitempty"`
Telno struct {
S string `json:"S"`
} `json:"telno,omitempty"`
Name struct {
S string `json:"S"`
} `json:"name,omitempty"`
} `json:"Items"`
Count int `json:"Count"`
ScannedCount int `json:"ScannedCount"`
ConsumedCapacity interface{} `json:"ConsumedCapacity"`
}
つまり、両方の属性を持った構造体を定義して、取得できなかったものは omitempty
すれば良いようだ。
※omitempty
は、Unmarshal()
時に該当の属性がJSONに入っていなかった場合に、空値にしてくれる。
上記は、あくまでもJSONへのマッピングなので、DynamoDBAttributeValue
から Unmarshal()
するには、以下のように定義する。
type item struct {
Id string `dynamodbav:"id"`
Type string `dynamodbav:"type"`
Name string `dynamodbav:"name,omitempty"`
BirthDt string `dynamodbav:"birthdt,omitempty"`
TelNo string `dynamodbav:"telno,omitempty"`
PostalCode string `dynamodbav:"postalcode,omitempty"`
Department string `dynamodbav:"department,omitempty"`
Grade string `dynamodbav:"grade,omitempty"`
}
ただし、Query()
で複数レコード帰ってくるときは配列なので、さらに以下のように配列化するための構造体を作る。
type items struct {
Item []item
}
この構造体を活用して、以下のように Query()
の結果を Unmarshal()
しよう。
var items items
result, err := ddb.Query(&dynamodb.QueryInput{
TableName: aws.String("table-name"),
KeyConditionExpression: aws.String("#id = :id"),
ExpressionAttributeNames: map[string]*string{
"#id": aws.String("id"),
},
ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{
":id": {
S: aws.String(string(id)),
},
},
})
err = dynamodbattribute.UnmarshalListOfMaps(result.Items, &items.Item)
これで、無事、item
の配列にマッピングできた状態になる。
さて、これであとは煮るなり焼くなり好きにすれば良いが、せっかくなので、2種類のレコードをお手軽に取得できるようにしてみよう。
※単純に JSON を返せば良いのであれば、さらにここから、別の JSON 定義で marshal()
すれば良いが、今回は別の構造体にマッピングしてみる。
type personal struct {
Id string
Name string
BirthDt string
TelNo string
PostalCode string
}
type company struct {
Id string
Department string
Grade string
}
type itemsInterface interface {
getPersonal() personal
getCompany() company
}
func (items items) getPersonal() personal {
for _, item := range items.Item {
if item.Type == "personal" {
return personal{
Id: item.Id,
Name: item.Name,
BirthDt: item.BirthDt,
TelNo: item.TelNo,
PostalCode: item.PostalCode,
}
}
}
return personal{}
}
func (items items) getCompany() company {
for _, item := range items.Item {
if item.Type == "company" {
return company{
Id: item.Id,
Department: item.Department,
Grade: item.Grade,
}
}
}
return company{}
}
あとは、Unmarshal()
後に以下のように呼び出せばよい。
personal := items.getPersonal()
company := items.getCompany()
これでもまだ冗長な気がするが、それでも単に GetItem()
するよりは、スマートに作れている……かな……?