こんにちは。Tomoyuki(@tomoyuki65)です。
Webサービス開発にあたり、アクセスが一気に増えても、とにかく速くさばきたいといった特定のユースケースにおいては、一般的なRDB(リレーショナルデータベース)ではなくNoSQLが使われたりします。
特に日本の業務における、よく利用されるクラウドインフラはAWSであり、そんなAWSにおけるNoSQLのサービスといえば「DynamoDB」になります。
ということでこの記事では、Go言語(Golang)でAWS DynamoDB(NoSQL)の使い方についてまとめます。
Go言語(Golang)でAWS DynamoDB(NoSQL)の使い方|Docker環境構築+CRUD APIサンプル
AWS DynamoDB(NoSQL)とは?
AWS DynamoDBは、AWS(Amazon Web Services)が提供するフルマネージド型のNoSQLデータベースサービスです。
従来のRDBのように厳密なテーブル構造やスキーマを持たないデータベースであり、データをキーバリュー形式のシンプルな構造で保存する仕様で、柔軟なデータ構造を持たせることができます。
また、大量のデータを効率よく処理できるよう設計されており、分散処理やスケーリングにも優れているため、高トラフィックなシステムでも安定して利用できるのが特徴です。
DynamoDBを使うにあたり重要なこと
DynamoDBを使うにあたり、主キーの設計が非常に重要になってます。
特に必須のPK(パーティションキー)と、任意のSK(ソートキー)の組み合わせによってデータ取得方法が決まるため、どのようなアクセスパターンでデータを読み書きするかを事前に考えて設計する必要があります。
さらに注意すべきなのが「ホットパーティション問題」です。
これは特定のパーティションキーにアクセスが集中してしまい、その部分だけ負荷が高くなってしまう現象です。
DynamoDBはパーティションキーをもとにデータを分散しており、特定のPKに対してアクセスが集中するとスケーラビリティを活かしきれなくなります。
そのため、特定のデータにアクセスが集中しそうな場合においては、PK(パーティションキー)を適切に分散させる設計が重要になります。
※例えば、PKを「order#2026-03-25#01」、「order#2026-03-25#02」のようにサフィックスとして数値を付与し、アクセスを複数のパーティションに分散させるといった工夫が有効です。(ただし、データを取得する際は「order#2026-03-25#01」と「order#2026-03-25#02」を両方取得してマージさせるなどの対応が必要になる)
このような点も踏まえて、最初の設計段階でしっかり検討しておくことが非常に重要です。
DockerでDynamoDBのコンテナを立てる方法
ではDockerを使い、ローカル開発環境としてDynamoDBのコンテナを立ててみます。
まずは以下のコマンドを実行し、ファイルを作成します。
$ mkdir go-dynamodb && cd go-dynamodb
$ touch compose.yml
次に作成したファイルを以下のように記述します。
・「compose.yml」
services:
dynamodb:
image: amazon/dynamodb-local:latest
container_name: dynamodb-local
command: "-jar DynamoDBLocal.jar -sharedDb -dbPath ./data"
user: root # volumeがroot所有のため書き込み対応
ports:
- "8000:8000"
volumes:
- dynamodb_data:/home/dynamodblocal/data
volumes:
dynamodb_data:
※ボリュームを使ってデータを永続化したい場合は、rootユーザーで起動する必要があるため、「user: root」も付けてます。(dataディレクトリに書き込み権限が必要)
次に以下のコマンドを実行し、Dockerコンテナの起動を行います。
$ docker compose up -d
次に以下のコマンドを実行し、Dockerコンテナの起動を確認します。
$ docker compose ps
コマンド実行後、以下のようにdynamodbコンテナが起動していればOKです。
![]()
Amazon公式の無料GUIクライアントツール「NoSQL Workbench」のダウンロードとインストール
AWS DynamoDBのテーブル設計、データモデルの可視化、クエリ操作、コード生成等をしたい場合は、Amazon公式が無料で提供しているGUIのクライアントツール「NoSQL Workbench」が利用できます。
そんなNoSQL Workbenchをインストールしたい場合は、Amazon公式のドキュメント「DynamoDB 用の NoSQL Workbench のダウンロード」ページからダウンロード可能です。

