web-dev-qa-db-ja.com

バインドされたパラメーターを複数回使用する

私は、ユーザーがさまざまな種類の情報を含む可能性がある、私のデータベースにかなり基本的な検索エンジンを実装しようとしています。検索自体は、結果が常に3つの列にマージされる2つの和集合選択で構成されています。

ただし、返されるデータは別のテーブルからフェッチされています。

各クエリはマッチメイキングに$ termを使用し、準備されたパラメーターとして ":term"にバインドしました。

今、マニュアルは言う:

PDOStatement :: execute()を呼び出すときに、ステートメントに渡す値ごとに固有のパラメーターマーカーを含める必要があります。準備済みステートメントで同じ名前の名前付きパラメーターマーカーを2回使用することはできません。

各:termパラメータを:termX(term = n ++の場合はx)に置き換える代わりに、より良い解決策があるはずだと考えました。

または、Xの:termXをバインドする必要がありますか?

編集これに対する私のソリューションの投稿:

$query = "SELECT ... FROM table WHERE name LIKE :term OR number LIKE :term";

$term = "hello world";
$termX = 0;
$query = preg_replace_callback("/\:term/", function ($matches) use (&$termX) { $termX++; return $matches[0] . ($termX - 1); }, $query);

$pdo->prepare($query);

for ($i = 0; $i < $termX; $i++)
    $pdo->bindValue(":term$i", "%$term%", PDO::PARAM_STR);

こちらがサンプルです。 sqlfiddleの時間はありませんが、必要に応じて後で追加します。

(
    SELECT
        t1.`name` AS resultText
    FROM table1 AS t1
    WHERE
        t1.parent = :userID
        AND
        (
            t1.`name` LIKE :term
            OR
            t1.`number` LIKE :term
            AND
            t1.`status` = :flagStatus
        )
)
UNION
(
    SELECT
        t2.`name` AS resultText
    FROM table2 AS t2
    WHERE
        t2.parent = :userParentID
        AND
        (
            t2.`name` LIKE :term
            OR
            t2.`ticket` LIKE :term
            AND
            t1.`state` = :flagTicket
        )
)
34
Daniel

同じ問題を何度か経験しましたが、かなりシンプルで優れた解決策を見つけたと思います。パラメータを複数回使用する場合は、MySQL User-Defined Variableに格納するだけです。
これにより、コードがより読みやすくなり、PHPで追加の関数を必要としません。

$sql = "SET @term = :term";

try
{
    $stmt = $dbh->prepare($sql);
    $stmt->bindValue(":term", "%$term%", PDO::PARAM_STR);
    $stmt->execute();
}
catch(PDOException $e)
{
    // error handling
}


$sql = "SELECT ... FROM table WHERE name LIKE @term OR number LIKE @term";

try
{
    $stmt = $dbh->prepare($sql);
    $stmt->execute();
    $stmt->fetchAll();
}
catch(PDOException $e)
{
    //error handling
}

唯一の欠点は、追加のMySQLクエリを実行する必要があるということかもしれませんが、私にはそれだけの価値があります。
User-Defined VariablesはMySQLでセッションにバインドされているため、変数@termがマルチユーザー環境で副作用を引き起こすことを心配する必要もありません。

21
low_rents

2つの使用されている用語の名前を変更して問題を解決する2つの関数を作成しました。 1つはSQLの名前を変更し、もう1つはバインディングの名前を変更します。

    /**
     * Changes double bindings to seperate ones appended with numbers in bindings array
     * example: :term will become :term_1, :term_2, .. when used multiple times.
     *
     * @param string $pstrSql
     * @param array $paBindings
     * @return array
     */
    private function prepareParamtersForMultipleBindings($pstrSql, array $paBindings = array())
    {
        foreach($paBindings as $lstrBinding => $lmValue)
        {
            // $lnTermCount= substr_count($pstrSql, ':'.$lstrBinding);
            preg_match_all("/:".$lstrBinding."\b/", $pstrSql, $laMatches);

            $lnTermCount= (isset($laMatches[0])) ? count($laMatches[0]) : 0;

            if($lnTermCount > 1)
            {
                for($lnIndex = 1; $lnIndex <= $lnTermCount; $lnIndex++)
                {
                    $paBindings[$lstrBinding.'_'.$lnIndex] = $lmValue;
                }

                unset($paBindings[$lstrBinding]);
            }
        }

        return $paBindings;
    }

    /**
     * Changes double bindings to seperate ones appended with numbers in SQL string
     * example: :term will become :term_1, :term_2, .. when used multiple times.
     *
     * @param string $pstrSql
     * @param array $paBindings
     * @return string
     */
    private function prepareSqlForMultipleBindings($pstrSql, array $paBindings = array())
    {
        foreach($paBindings as $lstrBinding => $lmValue)
        {
            // $lnTermCount= substr_count($pstrSql, ':'.$lstrBinding);
            preg_match_all("/:".$lstrBinding."\b/", $pstrSql, $laMatches);

            $lnTermCount= (isset($laMatches[0])) ? count($laMatches[0]) : 0;

            if($lnTermCount > 1)
            {
                $lnCount= 0;
                $pstrSql= preg_replace_callback('(:'.$lstrBinding.'\b)', function($paMatches) use (&$lnCount) {
                    $lnCount++;
                    return sprintf("%s_%d", $paMatches[0], $lnCount);
                } , $pstrSql, $lnLimit = -1, $lnCount);
            }
        }

        return $pstrSql;
    }

