3. メモを保存する
pw dev を再起動すると、2章で書いたメモは全部消えています。一覧はスライスの中にあり、
スライスはプロセスの中にあるからです。
これをデータベースに移すには3つの部品が要ります。テーブルを作るマイグレーション、
型付きの Go 関数にコンパイルされる .pw.sql、そしてそれを呼ぶハンドラ。所要時間は30分ほど。
ハンドラは3つ目であって、他の2つが動くかどうかを見るために先に用意するものでは
ありません。マイグレーションが作ったテーブルも、宣言した文も、コンソールが単体で
動かします。
0. データベースを足す
Section titled “0. データベースを足す”**1章でデータベースを断った場合は、ここで入れてください。**このチュートリアル通りに
進めていればその状態です。すでに config.dev.toml に [middleware.rdb] があり、
migrations/ が存在するなら、この節は飛ばして1節へ進んでください。
pw add引数無しで実行すると、このプロジェクトがまだ持っていない機能が並びます。
database(説明に rdb 設定・マイグレーションディレクトリ・型付き SQL の例と出ているもの)
を選んでください。pw add database と直接書いても同じです。
続けてエンジンを聞かれるので SQLite を選びます。ファイル1つで動き、別に起動して
おくサーバーが要りません。確認画面を承認すると、config.dev.toml の
[middleware.rdb]、migrations/ ディレクトリ、queries/ と、それを読ませる
generate.queries が入ります。
migrations/00001_init.sql は中身が全部コメントです。データベースを足しただけで
テーブルが1つ増えていた、ということにはなりません。書いてあるのはマイグレーションの形と、
このプロジェクトのエンジンの方言だけで、自分の version 1 はその下に書きます。
1. テーブルのマイグレーション
Section titled “1. テーブルのマイグレーション”マイグレーションは goose 形式のただの SQL ファイルで、
適用される順に番号が振られます。migrations/00002_create_memos.sql を作ります。
-- migrations/00002_create_memos.sql-- +goose UpCREATE TABLE memos ( id INTEGER PRIMARY KEY, body TEXT NOT NULL);
-- +goose DownDROP TABLE memos;-- migrations/00002_create_memos.sql-- +goose UpCREATE TABLE memos ( id SERIAL PRIMARY KEY, body TEXT NOT NULL);
-- +goose DownDROP TABLE memos;-- migrations/00002_create_memos.sql-- +goose UpCREATE TABLE memos ( id INT AUTO_INCREMENT PRIMARY KEY, body VARCHAR(255) NOT NULL);
-- +goose DownDROP TABLE memos;2つの注釈がファイルを分けます。Up はこのバージョンがすることで、Down はそれを
取り消すものです。いま Down を書くのは安上がりですが、3バージョン先のスキーマから
これを復元するのはそうではありません。
00001_init.sql は最初からありましたが、何も作りません。中身はコメントだからです。
放っておいてください。適用済みで、何のコストもなく、そして適用済みのマイグレーションに
番号を振り直すことだけは、絶対にしてはいけないことです。
上のタブは同じマイグレーションを各エンジンの方言で書いたものです。互いに翻訳する仕組みは
ありません。pw add database は選んだエンジンの方言で書いているので、これもそれに
合わせてください。
pw dev は起動時と、マイグレーションディレクトリのファイルが変わるたびに未適用のものを
適用します。つまりこのファイルは保存するだけです。
up 2 00002_create_memos.sql 1msversion 1 -> 2ループの外では pw migrate up が同じことをし、pw migrate status が適用状況を答えます。
マイグレーションを参照してください。
その行が言っているのは「ファイルが走った」までです。何ができたのかはペイン1つ隣に
あります。http://127.0.0.1:18081 のコンソールを開いて
data を選んでください。
ヘッダに接続先、エンジン、そしてスキーマのバージョン——ループがいま出した 2 ——が並び、
テーブル一覧に memos が、行0件で入っています。読む価値があるのは schema タブです。
マイグレーションが書いた説明ではなく、データベースが理解した形のテーブルがそこにあります。
フレームワークが持つテーブルも、その印付きで並んでいます。さっきのバージョン番号の
出どころは goose_db_version で、適用済みのマイグレーションに番号を振り直すと半日
溶けるのもここが理由です。「そのファイルはもう走った」という記録は、この表の行しか
ありません。
2. コンパイルされる SQL
Section titled “2. コンパイルされる SQL”queries/memos.pw.sql を作ります。
package queries
type Memo { id: int body: string}
export statement ListMemos(): sql.many<Memo> {SELECT id, body FROM memos ORDER BY id DESC}
export statement CreateMemo(body: string): sql.exec {INSERT INTO memos (body) VALUES ({body})}形は .pw.html と同じです。パッケージ行、結果型の宣言、型付きパラメータを持つ
エクスポート宣言。pw generate は export statement ごとに Go の関数をソースの隣に
書き出します。
シグネチャを決めるのは結果の種別です。sql.many<Memo> は
iter.Seq2[Memo, error] を返します。行はスライスに溜めずに流れるので、大きなテーブルが
そのまま大きなアロケーションになりません。sql.exec は sql.Result を返します。
INSERT が差し出せるのはそれです。
{body} はパラメータで、プリペアドステートメントのプレースホルダになります。ここでは ?、
PostgreSQL なら $1。決めるのは popcornweb.toml の project.database です。
生成器はテンプレート式を SQL テキストに連結しませんし、手書きのプレースホルダは拒否します。
この書き方をしたステートメントがインジェクションになる余地はありません。境界はまさにそこで、
パラメータが束縛するのは値であって、テーブル名でも列名でも並び順でもありません。
あとで生成を止める規則が2つあり、出会う前に知っておく価値があります。WHERE のない
UPDATE と DELETE はその場で拒否されます。そして SELECT の列は、順序も名前も、宣言した
結果型と一致していなければなりません。Memo がそのステートメントの返しうる行の正確な
説明でいられるのは、この規則のおかげです。条件付き SQL、スライスの展開、再利用可能な
predicate はクエリーにあります。
3. 呼ぶものが無いまま実行する
Section titled “3. 呼ぶものが無いまま実行する”保存すると pw dev が再生成します。プロジェクトに Go の関数が2つ増えて、呼び出し側は
ゼロです。queries を import しているものはまだありません。コンパイルが通ったこと自体に
意味はあります。列名の打ち間違いは、本番で失敗するリクエストではなくビルドエラーに
なりました。とはいえ、コンパイルの通る文が間違った行を返すことはできます。それは
コンパイラの答える問いではありません。
もう一度 data ペインへ行き、declared queries をたどってください。2つの文が
引数付きで並んでいます。CreateMemo を開き、body に eggs と入れて実行します。
1行が変更され、結果の上に、実行された SQL が出ます。
INSERT INTO memos (body) VALUES (?)ペインがソースを写して見せているのではありません。アプリケーションが呼ぶのと同じ
生成済みビルダーを呼んでいます。条件によって SQL が組み変わる文なら、ここでも
リクエスト時とまったく同じ組み立てが起きます。ListMemos も実行してください。
引数が無いのでボタンだけですが、さっきの行が返ってきます。
これを成立させているのは、生成されたファイル1つです。文をペインに登録するのは
パッケージの初期化で、初期化が走るのはそのパッケージがリンクされたときだけ。
呼び出し側より先に書かれた文は、どこからもリンクされません。そこで生成器は、
queries パッケージを開発用バイナリに引き込むためだけのファイルを書きます。
pwdev ビルドタグが付いているので、pw build はそれを1バイトもリンクしません。
もう1件入れてもかまいません。本物のテーブルの本物の行です。data ペインにも出ますし、 5節でそれを見つけることになります。
4. ハンドラをテーブルにつなぐ
Section titled “4. ハンドラをテーブルにつなぐ”package handlers
import ( "net/http"
"memoapp/queries" // 追加
"github.com/shibukawa/popcornweb/pw")
// init は2章のまま。ルートは変わらない。func init() { mux.HandleFunc("GET /{$}", home) mux.HandleFunc("POST /memos", createMemo)}
// home lists every memo that has been written.//// 変更: memos.list() は失敗しなかったが、テーブルを読む方は失敗しうる。func home(w http.ResponseWriter, r *http.Request) { // 生成された関数は行を流してきます。テンプレートが要るのはスライスなので、 // ここで溜めます。行ごとの err は、その行を読んだ時点の失敗です。 var list []Memo for row, err := range queries.ListMemos(r.Context()) { if err != nil { pw.WriteProblem(w, r, err) return } list = append(list, Memo{Id: row.Id, Body: row.Body}) } pw.WriteHTML(w, r, Home(HomeParams{Memos: list}))}
// createMemoInput は送られてきたフォーム。2章のまま。type createMemoInput struct { // Body はメモ本文。必須で、200文字まで。 Body string `payload:"body" check:"required,maxlen=200"`}
// createMemo は1件保存して一覧へ戻す。func createMemo(w http.ResponseWriter, r *http.Request) { input, err := pw.Parse[createMemoInput](r) if err != nil { pw.WriteProblem(w, r, err) return } // 変更: memos.add(input.Body) の置き換え。 if _, err := queries.CreateMemo(r.Context(), input.Body); err != nil { pw.WriteProblem(w, r, err) return } http.Redirect(w, r, "/", http.StatusSeeOther)}そして handlers/memos.go を削除します。スライスは消えました。
このファイルで立ち止まる価値のある点が2つあります。
接続はコンテキストが運びます。 queries.CreateMemo にハンドルは渡していません。
context.Context を受け取り、その中からプールを見つけます。同じ呼び出しが
pw.Transaction の中では実行中のトランザクションを見つけます。生成された1つの関数が
両方の場所で動き、*sql.Tx を取る変種が要らないのはそのためです。
Memo 型が2つになりました。 変換しているのが home の中のループです。queries.Memo は
行の説明で、handlers.Memo はページが描画するものの説明です。統合すれば今日のコードは
短くなりますが、境界としては明日そのぶん悪くなります。ページが表示をやめる最初の列、
あるいはどの列も供給しない最初のフィールドが出てきたとき、両方の役目を背負った型の中で
決着をつけることになるからです。
5. 動かす
Section titled “5. 動かす”保存してください。pw dev が queries/memos_pw_gen.go を再生成し、リビルドし、
再起動します。ページをリロードすると、eggs がもう並んでいます。3節でコンソールから
入れた行を、このハンドラがそのまま読んでいるからです。あれは模擬でも下書きでも
ありませんでした。
フォームからもメモを追加してください。そのあと Ctrl-C で pw dev を止めて、
もう一度起動します。一覧はそのままです。2章では書けなかった一文がこれです。
data ペインで memos を選べば、同じ行を反対側から見られます。