ダウンロード後、MacOSの場合はファイル「NoSQL_Workbench-arm64.dmg」をクリックします。

次にポップアップが表示されるので、「NoSQL Workbench.app」をアプリケーションフォルダに移動してインストールします。

アプリをインストール後、「NoSQL Workbench.app」をクリックして起動します。

アプリ起動後、ポップアップが表示されるので、「開く」をクリックします。

アプリ起動後、ポップアップが表示されるので確認して「Close」をクリックします。

これでNoSQL Workbenchの起動が完了です。

NoSQL WorkbenchからローカルのDynamoDBコンテナへの接続方法
次にNoSQL WorkbenchからローカルのDynamoDBコンテナへ接続するため、画面右側にある「Add connection」をクリックします。

次に接続設定の画面が表示されるので、「Local connection」をクリックします。

次にConnection nameは必要に応じて修正し、画面右下の「Commit」をクリックします。

これでConnectionsの欄に設定が追加されるので、Connection nameをクリックします。

これでローカルのDynamoDBに対する操作画面が表示されます。

NoSQL Workbenchの使い方
テーブル作成
DynamoDBを使う際は、まずテーブルを作成する必要があります。
テーブルを作成したい場合は、画面左のTablesメニューにある「+」をクリックします。

次にテーブル作成が面が表示されるので、Table name(必須)、Partition key(必須)、Sort key(任意)を設定します。

例えばチャットのデータを保存するようなのをイメージした「ChatTable」を例にすると、Table name「ChatTable」、「PK」(String)、「SK」(String)を設定後、画面右下の「Next」をクリックします。

次にGSI(Global Secondary Index)のオプション設定画面が表示されますが、これは上記で設定したPK以外で検索が必要になる場合に設定が必要です。
ただし、GSIはテーブル単位で20個までしか作成できないので、事前にしっかり設計するようにして下さい。
GSIを作成する場合は、画面右側の「Add global secondary index」をクリックします。

次にGSIの設定画面が表示されるので、Index name、Partition key、Sort key、Projection typeを設定します。

例えばIndex name「GSI1」、Partition key「GSI1PK」(String)、Sort key「GSI1SK」(String)、Projection type「All」を設定し、画面右下の「Create GSI」をクリックします。

※Projection type「All」は、元テーブルの全フィールドをGSIにコピーします。
次に先ほどのテーブル作成画面に戻り、GSIが追加されているので、画面右下の「Next」をクリックします。

次にテーブル設定の確認画面が表示されるので、問題なければ画面右下の「Create table」をクリックします。

これでテーブルが作成され、左のTablesメニューのところに表示されます。

テーブルにデータ登録
上記で作成したテーブルにデータを登録したい場合は、画面左のTablesで対象のテーブルを選択し、画面右側のメニュー「Actions」から「Create item」をクリックします。

次にデータ作成画面が表示されるので、PKとSKを設定します。

例えばPK「ROOM#xxxx-xxxx-0001」、SK「MESSAGE#2026-03-25T10:00:00.123Z#yyyy-yyyy-0001」を設定し、次に内容を登録するため「+ Add other attributes」を必要な個数分だけクリックします。

次に今回はAttributeを10個作り、以下のように設定後、画面右下の「Run」をクリックします。
"entity_type": "MESSAGE",
"room_id": "xxxx-xxxx-0001",
"message_id": "yyyy-yyyy-0001",
"sender_id": "xxxx-yyyy-zzzz-0001",
"type": "text",
"text": "こんにちは!",
"created_at": "2026-03-25T10:00:00.123Z",
"updated_at": "2026-03-25T10:00:00.123Z",
"GSI1PK": "USER#xxxx-yyyy-zzzz-0001",
"GSI1SK": "MESSAGE#2026-03-25T10:00:00.123Z"

これで画面下に登録されたデータが表示されればOKです。

