WordPressのWP_Query
やpre_get_posts
アクション内で投稿を取ってくる時に、一緒にできない複数の条件(複数のSELECT文が必要な条件)で投稿を取得したいような時のメモ。
メインループ外の場合は2回WP_Query
を実行(SQLを発行)してそれぞれ取得したデータの配列をゴニョゴニョとマージしてしまうか、2回while( $the_query->have_posts() )
で回してしまう方法もあります。(処理が多いかもですが、現実的にはこれが単純で簡単だと思います。)
サンプル
実現したいこと
story
というカスタム投稿(post_type
)を全て取得
数字の入るカスタムフィールド_sotry_no
の値の順に並べて、
その後ろに、_sotry_no
が存在しない投稿を続ける
UNION (MySQL)
WordPressで使われているMySQL的にはUNION
を使用すると1回のSQLで複数のSELECT文からデータが取得できるようです。
UNIONは複数のSELECT文によってデータをそれぞれ取得し、その結果を結合した上で1つのデータとして取得する場合に使います。書式は次の通りです。SELECT col_name1, ... FROM tbl_name1 UNION [ALL | DISTINCT] SELECT col_name2, ... FROM tbl_name2 UNION [ALL | DISTINCT] SELECT col_name3, ... FROM tbl_name3;出典: 取得データの結合(UNION句) - データの取得 - MySQLの使い方
また、UNIONを使って結合する、それぞれのSELECT文で並び替えを行いたい場合は、SELECT文にLIMITが含まれる必要がありました。
ex1. pre_get_posts() のメインクエリ書き換え内で別の
管理画面の一覧をカスタムフィールド_story_no
で並び替えるようにしようとした場合、
_story_no
フィールドが登録されていない状態で投稿(story
)が作成されてしまうと、管理画面の一覧に表示されなくなってしまい編集すら行うことができなくなってしまう問題がありました。
WP_Queryを使える箇所であれば2回WP_Query()
を使えば問題がないのですが、管理画面の一覧などpre_get_posts
でメインクエリを書き換える必要がある箇所はpre_get_posts
のアクション内でSQLを書き換えざるを得ません。
posts_request
フィルターを使えば実行されるSQLを直接書き換えることが可能なようなので、これを利用します。
[参考] Plugin API/Filter Reference/posts request « WordPress Codex
<?php // functions.php add_action( 'pre_get_posts', 'my_posts_per_page' ); function my_posts_per_page( $wp_query ) { // 管理画面 if( is_admin() ) { // 投稿タイプ story if( $wp_query->is_post_type_archive('story') ) { // カスタムフィールド _story_no で検索して表示させる $wp_query->set('meta_key', '_story_no'); $wp_query->set('orderby', array( 'meta_value_num' => 'DESC', 'date' => 'DESC' )); /** * WP_QueryのSQLを置き換えるフィルター * @param: $request (Strings) ... 元のSQLのテキスト */ add_filter( 'posts_request', function( $request ) { global $wpdb; // config.phpで設定されているDBのprefixを取得 $prefix = $wpdb->base_prefix; // UNIONで追加するSQL文を作成 $sql = "SELECT p.* FROM {$prefix}posts AS p WHERE p.post_type = 'story' AND p.post_status = 'publish' AND NOT EXISTS( SELECT meta.meta_id FROM {$prefix}postmeta AS meta WHERE meta.post_id = p.ID AND meta.meta_key = '_story_no' ) GROUP BY p.ID ORDER BY p.post_date DESC LIMIT 0,18446744073709551615"; // 実際に実行したいSQL文を返す // ※ 管理画面の一覧の$requestはデフォルトでLIMITが設定されれいるので別途LIMIT節を追加する必要なし return "({$request}) UNION ({$sql})"; }); } } }
これで管理画面一覧でも特定のカスタムフィールドが無い投稿も表示され編集できるようになりました。
ex2. WP_Query() でUNION
を使う方法
残念ながらWP_Query()の引数の渡し方で複数のSELECTをマージできるような機能、UNION
を使ったSQLを作れる機能は無いようです。
[参考] Class Reference/WP Query « WordPress Codex
WP_Queryを2回実行しないのであれば、やはりposts_request
フィルターでSQLを変更するしかなさそうです。
<?php // functions.php function get_my_story() { $arg = [ 'post_type' => 'story', // 該当する story を全件取得 'posts_per_page' => -1, 'meta_key' => '_story_no', // _story_no が0以下のものは表示しない 'meta_query' => [ [ 'key' => '_story_no', 'value' => 0, 'compare' => '>', ], ], // _story_no が小さい順に並べる 'orderby' => [ 'meta_value_num' => 'ASC', 'date' => 'ASC', ], ]; // SQLを書き換えるためのフィルター add_filter( 'posts_request', 'my_posts_request_filter'); // WP_Query()が実行される際に↑の関数が実行される $the_query = new WP_Query($arg); // 同じページに他の WP_Query() があるとフィルターが動作してしまうので、フィルターを削除する remove_filter( 'posts_request', 'my_posts_request_callback'); if( $the_query->have_posts() ) { while( $the_query->have_posts() ) { // 処理 } } // 略 }; /** * WP_QueryのSQLを置き換えるフィルター * @param: $request (Strings) ... 元のSQLのテキスト */ function my_posts_request_filter( $request ) { global $wpdb; $prefix = $wpdb->base_prefix; $sql = "SELECT p.* FROM {$prefix}posts AS p WHERE p.post_type = 'story' AND p.post_status = 'publish' AND NOT EXISTS( SELECT meta.meta_id FROM {$prefix}postmeta AS meta WHERE meta.post_id = p.ID AND meta.meta_key = '_story_no' ) GROUP BY p.ID ORDER BY p.post_date DESC LIMIT 0,18446744073709551615"; // 実際に実行したいSQL文を返す // ※ 全件取得する元のSQLのORDER BYが効くようにするには LIMITが必要なので大きな値でLIMIT節を追加する return "({$request} LIMIT 0,18446744073709551615) UNION ALL ({$sql})"; }
表側のページの場合他の箇所でもWP_Queryが使われていたりする場合があるので、必要な箇所でのフィルター処理が完了したらremove_filter
でフィルターを削除しておかないと、他のWP_Queryが使われている場所でもフィルターが実行されてしまうので注意が必要です。
WordPressの管理画面を見やすくなるようにと特定のカスタムフィールドで並び替えをしていたら、カスタムフィールドが空文字やNULLなら良いのですが、DBに登録されなかった場合、一覧に表示されず編集できなくなってしまう状況になり盛大にハマってしまいました。
いつもカスタムフィールドはCMB2というプラグインを使いコードベースで作成しているのですが、表示に際して必須なカスタムフィールドがある場合は必ず初期値を指定しておくべきだなと痛感しました。(初期値があれば今回のようにSQLを直接書き換える必要もなかったのです...)
MySQLとかWordPressで覚えた程度だったのでUNION
の存在を知らず、UNIONで結合したSELECT文をそれぞれ別にソートする方法にすごくハマりました...
おかげで良い勉強になりなったので良かったです。
結局UNIONの問題と、存在しないカスタムフィールドを取得する複合的な問題だったので、それぞれ別の記事を作成するしました。
- multisite - Can i merge 2 new WP_Query($variable) 's? - WordPress Development Stack Exchange
- 取得データの結合(UNION句) - データの取得 - MySQLの使い方
- MySQLのUNIONの便利な使い道 - Qiita
- mysql - How do i append UNION SQL Query with default search query - WordPress Development Stack Exchange
- mysql - How to use ORDER BY inside UNION - Stack Overflow
- WordPressでシステム開発をする時に必要なクエリ操作について - Qiita
- https://paulund.co.uk/table-prefix-wordpress
- Plugin API/Filter Reference/posts request « WordPress Codex
- WordPressのサイト内検索の検索条件をカスタマイズする | webOpixel
- Function Reference/remove filter « WordPress Codex
- php - Select all records using MySQL LIMIT and OFFSET query - Stack Overflow
- 作者: Bill Karwin,和田卓人,和田省二,児島修
- 出版社/メーカー: オライリージャパン
- 発売日: 2013/01/26
- メディア: 大型本
- 購入: 9人 クリック: 698回
- この商品を含むブログ (46件) を見る