メインクエリを書き換える pre_get_posts 入門 ― query_postsを使ってはいけない理由

「お知らせ一覧は1ページ10件にしたい」「カテゴリーアーカイブは古い順に並べたい」「検索結果から特定の投稿タイプを除外したい」。こうしたとき、テンプレートの中で query_posts() を使っていませんか。昔の解説記事には頻繁に登場しますが、公式ドキュメントでも可能な限り避けるべき関数とされています。

代わりに使うのが pre_get_posts というアクションフックです。この記事では、メインクエリを安全に書き換える方法と、query_postsを避けるべき理由、get_postsやWP_Queryとの使い分けを解説します。

メインクエリとは

WordPressは、ページにアクセスがあると、URLをもとに「どの記事を取得するか」を判断して、データベースに問い合わせを行います。このとき自動で実行されるのがメインクエリです。トップページ、カテゴリーアーカイブ、検索結果、投稿の個別ページなど、すべての表示がこのメインクエリを元にしています。

テンプレートで have_posts() と the_post() を使ったループは、このメインクエリの結果を表示しています。つまり、表示件数や並び順を変えたい場合は、メインクエリの条件そのものを変えるのが本来の方法です。

query_postsを使ってはいけない理由

query_posts()は、テンプレートの中でメインクエリを上書きする関数です。手軽に見えますが、次の問題があります。

  • クエリが二重に実行される:WordPressが自動で実行したあとに、もう一度クエリを実行するため、無駄な負荷がかかる。
  • ページネーションが壊れやすい:件数やページ数の情報が正しく引き継がれず、2ページ目以降が表示されなくなる。
  • グローバル変数を書き換える:後続の処理やプラグインに、予期しない影響を与える。
  • リセットが必要:wp_reset_query()を忘れると、サイドバーやフッターの表示が崩れる。

WordPressの公式ドキュメントでも、query_postsは「使用を避けるべき」とされています。メインクエリを変更するときは、pre_get_postsを使います。

pre_get_postsの基本

pre_get_postsは、クエリがデータベースに問い合わせを行う直前に呼ばれるフックです。ここでクエリの条件を変更すれば、最初から目的の条件で、1回だけクエリが実行されます。

add_action( 'pre_get_posts', 'my_modify_main_query' );

function my_modify_main_query( $query ) {
	// 管理画面と、メインクエリ以外は対象外にする
	if ( is_admin() || ! $query->is_main_query() ) {
		return;
	}

	// お知らせのアーカイブだけ、表示件数を10件にする
	if ( $query->is_post_type_archive( 'news' ) ) {
		$query->set( 'posts_per_page', 10 );
	}
}

この書き方のポイントは、次の3点です。

  1. 関数の引数 $query は、参照渡しのため、set() で変更すればそのまま反映される(return不要)
  2. is_admin() で、管理画面のクエリを除外する
  3. $query->is_main_query() で、サブクエリ(ウィジェットなど)を除外する

特に2と3の判定は、必ず書いてください。抜けていると、管理画面の投稿一覧や、ウィジェットのクエリまで書き換わり、原因の分かりにくい不具合になります。

よく使うパターン集

表示件数を変える

if ( $query->is_post_type_archive( 'news' ) ) {
	$query->set( 'posts_per_page', 10 );
}

並び順を変える(古い順)

if ( $query->is_category() ) {
	$query->set( 'orderby', 'date' );
	$query->set( 'order', 'ASC' );
}

検索結果の対象を絞る

if ( $query->is_search() ) {
	$query->set( 'post_type', array( 'post', 'news' ) );
}

特定のカテゴリーを除外する

if ( $query->is_home() ) {
	$query->set( 'category__not_in', array( 5 ) );
}

カスタムフィールドで並べ替える

if ( $query->is_post_type_archive( 'event' ) ) {
	$query->set( 'meta_key', 'event_date' );
	$query->set( 'orderby', 'meta_value' );
	$query->set( 'order', 'ASC' );
}

どのパターンも、まず条件分岐で「どのページのクエリか」を限定し、そのうえで set() を呼ぶ形が共通しています。

条件分岐タグの使いどころ

pre_get_postsの中では、is_home() や is_category() などの条件分岐タグを使えます。ただし、実行のタイミングがテンプレート読み込みの前のため、グローバルな条件分岐ではなく、$queryのメソッドを使うのが安全です。

用途 推奨する書き方
トップページ $query->is_home()
カテゴリー $query->is_category()
検索結果 $query->is_search()
投稿タイプのアーカイブ $query->is_post_type_archive( 'news' )
タクソノミーのアーカイブ $query->is_tax( 'news_cat' )

フロントページを固定ページにしている場合は、is_home() の意味が変わる点にも注意が必要です。トップページに最新記事を出したいのか、投稿ページ(ブログ)を指しているのかを、設定画面で確認しておきましょう。

get_posts・WP_Query・pre_get_postsの使い分け

クエリを扱う方法は複数あり、混乱しやすい部分です。目的別に整理します。

やりたいこと 使うもの
そのページのメインの一覧を変更する pre_get_posts
ページ内に「関連記事」「新着」を別枠で出す WP_Query(ループ付き)
記事の配列を取得するだけ(ループ不要) get_posts()
メインクエリを書き換える query_postsは使わない

判断の基準は、「そのページが本来表示すべき一覧を変えたいのか、それとも別の一覧を追加で表示したいのか」です。前者はpre_get_posts、後者はWP_Queryです。

なお、get_posts()は内部でWP_Queryを使っていますが、デフォルトでは投稿の固定表示(sticky)を無視するなど、細かい挙動の違いがあります。また、suppress_filters がtrueになっているため、多くのフィルターの影響を受けません。ただし、pre_get_posts はこの設定にかかわらず実行されるため、is_main_query() の判定を入れずに書き換えると、get_posts()の結果にも影響します。

ハマりやすいポイント

1. 固定ページでは意味がない

固定ページのクエリは、特定のページを1件取得するものです。件数や並び順を変えても意味がありません。固定ページ内に一覧を出す場合は、WP_Queryを使います。

2. 条件分岐を絞らないと全体に影響する

pre_get_postsは、サイトの全ページで実行されます。条件分岐を書かずに set( 'posts_per_page', 3 ) と書くと、トップページも検索結果もカテゴリーも、すべて3件になります。必ず対象のページを絞りましょう。

3. 優先度とプラグインの影響

プラグインも同じフックで書き換えを行っていることがあります。思った通りにならない場合は、フックの優先度(第3引数)を調整するか、他のプラグインを一時的に停止して確認します。

4. ページネーションの件数が合わない

posts_per_pageを変更しても、WordPress標準の「ブログページに表示する最大投稿数」の設定とは別に動きます。ページ送りのリンクが想定と違うときは、設定値と、pre_get_postsの値の両方を確認してください。

まとめ

メインクエリの変更は、pre_get_postsで行うのが現在の正しい方法です。

  • query_postsは、二重クエリやページネーション崩れの原因になるため、使わない
  • pre_get_postsは、is_admin() と is_main_query() の判定を必ず入れる
  • 対象ページを $query->is_xxx() で絞ってから、set() で条件を変更する
  • 別枠の一覧はWP_Query、配列だけ欲しいときはget_posts()を使う

古い記事を参考にすると、query_postsが登場することがあります。そのまま真似せず、pre_get_postsに置き換えて実装するようにしてください。

コメント

タイトルとURLをコピーしました