PR

Go言語(Golang)でAWS DynamoDB(NoSQL)の使い方|Docker環境構築+CRUD APIサンプル

3. 応用

こんにちは。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を分散する設計が必要になるので注意しましょう。

今回は基本的な使い方についてまとめたので、興味がある方はぜひ参考にしてみて下さい。

 

この記事を書いた人
Tomoyuki

SE→ブロガーを経て、現在はSoftware Engineer(Web/Gopher)をしています!

Tomoyukiをフォローする
3. 応用
スポンサーリンク
Tomoyukiをフォローする

コメント

タイトルとURLをコピーしました