PHP
PDO

【PHP】PDOのプリペアドステートメントで疑問符パラメータと名前付きパラメータを混在させるとどうなるか

はじめに

PHPのPDOで、プリペアドステートメントで値をバインドする方法には、疑問符パラメータを使う方法と名前付きパラメータを使う方法があります。

そもそもPDO::prepareで「名前と疑問符パラメータを同一 SQL ステートメント中で使用することはできません。」とあるので混ぜなければ良いのですが、「混ぜるな危険」と書いてあると混ぜたくなるのが人情というものでしょう。というわけで実験してみました。

一応、「基本的にはバインド箇所がわかりやすい名前付きパラメータを使いたいけど、次のようにIN句などで動的にプレースホルダを作る場合に疑問符パラメータも使いたい」という動機もあるにはあります。

<?php
// バインド用パラメータ
$params = array(1, 21, 63, 171);

// パラメータのプレースホルダ用の文字列を、パラメータ数に合わせて作成
$place_holders = implode(',', array_fill(0, count($params), '?'));

$sql = "
    SELECT name, value
    FROM table
    WHERE id IN ($place_holders) AND value >= :value
";

使用したPHPのバージョン: 7.1.12
使用したデータベース: SQLite

実験結果

テストにはこのようなシンプルなテーブルを用いました。

id name
1 Alice
2 Bob
3 Cindy

以下のようにプリペアドステートメントを実行します。
パラメータがどのようにバインドされているのかはdebugDumpParams関数で調べます。
SELECT文の結果はprint_rで出力しています。

まずは普通に疑問符パラメータだけを使った場合

const DB_FILE_PATH = __DIR__ . "/database.sqlite";

$dbh = new PDO("sqlite:" . DB_FILE_PATH);
$dbh->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
$dbh->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC);

$sql = "
    SELECT id, name
    FROM test_table
    WHERE name = ? OR name = ? OR name = ?
";

$params = array('Alice', 'Bob', 'Cindy');

$stmt = $dbh->prepare($sql);
$stmt->execute($params);
$stmt->debugDumpParams(); // バインドについての情報を出力
print_r($stmt->fetchAll()); // SELECT文の結果を出力

少し見づらいですが、出力は以下のようになります。
paramno がパラメータの位置(0で開始)を表しているようですね。

SQL: [75] 
    SELECT id, name
    FROM test_table
    WHERE name = ? OR name = ? OR name = ?

Params:  3
Key: Position #0:
paramno=0
name=[0] ""
is_param=1
param_type=2
Key: Position #1:
paramno=1
name=[0] ""
is_param=1
param_type=2
Key: Position #2:
paramno=2
name=[0] ""
is_param=1
param_type=2
Array
(
    [0] => Array
        (
            [id] => 1
            [name] => Alice
        )

    [1] => Array
        (
            [id] => 2
            [name] => Bob
        )

    [2] => Array
        (
            [id] => 3
            [name] => Cindy
        )

)

これだけではわかりにくいので、名前付きプレースホルダの場合も見てみましょう。
SQL文とパラメータ以外は同じなので省略しています。

$sql = "
    SELECT id, name
    FROM test_table
    WHERE name = :name1 OR name = :name2 OR name = :name3
";

$params = array(':name1' => 'Alice', ':name2' => 'Bob', ':name3' => 'Cindy');

結果はこうなりました。
プレースホルダの名前が入っているので、debugDumpParamsの出力はこちらの方がわかりやすいですね。

SQL: [90] 
    SELECT id, name
    FROM test_table
    WHERE name = :name1 OR name = :name2 OR name = :name3

Params:  3
Key: Name: [6] :name1
paramno=0
name=[6] ":name1"
is_param=1
param_type=2
Key: Name: [6] :name2
paramno=1
name=[6] ":name2"
is_param=1
param_type=2
Key: Name: [6] :name3
paramno=2
name=[6] ":name3"
is_param=1
param_type=2
Array
(
    [0] => Array
        (
            [id] => 1
            [name] => Alice
        )

    [1] => Array
        (
            [id] => 2
            [name] => Bob
        )

    [2] => Array
        (
            [id] => 3
            [name] => Cindy
        )

)

では混ぜてみましょう。
先ほどの :name2 の代わりに疑問符プレースホルダを入れてみました。

// :name2 を ? に変更
$sql = "
    SELECT id, name
    FROM test_table
    WHERE name = :name1 OR name = ? OR name = :name3
";

// ':name2' => 'Bob' を 'Bob' に変更
$params = array(':name1' => 'Alice', 'Bob', ':name3' => 'Cindy');

結果

SQL: [85] 
    SELECT id, name
    FROM test_table
    WHERE name = :name1 OR name = ? OR name = :name3

Params:  3
Key: Name: [6] :name1
paramno=0      <--------- !!
name=[6] ":name1"
is_param=1
param_type=2
Key: Position #0:
paramno=0      <--------- !!
name=[0] ""
is_param=1
param_type=2
Key: Name: [6] :name3
paramno=2
name=[6] ":name3"
is_param=1
param_type=2
Array
(
    [0] => Array
        (
            [id] => 2
            [name] => Bob
        )

    [1] => Array
        (
            [id] => 3
            [name] => Cindy
        )

)

