Golang(aws-sdk-go)でDynamoDBを操作するための基礎知識

f:id:pinkie79:20220315150656p:plain

皆さん、初めまして。資産運用システム課の藤田です。

 

私はこのたび社内のスキルチェンジプロジェクトに配属され、今まで触れてこなかった技術を学ぶ楽しさを知りました。

スキルチェンジプロジェクトとは、AWSおよび開発技術習得を目的とし、このプロジェクトに参加した経験を活かして、新しい知識・技術を効率的にインプット及びアウトプットできることを目標としています。

 

スキルチェンジプロジェクトについて詳しく説明した記事はこちら👇

blog.css-net.co.jp

 

この場をお借りして、自力でGo言語とAWSサービスのDynamoDBについて理解できたことをアウトプットさせていただきます。概要としては、DynamoDBの基礎知識を紹介し、DynamoDBを実際にGo言語(aws-sdk-go)で操作した内容をお伝えします。

 

1.DynamoDBについて

DynamoDBとは、AWSが提供するフルマネージドの NoSQL データベースサービスです。
特徴として、下記3点があげられます。

  • 非リレーショナルデータベース(NoSQL)であり、リレーショナルデータベースの操作を行えません。
    しかし、非定型な構造を持ち、データの管理を柔軟に行うことができます。
  • フルマネージドであるため、メンテナンス不要で、スケールアウト/スケールダウンが容易にできます。
  • 3つのAZ(Availability Zone)で、高可用性を実現しています。

2.DynamoDBで使われる用語

  • Item: 行、項目
  • Attributes: 列、属性
  • Partition key(ハッシュキー): 必須、主キーのような役割
  • Sort key: ソートが行われるキー
  • Partition keyのみ、またはPartition keyとSort Keyで、ユニークなItemを表現
  • Secondary index: Partition keyとSort Key以外の属性を使ってデータを取得する際に使用するもの

3.セカンダリインデックスについて

セカンダリインデックスを設定することにより、クエリによるデータの取得を実行できます。(パーティションキー、またはパーティションキー/ソートキーで、データを検索する場合はセカンダリインデックスの設定は不要です。)

そのため、どのようにデータを取得するかを考えた上で、セカンダリインデックスの作成を進めていかなければなりません。


セカンダリインデックスには、グローバルセカンダリインデックス、ローカルセカンダリインデックスの2種類が存在します。

3-1.グローバルセカンダリインデックス

パーティションキー、またはパーティション/ソートキーがテーブル作成時のものと異なるインデックス。テーブル作成時、テーブル作成後、どちらでも設定できます。

3-2.ローカルセカンダリインデックス

パーティションキーはテーブル作成で設定したものと同じで、ソートキーが異なるインデックス。テーブル作成時にのみ設定できます。

4.テーブル作成

テーブルの作成時、主として、"テーブル名"、"属性"、"キャパシティーモード"、"キー項目"、"セカンダリインデックス"の設定を行います。


Itemの追加時に、属性を自由に設定することができるため、リレーショナルデータベースのように、前もって属性を設計する必要はなく、必要最低限の属性(キー項目)のみを設定します。

 

プログラム例では、下記の設定でテーブル作成を行います。

  • リージョン: 東京
  • テーブル名: Golang-CreateTable
  • 属性: ID(Number), TITLE(String), PUBLISHED_YEAR(Number), AUTHOR(String)
  • パーティションキー: ID(Number)
  • ソートキー: TITLE(String)
  • キャパシティーモード: オンデマンド
  • グローバルセカンダリインデックス
    • インデックス名: PUBLISHED_YEAR-AUTHOR-index
    • パーティションキー: PUBLISHED_YEAR(Number)
    • ソートキー: AUTHOR(String)
    • 非キー属性: ID(Number), TITLE(String)
    • プロジェクションタイプ: INCLUDE(非キー属性も含める)
  • ローカルセカンダリインデックス
    • インデックス名: ID-PUBLISHED_YEAR-index
    • パーティションキー: ID(Number)
    • ソートキー: PUBLISHED_YEAR(Number)
    • 非キー属性: TITLE(String)
    • プロジェクションタイプ: ALL(すべてのテーブル属性を表示)

 

テーブル作成のプログラム例(aws-sdk-go)

package main

import (
	"fmt"
	"log"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/dynamodb"
)

