なぜ固定ページでquery_postsを使うのはよくないのか?

ループとページング
WordPressを使用していてループを実現する時にquery_postsを使うことは多いかと思われます。
しかし、最近ではquery_postsを使うのはあまり良くないと言われている。

よくあるサイト構成の例

例えば、よくある構成として以下のようなサイトがあるとする。
よくあるサイト構成

上画像ようなケースの場合、トップページ直下にある「商品紹介」「会社概要」「採用情報」「お問い合わせ」「ブログ一覧」の5つのページをすべて固定ページとして作っておく。
そして「ブログ一覧」ページの中でquery_postsを使ってループとページング(正式にはページネーションと言うらしい)をすることが考えられる。
その時のコードはおそらく以下のような感じになるのではないでしょうか?

<?php query_posts(array('posts_per_page' => 表示する数, 'paged' => get_query_var('paged'))); ?>
<?php if(have_posts()): while(have_posts()): the_post(); ?>

  ~ループさせるコードをここに書く~

<?php endwhile; endif; ?>
<?php wp_reset_query(); ?>

実際、私もちょっと前まで上のようにやっていた。
なぜなら、私が今まで読んだWordPressの技術書、例えば以下の本など、

  1. WordPressデザインブック3.x対応(2011年9月発売)
  2. 本格ビジネスサイトを作りながら学ぶ WordPressの教科書(2012年3月発売)
  3. 本格ビジネスサイトを作りながら学ぶ WordPressの教科書2(2013年8月発売)

これらの本では上記のように固定ページでquery_postsを使用していた。
でも、2015年現在ではこのやり方はあまり良くないらしい。
というよりも、そもそもquery_postsを使うこと自体が良くないと言われている。

そんなこんなで、フォーラムにもたびたび投稿されるquery_posts由来のトラブル。まさにquery_posts狂想曲。引用元:query_postsを捨てよ、pre_get_postsを使おう【追記あり】【報告あり】

お祭り好きなこの私も当然ながらこの騒ぎに参加し、以下のようなことを言いながらどんちゃん騒ぎをしたものだ。

  1. ページングが正常に機能しない。
  2. query_postsの引数にarrayの配列形式だとどう書けばいいの?
  3. 一つのページ内にループを2つ作ると、2つめがおかしくなる。
  4. そもそもメインループとかメインクエリって何だよ?

そんなことを言いながら多くのサイトを見て回って調べた結果、上記の問題点はすべて解決でき、query_postsを使ってのループ表示やページ送りは固定ページやアーカイブページでも正常に行うことができていた。

しかし!

正常に表示や機能をしていても、query_postsの問題点はまだあった。

query_postsがよくない理由:データベースの重複読み込み

WordPressの仕組みでは、読み込まれるURLを元にそれが固定ページなのか投稿ページなのかそれともアーカイブページなのかなどを判別し、その判別を元にデータベースから該当するデータを読み出すことになる。
そしてその後に該当するテンプレートファイルを選択して画面に表示させる。
参考ページ:クエリ概要 – WordPress Codex 日本語版

つまり、query_postsが記述されているテンプレートファイルが選択される時点で、すでにデータベースのデータは読み込まれてしまっていることになる。
であるから、テンプレートファイル内でquery_postsによって再びデータベースを読み込み直すのはパフォーマンス面で無駄である。

get_postsでquery_postsと同じことは実現できるか?

そこで私が試したのがget_postsでquery_postsと同じループ表示をできるかということである。
これが実現できれば、今後はもうquery_postsを使わなくて済む。

そして色々調べて試してみた結果、

get_postsによってループ表示はできる。
しかし、ページング(ページネーション)はできない。

という結論に至った。(私が調べた範囲では)
つまりget_postsはquery_postsの完全な代替にはならないが、ページングを行わないループ表示に使うと良い。
(例:このブログのサイドバーにある「最近書いた記事」のように)

pre_get_postsでページング(ページネーション)を行えるか?

次に私が試したのがpre_get_postsによってループ表示とページングを実現できるか、ということである。
上記引用元のページや他いくつかのサイトを参考に試した結果、

pre_get_postsによってループ表示とページング(ページネーション)は実現可能。

であった。
その際のコードは以下のような感じである

function change_posts_per_page($query){
  // 管理画面またはメインクエリでない場合は何もせず終了
  if(is_admin() || ! $query->is_main_query()){
    return;
  }
  
  //home.phpまたはアーカイブページまたはsearch.phpの場合は10件表示する
  if($query->is_home() || $query->is_archive() || $query->is_search()){
    $query->set('posts_per_page', '10');
    return;
  }
}
add_action('pre_get_posts', 'change_posts_per_page');

function.phpおよびpre_get_postsフックはクエリーが実行される前に呼び出されるので、query_postsのようにデータベースの重複読み込みは発生しない。
参考ページ:プラグイン API/アクションフック一覧/pre get posts – WordPress Codex 日本語版

実際にpre_get_postsを使うようになると、各ページの表示数などをfunction.phpで一元管理できるのでけっこう便利である。

現時点での最適解

hokkaido
私が思う現時点での最適解は、

ページングを行うループ表示に関してはpre_get_posts。
ページングを行わないループ表示に関してはget_posts。

となった。
もちろんquery_postsはまったく使う必要はない。

注意点:pre_get_postsは固定ページでは機能しない

注意点としては、pre_get_postsは固定ページでは機能しないことである。
つまり固定ページ内でループ表示とページングを行うことはできない。
ではどうするか?

例えば、カテゴリごとのアーカイブならcategory.phpを使えば一発ですね。
月別アーカイブならdate.phpで一発です。
それらを分けずにまとめてやりたいならarchive.phpだけでも良いですね。
参考ページ:テンプレート階層 – WordPress Codex 日本語版

しかしここで問題になるのは、すべての投稿ページのアーカイブである。

冒頭の図にある「ブログ一覧」のページのような全投稿ページのアーカイブを作りたい場合は、WordPress管理画面の「設定」→「表示設定」の中にある「フロントページの表示」を使います。
これに関しては後日また解説します。(書きました→query_postsを使わずに固定ページにすべての投稿一覧をページング表示させる方法)

またはカスタム投稿タイプを使えば、archive-カスタム投稿タイプ名.phpを使うことができるので簡単です。
このテンプレートファイルならば、この投稿タイプに属するすべての記事のアーカイブになります。
そして、この場合は前述のpre_get_posts内のif分岐の部分を、

if(is_post_type_archive()){
  $query->set('posts_per_page', '5');
  return;
}

のようにis_post_type_archive()を使えば投稿タイプアーカイブページであるかどうかを判定できます。

追記:このページの内容はすべて自分で調べたものなので、もし間違っている点などありましたら是非教えてください。