はじめに (対象読者・この記事でわかること)
この記事は、Goでサーバーレスアプリケーションを開発しているエンジニアや、AWS の DynamoDB を利用したデータ永続化に関心がある方を対象としています。特に、RDB のような UPDATE ... SET 文を書き慣れている開発者が、NoSQL の DynamoDB に対して同様の感覚で更新処理を実装したいときに役立ちます。この記事を読むことで、Go 言語の公式 SDK(github.com/aws/aws-sdk-go-v2)を用いて、SQL ライクな構文で DynamoDB のアイテムを安全かつ効率的に更新する方法が理解でき、実際にコードを書きながら手順を体験できるようになります。
前提知識
この記事を読み進める上で、以下の知識があるとスムーズです。
- Go の基本的な文法とモジュール管理(
go.mod、go getなど) - AWS の認証情報(IAM ロール、~/.aws/credentials)に関する基礎知識
- DynamoDB のテーブル構造と基本的な操作(PutItem、GetItem など)
DynamoDBの更新をSQLライクに書く背景と概要
DynamoDB はキー・バリュー型・ドキュメント型の NoSQL データベースであり、トランザクションや条件付き更新をサポートしていますが、従来の RDBMS のような「UPDATE SET WHERE」構文は直接は提供されていません。そのため、Go で DynamoDB を操作する際に「SQL ライクに書く」ことを求める声が多く、以下の二つの課題が浮上します。
-
可読性と保守性
複数属性を同時に更新する場合、UpdateItemInputにExpressionAttributeNamesとExpressionAttributeValuesを手作業で組み立てるとコードが散らかりやすく、変更時にミスが入りやすいです。SQL のSET col1 = val1, col2 = val2のようにシンプルに記述できれば、チーム全体の認識統一が図れます。 -
条件付き更新と競合防止
RDB ではWHERE句で楽に条件を付けられますが、DynamoDB ではConditionExpressionを別途用意しなければなりません。SQL のように「WHERE 条件に合致したら UPDATE」という流れを自然に再現したいケースが多いです。
この背景から、Go のコード上で「SQL 風」な文字列を生成し、AWS SDK に渡すラッパーライブラリやヘルパー関数を自作・活用するアプローチが有効です。以下では、代表的な実装パターンとその利点・留意点を順に解説します。
Goで実装するSQLライクなアップデート手法
本セクションでは、実際に Go で DynamoDB の更新処理を SQL ライクに書くためのステップを、コード例を交えて詳細に解説します。全体像は次の 3 つのフェーズに分かれます。
- SQL 風文字列をパースし、Expression に変換
- ExpressionAttributeNames/Values を自動生成
- AWS SDK の
UpdateItemに組み込み実行
ステップ1:SQL 風文字列のパース
まずは、ユーザーが書く「UPDATE Table SET attr1 = :v1, attr2 = :v2 WHERE pk = :pk AND version = :v」という文字列を解析します。Go で手軽に実装できるのは、正規表現と strings パッケージを組み合わせたシンプルパーサです。
Gotype UpdateBuilder struct { Table string SetClause map[string]string // attr -> placeholder WhereClause map[string]string // attr -> placeholder } // ParseSQLLikeUpdate は SQL 風 UPDATE 文を解析し、UpdateBuilder に変換します。 func ParseSQLLikeUpdate(query string) (*UpdateBuilder, error) { // 基本的な構文チェック re := regexp.MustCompile(`(?i)^UPDATE\s+(\w+)\s+SET\s+(.+?)\s+WHERE\s+(.+)$`) matches := re.FindStringSubmatch(strings.TrimSpace(query)) if len(matches) != 4 { return nil, fmt.Errorf("invalid query format") } ub := &UpdateBuilder{ Table: matches[1], SetClause: make(map[string]string), WhereClause: make(map[string]string), } // SET 部分を分解 for _, part := range strings.Split(matches[2], ",") { kv := strings.Split(strings.TrimSpace(part), "=") if len(kv) != 2 { return nil, fmt.Errorf("invalid SET clause: %s", part) } attr := strings.TrimSpace(kv[0]) placeholder := strings.TrimSpace(kv[1]) ub.SetClause[attr] = placeholder } // WHERE 部分を分解(AND で連結された単純条件を想定) for _, cond := range strings.Split(matches[3], "AND") { kv := strings.Split(strings.TrimSpace(cond), "=") if len(kv) != 2 { return nil, fmt.Errorf("invalid WHERE clause: %s", cond) } attr := strings.TrimSpace(kv[0]) placeholder := strings.TrimSpace(kv[1]) ub.WhereClause[attr] = placeholder } return ub, nil }
ポイント
- 正規表現で
UPDATE テーブル SET ... WHERE ...の構造だけを抽出し、複雑なサブクエリや JOIN はサポートしません。シンプルな単一テーブル更新に特化しています。 SetClauseとWhereClauseはそれぞれ属性名とプレースホルダー(:v1など)を対応付けた map です。後続ステップで DynamoDB の Expression に変換します。
ステップ2:ExpressionAttributeNames と ExpressionAttributeValues の自動生成
DynamoDB では属性名に予約語が含まれる可能性があるため、#attr 形式のエイリアスが必要です。また、値は :val でバインドします。ここでは、UpdateBuilder からこれらを自動生成する関数を実装します。
Gotype DynamoExpression struct { UpdateExpression string ConditionExpression string ExpressionAttributeNames map[string]string ExpressionAttributeValues map[string]types.AttributeValue } // BuildDynamoExpression は UpdateBuilder を受け取り、DynamoDB 用の Expression を生成します。 func BuildDynamoExpression(ub *UpdateBuilder, args map[string]interface{}) (*DynamoExpression, error) { expr := &DynamoExpression{ ExpressionAttributeNames: make(map[string]string), ExpressionAttributeValues: make(map[string]types.AttributeValue), } // SET 部分 var setParts []string for attr, placeholder := range ub.SetClause { alias := "#_" + attr expr.ExpressionAttributeNames[alias] = attr // プレースホルダーから実際の値を取得 val, ok := args[placeholder[1:]] // ":v1" の ":" を除去 if !ok { return nil, fmt.Errorf("missing value for placeholder %s", placeholder) } attrVal, err := attributevalue.Marshal(val) if err != nil { return nil, err } expr.ExpressionAttributeValues[placeholder] = attrVal setParts = append(setParts, fmt.Sprintf("%s = %s", alias, placeholder)) } expr.UpdateExpression = "SET " + strings.Join(setParts, ", ") // WHERE 部分は ConditionExpression として扱う var condParts []string for attr, placeholder := range ub.WhereClause { alias := "#_" + attr expr.ExpressionAttributeNames[alias] = attr val, ok := args[placeholder[1:]] if !ok { return nil, fmt.Errorf("missing value for placeholder %s", placeholder) } attrVal, err := attributevalue.Marshal(val) if err != nil { return nil, err } expr.ExpressionAttributeValues[placeholder] = attrVal condParts = append(condParts, fmt.Sprintf("%s = %s", alias, placeholder)) } expr.ConditionExpression = strings.Join(condParts, " AND ") return expr, nil }
ポイント
argsは呼び出し側が提供するマップで、:v1→ 実際の Go の値 という形で渡します。これにより、SQL 風文字列と実データの分離が実現できます。attributevalue.Marshal(AWS SDK v2 のユーティリティ)で Go の型を DynamoDB の属性値に変換します。ExpressionAttributeNamesに#_属性名というプレフィックスを付与することで、予約語衝突を回避します。
ステップ3:AWS SDK に組み込んで実行
上記で生成した DynamoExpression を dynamodb.UpdateItemInput にそのままマッピングすれば、SQL ライクな更新が完了します。
Gofunc ExecuteSQLLikeUpdate(ctx context.Context, client *dynamodb.Client, query string, args map[string]interface{}) (*dynamodb.UpdateItemOutput, error) { // 1. パース ub, err := ParseSQLLikeUpdate(query) if err != nil { return nil, err } // 2. Expression 生成 expr, err := BuildDynamoExpression(ub, args) if err != nil { return nil, err } // 3. UpdateItem の実行 input := &dynamodb.UpdateItemInput{ TableName: aws.String(ub.Table), Key: map[string]types.AttributeValue{"pk": expr.ExpressionAttributeValues[":pk"]}, UpdateExpression: aws.String(expr.UpdateExpression), ConditionExpression: aws.String(expr.ConditionExpression), ExpressionAttributeNames: expr.ExpressionAttributeNames, ExpressionAttributeValues: expr.ExpressionAttributeValues, ReturnValues: types.ReturnValueAllNew, } return client.UpdateItem(ctx, input) }
利用例
Goctx := context.TODO() cfg, _ := config.LoadDefaultConfig(ctx) svc := dynamodb.NewFromConfig(cfg) sql := "UPDATE Users SET age = :newAge, status = :newStatus WHERE pk = :userId AND version = :ver" params := map[string]interface{}{ "newAge": 30, "newStatus": "ACTIVE", "userId": "user-123", "ver": 5, } res, err := ExecuteSQLLikeUpdate(ctx, svc, sql, params) if err != nil { log.Fatalf("update failed: %v", err) } fmt.Printf("updated item: %v\n", res.Attributes)
この例では、pk と version を条件に、age と status を同時に更新しています。SQL ライクに書いた文字列と、実際の値マップだけで完結するため、コードの可読性が大幅に向上します。
ハマった点やエラー解決
| 項目 | 内容 | 解決策 |
|---|---|---|
| プレースホルダーの名前が重複 | 同じ :v1 を複数箇所で使用した場合、ExpressionAttributeValues に同一キーが上書きされる |
プレースホルダーはユニークにするか、BuildDynamoExpression 内で自動的にインデックス付きに変換(:v1_1, :v1_2) |
| 予約語衝突 | 属性名が name や status など DynamoDB の予約語だった場合、エラーになる |
ExpressionAttributeNames に必ずエイリアス (#_attr) を付与し、UpdateExpression ではエイリアスを使用 |
| データ型不一致 | Go の int を Dynamo の N に変換した際、文字列として扱われるケースがある |
attributevalue.Marshal が自動で数値型を N に変換するが、float64 になる場合は明示的に int64 にキャスト |
| 条件式が足りない | WHERE 句を忘れると全テーブルスキャン的に全アイテムが更新対象になる危険 |
パーサーで必ず WHERE が存在することを検証し、無い場合はエラーで止める |
解決策まとめ
- 正規表現+文字列分割でシンプルな SQL 風構文をパースし、
UpdateBuilderにマッピング。 - 自動エイリアス生成により、予約語や特殊文字に安全に対処。
attributevalue.Marshalで Go の型を DynamoDB の型へシームレス変換。- エラーハンドリングを徹底し、プレースホルダー不足や条件式欠如を事前に検知。
これらを組み合わせたラッパー関数 ExecuteSQLLikeUpdate をプロジェクト内で共有すれば、チーム全体が「SQL 風に DynamoDB を更新」する統一感のあるコードベースを実現できます。
まとめ
本記事では、Go 言語で DynamoDB の更新処理を SQL ライクに記述する手順を、SQL 風文字列のパース → Expression の自動生成 → SDK 呼び出し の 3 ステップで解説しました。
- 可読性向上:SQL ライクな記法で更新ロジックを一行で表現可能
- 安全性確保:予約語回避と条件式の自動バリデーションでエラーを低減
- 再利用性:ラッパー関数を共通化すれば、プロジェクト全体で統一的なデータ更新手法が実装できる
このアプローチを導入することで、Go 開発者は RDB と同様の感覚で DynamoDB を扱えるようになり、生産性とコード品質の両立が期待できます。次回は、トランザクションやバッチ更新を組み合わせた高度なパターンについても紹介する予定です。
参考資料
- AWS SDK for Go V2 – DynamoDB クライアント
- DynamoDB Update Expressions – Official Documentation
- 「Go言語によるAWSプログラミング入門」 (書籍) – 第4章 DynamoDB 編