func main() {
	ddb := dynamodb.New(session.New(), aws.NewConfig().WithRegion("ap-northeast-1"))

	params := &dynamodb.CreateTableInput{
		AttributeDefinitions: []*dynamodb.AttributeDefinition{
			{
				AttributeName: aws.String("ID"),
				AttributeType: aws.String("N"), // データ型(Number:N)
			},
			{
				AttributeName: aws.String("TITLE"),
				AttributeType: aws.String("S"), // データ型(String:S)
			},
			{
				AttributeName: aws.String("PUBLISHED_YEAR"),
				AttributeType: aws.String("N"), // データ型(String:N)
			},
			{
				AttributeName: aws.String("AUTHOR"),
				AttributeType: aws.String("S"), // データ型(String:S)
			},
		},
		BillingMode: aws.String("PAY_PER_REQUEST"), // キャパシティーモードをオンデマンドに指定
		KeySchema: []*dynamodb.KeySchemaElement{
			{
				AttributeName: aws.String("ID"),
				KeyType:       aws.String("HASH"), // HASH(Partition key)を設定
			},
			{
				AttributeName: aws.String("TITLE"),
				KeyType:       aws.String("RANGE"), // RANGE(Sort key)を設定
			},
		},
		GlobalSecondaryIndexes: []*dynamodb.GlobalSecondaryIndex{
			{
				IndexName: aws.String("PUBLISHED_YEAR-AUTHOR-index"),
				KeySchema: []*dynamodb.KeySchemaElement{
					{
						AttributeName: aws.String("PUBLISHED_YEAR"), // インデックス名
						KeyType:       aws.String("HASH"),           // インデックスの型(HASH または RANGE)
					},
					{
						AttributeName: aws.String("AUTHOR"), // インデックス名
						KeyType:       aws.String("RANGE"),  // インデックスの型(HASH または RANGE)
					},
				},
				Projection: &dynamodb.Projection{
					NonKeyAttributes: aws.StringSlice([]string{"ID", "TITLE"}), // 非キー属性名
					ProjectionType:   aws.String("INCLUDE"),                    //指定した非キー属性も含める
				},
			},
		},
		LocalSecondaryIndexes: []*dynamodb.LocalSecondaryIndex{
			{
				IndexName: aws.String("ID-PUBLISHED_YEAR-index"),
				KeySchema: []*dynamodb.KeySchemaElement{
					{
						AttributeName: aws.String("ID"),   // インデックス名
						KeyType:       aws.String("HASH"), // インデックスの型(HASH または RANGE)
					},
					{
						AttributeName: aws.String("PUBLISHED_YEAR"), // インデックス名
						KeyType:       aws.String("RANGE"),          // インデックスの型(HASH または RANGE)
					},
				},
				Projection: &dynamodb.Projection{
					ProjectionType: aws.String("ALL"), //すべてのテーブル属性を表示
				},
			},
		},

		TableName: aws.String("Golang-CreateTable"), // テーブル名
	}

	_, err := ddb.CreateTable(params)

	if err != nil {
		log.Fatalf("Got error calling CreateTable: %s", err)
	}

	fmt.Println("Created the table")
}じ

実行結果f:id:pinkie79:20220315092904p:plain

f:id:pinkie79:20220315092910p:plain

5.Itemの追加

Itemの追加を行う際、属性として入力必須のものは、パーティションキー及びソートキーです。
他の属性については、データ型、列名を指定して自由に入力できるようになっています。

プログラム例では、以下の内容のItemを追加します。

ID

TITLE

PUBLISHED_YEAR

AUTHOR

COUNTRY

EXIST

1

"John&Evelyn"

1999

"Olivia"

"UK"

true

 

Item追加のプログラム例(aws-sdk-go)

package main

import (
	"fmt"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/dynamodb"
)

func main() {
	ddb := dynamodb.New(session.New(), aws.NewConfig().WithRegion("ap-northeast-1"))

	param1 := &dynamodb.PutItemInput{
		TableName: aws.String("Golang-CreateTable"),
		Item: map[string]*dynamodb.AttributeValue{
			"ID": {
				N: aws.String("1"), // データ型(Number:N)
			},
			"TITLE": {
				S: aws.String("John&Evelyn"), //データ型(String:S)
			},
			"PUBLISHED_YEAR": {
				N: aws.String("1999"), // データ型(String:S)
			},
			"AUTHOR": {
				S: aws.String("Olivia"), // データ型(Number:N)
			},
			"COUNTRY": {
				S: aws.String("UK"),
			},
			"EXIST": {
				BOOL: aws.Bool(true),
			},
		},
	}

	_, err := ddb.PutItem(param1) //実行

	if err != nil {
		fmt.Println(err.Error())
	}

	fmt.Println("Put a first Item")
}

実行結果

f:id:pinkie79:20220315092947p:plain

f:id:pinkie79:20220315092952p:plain

6.Itemの検索

1.パーティションキー、またはパーティション/ソートキーで、一意なデータを検索する場合、GetItemを使用

プログラム例では、5. Itemの追加で追加したItemを以下の内容で取得します。

  • パーティションキー及びソートキー

 パーティションキー 

 ソートキー

 ID

 TITLE

 1

 "John&Evelyn" 

  • 取得する属性: ID, TITLE, AUTHOR, EXIST

GetItemのプログラム例(aws-sdk-go)

package main

import (
	"fmt"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/dynamodb"
)

