2026年5月29日

2026年5月29日

WordPressのwp_cache_set()/wp_cache_get()でカスタムキャッシュを実装する方法:キー設計と無効化まで

wp_cache_set() と wp_cache_get() を使ったカスタムキャッシュは、「重い処理の結果をグループとキーで保存し、データが変わったら古いキーを使わなくする」ところまで作って初めて役に立ちます。この記事では、関数の正しい使い方、キーとグループの設計、$found 引数による判定、wp_cache_get_last_changed() を使ったバージョン付きキーでの無効化、永続オブジェクトキャッシュがない環境での考え方を、独自テーブルの集計を例に説明します。

すでに古いデータが表示されていて WP_Query のキャッシュを消したい場合は、別記事の「WP_Query のキャッシュを手動でクリアする方法」が対象です。この記事は、自分でキャッシュ層を設計・実装する側の解説です。

オブジェクトキャッシュAPIの基本

WordPress のオブジェクトキャッシュは、キー・値・グループの組でデータを保存する仕組みです。主な関数とシグネチャは次のとおりです。

wp_cache_set( $key, $data, $group = '', $expire = 0 );                 // 保存(上書き)
wp_cache_get( $key, $group = '', $force = false, &$found = null );     // 取得。見つからなければ false
wp_cache_add( $key, $data, $group = '', $expire = 0 );                 // 未登録のときだけ保存
wp_cache_delete( $key, $group = '' );                                  // 削除
wp_cache_get_multiple( $keys, $group = '', $force = false );           // 複数キーをまとめて取得(5.5 以降)
wp_cache_set_multiple( $data, $group = '', $expire = 0 );              // 複数キーをまとめて保存(6.0 以降)

ここで一番大事なのは、保存先が環境によって変わることです。

  • 永続オブジェクトキャッシュがない環境(標準の状態):キャッシュはそのリクエストの間だけメモリに残り、ページの表示が終われば消えます。$expire は意味を持ちません。同じリクエスト内で同じ処理を何度も呼ぶ場合には効きますが、次のアクセスでは毎回計算し直します。
  • 永続オブジェクトキャッシュがある環境(Redis や Memcached と object-cache.php ドロップイン):リクエストをまたいで保存され、$expire 秒後に期限切れになります。ただしメモリが足りなくなると、期限前でも追い出されることがあります。

つまり「キャッシュに入れたはずのデータが無い」ことは正常な状態です。実装は、キャッシュが空でも元データから作り直せる形にしておきます。現在の環境は wp_using_ext_object_cache() が true を返すかどうか、または WP-CLI の wp cache type で確認できます。

例:独自テーブルのレビュー集計をキャッシュする

ここでは、プラグインが {$wpdb->prefix}myplugin_reviews テーブル(post_id、rating、status など)にレビューを保存していて、投稿ごとの「件数と平均評価」を表示する場面を例にします。集計クエリはレビューが増えるほど重くなるので、キャッシュする価値があります。コードは独自プラグインか mu-plugin に置き、ステージングで動作を確かめてから本番に入れてください。

<?php
const MYPLUGIN_CACHE_GROUP = 'myplugin_reviews';

function myplugin_get_rating_summary( $post_id ) {
	global $wpdb;

	$post_id = absint( $post_id );
	$key     = 'summary_' . $post_id;
	$found   = false;

	$summary = wp_cache_get( $key, MYPLUGIN_CACHE_GROUP, false, $found );
	if ( $found ) {
		return $summary;
	}

	$row = $wpdb->get_row(
		$wpdb->prepare(
			"SELECT COUNT(*) AS cnt, AVG(rating) AS avg_rating FROM %i WHERE post_id = %d AND status = %s",
			$wpdb->prefix . 'myplugin_reviews',
			$post_id,
			'approved'
		),
		ARRAY_A
	);

	$count   = isset( $row['cnt'] ) ? (int) $row['cnt'] : 0;
	$summary = array(
		'count'   => $count,
		'average' => $count > 0 ? round( (float) $row['avg_rating'], 1 ) : 0.0,
	);

	wp_cache_set( $key, $summary, MYPLUGIN_CACHE_GROUP, DAY_IN_SECONDS );

	return $summary;
}

