topic: db (PDO / SQL / マルチDB)

PDO 経由で RDB を操作するトピック。SQLite を既定 に、DOJO_DB_DRIVER=mysql|pgsql で MySQL / PostgreSQL にも接続できる構造で組んである。

このトピックで身につくこと

  • PDO で DB に接続して SELECT / INSERT / UPDATE / DELETE を実行できる
  • プレースホルダ (? / :name) で値を差し込み、SQL インジェクションを防げる
  • fetch / fetchAll / fetchColumn のモードを使い分けられる
  • トランザクション (beginTransaction / commit / rollBack) で複数 SQL をまとめられる
  • SQLite / MySQL / PostgreSQL の DSN の違い方言の差 を読み分けられる

前提知識の要点

連想配列 (['key' => 値]) が読めれば start できる。クラス・例外は必須ではない (new PDO(...)try/catch だけ書ければよい)。

// PDO の最小コード
$pdo = new PDO('sqlite:' . getenv('DOJO_DB_PATH'));
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

$stmt = $pdo->prepare('SELECT name FROM users WHERE age >= ?');
$stmt->execute([20]);
foreach ($stmt as $row) {
    echo $row['name'], "\n";
}
▶ 3v4l で実行
  • new PDO(<DSN>) で接続。DSN は 「どの DB のどこに繋ぐか」 を表す文字列
  • setAttribute(ERRMODE_EXCEPTION) を付けて例外で気付く形にする (これ無しは事故の元)
  • 値を SQL に埋めるときは 必ず prepare + execute"WHERE name = '$name'" と連結すると SQL インジェクションになる

chapter 一覧

# chapter 内容
1 ch01-what-is-db/ DB / テーブル / 行 / 列 の概念 (ドリルなし)
2 ch02-pdo-connect/ PDO で DB に接続する / DSN
3 ch03-select-basic/ SELECT で行を取り出す
4 ch04-select-where/ WHERE / プレースホルダ / prepare + execute
5 ch05-insert/ INSERT で 1 件追加する
6 ch06-update-delete/ UPDATEDELETE / rowCount
7 ch07-fetch-modes/ FETCH_ASSOC / FETCH_NUM / fetchColumn
8 ch08-transactions/ トランザクション (beginTransaction / commit / rollBack)
9 ch09-sql-injection/ プリペアド vs 連結 / 安全な書き方
10 ch10-multi-db/ SQLite / MySQL / PostgreSQL の DSN 比較
11 ch11-mysql-specific/ MySQL 固有の文法と運用
12 ch12-postgres-specific/ PostgreSQL 固有の文法と機能

合計 12 chapter / 25 drill / 所要 3〜4 時間

採点ランナーと DB の関係

採点ランナーは 既定で SQLite を一時ファイルに作って tests/setup.sql を流し込み、そのパスを環境変数 DOJO_DB_PATH として受講生コードに渡す。

# 既定 (SQLite)
php scripts/grade.php topics/db/ch03-select-basic/drill/01-select-all/

# MySQL コンテナ側で採点
DOJO_DB_DRIVER=mysql php scripts/grade.php topics/db/ch11-mysql-specific/drill/01-...

# PostgreSQL コンテナ側で採点
DOJO_DB_DRIVER=pgsql php scripts/grade.php topics/db/ch12-postgres-specific/drill/01-...

ch11 / ch12 は その DB 専用の SQL (例: MySQL の AUTO_INCREMENT、PostgreSQL の RETURNING) を扱うので、setup.mysql.sql / setup.pgsql.sql で方言差を吸収している。Docker は docker/compose.ymldocker compose up -d で立てる (詳細は docker/README.md)。

進め方

  1. 各 chapter の slide.md を読む (4〜5 分)
  2. drill/ 配下の問題を順番に解く (ch01 は読むだけ)
  3. 採点: php scripts/grade.php topics/db/<chapter>/drill/<drill>/
  4. MySQL / PostgreSQL を試したい時だけ Docker を立てて DOJO_DB_DRIVER=... を付ける

つまづきポイント

症状 多くの原因
SQLSTATE[HY000] で何が起きたか分からない setAttribute(ERRMODE_EXCEPTION) を付け忘れている。例外で原因を出させる
prepare した SQL が動かない プレースホルダ ? の数と execute([...]) の配列要素数がズレている
値が文字列リテラルとして埋まらない プレースホルダはクォート不要。"WHERE name = ?" に対して execute(['太郎'])
LIMIT ? が SQLSTATE エラー 一部 PDO + MySQL で型推論が効かない。bindValue($p, $v, PDO::PARAM_INT) で明示
last_insert_id が 0 / RETURNING が無い MySQL は lastInsertId()、PostgreSQL は INSERT ... RETURNING id。DB ごとに違う
setup.sql の DDL が SQLite で通らない AUTO_INCREMENT (MySQL) / SERIAL (PostgreSQL) は SQLite では別表記。INTEGER PRIMARY KEY AUTOINCREMENT を使う

関連トピック

トピック 関係
array-assoc fetch(PDO::FETCH_ASSOC) の戻り値が連想配列
exception PDO は例外モード前提で書く
web Web 経由で DB に書き込む実践
session-cookie ログイン情報を DB に置き、Session で参照

トピックを並列で参照する全体地図は TOPICS_INDEX.md にある。

案件 (dojo_map.tsv) での参照

topic_slug    chapter_dir
db            topics/db/ch02-pdo-connect
db            topics/db/ch04-select-where
db            topics/db/ch08-transactions
db            topics/db/ch09-sql-injection
...

slug db で参照可。11-db / db どちらの path でもアクセスできる (シンボリックリンク)。

このレッスンの章

  1. ch01 ch01 — DB とは / テーブル・行・列
  2. ch02 ch02 — PDO で SQLite 接続 / DSN 比較
  3. ch03 ch03 — SELECT 基本 / fetchAll / ループ
  4. ch04 ch04 — WHERE / プレースホルダ / prepare + execute
  5. ch05 ch05 — INSERT で 1 件追加する
  6. ch06 ch06 — UPDATE と DELETE
  7. ch07 ch07 — fetch のモード
  8. ch08 ch08 — トランザクション
  9. ch09 ch09 — SQL インジェクション / プリペアド vs 連結
  10. ch10 ch10 — DSN 比較 / 複数 DB 接続
  11. ch11 ch11 — MySQL 固有の文法と運用
  12. ch12 ch12 — PostgreSQL 固有の文法と機能