Golangはじめて物語(第8話: DynamoDBのデータモデリングのベストプラクティスに従った場合の実装方法)

# はじめに
最近、DynamoDB のデータモデリングを勉強しているが、これに従ってみようとすると、あれ？なんか Golang での実装が難しくないか？と思って調べてみた。
※Python だったらディクショナリ型を使えばお手軽に扱える気がするのだけど。

データモデリングのベストプラクティスについては、以下の記事を参考にすると良い。

- 【DevelopersIO】[[レポート]DynamoDBデータモデリング (CMY304) #reinvent](https://dev.classmethod.jp/articles/dynamodb-data-modeling-cmy304/)
- [DynamoDBデータモデリング虎の巻：第壱巻 〜前提知識編〜](http://marcy.hatenablog.com/entry/2018/07/31/213705)

# 何が難しいか？
いずれの記事でも、同じハッシュキーにできるものは1つのテーブルに寄せて、属性が異なるものはソートキーで種別を分ける、と書かれている。
例えば、あるハッシュキーに紐づく情報をqueryで一発で引くと、以下のように ```personal``` と ```company``` といった異なる属性を持ったアイテムが取得されることになる。

```JSON
{
    "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](https://mholt.github.io/json-to-go/) に食わせてみると、以下のように出力される。

```golang
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()``` するには、以下のように定義する。

```golang
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()``` で複数レコード帰ってくるときは配列なので、さらに以下のように配列化するための構造体を作る。

```golang
type items struct {
	Item []item
}
```

この構造体を活用して、以下のように ```Query()``` の結果を ```Unmarshal()``` しよう。

```golang
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()``` すれば良いが、今回は別の構造体にマッピングしてみる。

```golang
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()``` 後に以下のように呼び出せばよい。

```golang
	personal := items.getPersonal()
	company  := items.getCompany()
```

これでもまだ冗長な気がするが、それでも単に ```GetItem()``` するよりは、スマートに作れている……かな……？