2017年02月08日
Movable Typeのカスタムフィールドの構造を読み解く (AND/OR 検索の実装)
Movable Type のデータベースの構造を覗いて見たことのある人は、カスタムフィールドが元オブジェクトとは別のテーブルに保存されていることに気づいていると思います。mt_entry(記事/ウェブページ)なら mt_entry_meta、mt_category(カテゴリ/フォルダ)なら mt_category_meta テーブルが作成され、そこに1フィールドの値が1レコードとして保存されています。
記事カスタムフィールドが50個作成されている場合、1つの記事に対して50の mt_entry_meta レコードが作成されます。尚、この仕様は保存時のパフォーマンスに影響を及ぼしますが、こちらのパッチプログラムはその問題解消への一つの答えとなります(MySQL限定ですが)。
本題に戻ります。このデータ構造を良く見ていくと、データの検索にカスタムフィールドを利用するのは少々厄介だと感じられるかもしれません。
サンプルプログラム
使い方はまた改めてご紹介しようと思います。今回は以下の記事を読み解く参考までに上げておきます。
OR検索の実装
OR検索は比較的楽に実現できます。この記事ではPHP、MTのダイナミックパブリッシングの仕組みを利用して実装します。
カスタムフィールドの情報はDBの mt_field テーブルに格納されています。
blog_id = 0 がシステムでのカスタムフィールド、blog_id = 数字 のあるものが、ブログ/ウェブサイトのカスタムフィールドです。(fig1.png)
なので、まずこれを検索します。blog_idが1、記事フィールド、ベースネームが field_name1、field_name2 のカスタムフィールドを検索するコードは以下のようになります。
$whereOrderBy = "( field_basename='field_name1' OR field_basename='field_name2' ) AND ( field_blog_id = 1 OR field_blog_id = 0 ) AND field_obj_type='entry'";
require_once( 'class.mt_field.php' );
$field = new Field;
$fields = $field->Find( $where, FALSE, FALSE, array() );
各フィールドが meta テーブルのどのカラムに値を格納するかの定義は、グローバル変数 $customfield_types に配列で指定されています。
global $customfield_types;
$params = array();
if ( $fields ) {
foreach( $fields as $field ) {
$field_type = $field->field_type;
$basename = $field->field_basename;
$params[ $basename ][ 'column_def' ] = $customfield_types[ $field_type ][ 'column_def' ];
}
}
$paramsは下記のようになります(カスタムフィールドが1行テキストなどの場合)。
array(2) {
["field_name1"]=>
array(3) {
["column_def"]=>
string(9) "vchar_idx"
}
["field_name2"]=>
array(3) {
["column_def"]=>
string(9) "vchar_idx"
}
}
field.field_name1 が 'Foo' もしくは(OR) field.field_name2 が 'Bar' である記事の検索は下記のようなコードになります。Findメソッドの第四引数にJOIN条件を指定し、distinct(重複を取り除く)に1を指定します。
require_once( 'class.mt_entry.php' );
$entry = new Entry;
$whereOrderBy = "entry_blog_id = 1 AND entry_status=2 AND
( ( entry_meta_vchar_idx = 'Foo' AND entry_meta_type='field.field_name1' )
OR ( entry_meta_vchar_idx = 'Bar' AND entry_meta_type='field.field_name2' ) )";
$extras[ 'join' ] = array(
"mt_entry_meta" => array(
'condition' =>
"mt_entry.entry_id=mt_entry_meta.entry_meta_entry_id",
),
);
$extras[ 'distinct' ] = 1;
$entries = $entry->Find( $whereOrderBy, FALSE, FALSE, $extras );
残念ながらこの検索ではソート条件にカスタムフィールドを指定することはできません(標準カラムをソート条件に指定する場合は$whereOrderByに追加、limit/offsetを指定するには $extras に指定します)。
$whereOrderBy .= " ORDER BY mt_entry_authored_on DESC";
$extras[ 'offset' ] = 20;
$extras[ 'limit' ] = 20;
AND検索の実装
異なるフィールドのデータが metaテーブルの同一のカラムに格納されている仕様上、一つのクエリで AND検索やソート条件にカスタムフィールドを指定することはできません(サブクエリや複雑なJOIN条件を組み立てればできるかもしれません。良いやり方のわかる方は是非フィードバックをお願いします)。
そこで、一つのやり方として、OR検索の結果をループ処理してすべての条件に合致する記事のIDを抽出し、その値で記事を絞り込む方法について考えてみます。このケースでは mt_entry_meta を直接検索します。
$cf_sql = "SELECT entry_meta_entry_id,entry_meta_type,entry_meta_vchar_idx
FROM mt_entry_meta WHERE
( ( entry_meta_vchar_idx = 'Foo' AND entry_meta_type='field.field_name1' )
OR ( entry_meta_vchar_idx = 'Bar' AND entry_meta_type='field.field_name2' ) )";
$match_fld = $ctx->mt->db()->Execute( $cf_sql );
$match_cnt = $match_fld->RecordCount();
if ( $match_cnt ) {
$match_cnt--;
$id_col = "entry_meta_entry_id";
$type_col = "entry_meta_type";
$results = array();
for ( $i = 0; $i <= $match_cnt; $i++ ) {
$match_fld->Move( $i );
$row = $match_fld->FetchRow();
$id = $row[ $id_col ];
$type = $row[ "entry_meta_type" ];
$type = preg_replace( '/^field\./', '', $type );
$col = $params[ $type ][ 'column_def' ];
$col = "entry_meta_${col}";
$value = $row[ $col ];
$results[ $id ][ $type ] = $value;
}
}
$results は以下のようになります(idが 5と7の記事が該当した場合)。
array(2) {
[5]=>
array(1) {
["field_name1"]=>
string(1) "Foo"
}
[7]=>
array(2) {
["field_name1"]=>
string(1) "Foo"
["field_name2"]=>
string(1) "Bar"
}
}
この後、$results をループ処理して、すべての条件にマッチしているか(AND検索)をチェックします(※ このループを工夫することでさらに複雑な AND OR の条件指定やソート順などを指定することもできます)。
foreach ( $results as $id => $field_values ) {
$match_all = 1;
foreach ( $params as $key => $values ) {
$result = $results[ $id ][ $key ];
if (! isset( $result ) ) {
$match_all = NULL;
break;
}
}
if ( $match_all ) {
$ids[] = $id;
}
}
$ids = array_unique( $ids );
$ids に AND検索を満たした記事のIDが配列で格納されます。後はこの IDで記事を絞り込み検索します。
$ids = join( ',', $ids );
$whereOrderBy .= "entry_blog_id= 1 AND
entry_status=2 AND entry_id in ( ${ids} )";
require_once( 'class.mt_entry.php' );
$entry = new Entry;
$entries = $entry->Find( $whereOrderBy, FALSE, FALSE, array() );
コメントを投稿する