データ検索の「Query」機能を試す
次にテーブルに登録したデータ検索を試したい場合は、画面左のTablesで対象のテーブルを選択し、画面上のメニュー「Query」をクリックします。
PKとSKで検索できるので、有効な値(今回の例では「ROOM#xxxx-xxxx-0001」)を入力し、右側の「Query」をクリックして検索します。
対象データがあれば、画面下に表示されます。

もし、対象外のPKを入力して検索した場合は、下図のように表示されません。

次に上記で登録したGSIで検索したい場合は、画面左のTablesで対象のテーブルのGSIを選択し、画面上のメニュー「Query」をクリックします。
PKとSKで検索できるので、有効な値(今回の例では「USER#xxxx-yyyy-zzzz-0001」)を入力し、右側の「Query」をクリックして検索します。
対象データがあれば、画面下に表示されます。

もし、対象外のPKを入力して検索した場合は、下図のように表示されません。

後からGSIを追加する方法
後からGSIを追加したい場合は、画面左のTablesで対象のテーブルの「…」をクリックしてメニューを開き、「Create GSI」をクリックします。

次にGSIの設定画面が表示されるので、Index name、Partition key、Sort key、Projection typeを設定します。
今回は「message_id」だけで対象データが取得できるように、Index name「GSI2」、Partition key「message_id」を設定し、画面右下の「Create GSI」をクリックします。

実行後、画面左のTablesにGSI2が追加されればOKです。

「PartiQL editor」機能について
画面上のメニューには他にも「PartiQL editor」機能があり、これはSQLっぽい書き方でDynamoDBを操作できるエディタになります。
例えば以下のようにデータ取得のSQLを書き、画面右側の「Run」をクリックして実行すると、対象データを取得できます。

その他の「More operations」機能について
その他、画面上のメニュー「More operations」では、「TransactGetItems」(複数アイテムを一貫性を保って同時取得)、「TransactWriteItems」(複数の書き込みをトランザクションとして実行)、「PartiQLTransaction」(SQL風の書き方でトランザクション実行)、「PartiQLBatch」(SQL風のバッチ処理(非トランザクション))といったメニューがあります。

AWS CLIツールについて
上記ではGUIのクライアントツール「NoSQL Workbench」でテーブルの登録などを行いましたが、AWS CLIツールからも可能です。
AWS CLIツールのインストールについては、macOSやLinuxでパッケージマネージャーのHomebrewを利用している場合、以下のコマンドで簡単にインストール可能です。
$ brew install awscli
インストール後、以下のコマンドを実行し、コマンドをパスが通っていることを確認します。
$ aws --version
実行後、以下のようにバージョンが表示されればパスが通っているのでOKです。

AWS CLIツールのコンフィグ設定については、デフォルト設定は「aws configure」、プロファイルとしての設定は「aws configure –profile <プロファイル名>」のコマンドを使って登録できます。
例えばAWS CLIツールでテーブルを登録などをしたい場合は、オプション「–endpoint-url」を利用するとローカル開発環境にあるDynamoDBコンテナに対して実行できます。
・テーブル作成の例
$ aws dynamodb create-table \
--table-name ChatTable \
--attribute-definitions \
AttributeName=PK,AttributeType=S \
AttributeName=SK,AttributeType=S \
AttributeName=GSI1PK,AttributeType=S \
AttributeName=GSI1SK,AttributeType=S \
--key-schema \
AttributeName=PK,KeyType=HASH \
AttributeName=SK,KeyType=RANGE \
--global-secondary-indexes '[
{
"IndexName": "GSI1",
"KeySchema": [
{"AttributeName": "GSI1PK", "KeyType": "HASH"},
{"AttributeName": "GSI1SK", "KeyType": "RANGE"}
],
"Projection": {
"ProjectionType": "ALL"
}
}
]' \
--billing-mode PAY_PER_REQUEST \
--endpoint-url http://localhost:8000 \
--region ap-northeast-1
Go言語(Golang)でDynamoDBを操作するCRUDのAPIサンプルを作って試す
Go言語(Golang)でサンプルAPIの準備
次にGoのサンプルAPIを準備します。まずは以下のコマンドを実行し、上記で起動中のDockerコンテナを止めます。
$ docker compose down
次に以下のコマンドを実行し、各種ファイルを作成します。
$ mkdir -p docker/local/go && touch docker/local/go/Dockerfile
$ mkdir src && touch src/main.go
$ touch .env
次に作成したファイルをそれぞれ以下のように記述します。
・「docker/local/go/Dockerfile」
FROM golang:1.26.1-alpine3.23
WORKDIR /go/src
COPY ./src .
# go.modがあれば依存関係をインストール
RUN if [ -f ./go.mod ]; then \
go install; \
fi
# 開発用のライブラリをインストール
RUN go install github.com/air-verse/air@v1.64.5
RUN go install honnef.co/go/tools/cmd/staticcheck@latest
EXPOSE 8080
・「src/main.go」
package main
import (
"log/slog"
"net/http"
"github.com/labstack/echo/v4"
)
func main() {
// echoのルーター設定
e := echo.New()
e.GET("/", func(c echo.Context) error {
// レスポンス結果の設定
res := map[string]string{
"message": "Hello World !!",
}
return c.JSON(http.StatusOK, res)
})
// ログ出力
slog.Info("start go-dynamodb")
// サーバー起動
e.Logger.Fatal(e.Start(":8080"))
}
・「.env」
ENV=local
AWS_REGION=ap-northeast-1
※環境変数「AWS_REGION」の設定は必要みたいです。
次にファイル「compose.yml」を以下のように修正します。
services:
api:
container_name: go-dynamodb-api
build:
context: .
dockerfile: ./docker/local/go/Dockerfile
command: air -c .air.toml
volumes:
- ./src:/go/src
ports:
- "8080:8080"
env_file:
- .env
environment:
- DYNAMODB_ENDPOINT=http://dynamodb:8000
- AWS_ACCESS_KEY_ID=dummy
- AWS_SECRET_ACCESS_KEY=dummy
tty: true
stdin_open: true
depends_on:
- dynamodb
dynamodb:
image: amazon/dynamodb-local:latest
container_name: dynamodb-local
command: "-jar DynamoDBLocal.jar -sharedDb -dbPath ./data"
user: root # volumeがroot所有のため書き込み対応
ports:
- "8000:8000"
volumes:
- dynamodb_data:/home/dynamodblocal/data
volumes:
dynamodb_data:
※ローカル開発環境でapiコンテナからdynamodbコンテナへ接続するには、環境変数「AWS_ACCESS_KEY_ID」、「AWS_SECRET_ACCESS_KEY」の設定も必要です。
次に以下のコマンドを実行し、Dockerコンテナをビルドします。
$ docker compose build --no-cache
次に以下のコマンドを実行し、Go言語の初期化処理を行います。
$ docker compose run --rm api go mod init go-dynamodb
$ docker compose run --rm api go mod tidy
$ docker compose run --rm api air init
次に以下のコマンドを実行し、Dockerコンテナの再ビルドと起動を行います。
$ docker compose build --no-cache
$ docker compose up -d
次に以下のコマンドを実行し、Dockerコンテナの起動を確認します。
$ docker compose ps
コマンド実行後、以下のようにapiとdynamodbのコンテナがそれぞれ起動していればOKです。

次にブラウザで「http://localhost:8080」を開き、JSON形式で想定通りのメッセージが出力されていればOKです。

DynamoDBを操作するCRUDのAPIを追加
次にGo言語(Golang)でDynamoDBを操作するCRUDのAPIサンプルを作って試します。
まずは以下のコマンドを実行し、各種ファイルを作成します。
$ mkdir -p src/internal/infrastructure/database && touch src/internal/infrastructure/database/dynamodb.go
$ mkdir -p src/internal/infrastructure/persistence/dynamodb/chat && touch src/internal/infrastructure/persistence/dynamodb/chat/chat_model.go
次に作成したファイルを以下のように記述します。
・「src/internal/infrastructure/database/dynamodb.go」
package database
import (
"context"
"os"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
)
func NewDynamoDB(ctx context.Context) (*dynamodb.Client, error) {
region := os.Getenv("AWS_REGION")
endpoint := os.Getenv("DYNAMODB_ENDPOINT")
cfg, err := config.LoadDefaultConfig(
ctx,
config.WithRegion(region),
)
if err != nil {
return nil, err
}
var client *dynamodb.Client
if endpoint != "" {
// ローカル開発環境の場合
client = dynamodb.NewFromConfig(cfg, func(o *dynamodb.Options) {
o.BaseEndpoint = aws.String(endpoint)
})
} else {
// 本番環境の場合
client = dynamodb.NewFromConfig(cfg)
}
return client, nil
}
※これはDynamoDBへの接続用の設定ファイルです。ローカル開発環境では環境変数「DYNAMODB_ENDPOINT」を利用する必要があります。
・「src/internal/infrastructure/persistence/dynamodb/chat/chat_model.go」
package chat
type ChatTable struct {
PK string `dynamodbav:"PK"`
SK string `dynamodbav:"SK"`
EntityType string `dynamodbav:"entity_type"`
RoomID string `dynamodbav:"room_id"`
MessageID string `dynamodbav:"message_id"`
SenderID string `dynamodbav:"sender_id"`
Type string `dynamodbav:"type"`
Text string `dynamodbav:"text"`
CreatedAt string `dynamodbav:"created_at"`
UpdatedAt string `dynamodbav:"updated_at"`
GSI1PK string `dynamodbav:"GSI1PK"`
GSI1SK string `dynamodbav:"GSI1SK"`
}
※これはDynamoDBのデータをGoへマッピングするための構造体を定義するファイルです。
次にファイル「src/main.go」を以下のように修正します。
package main
import (
"errors"
"fmt"
"log/slog"
"net/http"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/feature/dynamodb/attributevalue"
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
"github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
"github.com/google/uuid"
"github.com/labstack/echo/v4"
"go-dynamodb/internal/infrastructure/database"
"go-dynamodb/internal/infrastructure/persistence/dynamodb/chat"
)
// チャット情報作成用リクエストボディの構造体
type CreateChatRequestBody struct {
RoomID string `json:"room_id"`
EntityType string `json:"entity_type"`
SenderID string `json:"sender_id"`
Type string `json:"type"`
Text string `json:"text"`
}
// チャット情報のメッセージ更新用リクエストボディの構造体
type UpdateChatRequestBody struct {
SenderID string `json:"sender_id"`
Text string `json:"text"`
}
func main() {
// echoのルーター設定
e := echo.New()
e.GET("/", func(c echo.Context) error {
// レスポンス結果の設定
res := map[string]string{
"message": "Hello World !!",
}
return c.JSON(http.StatusOK, res)
})
// サンプルAPI(CRUD処理)を追加
apiV1 := e.Group("/api/v1")
// チャット情報作成
apiV1.POST("/chat", func(c echo.Context) error {
// リクエストボディの取得
var reqBody CreateChatRequestBody
if err := c.Bind(&reqBody); err != nil {
return err
}
ctx := c.Request().Context()
// DynamoDBの初期化
dynamoDB, err := database.NewDynamoDB(ctx)
if err != nil {
errMsg := fmt.Sprintf("failed to initialize DynamoDB: %v", err)
return echo.NewHTTPError(http.StatusInternalServerError, errMsg)
}
// UUIDでメッセージIDを設定
messageID := uuid.New().String()
// 現在日時の設定(ナノ秒まで対応)
now := time.Now().UTC().Format(time.RFC3339Nano)
// PK、SK、GSI1PK、GSI1SKの設定
pk := "ROOM#" + reqBody.RoomID
sk := "MESSAGE#" + now + "#" + messageID
gsi1pk := "USER#" + reqBody.SenderID
gsi1sk := "MESSAGE#" + now
// チャット作成
chat := chat.ChatTable{
PK: pk,
SK: sk,
EntityType: reqBody.EntityType,
RoomID: reqBody.RoomID,
MessageID: messageID,
SenderID: reqBody.SenderID,
Type: reqBody.Type,
Text: reqBody.Text,
CreatedAt: now,
UpdatedAt: now,
GSI1PK: gsi1pk,
GSI1SK: gsi1sk,
}
// DynamoDB形式に変換
item, err := attributevalue.MarshalMap(chat)
if err != nil {
return c.JSON(http.StatusInternalServerError, err.Error())
}
// データ保存
_, err = dynamoDB.PutItem(ctx, &dynamodb.PutItemInput{
TableName: aws.String("ChatTable"),
Item: item,
})
if err != nil {
errMsg := fmt.Sprintf("failed to put item into DynamoDB table: %v", err)
return echo.NewHTTPError(http.StatusInternalServerError, errMsg)
}
return c.JSON(http.StatusCreated, chat)
})
// 全てのチャット情報を取得
apiV1.GET("/chat", func(c echo.Context) error {
ctx := c.Request().Context()
// DynamoDBの初期化
dynamoDB, err := database.NewDynamoDB(ctx)
if err != nil {
errMsg := fmt.Sprintf("failed to initialize DynamoDB: %v", err)
return echo.NewHTTPError(http.StatusInternalServerError, errMsg)
}
// 対象データ取得
out, err := dynamoDB.Scan(ctx, &dynamodb.ScanInput{
TableName: aws.String("ChatTable"),
})
if err != nil {
errMsg := fmt.Sprintf("failed to scan DynamoDB table: %v", err)
return echo.NewHTTPError(http.StatusInternalServerError, errMsg)
}
// マッピング
var chats []chat.ChatTable
if err := attributevalue.UnmarshalListOfMaps(out.Items, &chats); err != nil {
errMsg := fmt.Sprintf("failed to unmarshal DynamoDB items: %v", err)
return echo.NewHTTPError(http.StatusInternalServerError, errMsg)
}
return c.JSON(http.StatusOK, chats)
})
// 対象ルームかつ特定のエンティティタイプの情報を全て取得
apiV1.GET("/chat/:room_id/:entity_type", func(c echo.Context) error {
// リクエストパラメータの取得
roomID := c.Param("room_id")
if roomID == "" {
return echo.NewHTTPError(http.StatusBadRequest, "room_id is required")
}
entityType := c.Param("entity_type")
if entityType == "" {
return echo.NewHTTPError(http.StatusBadRequest, "entity_type is required")
}
// PKとSKの設定
pk := "ROOM#" + roomID
sk := entityType + "#"
ctx := c.Request().Context()
// DynamoDBの初期化
dynamoDB, err := database.NewDynamoDB(ctx)
if err != nil {
errMsg := fmt.Sprintf("failed to initialize DynamoDB: %v", err)
return echo.NewHTTPError(http.StatusInternalServerError, errMsg)
}
// 対象データ取得
out, err := dynamoDB.Query(ctx, &dynamodb.QueryInput{
TableName: aws.String("ChatTable"),
KeyConditionExpression: aws.String("PK = :pk AND begins_with(SK, :sk)"),
ExpressionAttributeValues: map[string]types.AttributeValue{
":pk": &types.AttributeValueMemberS{Value: pk},
":sk": &types.AttributeValueMemberS{Value: sk},
},
ScanIndexForward: aws.Bool(false), // 降順でソート
})
if err != nil {
errMsg := fmt.Sprintf("failed to query DynamoDB table: %v", err)
return echo.NewHTTPError(http.StatusInternalServerError, errMsg)
}
// マッピング
var chats []chat.ChatTable
if err := attributevalue.UnmarshalListOfMaps(out.Items, &chats); err != nil {
errMsg := fmt.Sprintf("failed to unmarshal DynamoDB items: %v", err)
return echo.NewHTTPError(http.StatusInternalServerError, errMsg)
}
return c.JSON(http.StatusOK, chats)
})
// チャット情報のメッセージを更新
apiV1.PUT("/chat/:message_id", func(c echo.Context) error {
// リクエストパラメータの取得
messageID := c.Param("message_id")
if messageID == "" {
return echo.NewHTTPError(http.StatusBadRequest, "messageID is required")
}
// リクエストボディの取得
var reqBody UpdateChatRequestBody
if err := c.Bind(&reqBody); err != nil {
return err
}
ctx := c.Request().Context()
// DynamoDBの初期化
dynamoDB, err := database.NewDynamoDB(ctx)
if err != nil {
errMsg := fmt.Sprintf("failed to initialize DynamoDB: %v", err)
return echo.NewHTTPError(http.StatusInternalServerError, errMsg)
}
// GSI2を利用してmessage_idに紐づくデータを取得
queryResult, err := dynamoDB.Query(ctx, &dynamodb.QueryInput{
TableName: aws.String("ChatTable"),
IndexName: aws.String("GSI2"),
KeyConditionExpression: aws.String("message_id = :message_id"),
ExpressionAttributeValues: map[string]types.AttributeValue{
":message_id": &types.AttributeValueMemberS{Value: messageID},
},
Limit: aws.Int32(1),
})
if err != nil {
errMsg := fmt.Sprintf("failed to query DynamoDB table: %v", err)
return echo.NewHTTPError(http.StatusInternalServerError, errMsg)
}
// 対象データが存在しない場合
if len(queryResult.Items) == 0 {
return echo.NewHTTPError(http.StatusNotFound, "対象データは存在しませんでした。")
}
// 対象データのPKとSKを取得
item := queryResult.Items[0]
pk := item["PK"].(*types.AttributeValueMemberS).Value
sk := item["SK"].(*types.AttributeValueMemberS).Value
// 更新処理
updateResult, err := dynamoDB.UpdateItem(ctx, &dynamodb.UpdateItemInput{
TableName: aws.String("ChatTable"),
Key: map[string]types.AttributeValue{
"PK": &types.AttributeValueMemberS{Value: pk},
"SK": &types.AttributeValueMemberS{Value: sk},
},
UpdateExpression: aws.String("SET #t = :text, updated_at = :updated_at"),
ExpressionAttributeNames: map[string]string{
"#t": "text", // 予約語回避
},
ExpressionAttributeValues: map[string]types.AttributeValue{
":text": &types.AttributeValueMemberS{Value: reqBody.Text},
":updated_at": &types.AttributeValueMemberS{
Value: time.Now().UTC().Format(time.RFC3339Nano),
},
":sender_id": &types.AttributeValueMemberS{Value: reqBody.SenderID},
},
ConditionExpression: aws.String("sender_id = :sender_id"), // 処理前の条件チェック
ReturnValues: types.ReturnValueAllNew, // 更新結果を戻り値に返す
})
if err != nil {
// ConditionExpressionチェックでエラーの場合
var cce *types.ConditionalCheckFailedException
if errors.As(err, &cce) {
return echo.NewHTTPError(http.StatusForbidden, "forbidden")
}
errMsg := fmt.Sprintf("failed to update item in DynamoDB table: %v", err)
return echo.NewHTTPError(http.StatusInternalServerError, errMsg)
}
// マッピング
var updatedChat chat.ChatTable
if err := attributevalue.UnmarshalMap(updateResult.Attributes, &updatedChat); err != nil {
errMsg := fmt.Sprintf("failed to unmarshal DynamoDB attributes: %v", err)
return echo.NewHTTPError(http.StatusInternalServerError, errMsg)
}
return c.JSON(http.StatusOK, updatedChat)
})
// チャット情報のメッセージを削除
apiV1.DELETE("/chat/:message_id", func(c echo.Context) error {
// リクエストパラメータの取得
messageID := c.Param("message_id")
if messageID == "" {
return echo.NewHTTPError(http.StatusBadRequest, "messageID is required")
}
ctx := c.Request().Context()
// DynamoDBの初期化
dynamoDB, err := database.NewDynamoDB(ctx)
if err != nil {
errMsg := fmt.Sprintf("failed to initialize DynamoDB: %v", err)
return echo.NewHTTPError(http.StatusInternalServerError, errMsg)
}
// GSI2を利用してmessage_idに紐づくデータを取得
queryResult, err := dynamoDB.Query(ctx, &dynamodb.QueryInput{
TableName: aws.String("ChatTable"),
IndexName: aws.String("GSI2"),
KeyConditionExpression: aws.String("message_id = :message_id"),
ExpressionAttributeValues: map[string]types.AttributeValue{
":message_id": &types.AttributeValueMemberS{Value: messageID},
},
Limit: aws.Int32(1),
})
if err != nil {
errMsg := fmt.Sprintf("failed to query DynamoDB table: %v", err)
return echo.NewHTTPError(http.StatusInternalServerError, errMsg)
}
// 対象データが存在しない場合
if len(queryResult.Items) == 0 {
return echo.NewHTTPError(http.StatusNotFound, "対象データは存在しませんでした。")
}
// 対象データのPKとSKを取得
item := queryResult.Items[0]
pk := item["PK"].(*types.AttributeValueMemberS).Value
sk := item["SK"].(*types.AttributeValueMemberS).Value
// 削除処理
_, err = dynamoDB.DeleteItem(ctx, &dynamodb.DeleteItemInput{
TableName: aws.String("ChatTable"),
Key: map[string]types.AttributeValue{
"PK": &types.AttributeValueMemberS{Value: pk},
"SK": &types.AttributeValueMemberS{Value: sk},
},
})
if err != nil {
errMsg := fmt.Sprintf("failed to delete item in DynamoDB table: %v", err)
return echo.NewHTTPError(http.StatusInternalServerError, errMsg)
}
return c.NoContent(http.StatusNoContent)
})
// ログ出力
slog.Info("start go-dynamodb")
// サーバー起動
e.Logger.Fatal(e.Start(":8080"))
}
※更新処理ではUpdateExpressionとExpressionAttributeNamesで予約語回避が必要でした。また、今回はトランザクションは利用していませんが、必要な場合は「TransactWriteItems」(複数の書き込み「PutItem, UpdateItem, DeleteItem」を1つのトランザクションとしてまとめて実行)、「TransactGetItems」(複数のアイテムを1回のトランザクションで取得)を利用します。
次に以下のコマンドを実行し、go.modの更新からDockerコンテナの再ビルドおよび再起動を行います。
$ docker compose exec api go mod tidy
$ docker compose down
$ docker compose build --no-cache
$ docker compose up -d
CRUDのAPIを実行して試す
次に上記で作成したCRUDのAPIをPostmanを使って試します。
まずはPOSTメソッドで「http://localhost:8080/api/v1/chat」を実行し、下図のようにステータス201で想定通りの結果になればOKです。

次にGETメソッドで「http://localhost:8080/api/v1/chat」を実行し、下図のようにステータス200で想定通りの結果になればOKです。

次にGETメソッドで「http://localhost:8080/api/v1/chat/{対象データのroom_id}/{対象のentity_type}」を実行し、下図のようにステータス200で想定通りの結果になればOKです。

次にPUTメソッドで「http://localhost:8080/api/v1/chat/{対象データのmessage_id}」を実行し、下図のようにステータス200で想定通りの結果になればOKです。

次に再度GETメソッドで「http://localhost:8080/api/v1/chat/{対象データのroom_id}/{対象のentity_type}」を実行し、下図のようにステータス200で想定通りの結果になればOKです。

次にDELETEメソッドで「http://localhost:8080/api/v1/chat/{対象データのmessage_id}」を実行し、下図のようにステータス204で想定通りの結果になればOKです。

次に再度GETメソッドで「http://localhost:8080/api/v1/chat」を実行し、下図のようにステータス200で想定通りの結果になればOKです。

ローカル開発環境としてDynamoDBのDockerコンテナ(DynamoDB local)を利用する際の注意点
ローカル開発環境としてDynamoDBのDockerコンテナ(DynamoDB local)を利用する際の注意点として、TransactionConflictExceptions(トランザクションの競合エラー)が再現されないようです。
そのため、TransactionConflictExceptionsに関するテストをしたい場合は、別途モックなどを準備してシミュレートする必要があります。
詳しくはAWS公式ドキュメント「DynamoDB local の使用に関する注意事項」ページをご確認下さい。
最後に
今回はGo言語(Golang)でAWS DynamoDB(NoSQL)の使い方についてまとめました。
NoSQLといえばGoogle Cloud Firestoreのイメージ(ドキュメント形式)がありましたが、AWS DynamoDBはキーバリュー形式かつ主キーの設計が非常に重要なデータベースだったので、その点は難しいなと感じました。
特に実務においてはアクセスパターンによってPK(必須)やSK(任意)、そしてGSIなどを設計する必要があるのと、処理内容によってはホットパーティション問題を考慮してPKを分散する設計が必要になるので注意しましょう。
今回は基本的な使い方についてまとめたので、興味がある方はぜひ参考にしてみて下さい。


コメント