2007年12月03日

検索機能の強化:IDで検索できるようにしてみる

前回はMySQLの照合順序で右往左往してしまい、検索機能強化がどんどん後回しになっていたわけですが、今回から(何事もなければ…)何回かに分けて検索機能の強化について書いてみたいとおもいます。

検索機能の強化といっても大したことを考えているわけではなく、いわゆるAjaxでトレンドになったいんくりめんたるさあちとか、全文検索でファジー(死語?)にどーのこーのとかいうような高級なことをやろうとしているわけではありません。

一応ロードマップ?としてはまず今回はIDで検索できるようにスクリプトを改造します。次に検索フィールド内でAND検索ができるようにします。これはグーグルのようにスペースで区切った単語をAND検索するというものです。さらに合併日で検索できるようにします。

以上の検索方法は大元のスクリプトには、もちろん手続き型の例のアレですが、一応実装されていて、CodeIgniter?というかオブジェクト指向的?に実装したらどうなるのかな?という実験みたいなものです。で、まあこういうことを総称して検索機能の強化と言ってるだけでして、要するに自分用のメモです(^^;。

というわけで今回はIDでの検索です。これは↑の3つの中ではおそらく一番実装が楽だろう?という予想の元に、まず試してみることにしました。

IDでの検索というのはどういうことかというと、今まで実装してきた検索方法はNormalSearchにしてもKanaSearchにしても名称とかふりがな(読み)のような実体?というか現物?というかなんかそんな感じのフィールドを検索対象にしていましたが、IDでの検索というのは文字通りそれぞれのテーブルのプライマリーキーになっているようなフィールド(ほとんどidという名前がついている)で検索しようということです。

この検索方法がどのようなときに必要か?というと、例えば都道府県別で合併市町村の一覧を出力したいとします。UI的には都道府県をダダっと並べておいて、それをクリックしたらその都道府県での合併市町村の一覧を表示する〜といったようなものをイメージしています。

<都道府県別一覧>
北海道|青森|秋田|岩手|・・・|鹿児島|沖縄|

↑こんな感じで…

この時都道府県名にアンカーをつけて適宜メソッドを呼び出すわけですが、一見NormalSearch(検索フィールドからのサーチ)でも大丈夫そうな気がします。ですが例えば上記では北海道を除いて煩雑さを避けるために○○県の県の部分を省略しています。これをこのままアンカーに利用してNormalSearchに送信すれば事は足りる?とおもわれます。が!
anchor('gappei/search/'.urlencode('京都'), '京都')

のようなことを不用意にやってしまうと、この場合なら京都府の一覧とともに東“京都”の一覧も表示されてしまったりするわけです。

この例だと回避策としては「県」を省略せずに渡してやれば済む話ですが、前回の「伊達市」のように異都道府県同一市町村名?みたいなパターンだとNormalSearchでやろうとすれば北海道&伊達市、福島県&伊達市のようにANDで絞込検索?のような形にするしかありません。

まあそれでも言いといえばいいのですが、やはりダイレクトに検索?というか指定できたほうが便利なことは確かです。この市町村合併データベースのような例だとあまり必要性はないかもしれませんが、映画やCD・書籍などのデータベースだと同名異作品という場合は頻発するのではないかとおもわれます。

ということで前置きはこのぐらいにして、実装の過程をメモっておきます。

まず五十音パッドの検索部分の実装(多態編)で作成した検索オブジェクトクラスに新たにIdSearchというクラスを作ります。
class IdSearch extends SearchMode
{
function __construct(array $condition)
{
parent::__construct($condition);
}
}

他の検索オブジェクトクラスと同じようにSearchModeクラスを継承する形にします。次にwhere()の実装ですが、これは$condition(検索条件:コンストラクタに渡される)の形を、
参照するフィールド名 => ID番号

のように渡すことにするので、以下のようにほとんどNormalSearchのwhere()と同じようになります。
function where()
{
$CI =& get_instance();

$CI->db->where($this->getCriteria());
}

NormalSearchの場合はdb->like()でクエリを生成していましたが、今回は「=」でいいわけで、db->where()にそのまま検索条件配列を渡しています。次にgetSearchTitle()ですがこれはとりあえず
function getSearchTitle()
{
return 'ID Search';
}

とだけ書いて後回しにして、まずはControllerで新たにメソッドを定義します。
function idSearch($field, $id=0)
{
$this->cond = new IdSearch(array($field => $id));
$this->phpsession->save('searchObj', serialize($this->cond));
redirect('gappei/index/');
}

$fieldと$idをそれぞれURIの第3,4セグメントから受け取って、先ほど作ったクラスをインスタンス化してindex()にリダイレクトしているだけです。

これでとりあえずデータは受取れるはずですが、まだViewというかUIを作っていないのでさしあたってURL入力欄から
http://localhost/gappei/idSearch/pid/1

のように入力して試してみます。

それぞれのidのフィールド名は、

pid:都道府県ID
nid:新市町村ID
oid:旧市町村ID

のようになっているので、上記のURIだと北海道の一覧が出力されます。ここでいろいろパラメータを変えて試してみると、まあ当然ですがフィールド名に無いモノを指定したり、IDの部分(URIの第4セグメント)を文字列にしたりするとエラーが出るので、Controllerにヴァリデート(入力チェック)のコードを書きます。

Controller/idSearch()
function idSearch($field, $id=0)
{
/*$idは初期値を0にしてURIの第4セグメントが空の送信に対処する
$idは数値以外は削除
$fieldはpid, nid, oid以外は何もしないでindex()にリダイレクト
※$idの最大値のチェックは行わない、仮に送信された場合でもデータ件数0になるだけ*/
$id = preg_replace("/[^0-9]/", "", $id);

if (preg_match("/^(p|n|o)id$/", $field))
{
: (インスタンス作成等)

ほとんどヴァリデーションのコードを自力で書くことにした。でやったことと同じような感じです。これでとりあえず想定されるようなURIからのエラーは出なくなりました。

次に後回しにしていたgetSearchTitle()をちゃんと書きます。
function getSearchTitle()
{
$CI =& get_instance();

$criteria = $this->getCriteria();
$key = key($criteria);
$id = $criteria[$key];

$searchTitle = '';

switch ($key) {
case 'pid':
$CI->db->select('name')->from('prefs')->where(array('id' => $id));
break;
case 'nid':
$CI->db->select('city as name')->from('newcities')->where(array('id' => $id));
break;
case 'oid':
$CI->db->select('oc as name')->from('oldcities')->where(array('oid' => $id));
$searchTitle = '旧';
break;
}

$row = $CI->db->get()->row_array();
$searchTitle .= (count($row) > 0) ? $row['name'] : '';

return $searchTitle;
}

まあ見ての通りあんまり綺麗じゃない(ってか汚い!)コードです(--;。

単に名称を出力するだけなのに、何故かこんなに長くなっちゃいました。これはひとえにメソッドが引数を受け取っていないことからくるモノと言えそうです。

冒頭述べたように大抵の場合はUI?というか、要するにHTMLのアンカーの文字列イコール検索タイトルなわけです。なのでそれをメソッドの引数に渡すことができれば単純にそれをreturnするだけで済むはずなんですが、他のクラスとの兼ね合いから引数を取らない形でgetSearchTitle()が定義されているので、検索オブジェクトを元にいちいちDBに問い合わせしてタイトルを得ているわけです。

そもそも取ってくるテーブルのフィールド名がまちまちなため、まず検索オブジェクトのキーを元にswitch文で分岐して、それぞれのテーブルに合ったクエリを生成しています。旧市町村の場合は頭に「旧」と付けたかったのでこれはハードコーディングです(--;。

最後に$rowをcountしているのは結果セットが無い場合$row['name']を代入しようとするとエラーがでてしまうためです。これはidの値が最大値を超えた場合に起こり得ます。ここでこのように実装しておくことでController側のメソッドでは最大値のチェックをしていません。

まあ汚いですけど親クラスのgetSearchTitle()の定義に従って引数を受け渡さないように書くことで、Controller(のindex()メソッド)は一切変更せずに検索タイトルを受取ることが出来ます。
//検索タイトル取得
$data['searchTitle'] = htmlspecialchars($searchObj->getSearchTitle());

最後にViewを作ります。一応構想としては都道府県別・新市町村別・旧市町村別などの項目でワンクリックで目的の名称の一覧を表示できるようなモノを考えていましたが、この合併データベースでは新市町村別・旧市町村別などはずらずらと項目を並べてもほとんど意味がないため、都道府県別だけに留めました。

id_search.png

まあこんな感じのモノを五十音パッドの下に表示するということです。他の項目別メニュー?も階層的にしてクリックしたら展開する〜みたいなのまで作ろうとも考えましたが、あまり本筋とは関係ない部分なので止めました。

またこの出力も一応ファイル的にはlist.phpとして分離してgappei_detail.phpやgappei_simple.phpなどのメインViewからロードしていますが、list.php自体は単なるHTMLです。
<h3 class="ui_title">都道府県別</h3>
<p class="prefs">
<?=anchor('gappei/idSearch/pid/1', '北海道');?><br />
<?=anchor('gappei/idSearch/pid/2', '青森');?>|
<?=anchor('gappei/idSearch/pid/3', '岩手');?>|

:(中略)

<?=anchor('gappei/idSearch/pid/46', '鹿児島');?>|
<?=anchor('gappei/idSearch/pid/47', '沖縄');?>
</p>

HTMLヘルパを使ってul()メソッドで出力しようかな?とも考えたのですが、<ul>だと一行一都道府県になってしまい無駄に長くなるので却下しました。このようなある意味ランダム?な表示だとやはり手動でやるしかないですかね〜?まあとりあえず今回はUIよりもID検索が主なのでこれでヨシとしました。

何度も言うようですが、この合併データベースだとID検索はあまり意味がないように思えますが、データベースの内容によってはこれが結構重宝するんです。というか無いとヤバイ(謎)。

というわけで次回は検索フィールド内でのAND検索の実装について書いてみようとおもいます。
ラベル:codeigniter PHP
posted by ciallost at 05:15| Comment(0) | TrackBack(0) | 日記 | このブログの読者になる | 更新情報をチェックする
この記事へのコメント
コメントを書く
お名前:

メールアドレス:

ホームページアドレス:

コメント:

認証コード: [必須入力]


※画像の中の文字を半角で入力してください。

この記事へのトラックバック