2007年12月10日

検索機能の強化:日付の検索その1

前回は同一検索フィールド内でのAND検索の実装のメモを書きましたが、今回は日付の検索機能の実装をメモっていきます。

このアプリでは市町村の合併日フィールドをキーに検索できるようにしたいのですが、これがかなり後回しになったのは正直メンドイからです(^^;。日付というある意味扱いにくいデータ?なこともさることながら、これまでの検索関係の実装に加えて合併日検索?を盛り込もうとしたときに、正直どのように書けばいいのか?迷ってしまったわけです。

まず一般的に日付データ?というか日付入力が扱いにくいのは、ネット上でも言及され尽くしている感がありますが、要はプログラム(マー)の意図した書式で如何にユーザに入力させるか?という部分で、これに関しては様々な方法がありUIの進化の過程?も垣間見ることができます。

テキストフィールド
 ↓
年月日個別のテキストフィールド
 ↓
年月日個別のプルダウンメニュー
 ↓
カレンダー

みたいな感じで進化してきているようです。おそらくは当初は単なるテキストフィールドで2007/12/08のように完全にフォーマットに従って入力させようと試みていたのが、区切り文字にバリエーションを持たせたり(2007-12-08)とか、区切りなし(20071208)で入力したいとか、0で埋めたくない(2007/12/8)とか…いろいろな要望にこたえる形でのこととおもわれます。

また意図しない書式で入力された場合の入力チェックの煩雑さというのも、このような進化に影響を与えていることとおもわれます。で、最近?はカレンダーでクリックするやつが流行ってる?ようですが、この方法は入力チェックの観点からすると楽といえば楽です。ただ個人的には日付入力ごときでカレンダーのような大袈裟な過去のアナログ時代の遺物みたいなものが出てくるのがどうしても我慢できなくてこの方法は採りたくなかったわけで…。理想的にはテキストフィールド一発でやりたい!というのがまず一つ目のメンドイ原因です。

メンドイ理由の2つ目は、検索オブジェクトなるモノを作ってしまった結果、無理矢理にでもオブジェクト指向的にならざるを得ず、かといってそんなの知らないよ!状態だったので、まずはその辺から勉強しなきゃならなくて時間はかかるし不安は残るし…で。結局なんちゃってデザインパターンのようなモノを書いたわけですが、たぶん1週間後にはきれいさっぱり忘れそうなのでまずはそれからメモっておきます。

まず合併日で検索ということでViewのformにテキストフィールドを追加します。

View/serach_form.phpに合併日フォーム追加
$inputFieldName = array('pref', 'nc', 'oc', 'gdate');
合併日<br />
<?=form_input($input['gdate'])?>

$inputFieldNameはテキストフィールドのname属性を配列にして処理しているので、最初に名前の配列を生成しています。その配列に新たにgdateを加えています。

とりあえずこれだけで合併日の検索?はできるようになります。が、これは以下のようなlike節を生成しているだけで、つまりは合併日フィールドを文字列として検索していることになります。
WHERE gdate like '%2007-12-08%'

また、このままでは検索タイトル(検索結果ページの上部に「新市町村:〜」の検索結果、のように表示している部分)を出力している部分のコードでエラーが出てしまいます。なので検索オブジェクトクラスファイルのNormalSearchクラスで検索タイトル(HTMLで表示するための文字列)を取得するgetSearchTitle()内のswitch文で分岐している部分に、新たに合併日(case 'gdate')を追加します。
function getSearchTitle()
{
$searchTitle = '';

foreach ($this->getCriteria() as $key => $word)
{
switch ($key) {
case 'pref':
$searchKey = '都道府県';
break;
case 'nc':
$searchKey = '新市町村';
break;
case 'oc':
$searchKey = '旧市町村';
break;
case 'gdate':
$searchKey = '合併日';
break;
}

$searchTitle .= "「".$searchKey.":".$word."」";
}

$searchTitle .= 'の検索結果';

return $searchTitle;
}

この部分のコードは元々あまり綺麗でなく自分でもなんとかしようとは思っているのですが、とりあえずはここに「合併日」を追加します。

これでエラーは出なくなりましたが、文字列検索ではどうしようもないので、ちゃんと日付として範囲検索などもできるように考えてみます。まず入力フォーマットをでっちあげ決めます。

・日付はYYYY.MM.DDのドット区切り
・範囲指定は[-]ハイフン
ex:2005.04.01 又は 2005.04.01-2005.12.31

たったこれだけなんですが、入力する際に完全に↑のフォーマットに従って入力されればよいものの、想定される範囲で考えても入力チェックはかなり複雑になりそうな予感がします。ま、とりあえずは「数値、[.]ドット、[-]ハイフン」以外は無条件で削除することにして、それをcontrollerに書きます。

Controller/function search()
//[.]ドットは年月日の区切り、[-]ハイフンは範囲指定
$conditions['gdate'] = preg_replace("/[^0-9.-]/", "", $conditions['gdate']);

日付のデリミッタを[.]ドットにしたのは、範囲指定の記号に[-]ハイフンを使いたかったからです。ただこのため現状の合併日フィールドの書式(YYYY-MM-DD)と異なってしまうために、これをドット区切り表記に変更することにしました。

データ取得クエリを生成している部分の外側のSELECTでフィールドを列記している部分を以下のように変更します。
SELECT ... gdate ...

SELECT ... DATE_FORMAT(gdate, '%Y.%m.%d') as gdate ...

MySQLのDATE_FORMAT関数を使って変換しているだけです。注意しなければならないのは、データ取得クエリにはFROM句でサブクエリを使用していますが、その部分やWHERE句では上記の変換をせずに、外側のSELECTに適用するということです。
SELECT〜
FROM (
SELECT〜
WHERE〜
)
WHERE〜

↑のような構造の?SQLの場合
内側のWHERE句>内側のSELECT>外側のWHERE句>外側のSELECTの順に解釈される?とおもわれ、仮に内側のSELECT部分などにDATE_FORMAT()を使用してしまうと、gdateをWHERE句に用いた場合にデータを取ってこれなくなってしまいます。

model/function getData()
//結果取得SQL
$this->db->select("pid, pref, nid, nc, yomi, oid, oc, oyomi, gdate");

$this->db->select("pid, pref, nid, nc, yomi, oid, oc, oyomi,
DATE_FORMAT(gdate, '%Y.%m.%d') as gdate");

これで合併日が(YYYY.MM.DD)の形式になりました。

で、ここからが本題なのですが、formからのテキストフィールドに入力された値をキーに検索する時はNormalSearchクラスの検索オブジェクトクラスがインスタンス化されています。これは五十音パッドの検索部分の実装(多態編)で作成したとおりなのですが、searchModeという抽象クラスを継承したそれぞれの検索クラスがあるわけです。
               searchMode
  ┌──────┼─────┐
NormalSearch CapSearch IdSearch

このような形になっていると、検索機能の強化:IDで検索できるようにしてみるで述べたように、新たに検索機能?を付け加える際にはsearchModeを継承して新しいクラスを作ればいいようになっています。

ところが今回のように合併日を検索しようとする場合、formが一緒になっていることからもわかるように、合併日を単体で検索するだけでなく都道府県、新市町村、旧市町村などと組み合わせてAND検索をしたいわけですが、上記のクラス構成でsearchModeを継承して新たにGdateSearchなどというクラスを作ってしまうと、NormalSearchと組み合わせた検索?ができなくなってしまいそうです。(※そのような構成でも組み合わせる方法があるのかもしれませんが???)

そこでいろいろ調べた結果Decoratorパターンを使えばなんとなくいいような気がしてきたので、見よう見真似でそれを実装してみることにしました。ちなみにその際に下記のサイト様を参項にさせていただきました。

まさるblog 越後在住ソフトウェアエンジニアの記録
デザインパターンを学ぶ〜その6:Decoratorパターン(1)〜


koshigoewiki:php:デザインパターン:decoratorパターン

あとデザインパターン全体に対しての下記サイト様の言及が非常に参考になりました。

Happie's page SE・PG入門 デザインパターンを読み解く

で、Decoratorパターンですが、自分が理解したところをぶっちゃけて言ってしまえば、これはいわゆるラッパーで、デコレートされるクラスと共通の抽象クラスを継承しているデコレートクラスを作り、呼び出し側では元クラスの替わりにそのデコレートクラスを呼び出す、といった感じだと思います。その際デコレートクラスではメンバに元クラスを保持するようにして、メソッド内ではデコレートする処理と共に元クラスのメソッドも呼び出すような格好で実装?する…みたいな感じでしょうか???

まあ詳しい説明&正しい説明は他のちゃんとしたサイトを参照してもらうとして、コードだけ晒しておきます。(このエントリかなり長くなっちゃったし…)

検索オブジェクトクラス(search.php)
abstract class SearchMode
{
private $criteria;

function __construct(array $condition)
{
$this->criteria = $condition;
}

final public function getCriteria()
{
$criteria = array_diff($this->criteria, array('', NULL));
return $criteria;
}

abstract public function where();

abstract public function getSearchTitle();
}

abstract class OtherSearch extends SearchMode
{
protected $searchMode;

function __construct($searchMode, $condition)
{
parent::__construct($condition);
$this->searchMode = $searchMode;
}
}

class GdateSearch extends OtherSearch
{
public function __construct($searchMode, $condition)
{
parent::__construct($searchMode, $condition);
}

function where()
{
$CI =& get_instance();

$criteria = $this->getCriteria();
$date = explode('-', $criteria['gdate']);

$CI->db->where('gdate >=', $date[0]);
$CI->db->where('gdate <=', $date[1]);

$this->searchMode->where();
}

function getSearchTitle()
{
$criteria = $this->getCriteria();
$newSearchTitle = "「合併日:".$criteria['gdate']."」の検索結果";
$oldSearchTitle = $this->searchMode->getSearchTitle();

$searchTitle = str_replace("の検索結果", $newSearchTitle, $oldSearchTitle);

return $searchTitle;
}
}

class NormalSearch extends SearchMode
{
function __construct(array $condition)
{
parent::__construct($condition);
}

function where()
{
$CI =& get_instance();

foreach ($this->getCriteria() as $field => $condition)
{
if ($field == 'gdate') {continue;}

$sameFieldConditions = explode(" ", $condition);

foreach ($sameFieldConditions as $cond)
{
$CI->db->like($field, $cond);
}
}
}

function getSearchTitle()
{
$searchTitle = '';

foreach ($this->getCriteria() as $key => $word)
{
if ($key == 'gdate') {continue;}

switch ($key) {
case 'pref':
$searchKey = '都道府県';
break;
case 'nc':
$searchKey = '新市町村';
break;
case 'oc':
$searchKey = '旧市町村';
break;
/***** GdateSearch内で処理 *****/
}

$searchTitle .= "「".$searchKey.":".$word."」";
}

$searchTitle .= 'の検索結果';

return $searchTitle;
}
}

このファイル内には他にCapSearchクラスとIdSearchクラスが書いてありますがそれは省略してあります。新たに付け加えた部分はOtherSearchとGdateSearchで、OtherSearchはデコレータのインタフェース?でコンストラクタに元クラスのオブジェクトと検索条件を引数とするようにして、上述したようにメンバとして検索オブジェクトを保持します。GdateSearchが今回のメイン?でOtherSearchを継承したデコレータクラス?になります。

where()メソッド内では範囲指定([-]ハイフン区切り)された検索条件からクエリを生成します。当然のことながらこのWHERE句はNormalSearchとは全然違ったモノになります。そして最後に元クラスのwhere()を呼んでいます。CodeIgniterのアクティブレコード的にはdb->where()を連続して呼ぶとANDで連結するようなWHERE句になります。

getSearchTitle()は単純に元クラスのgetSearchTitle()で検索タイトルを取得して、それに「合併日:〜」を付け足しているだけです。ただし「〜の検索結果」という文字列を最後にしたかったので、単純に+するのではなく(↑の方の牛丼の例ではそのような形になっていましたが)str_replaceで該当部分を置き換えています。この結果↑の方で書いたNormalSearchクラスのgetSearchTitle()内でswitch文で分岐していたcase 'gdate'の部分は削除しました。

最後にControllerのsearch()ファンクションでNormalSearchをインスタンス化している部分の後にデコレータとしてGdateSearchをインスタンス化するコードを加えます。
function search()
{


if (implode('', $conditions) <> '')
{
$this->cond = new NormalSearch($conditions);

if ($conditions['gdate'] <> '')
{
$this->cond = new GdateSearch($this->cond, $conditions);
}


}
}

GdateSearchでは上述のように検索オブジェクトと検索条件の2つのパラメータが必要なので、先にインスタンス化されてメンバとして保持されている$condを検索条件$conditionと一緒に渡します。

というわけでなんとか合併日で検索できるようになりました。

例によって表示結果w
gdate_search.png

次回はこのフィールドの入力チェックに関してのメモです。
ラベル:codeigniter PHP
posted by ciallost at 03:22| Comment(0) | TrackBack(0) | 日記 | このブログの読者になる | 更新情報をチェックする
この記事へのコメント
コメントを書く
お名前:

メールアドレス:

ホームページアドレス:

コメント:

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


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

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