2026年5月21日

2026年5月21日

WordPressのTransient APIエラーを解決する方法

はじめに

WordPressのget_transient()が期待どおりfalse以外の値を返さない・set_transient()で保存したはずのデータが次のリクエストで消えている・Transientに保存したオブジェクトがシリアライズエラーで取り出せない・Redisなどのオブジェクトキャッシュを導入したらTransientの挙動が変わったといった問題は、Transient APIの正しい使い方とキャッシュ階層の仕組みを理解することで解決できます。

症状・原因

  • get_transient()の戻り値を== falseで比較しており、0や空文字など正当なfalsy値がキャッシュミスと判定されている
  • Transientの有効期限に0を渡しており、永続化されずにすぐ削除されている
  • 外部オブジェクトキャッシュ(Redis/Memcached)がTransientを管理しており、wp_optionsテーブルに保存されていない
  • データベースのwp_optionsテーブルのautoloadがyesになったTransientが多すぎてメモリを圧迫している

解決手順

ステップ1:Transientの状態を診断する

# Transientの一覧を確認
wp eval "
global \$wpdb;
\$transients = \$wpdb->get_results(
    \"SELECT option_name, LENGTH(option_value) as size
     FROM {\$wpdb->options}
     WHERE option_name LIKE '_transient_%'
     AND option_name NOT LIKE '_transient_timeout_%'
     ORDER BY size DESC
     LIMIT 20\"
);
foreach (\$transients as \$t) {
    echo \$t->option_name . ': ' . number_format(\$t->size) . ' bytes' . PHP_EOL;
}
"

# 特定のTransientを確認
wp eval "var_dump(get_transient('my_cache_key'));"

# Transientの有効期限を確認
wp eval "
\$timeout = get_option('_transient_timeout_my_cache_key');
echo \$timeout ? date('Y-m-d H:i:s', \$timeout) : 'No timeout set';
"

# オブジェクトキャッシュが有効か確認
wp eval "echo wp_using_ext_object_cache() ? 'External Cache: YES' : 'External Cache: NO';"

# 期限切れTransientを一括削除
wp transient delete --expired

ステップ2:Transientを正しく使用する

// ✅ Transientの正しい使い方
function get_cached_posts(string $cache_key, int $expiry = HOUR_IN_SECONDS): array {
    // ✅ === false で厳密比較(0や空配列は有効なキャッシュ値)
    $cached = get_transient($cache_key);
    if ($cached !== false) {
        return $cached;
    }

    // キャッシュミス:データを取得
    $posts = get_posts([
        'post_type'      => 'post',
        'post_status'    => 'publish',
        'posts_per_page' => 10,
        'no_found_rows'  => true,
    ]);

    // ✅ 有効期限は必ず正の整数を渡す(0は「有効期限なし」ではなく即削除)
    set_transient($cache_key, $posts, $expiry);

    return $posts;
}

// 投稿更新時にTransientを削除
add_action('save_post', function(int $post_id): void {
    delete_transient('my_posts_cache');
    delete_transient('my_homepage_cache');
    // 関連するすべてのTransientを削除
    delete_transient('sidebar_widgets_' . get_current_blog_id());
});

// カテゴリー変更時にTransientを削除
add_action('edited_term', function(int $term_id, int $tt_id, string $taxonomy): void {
    delete_transient('category_' . $term_id . '_posts');
}, 10, 3);

// 安全なTransientキーを生成(長いキーはハッシュ化)
function make_transient_key(string ...$parts): string {
    $key = implode('_', $parts);
    // Transientキーは最大172文字(プレフィックス込み)
    if (strlen($key) > 160) {
        $key = md5($key);
    }
    return $key;
}

ステップ3:複雑なデータをTransientに保存する

// オブジェクトをTransientに保存(シリアライズ)
function cache_wp_query_results(string $key, WP_Query $query, int $expiry = HOUR_IN_SECONDS): void {
    // WP_Queryオブジェクト全体はシリアライズできない場合がある
    // 投稿IDの配列を保存するのが安全
    $post_ids = wp_list_pluck($query->posts, 'ID');
    set_transient($key, $post_ids, $expiry);
}

function get_cached_query(string $key, array $query_args): WP_Query {
    $post_ids = get_transient($key);

    if ($post_ids !== false && is_array($post_ids)) {
        // IDから再クエリ(高速)
        return new WP_Query([
            'post__in'       => $post_ids,
            'orderby'        => 'post__in',
            'posts_per_page' => count($post_ids),
            'no_found_rows'  => true,
        ]);
    }

    // キャッシュミス
    $query = new WP_Query($query_args);
    cache_wp_query_results($key, $query);
    return $query;
}