dev では、生成されたステートメントは実行のたびに記録されます。
{ "level": "INFO", "msg": "sql executed", "sql": "\nINSERT INTO memos (body) VALUES (?)\n", "duration": 0.601708, "operation": "exec", "driver": "sqlite", "rows_affected": 1, "outcome": "ok", "args": "eggs"}設定した閾値より遅いステートメントは、実行計画と貼り付け可能な再現用スニペットを 連れてきます。コードは1行も変えずに、です (スロークエリー診断)。
データベースそのものは memoapp.db というファイルで、名前は config.dev.toml の
DSN が決めています。スキーマがまだ動いている間は、これを消して pw dev に
マイグレーションを再適用させるのが手軽なリセット方法です。
ここまでで手元にあるもの
Section titled “ここまでで手元にあるもの”- バージョン管理されたスキーマ。適用するのはコードをビルドするのと同じループ。
- SQL のまま書かれ、パラメータと行に型の付いた関数へコンパイルされるクエリー。
- 再起動しても中身の残るページ。
訪問者は全員、全員のメモを見ています。4章で、誰が尋ねているのかという概念を アプリケーションに与えます。
- 4. ログインする — 次の章。
- クエリー — 条件付き SQL、predicate、トランザクション。
- リレーショナルデータベース — エンジン、プールの上限、リードレプリカ。
- マイグレーションとシードデータ — スキーマ運用の残り。
