DynamoDB
DynamoDB はリレーショナルデータベースエンジンの追加候補ではありません。その隣に、
あるいは代わりに置く別系統のストアであり、両者は何も共有しません。
middleware.dynamo は独自のセクションと独自のクライアントを持ち、接続グループも、
トランザクションランナーも、.pw.sql もありません。セッションを SQLite に、イベントを
DynamoDB に置くプロジェクトは例外ではなく想定された形で、リレーショナルデータベースを
まったく持たないプロジェクトも同じです。
そもそもこれが存在できているのは、オブジェクトストレージと
同じ事情によります。aws-sdk-go-v2 は TinyGo でビルドできません。だからここでのクライアントは
tinygodriver のもので、その上の型付きレイヤーは
tinybind の dynamobind です。どちらも両方のターゲットでコンパイルできます。クライアントは
context の値ではなくプロセスのハンドルとして持ち回ります。context 経由のクライアント経路は
wasip1 ビルドでおよそ 37 KB あり、ハンドルはそのコストを払わずに済む形です。
設定セクションを作るのは import です。DynamoDB を使わないプロジェクトはキーも持たず、 ドライバもリンクしません。
import _ "github.com/shibukawa/popcornweb/database/dynamo"[middleware.dynamo]enabled = trueregion = "us-east-1"endpoint = "http://127.0.0.1:8000"access_key_id = "local"secret_access_key = "local"auto_migrate = truepw add dynamo は、このセクション、スターターのレコード、生成が読む generate.dynamo の
エントリ、そして pw dev が起動する dynamodb-local パッケージを書きます。上の値はローカル
サーバー向けです。署名を検証しないので資格情報は空でなければ何でもよく、リージョンは受け
付けられる placeholder です。これらは config.dev.toml に置き、デプロイの既定値にはしません。
| キー | 既定値 | 意味 |
|---|---|---|
enabled |
false |
起動時にクライアントを開き、プロセスのハンドルとして保持する |
region |
(空) | 空なら環境変数。どちらからも決まらなければ起動エラー |
endpoint |
(空) | 空ならリージョンのホスト。値を入れるとローカルや互換サーバー |
access_key_id |
(空) | 空ならドライバが環境変数から取る |
secret_access_key |
(空) | どこでもマスクされる |
session_token |
(空) | どこでもマスクされる |
table_prefix |
(空) | 宣言上のテーブル名の前に付く |
table_names |
[] |
宣言名から実際の名前への明示的な対応 |
timeout |
"10s" |
1 リクエストの上限 |
max_idle_conns |
4 |
想定並列数に合わせる |
verify_schema |
true |
起動時に登録済みテーブルを 1 回ずつ読み、食い違えば起動しない |
auto_migrate |
false |
起動時に不足テーブルを作る。開発専用 |
静的な資格情報の片方だけを書くと拒否されます。中途半端な組は環境変数へフォールバックして、
もっと分かりにくい場所で失敗するからです。3 つの資格情報キーはすべて ${NAME} を展開し、
展開後にマスクされるので、展開された秘密が起動サマリにもエラーにも出ることはありません。
起動時にはクライアントを構築し、リージョンか資格情報が無ければサービス開始前に失敗します。 シャットダウンではクライアントを閉じ、プールした接続を解放します。
レコードを宣言する
Section titled “レコードを宣言する”dynamo タグの付いた構造体がテーブルです。
package records
type Note struct { ID string `dynamo:"id,partitionkey"` CreatedAt time.Time `dynamo:"created_at,sortkey"` Body string `dynamo:"body"`}pw generate はこれを読み、アイテムのコーデック、キービルダー、テーブル定義、そしてその定義を
「望ましいスキーマ」に登録する init を出力します。宣言上のテーブル名は型名の snake_case——
ここでは note——で、デプロイに関する情報はソースに一切現れません。
生成は使われ方に従います。実際に呼ばれている方向のコーデックだけが出るので、読み取りを消せば
生成コードもその分縮みます。ディレクトリは popcornweb.toml の generate.dynamo に並べる
必要があり、これは pw add dynamo が書きます。どのディレクトリにも属さない .pw.dynamo は、
黙って飛ばされるのではなく、パスを名指しして 1 度だけ報告されます。
アイテムを 1 つ読み書きする
Section titled “アイテムを 1 つ読み書きする”クライアントと設定済みのテーブル名解決は、1 つのハンドルにまとまってこのパッケージが プロセス状態として保持します。リクエストの context には何も入らないので、呼び出し側が context の探索コストを払うことはありません。
func store(ctx context.Context, note Note) error { h, err := dynamo.Handle(ctx) if err != nil { return err } return dynamobind.StoreOn(ctx, h, "note", note)}
func load(ctx context.Context, id string, createdAt time.Time) (Note, error) { h, err := dynamo.Handle(ctx) if err != nil { return Note{}, err } return dynamobind.LoadOn[Note](ctx, h, "note", Note{ID: id, CreatedAt: createdAt}.ItemKey())}アイテム操作がテーブル名を取るのは、読み取るべき宣言を持たないからです。セクションを 有効にしないまま呼び出すと、panic ではなく「クライアントが無い」という名前付きのエラーを 受け取ります。宣言済みクエリはハンドルを取りません — 生成された本体が同じハンドルを 自分で解決するからで、呼び出し側が context だけで済むのはそのためです。
これらの呼び出しを包むラッパーはありません。database/dynamo が公開するのは設定、テーブル
レジストリ、マイグレータだけです。リレーショナル側にラッパーがあるのは、3 つのエンジンを
1 つに見せる必要があるからでした。こちらのクライアントは 1 つなので、ラッパーはすでに名前が
あるものに 2 つ目の名前を与えるだけになります。
宣言型クエリ
Section titled “宣言型クエリ”アクセスパターンはパッケージの隣の .pw.dynamo ファイルに書き、名前付きの関数 1 つになります。
export statement ReadingsSince(sensor: Sensor, from: int64): dynamo.many<Reading> { table reading key sensor = {sensor} and at > {from}}for reading, err := range records.ReadingsSince(ctx, sensor, from) { // ...}呼び出し側は属性も式もテーブルもクライアントも書きません。table 句はすべてのステートメントで
必須で、それがシグネチャからテーブル引数を消しています。key 句はパーティションキーの等値と、
=、<、<=、>、>=、between、begins_with のうち高々 1 つのソートキー条件です。
| 結果型 | 戻り値 |
|---|---|
dynamo.page<T> |
リクエスト 1 回。Count, ScannedCount, LastEvaluatedKey を持つ Page[T] |
dynamo.many<T> |
全ページを回すイテレータ |
何回リクエストするかは書き手の判断のままにしてある、というのが両方ある理由です。
生成時には、SQL のジェネレータにはできない検査ができます。ここではタグがスキーマであり、
ソースが食い違う相手の DDL が別に存在しないからです。名前が出てくる属性はすべて対象の型に
存在しなければならないので、宣言を直さずにタグを改名すると、最初のリクエストではなく
pw generate が失敗します。キー句に非キー属性を書けば、それが属する句を名指ししたエラーに
なります。パラメータの型は、そのタグが属性をどう保存するかと突き合わされます。そして属性は
無条件にエイリアスされるので、DynamoDB の 573 個の予約語のどれも式にそのまま現れません。
filter、projection、condition、update の各式は宣言できません。セカンダリインデックスも 同様なので、宣言型クエリはテーブル自身のキーに対して走ります。宣言で表せないものには、 検査のない文字列のキー条件が引き続き使えます。
タグのオプション、属性の型、キー述語、生成時の検査はすべて DynamoDB クエリフォーマットにあります。
宣言上の名前と、実際の名前
Section titled “宣言上の名前と、実際の名前”ソースが宣言するのは note です。デプロイ先では myapp-note-prod かもしれません。その差を
埋めるのは、一度だけ設置され、すべてのランタイム入口が実行する 1 つの関数です。
[middleware.dynamo]table_prefix = "myapp-"
[[middleware.dynamo.table_names]]declared = "note"deployed = "notes-prod-8f21c"明示的なエントリが優先され、次にプレフィックス、どちらも無ければ宣言名がそのまま使われます。
プレフィックスだけでは足りませんでした。listing も IAM もライフサイクルルールも読む S3 の
キープレフィックスと違い、DynamoDB のテーブル名にはサービスが見る構造がありません。CDK が
生成した物理名や、環境名が末尾に来る orders-prod は、何かを前に付けるだけでは作れないの
です。どちらのキーでも表せないデプロイのために、dynamo.WithTableResolver が合成された関数
ごと差し替えます。
1 つの関数がリクエスト経路とマイグレータの両方を担います。関数にしてある理由がそこです。
ハンドラが見つけられないテーブルをマイグレーションが作ってしまえば、どこにも見える形では
失敗しません。設定した名前も解決後の名前も、起動時に DynamoDB 自身の規則で検証されます。
どのコードも宣言していないテーブルを指す table_names のエントリは、黙って何もしない行では
なくエラーになります。
このストアにマイグレーションファイルはなく、バージョンテーブルもありません。望ましい状態は
登録されたテーブル定義の集合、観測される状態は DescribeTable、適用とはその 2 つの比較です。
plan, err := dynamo.Plan(ctx) // []TableChangeresult, err := dynamo.Migrate(ctx)無いテーブルは生成されたキーで作られ、アクティブになるまでポーリングされます。あるテーブルは
キー属性の名前が位置で比較され、それ以外は比較されません。DescribeTable が属性の型を返さない
ので、型だけ変わったキーは一致して見えます。改名・欠落・余分なキーは捕まります。食い違いは、
テーブル名と望ましい形と観測された形を挙げたエラーになります。「違う」とだけ言われても、
どちらが想定外なのかは分からないからです。
決してやらないことが 2 つあります。キーの変更は報告され、実行されません。ドライバに
UpdateTable が無く、そもそもパーティションキーは変更できません。そして、アカウントにあって
ソースに無いテーブルは、報告されるだけで削除されません。
バージョン管理が無いのは、必要が無いからです。DescribeTable が現物の形を返すので、再適用は
記録によってではなく構造上 no-op になり、手で加えられた変更は隠れるのではなく見えます。down
も無く、ここで変更を巻き戻すとは DeleteTable のことで、SQL のロールバックが壊さないデータを
壊します。
ただし作成は、開発とテストの手順です。auto_migrate = true は起動時にプランを適用しますが、
それ以外の環境では設定エラーになります。デプロイされたテーブルは、キューやバケットを作るのと
同じ道具が用意するものだからです。本番でフレームワークが受け持つのはもう半分、既定で有効な
verify_schema のほうです。デプロイの道具は自分が作ったものは知っていても、アプリケーションが
前提にしているものは知りません。その 2 つを比べられるのはアプリケーションだけです。無効化は
受け付けられ、そして警告されます。
TTL、保持期間、オートスケーリング、タグ、レプリケーションを比較の外に置いているのも、同じ線 です。それらはテーブルの持ち主のもので、差分を報告する仕組みは正しいデプロイのたびに鳴り、 やがて読まれなくなります。
この土台に無いもの
Section titled “この土台に無いもの”| 無いもの | 帰結 |
|---|---|
| トランザクション、PartiQL、Streams、DAX | ドライバが実装していない |
UpdateTable |
既存のテーブルは作り変えられない |
| セカンダリインデックスのタグ | 宣言型クエリはテーブル自身のキーを使う |
| filter と update の式 | 宣言できるのはキー条件、件数、方向、整合性まで |
| シングルテーブル設計 | 1 つの構造体が 1 つのテーブルを持つ。tinybind がそう決めている |
どれも Popcorn Web の選択ではありません。下のレイヤーが今できることの縁であり、そちらが 動けばこちらも動きます。
この上のセッション
Section titled “この上のセッション”sessionstore/dynamo はログインセッションをこのストアに置きます。リレーショナルデータベースを
まったく持たないデプロイのためのものです。このパッケージが開いたクライアントを借り、自分の
テーブルを登録するので、セッションテーブルも他と一緒に作られ、検証されます。そして期限切れの
レコードを消すのは、有効にした TTL か、さもなければ何もありません。
セッションストレージを参照してください。