// 外部APIのレスポンスをキャッシュ
function get_external_api_data(string $url): array|false {
    $cache_key = 'api_' . md5($url);
    $cached    = get_transient($cache_key);

    if ($cached !== false) {
        return $cached;
    }

    $response = wp_remote_get($url, [
        'timeout'     => 10,
        'redirection' => 3,
    ]);

    if (is_wp_error($response) || wp_remote_retrieve_response_code($response) !== 200) {
        // エラー時は短いTTLでキャッシュ(リトライを防ぐ)
        set_transient($cache_key, [], 5 * MINUTE_IN_SECONDS);
        return false;
    }

    $data = json_decode(wp_remote_retrieve_body($response), true);
    set_transient($cache_key, $data, HOUR_IN_SECONDS);
    return $data;
}

ステップ4:サイトTransientとネットワークTransient

// サイトTransient(wp_sitemeta に保存、マルチサイトのネットワーク全体で共有)
set_site_transient('network_settings_cache', $settings, DAY_IN_SECONDS);
$cached = get_site_transient('network_settings_cache');
delete_site_transient('network_settings_cache');

// 通常Transientとの違い
// get_transient()   → wp_options(サイト固有)
// get_site_transient() → wp_sitemeta(ネットワーク共有)またはwp_options(シングルサイト)

// オブジェクトキャッシュグループを使った短命なキャッシュ
// (Transientよりも高速、リクエスト内のみ有効)
wp_cache_set('my_key', $data, 'my_group', 60);
$cached = wp_cache_get('my_key', 'my_group');

// Transientよりも細かい制御が必要な場合はwp_cacheを使う
function get_post_related_cache(int $post_id): array {
    $group = 'post_related';
    $key   = "post_{$post_id}";

    $cached = wp_cache_get($key, $group);
    if ($cached !== false) {
        return $cached;
    }

    $related = compute_related_posts($post_id);
    wp_cache_set($key, $related, $group, 10 * MINUTE_IN_SECONDS);
    return $related;
}

ステップ5:Transientの一括管理とクリーンアップ

// プレフィックスで関連Transientを一括削除(wp_optionsベース時)
function delete_transients_by_prefix(string $prefix): int {
    global $wpdb;

    if (wp_using_ext_object_cache()) {
        // 外部キャッシュでは個別に削除するしかない
        return 0;
    }

    $deleted = $wpdb->query(
        $wpdb->prepare(
            "DELETE FROM {$wpdb->options}
             WHERE option_name LIKE %s
             OR option_name LIKE %s",
            $wpdb->esc_like('_transient_' . $prefix) . '%',
            $wpdb->esc_like('_transient_timeout_' . $prefix) . '%'
        )
    );

    return (int) ($deleted / 2); // 本体とtimeoutで2行1セット
}

// Transientのサイズ監視(大きすぎるTransientを検出)
function find_large_transients(int $threshold_bytes = 100000): array {
    global $wpdb;
    return $wpdb->get_results(
        $wpdb->prepare(
            "SELECT option_name, LENGTH(option_value) as size
             FROM {$wpdb->options}
             WHERE option_name LIKE '_transient_%'
             AND option_name NOT LIKE '_transient_timeout_%'
             AND LENGTH(option_value) > %d
             ORDER BY size DESC",
            $threshold_bytes
        )
    );
}

注意事項

  • get_transient()の戻り値は必ず=== false(厳密等価)で比較してください。== falseを使うと0''[]nullなどの正当な値がキャッシュミスと誤判定されます
  • set_transient()の第3引数(有効期限)に0を渡すと「有効期限なし」ではなく動作が不定になります。有効期限なしで保存したい場合はupdate_option()を使ってください

まとめ

WordPressTransient APIエラーの解決は①wp_optionsテーブルで_transient_*を確認・get_transient()で取得テスト・wp_using_ext_object_cache()で外部キャッシュ確認・wp transient delete --expiredで期限切れ削除、②=== falseで厳密比較・有効期限はHOUR_IN_SECONDS等の定数を使用・save_postフックで関連Transientを削除・長いキーはmd5()でハッシュ化、③WP_QueryはIDの配列で保存して再クエリ・外部APIエラー時は短いTTLでキャッシュ・json_decodeで配列に変換して保存、④set_site_transient()でマルチサイトのネットワーク共有・wp_cache_set/get()でリクエスト内のみ高速キャッシュ・グループを使ってカテゴリーごとに一括削除、⑤プレフィックスで一括削除・100KB超のTransientを検出して最適化・外部キャッシュ環境では個別削除が必要の手順で解決します。

お気軽にご相談ください

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