テーブル名に使っている %i プレースホルダーは WordPress 6.2 以降で使えます。表示側では esc_html( $summary['count'] ) のように出力時にエスケープします。

$found 引数でヒットを判定する理由

wp_cache_get() は、キーが見つからないときに false を返します。そのため、保存した値そのものが false や空の場合、戻り値だけでは「キャッシュに無い」のか「無いという結果がキャッシュされている」のか区別できません。第4引数の $found を渡すと、見つかったかどうかが参照渡しで返るので、これで判定します。

「レビュー0件」のような結果もキャッシュしておかないと、レビューのない投稿を表示するたびに集計クエリが走ります。結果を配列にしておく、$found で判定する、の2点で防げます。

キーとグループの決め方

グループは、プラグインやテーマの名前を付けて他と衝突しないようにします。キーには、結果を左右する引数をすべて含めます。投稿 ID だけでなく、件数、並び順、言語など、値によって結果が変わる要素は全部キーに入れます。引数が多い場合は、配列を md5( wp_json_encode( $args ) ) のようにハッシュしてキーにする方法もあります。

キャッシュしてはいけないものもあります。ログインユーザーごとに異なる内容、WordPress の nonce を含む HTML、権限によって表示が変わる情報を、ユーザーを区別しないキーで保存すると、別の人に他人の情報を表示してしまいます。こうしたデータはキーにユーザー ID を含めるか、そもそもキャッシュしない判断をします。

データが変わったら無効化する

キャッシュを作るのと同じくらい大事なのが、元データが変わったときに古い値を使わなくすることです。方法は2通りあります。

単一のキーは直接削除する

投稿ごとの集計のように、変わったデータとキーの対応がはっきりしている場合は、そのキーを削除します。レビューの保存や承認を行う関数の中で呼びます。

<?php
function myplugin_after_review_changed( $post_id ) {
	wp_cache_delete( 'summary_' . absint( $post_id ), MYPLUGIN_CACHE_GROUP );
	myplugin_bump_cache_version(); // 一覧系のキャッシュも無効にする(次の節)
}

// 例: レビューを承認する処理(呼び出し側で nonce と current_user_can() を確認済みとする)
function myplugin_approve_review( $review_id ) {
	global $wpdb;
	$table   = $wpdb->prefix . 'myplugin_reviews';
	$post_id = (int) $wpdb->get_var( $wpdb->prepare( "SELECT post_id FROM %i WHERE id = %d", $table, $review_id ) );

	$wpdb->update( $table, array( 'status' => 'approved' ), array( 'id' => absint( $review_id ) ), array( '%s' ), array( '%d' ) );

	if ( $post_id ) {
		myplugin_after_review_changed( $post_id );
	}
}

一覧系はバージョン付きキーで無効化する

「サイト全体の新着レビュー10件」のように、どのデータが変わると影響するか特定しにくいキャッシュは、キーにバージョン(最終変更時刻)を含める方法が向いています。WordPress コアも WP_Query の結果のキャッシュで同じ考え方を使っています。

<?php
function myplugin_bump_cache_version() {
	if ( function_exists( 'wp_cache_set_last_changed' ) ) {
		wp_cache_set_last_changed( MYPLUGIN_CACHE_GROUP ); // WordPress 6.3 以降
	} else {
		wp_cache_delete( 'last_changed', MYPLUGIN_CACHE_GROUP ); // 次回の取得時に新しい値が作られる
	}
}

function myplugin_get_latest_reviews( $limit = 10 ) {
	global $wpdb;

	$limit   = min( 50, max( 1, absint( $limit ) ) );
	$version = wp_cache_get_last_changed( MYPLUGIN_CACHE_GROUP );
	$key     = "latest_{$limit}_{$version}";
	$found   = false;

	$reviews = wp_cache_get( $key, MYPLUGIN_CACHE_GROUP, false, $found );
	if ( $found ) {
		return $reviews;
	}

	$reviews = $wpdb->get_results(
		$wpdb->prepare(
			"SELECT id, post_id, rating, created_at FROM %i WHERE status = %s ORDER BY created_at DESC LIMIT %d",
			$wpdb->prefix . 'myplugin_reviews',
			'approved',
			$limit
		),
		ARRAY_A
	);

	wp_cache_set( $key, $reviews, MYPLUGIN_CACHE_GROUP, HOUR_IN_SECONDS );

	return $reviews;
}

