とあるプログラムのソースコードを読んでいたところ見つかった工夫の紹介です。何がしかのフレームワークなりクエリビルダーのライブラリをよく使う場合、文字列ベースでSQLを構築することはあまりありませんが、どこかで文字列によるクエリ構築をする時に思い出せると便利な書き方です。
複数の条件を持つ SQL は次の様に書きます。
SELECT * FROM members WHERE name = 'hoge' AND email = 'hoge@example.com' AND company_name = 'hoge' ;
そしてよくある検索画面とクエリ構築の関係が次です。
/** @var string SQL文 */ $sql = 'SELECT * FROM members'; /** @var array 各プレースホルダーへあてはめる値 */ $bindings = []; if(検索ボックスの名前欄に入力がある){ $sql .= 'name = ? の WHERE 句を追加する'; $bindings[] = 名前欄の入力値; } if(検索ボックスのメールアドレス欄に入力がある){ $sql .= 'email = ? の WHERE 句を追加する'; $bindings[] = メールアドレス欄の入力値; } if(検索ボックスの会社名欄に入力がある){ $sql .= 'company_name = ? の WHERE 句を追加する'; $bindings[] = 会社名欄の入力値; } // SQL文とプレースホルダーの値を元にSQL文を実行 $検索結果 = データベース接続オブジェクト->SQL文実行メソッド($sql, $bindings);
いい感じのライブラリはsql, bindings, SQL実行用オブジェクトが使いやすく一体化していることが多いですが、文字列結合で SQL 文を作る必要がある時は大体こんな感じです。このコードの時点で冗長な if の繰り返しが気に食わないですが、手なりにコーディングすると実際は$sql .= 'name = ? の WHERE 句を追加する';
の部分が冗長になり、次の様にもっと見難くなります
/** @var string SQL文 */ $sql = 'SELECT * FROM members'; /** @var array 各プレースホルダーへあてはめる値 */ $bindings = []; /** @var bool 既に WHERE 句が現れているなら true */ $alreadyExistsWhere = false; if(検索ボックスの名前欄に入力がある){ if(!$alreadyExistsWhere){ $sql .= "WHERE name = ?"; $alreadyExistsWhere = true; } else { $sql .= "AND name = ?"; } $bindings[] = 名前欄の入力値; } if(検索ボックスのメールアドレス欄に入力がある){ if(!$alreadyExistsWhere){ $sql .= "WHERE email = ?"; $alreadyExistsWhere = true; } else { $sql .= "AND email = ?"; } $bindings[] = メールアドレス欄の入力値; } if(検索ボックスの会社名欄に入力がある){ if(!$alreadyExistsWhere){ $sql .= "WHERE company_name = ?"; $alreadyExistsWhere = true; } else { $sql .= "AND company_name = ?"; } $bindings[] = 会社名欄の入力値; } // SQL文とプレースホルダーの値を元にSQL文を実行 $検索結果 = データベース接続オブジェクト->SQL文実行メソッド($sql, $bindings);
いささか恣意的ですが(関数に抜き出せる、nameについては分岐不要など)この様に WHERE と AND を分岐する必要があります。例では単にある値で WHERE するだけですが、実際は複雑な条件をその場でどうこうする時もあります。一応、次の様に各条件について関数に分割すれば、何をやっているかわかりやすくなるメソッドが増えて多少はましになりますが、コード全体の長大さという問題は相変わらずです。
/** @var string SQL文 */ $sql = 'SELECT * FROM members'; /** @var array 各プレースホルダーへあてはめる値 */ $bindings = []; /** @var bool 既に WHERE 句が現れているなら true */ $alreadyExistsWhere = false; // それぞれを関数にまとめる。 // この方針でまとめるなら次の二つの方法あたりを使うともう少しましな見た目になります。 // 各関数をクラスのメソッドにして、常に引き回す各変数をクラスのプロパティにする // 各関数の引数にリファレンス渡しを適用して、常に引き回す各変数を副作用的にガンガン書き換える [$sql, $alreadyExistsWhere, $bindings] = whereName($alreadyExistsWhere, $sql, $bindings); [$sql, $alreadyExistsWhere, $bindings] = whereEmail($alreadyExistsWhere, $sql, $bindings); [$sql, $alreadyExistsWhere, $bindings] = whereCompanyName($alreadyExistsWhere, $sql, $bindings); // SQL文とプレースホルダーの値を元にSQL文を実行 $検索結果 = データベース接続オブジェクト->SQL文実行メソッド($sql, $bindings);
ここでこの記事の題が出てきます。WHERE 1 = 1
を記述することで次の様になり、冗長な分岐とフラグ変数が減ります。
/** @var string SQL文。この時点で WHERE 1 = 1 として後に続く WHERE を全て AND から始まる記述にする */ $sql = 'SELECT * FROM members WHERE 1 = 1'; /** @var array 各プレースホルダーへあてはめる値 */ $bindings = []; if(検索ボックスの名前欄に入力がある){ // 既に WHERE 1 = 1 があるので必ず AND で繋がる $sql .= 'AND name = ?'; $bindings[] = 名前欄の入力値; } if(検索ボックスのメールアドレス欄に入力がある){ $sql .= 'AND email = ?'; $bindings[] = メールアドレス欄の入力値; } if(検索ボックスの会社名欄に入力がある){ $sql .= 'AND company_name = ?'; $bindings[] = 会社名欄の入力値; } // SQL文とプレースホルダーの値を元にSQL文を実行 $検索結果 = データベース接続オブジェクト->SQL文実行メソッド($sql, $bindings);
最初にWHERE 1 = 1
を記述することで、後に続く条件を必ずAND カラム名 = 入力値
の形にでき、先述した自然言語でごまかした記述と変わらない量で SQL 文の構築を記述できました。クエリログを追う時、これを知らないと謎の 1 = 1 があると疑問が出てきますが、コーディングの上では大いに役に立ちます。