PDO のプレースホルダ(名前付き・疑問符)の使い分け

PDO のプリペアドステートメントでは、SQL に値を埋め込む際にプレースホルダを使う。プレースホルダには「名前付き」と「疑問符」の 2 種類があり、それぞれ書き方と向いている場面が異なる。

名前付きプレースホルダ

名前付きプレースホルダはコロン(:)に続けて任意の名前を書く。SQL のどの位置にどの値が入るのかが一目でわかるのが利点だ。

$stmt = $pdo->prepare(
    'SELECT * FROM users WHERE name = :name AND age = :age'
);
$stmt->execute( [
    ':name' => 'Alice',
    ':age'  => 30,
] );

execute に渡す連想配列のキーがプレースホルダ名と対応する。キーにコロンを付けても付けなくても動作するが、付けるほうが SQL との対応が明確になる。

疑問符プレースホルダ

疑問符プレースホルダは ? を使い、値は配列のインデックス順に対応する。

$stmt = $pdo->prepare(
    'SELECT * FROM users WHERE name = ? AND age = ?'
);
$stmt->execute( [ 'Alice', 30 ] );

1 つ目の ? に配列の 0 番目、2 つ目の ? に 1 番目の値がバインドされる。プレースホルダが少なければ簡潔に書けるが、数が増えると順番の管理が面倒になる。

2 つの使い分け

名前付きプレースホルダ

SQL が長い場合や、バインドする値が多い場合に向いている。名前で対応関係がわかるため、可読性が高くバグも起きにくい。

疑問符プレースホルダ

バインドする値が 1〜2 個程度で、SQL が短い場合に向いている。記述量が少なく済むが、値の順番を間違えやすい。

実務では名前付きプレースホルダを使うケースが多い。特にチーム開発では可読性の高さが重視されるためだ。

bindParam / bindValue との組み合わせ

execute に配列を渡す方法のほかに、bindParam や bindValue で 1 つずつバインドする方法もある。名前付き・疑問符のどちらでも使える。

名前付きプレースホルダの場合はプレースホルダ名を指定する。

$stmt = $pdo->prepare(
    'INSERT INTO users ( name, age ) VALUES ( :name, :age )'
);
$stmt->bindValue( ':name', 'Bob', PDO::PARAM_STR );
$stmt->bindValue( ':age', 25, PDO::PARAM_INT );
$stmt->execute();

疑問符プレースホルダの場合は 1 始まりのインデックスを指定する。

$stmt = $pdo->prepare(
    'INSERT INTO users ( name, age ) VALUES ( ?, ? )'
);
$stmt->bindValue( 1, 'Bob', PDO::PARAM_STR );
$stmt->bindValue( 2, 25, PDO::PARAM_INT );
$stmt->execute();

bindValue / bindParam を使うと PDO::PARAM_INT や PDO::PARAM_STR で型を明示できる。execute に配列を渡す方法ではすべて文字列として扱われるため、型を厳密に管理したい場合は bindValue や bindParam を使うほうが安全だ。

混在は禁止

1 つの SQL 文の中で名前付きと疑問符を混ぜることはできない。

// これはエラーになる
$stmt = $pdo->prepare(
    'SELECT * FROM users WHERE name = :name AND age = ?'
);

どちらか一方に統一する必要がある。プロジェクト全体でもスタイルを統一しておくと、コードの一貫性が保たれる。

IN 句での注意点

プレースホルダは 1 つにつき 1 つの値しかバインドできない。IN 句で複数の値を渡したい場合は、値の数だけプレースホルダを生成する必要がある。

$ids = [ 1, 2, 3 ];
$placeholders = implode( ',', array_fill( 0, count( $ids ), '?' ) );

$stmt = $pdo->prepare( "SELECT * FROM users WHERE id IN ( $placeholders )" );
$stmt->execute( $ids );

名前付きプレースホルダでも同様の工夫が必要になる。この手の処理が頻出するなら、ヘルパー関数を用意しておくとコードがすっきりする。