wp_cache_get_last_changed() はグループ内の last_changed という値を返し、まだなければ現在時刻から作って保存します。データが変わったときにこの値を更新すると、以降は新しいキーで問い合わせるため、古いキャッシュは参照されなくなります。古いキーは削除されずに残りますが、有効期限が来るかメモリの追い出しで消えるので、$expire を設定しておくのが前提です。

永続キャッシュがない環境ではトランジェントも検討する

標準の WordPress では wp_cache_set() の値はリクエストをまたがないため、上の集計は「1ページ内で何度も呼ぶ」場合にしか効きません。永続オブジェクトキャッシュを使えない環境でもリクエストをまたいで保存したいなら、set_transient() と get_transient() が選択肢になります。トランジェントは、永続オブジェクトキャッシュがあればそちらに、なければ wp_options テーブルに保存されるため、どちらの環境でも動きます。

使い分けの目安は次のとおりです。

  • 1リクエスト内の重複計算を省きたい、または永続オブジェクトキャッシュの導入を前提にできる:wp_cache_*
  • どの環境でもリクエストをまたいで保存したい、件数がそれほど多くない:トランジェント
  • キーの数が非常に多くなる(投稿数×条件の組み合わせなど):トランジェントだと wp_options が肥大化しやすいため、永続オブジェクトキャッシュの導入を先に検討する

運用で効いてくる細かな設定

マルチサイトでは、キャッシュはサイトごとに分かれます。ネットワーク全体で共有したいデータのグループは wp_cache_add_global_groups( array( 'myplugin_global' ) ) で登録します。逆に、永続化させたくない(リクエスト内だけでよい)グループは wp_cache_add_non_persistent_groups( array( 'myplugin_runtime' ) ) で登録します。どちらも init などの早い段階で呼びます。

アクセスの多いサイトで有効期限が切れた瞬間に、同時に多数のリクエストが重い集計を始める現象(キャッシュスタンピード)が気になる場合は、wp_cache_add() を簡易的なロックとして使う方法があります。wp_cache_add() はキーが存在しないときだけ保存して true を返すので、ロック用のキーの追加に成功したリクエストだけが再計算します。ただし、この操作が複数サーバー間で厳密に排他的になるかはキャッシュの実装次第なので、利用中のドロップインのドキュメントを確認してください。

保存する値の大きさにも注意が必要です。Memcached は初期設定で1件あたりのサイズ上限があり、大きな配列や HTML をそのまま入れると保存に失敗します。必要な列だけを保存し、投稿オブジェクトを丸ごと持つより ID の配列を保存して表示時に取得する、といった工夫が有効です。

動作の確認方法

実装したら、次の順で確認します。

  1. Query Monitor を入れ、対象ページを2回読み込む。永続オブジェクトキャッシュがある環境では、2回目に集計クエリが消えていればキャッシュが効いています。
  2. WP-CLI で値を直接確認する。wp cache get summary_123 myplugin_reviews で保存内容が表示されます(永続オブジェクトキャッシュがない環境では、CLI のプロセス内にしか値がないため表示されません)。
  3. レビューを承認・削除して、集計と新着一覧の表示がすぐに変わるかを確かめる。変わらなければ、無効化の呼び出し漏れがあります。

ここで扱った関数はすべてコアに含まれています(2026年10月現在)が、_multiple 系や wp_cache_set_last_changed() のように追加されたバージョンが比較的新しいものもあるため、古い WordPress を対象にする場合は function_exists() で確認してください。

まとめ

  • wp_cache_set() と wp_cache_get() は、永続オブジェクトキャッシュがなければリクエスト内だけ有効。キャッシュが空でも作り直せる実装にする。
  • ヒットの判定は戻り値ではなく $found 引数で行い、「0件」の結果もキャッシュする。
  • グループ名は衝突しない名前にし、キーには結果を左右する引数をすべて含める。ユーザーごとの情報を共有キーに入れない。
  • 単一のデータはキーを直接削除し、一覧系は wp_cache_get_last_changed() によるバージョン付きキーで無効化する。
  • 永続キャッシュを前提にできない環境では、トランジェントとの使い分けを検討する。

お気軽にご相談ください

お見積りへ お問い合わせへ
▲