:name1 にバインドしたはずの 'Alice' がどこかに行ってしまいました。
debugDumpParamsの出力を見ると、 paramno=0 が2つあります。
2つ目の 'Bob' が 'Alice' を上書きしてしまったようですね。
? には何もバインドされていませんが、特にエラーは出ません。

ここで、一番初めの疑問符プレースホルダの例の

$params = array('Alice', 'Bob', 'Cindy');

は以下と等価です。

$params = array(0 => 'Alice', 1 => 'Bob', 2 => 'Cindy');

そこで、'Bob'のキーを1にしてみましょう。

$sql = "
    SELECT id, name
    FROM test_table
    WHERE name = :name1 OR name = ? OR name = :name3
";

// 'Bob'のキーを 1 にする
$params = array(':name1' => 'Alice', 1 => 'Bob', ':name3' => 'Cindy');

今度は上手くいきました。

SQL: [85] 
    SELECT id, name
    FROM test_table
    WHERE name = :name1 OR name = ? OR name = :name3

Params:  3
Key: Name: [6] :name1
paramno=0
name=[6] ":name1"
is_param=1
param_type=2
Key: Position #1:
paramno=1
name=[0] ""
is_param=1
param_type=2
Key: Name: [6] :name3
paramno=2
name=[6] ":name3"
is_param=1
param_type=2
Array
(
    [0] => Array
        (
            [id] => 1
            [name] => Alice
        )

    [1] => Array
        (
            [id] => 2
            [name] => Bob
        )

    [2] => Array
        (
            [id] => 3
            [name] => Cindy
        )

)

なお、上手くいかなかった例でのパラメータ配列は以下と等価です。
数値添字配列の場合、添字と一致するparamnoのところにバインドされることがわかります。

// 'Bob' のキーは0だった
$params = array(':name1' => 'Alice', 0 => 'Bob', ':name3' => 'Cindy');

では、名前付きパラメータに数値添字配列を強引にバインドしてみましょう。

$sql = "
    SELECT id, name
    FROM test_table
    WHERE name = :name1 OR name = :name2 OR name = :name3
";

// 名前付きプレースホルダに数値添字配列をバインド
$params = array('Alice', 'Bob', 'Cindy');

予想がついたかもしれませんが、この場合はきちんと(?)バインドされます。
内部的には、名前付きパラメータも一旦は数値添字配列に置き換えられているようです。

SQL: [90] 
    SELECT id, name
    FROM test_table
    WHERE name = :name1 OR name = :name2 OR name = :name3

Params:  3
Key: Position #0:
paramno=0
name=[0] ""
is_param=1
param_type=2
Key: Position #1:
paramno=1
name=[0] ""
is_param=1
param_type=2
Key: Position #2:
paramno=2
name=[0] ""
is_param=1
param_type=2
Array
(
    [0] => Array
        (
            [id] => 1
            [name] => Alice
        )

    [1] => Array
        (
            [id] => 2
            [name] => Bob
        )

    [2] => Array
        (
            [id] => 3
            [name] => Cindy
        )

)

数値添字配列の添字は文字列でも大丈夫ですが、 == で等値と判定されるものであってもダメな場合があります。これはPHPの配列の仕様によるものと思われます。

$params = array(
    '0' => 'Alice', // OK
    1.0 => 'Bob', // OK
    '02' => 'Cindy' // NG
);

また、bindValueやbindParamでバインドする場合もこれまでの話と同じことが言えますが、パラメータの位置指定の開始が1からになります。

$sql = "
    SELECT id, name
    FROM test_table
    WHERE name = :name1 OR name = ? OR name = :name3
";

$stmt = $dbh->prepare($sql);

// bindValueでバインド
$stmt->bindValue(1, 'Alice', PDO::PARAM_STR);
$stmt->bindValue(2, 'Bob', PDO::PARAM_STR);
$stmt->bindValue(':name3', 'Cindy', PDO::PARAM_STR);

$stmt->execute();

実行結果

SQL: [85] 
    SELECT id, name
    FROM test_table
    WHERE name = :name1 OR name = ? OR name = :name3

Params:  3
Key: Position #0:
paramno=0
name=[0] ""
is_param=1
param_type=2
Key: Position #1:
paramno=1
name=[0] ""
is_param=1
param_type=2
Key: Name: [6] :name3
paramno=2
name=[6] ":name3"
is_param=1
param_type=2
Array
(
    [0] => Array
        (
            [id] => 1
            [name] => Alice
        )

    [1] => Array
        (
            [id] => 2
            [name] => Bob
        )

    [2] => Array
        (
            [id] => 3
            [name] => Cindy
        )

)

結論

疑問符パラメータの位置指定がきちんとできていれば、疑問符パラメータと名前付きパラメータを混在させることは可能です。その際、名前付きパラメータも疑問符パラメータであるとみなして疑問符パラメータの位置指定をする必要があります(名前付きパラメータの位置指定は普通にやればOK)。

ただし、このような内部仕様的なものに依存したプログラムを書くのはあまり良くないかもしれません。