DynamoDB+Goでmapのデータを取得する

# はじめに

GoでDynamoDBのmapのデータを取りたいときのメモ。
単純な構成は結構ググったら出てくるが、複雑な構成はググっても出てこず、試行錯誤した。

## TL;DR

* DBの要素にmapが入っている場合 -> [パターン1](#パターン1)
* mapがネストしている場合 -> [パターン2](#パターン2)
* リストの中にmapがある場合 -> [パターン3](#パターン3)
* mapを表現する構造体を定義する。
* mapを表現した構造体は、struct tagにキー名をつける。
* 各構造体のフィールドはエクスポートする。

## DBの構成

こんな構成。mapがネストしていたり、リストの中にmapが入っていたり。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/423952/d910f3c9-5d8b-e7a8-b6cb-820c0ba1cd72.png)

aws cliでget-itemするとこんな感じ。 

```shell
22:14:30 dynamo-sample aws dynamodb get-item --table-name user --key '{"id": {"S":  "sample"} }' --endpoint-url http://localhost:8000 --output json              
{
    "Item": {
        "basic": {
            "M": {
                "firstname": {
                    "S": "taro"
                },
                "address": {
                    "M": {
                        "city": {
                            "S": "Minatoku"
                        },
                        "pref": {
                            "S": "Tokyo"
                        }
                    }
                },
                "age": {
                    "N": "20"
                },
                "lastname": {
                    "S": "sample"
                }
            }
        },
        "logs": {
            "L": [
                {
                    "M": {
                        "time": {
                            "S": "2130"
                        }
                    }
                },
                {
                    "M": {
                        "time": {
                            "S": "2131"
                        }
                    }
                }
            ]
        },
        "id": {
            "S": "sample"
        }
    }
}

```

## パターン1

シンプルなmapの場合。

basicは、

* firstname
* address
* age
* lastname

というmapをもっている。このうち、firstnameを取得するには、

```go
package main

import (
	"fmt"
	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/guregu/dynamo"
	"log"
)

type User struct {
	Id    string `dynamo:"id"`
	Basic Basic  `dynamo:"basic"`
}

type Basic struct {
	First string `dynamo:"firstname"`
}

func main() {
	db := dynamo.New(session.New(), &aws.Config{
		Region:     aws.String("ap-northeast-1"),
		Endpoint:   aws.String("http://localhost:8000"),
		DisableSSL: aws.Bool(false),
	})

	table := db.Table("user")

	var user User
	if err := table.Get("id", "sample").One(&user); err != nil {
		log.Fatal(err)
	}

	fmt.Printf("%+v\n", user)
}

```

となる。

ポイントは、

* `User`テーブルを表現する構造体を定義する。
* `Basic`は中に複数のmapをもっているので、Basicを表現する構造体は別に定義する。
* struct tagはテーブル名と同じものにする。
* 構造体のフィールドはエクスポートする。(フィールド名の先頭は大文字)こうしないとデータが取れない。ここがわかってなくてハマった。

```go
type User struct {
	Id    string `dynamo:"id"`
	Basic Basic  `dynamo:"basic"`
}
```

次に、`Basic`構造体を定義する。
ポイントとしては、`struct tag`にはmapのキー名を定義する。ここがわからず、めちゃハマった。

```go
type Basic struct {
	First string `dynamo:"firstname"`
}
```

これで、実行すると、`{Id:sample Basic:{First:taro}}`のように出力される。
また、`user.Basic.First`のようにたどることができる。

## パターン2

ネストされたmapの場合。


basicは、

* firstname
* address
* age
* lastname

というmapをもっている。このうち、addressは、さらにPrefやCityといったmapをもっている。
とはいえ、`No1. シンプルなmap`と同じで、各mapを別構造体で定義し、struct tagにmapのキーを指定する。

```go
package main

import (
	"fmt"
	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/guregu/dynamo"
	"log"
)

type User struct {
	Id    string `dynamo:"id"`
	Basic Basic  `dynamo:"basic"`
}

type Basic struct {
	First string `dynamo:"firstname"`
	Address Address `dynamo:"address"`
}

type Address struct {
	Pref string `dynamo:"pref"`
	City string `dynamo:"city"`
}

func main() {
	db := dynamo.New(session.New(), &aws.Config{
		Region:     aws.String("ap-northeast-1"),
		Endpoint:   aws.String("http://localhost:8000"),
		DisableSSL: aws.Bool(false),
	})

	table := db.Table("user")

	var user User
	if err := table.Get("id", "sample").One(&user); err != nil {
		log.Fatal(err)
	}

	fmt.Printf("%+v\n", user)
}

```

これで実行すると、`{Id:sample Basic:{First:taro Address:{Pref:Tokyo City:Minatoku}}}`となる。
また、user.Basic.Address.Prefみたいにたどることができる。

## パターン3

リスト内にmapがある場合。
logsはリスト内にmapをもっている。
ただし、これもここまでと同じで構造体を定義すれば取得できる。

```go
package main

import (
	"fmt"
	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/guregu/dynamo"
	"log"
)

type User struct {
	Id    string `dynamo:"id"`
	Basic Basic  `dynamo:"basic"`
	Logs []Log `dynamo:"logs"`
}

type Basic struct {
	First string `dynamo:"firstname"`
	Address Address `dynamo:"address"`
}

type Address struct {
	Pref string `dynamo:"pref"`
	City string `dynamo:"city"`
}

type Log struct {
	Time string `dynamo:"time"`
}

func main() {
	db := dynamo.New(session.New(), &aws.Config{
		Region:     aws.String("ap-northeast-1"),
		Endpoint:   aws.String("http://localhost:8000"),
		DisableSSL: aws.Bool(false),
	})

	table := db.Table("user")

	var user User
	if err := table.Get("id", "sample").One(&user); err != nil {
		log.Fatal(err)
	}

	fmt.Printf("%+v\n", user)
}

```

## 最終的なコード

```go
package main

import (
	"fmt"
	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/guregu/dynamo"
	"log"
)

type User struct {
	Id    string `dynamo:"id"`
	Basic Basic  `dynamo:"basic"`
	Logs []Log `dynamo:"logs"`
}

type Basic struct {
	First string `dynamo:"firstname"`
	Last string `dynamo:"lastname"`
	Age int `dynamo:"age"`
	Address Address `dynamo:"address"`
}

type Address struct {
	Pref string `dynamo:"pref"`
	City string `dynamo:"city"`
}

type Log struct {
	Time string `dynamo:"time"`
}

func main() {
	db := dynamo.New(session.New(), &aws.Config{
		Region:     aws.String("ap-northeast-1"),
		Endpoint:   aws.String("http://localhost:8000"),
		DisableSSL: aws.Bool(false),
	})

	table := db.Table("user")

	var user User
	if err := table.Get("id", "sample").One(&user); err != nil {
		log.Fatal(err)
	}

	fmt.Printf("%+v\n", user)
}

```