使用例:

$lstrSqlQuery= $this->prepareSqlForMultipleBindings($pstrSqlQuery, $paParameters);
$laParameters= $this->prepareParamtersForMultipleBindings($pstrSqlQuery, $paParameters);
$this->prepare($lstrSqlQuery)->execute($laParameters);

変数の命名についての説明:
p:パラメータ、l:ローカル関数
str:文字列、n:数値、a:配列、m:混合

10
pascalvgemert

質問が投稿されてから変更されたかどうかはわかりませんが、今はマニュアルを確認すると、

準備されたステートメントで同じ名前の名前付きパラメーターマーカーを複数回使用することはできませんエミュレーションモードがオンでない限り

http://php.net/manual/en/pdo.prepare.php -(Emphasis mine。)

したがって、技術的には、$PDO_obj->setAttribute( PDO::ATTR_EMULATE_PREPARES, true );を使用してエミュレートされた準備を許可することもできます。良い考えではないかもしれませんが( この回答 で説明されているように)、エミュレートされた準備済みステートメントをオフにすることは、特定のインジェクション攻撃から保護する1つの方法ですが、 反対に書かれている 準備がエミュレートされているかどうかにかかわらず、セキュリティに影響はない(私にはわかりませんが、後者は前に述べた攻撃を念頭に置いていたとは思いません)。

完全を期すために、この回答を追加します。作業中のサイトでemulate_preparesをオフにすると、同様のクエリ(SELECT ... FROM tbl WHERE (Field1 LIKE :term OR Field2 LIKE :term) ...)を使用していたため、検索が中断し、明示的にPDO::ATTR_EMULATE_PREPARESからfalseに変更すると、失敗し始めました。

(PHP 5.4.38、MySQL 5.1.73 FWIW)

この質問から、同じクエリで名前付きパラメーターを2回使用できないことがわかりました(これは直観に反しているようですが、まあ)。 (何故か何度も見たのですが、どうやら見逃してしまいました。)

9

実用的なソリューション:

$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, TRUE);
$query = "SELECT * FROM table WHERE name LIKE :term OR number LIKE :term";
$term  = "hello world";
$stmt  = $pdo->prepare($query);
$stmt->execute(array('term' => "%$term%"));
$data  = $stmt->fetchAll();
3

ユーザー定義変数は、クエリへの値のバインドで同じ変数を複数回使用する1つの方法であり、そうすればうまくいきます。

//Setting this doesn't work at all, I tested it myself 
$pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, TRUE);

ユーザー定義変数を使用したくありませんでしたここに投稿されたソリューションの1つとまったく同じです。 パラメーターの名前変更もしたくありませんでしたここに掲載されている他のソリューションと同様です。したがって、ここでは、ユーザー定義変数を使用せず、少ないコードでクエリ内の名前を変更せずに機能する私のソリューションであり、クエリでparamが使用される回数を気にしません。私はすべてのプロジェクトでこれを使用しており、うまく機能しています。

//Example values
var $query = "select * from test_table where param_name_1 = :parameter and param_name_2 = :parameter";
var param_name = ":parameter";
var param_value = "value";

//Wrap these lines of codes in a function as needed sending 3 params $query, $param_name and $param_value. 
//You can also use an array as I do!

//Lets check if the param is defined in the query
if (strpos($query, $param_name) !== false)
{
    //Get the number of times the param appears in the query
    $ocurrences = substr_count($query, $param_name);
    //Loop the number of times the param is defined and bind the param value as many times needed
    for ($i = 0; $i < $ocurrences; $i++) 
    {
        //Let's bind the value to the param
        $statement->bindValue($param_name, $param_value);
    }
}

そして、ここに簡単な作業ソリューションがあります!

これが近い将来に誰かを助けることを願っています。

0
revobtz