func main() {
	ddb := dynamodb.New(session.New(), aws.NewConfig().WithRegion("ap-northeast-1"))

	params := &dynamodb.GetItemInput{
		TableName: aws.String("Golang-CreateTable"), // テーブル名

		Key: map[string]*dynamodb.AttributeValue{
			"ID": { // パーティションキー
				N: aws.String("1"), // 持ってくるキーの値
			},
			"TITLE": { // ソートキー
				S: aws.String("John&Evelyn"), // 持ってくるキーの値
			},
		},
		AttributesToGet: []*string{
			aws.String("ID"), // 取得するAttributes
			aws.String("TITLE"),
			aws.String("AUTHOR"),
			aws.String("EXIST"),
		},
		ConsistentRead: aws.Bool(true), // 最新を取得する

		//返ってくるデータの種類
		ReturnConsumedCapacity: aws.String("NONE"),
	}

	resp, err := ddb.GetItem(params)

	if err != nil {
		fmt.Println(err.Error())
	}

	//resp.Item[項目名].型 でデータへのポインタを取得
	fmt.Println(*resp.Item["ID"].N, *resp.Item["TITLE"].S, *resp.Item["AUTHOR"].S, *resp.Item["EXIST"].BOOL)
}

実行結果

f:id:pinkie79:20220315093025p:plain

2.セカンダリインデックスで指定したパーティションキー、またはパーティション/ソートキーで、データを検索する場合、Queryを使用

プログラム例では、5. Itemの追加で追加したItemを取得します。

  • セカンダリインデックスを使用して、Itemを取得

 パーティションキー 

 ソートキー 

 PUBLISHED_YEAR

 AUTHOR

 1999

 "Olivia"

  • 取得する属性は、テーブル設定時に指定したプロジェクションタイプに帰属する

Queryのプログラム例(aws-sdk-go)

package main

import (
	"fmt"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/dynamodb"
)

func main() {
	ddb := dynamodb.New(session.New(), aws.NewConfig().WithRegion("ap-northeast-1"))

	params := &dynamodb.QueryInput{
		// テーブルを指定
		TableName: aws.String("Golang-CreateTable"),
		// Attribute名を指定
		ExpressionAttributeNames: map[string]*string{
			"#published_year": aws.String("PUBLISHED_YEAR"),
			"#author":         aws.String("AUTHOR"),
		},
		// Attributeの値を指定
		ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{
			":published_year": {
				N: aws.String("1999"),
			},
			":author": {
				S: aws.String("Olivia"),
			},
		},
		KeyConditionExpression: aws.String("#published_year = :published_year and #author = :author"),
		// 対象のセカンダリインデックスを指定
		IndexName: aws.String("PUBLISHED_YEAR-AUTHOR-index"),
	}

	result, err := ddb.Query(params)

	if err != nil {
		fmt.Println(err.Error())
	}
	fmt.Println(result)
}

実行結果 

f:id:pinkie79:20220315093054p:plain

7.Itemの更新

Itemの更新を行う際、パーティションキー及びソートキーで、対象のItemを指定します。

プログラム例では、追加したItemを対象に、以下の内容を変更します。

 

 AUTHOR

 EXIST

 "Olivia"

 true

 "Emma" 

 false 

Updateのプログラム例(aws-sdk-go)

package main

import (
	"fmt"

	"github.com/aws/aws-sdk-go/aws"
	"github.com/aws/aws-sdk-go/aws/session"
	"github.com/aws/aws-sdk-go/service/dynamodb"
)

func main() {
	ddb := dynamodb.New(session.New(), aws.NewConfig().WithRegion("ap-northeast-1"))

	param := &dynamodb.UpdateItemInput{
		TableName: aws.String("Golang-CreateTable"), // テーブル名を指定

		Key: map[string]*dynamodb.AttributeValue{
			"ID": {
				N: aws.String("1"), // 既存キー名を指定
			},
			"TITLE": {
				S: aws.String("John&Evelyn"), // 既存キー名を指定
			},
		},

		ExpressionAttributeNames: map[string]*string{
			"#author": aws.String("AUTHOR"), // 属性名をプレースホルダに入れる
			"#exist":  aws.String("EXIST"),
		},
		ExpressionAttributeValues: map[string]*dynamodb.AttributeValue{
			":author": {
				S: aws.String("Emma"), // 値をプレースホルダに入れる
			},
			":exist": {
				BOOL: aws.Bool(false),
			},
		},
		UpdateExpression: aws.String("set #author = :author, #exist = :exist"), //プレースホルダを利用して更新の式を書く
		//あとは返してくる情報の種類を指定する
		ReturnConsumedCapacity:      aws.String("NONE"),
		ReturnItemCollectionMetrics: aws.String("NONE"),
		ReturnValues:                aws.String("NONE"),
	}

	_, err := ddb.UpdateItem(param) //実行

	if err != nil {
		fmt.Println(err.Error())
	}

	fmt.Println("Updated Item")
}

実行結果

f:id:pinkie79:20220315093120p:plain

f:id:pinkie79:20220315093124p:plain

 

ご覧いただき、ありがとうございました。

 

スキルチェンジプロジェクトでの経験を活かして、エンジニアとして成長し続けること、お客様の期待に自信を持って応えられる知識及び技術力を持つことを目標に、日々精進して参ります。

参考

AWS SDK for Go API Reference
aws-doc-sdk-examples(Golang)