2026年5月21日

2026年5月21日

ACFリレーションシップフィールドが正しく動かない問題を解決する方法

はじめに

ACFのRelationship(リレーションシップ)フィールドやPost Objectフィールドで、選択した投稿が保存されない・フロントエンドで取得できない・管理画面の検索が遅すぎるといった問題は、フィールド設定の誤り・データ形式の不一致・クエリの最適化不足が原因です。

症状・原因

  • Relationshipフィールドで選択した投稿が保存後に消える
  • get_field('related_posts')が投稿オブジェクトの配列ではなくIDの配列を返す
  • 管理画面でRelationshipフィールドの検索が重くタイムアウトする
  • 双方向リレーションシップが片方しか更新されない

解決手順

ステップ1:リレーションシップフィールドの状態を確認する

# Relationshipフィールドの保存データを確認
wp eval "
\$post_id = 123;
// ACFのget_fieldで取得(return_format設定に依存)
\$related = get_field('related_posts', \$post_id);
echo 'Type: ' . gettype(\$related) . PHP_EOL;
if (is_array(\$related)) {
    echo 'Count: ' . count(\$related) . PHP_EOL;
    foreach (\$related as \$item) {
        if (is_object(\$item)) {
            echo '  Post: ' . \$item->ID . ' - ' . \$item->post_title . PHP_EOL;
        } else {
            echo '  ID: ' . \$item . PHP_EOL;
        }
    }
}

// postmetaに直接保存されたデータを確認
\$raw = get_post_meta(\$post_id, 'related_posts', true);
echo 'Raw meta: ' . print_r(\$raw, true) . PHP_EOL;
"

ステップ2:フィールド設定とreturn_formatを修正する

// functions.php: ACF Relationshipフィールドをコードで定義

add_action('acf/init', function(): void {
    acf_add_local_field_group([
        'key'    => 'group_related_content',
        'title'  => '関連コンテンツ',
        'fields' => [
            [
                'key'           => 'field_related_posts',
                'label'         => '関連記事',
                'name'          => 'related_posts',
                'type'          => 'relationship',
                'post_type'     => ['post', 'product'],  // 対象の投稿タイプ
                'taxonomy'      => [],
                'filters'       => ['search', 'post_type', 'taxonomy'],
                'min'           => 0,
                'max'           => 5,
                'return_format' => 'object',  // 'id' or 'object'
            ],
            [
                'key'           => 'field_main_category',
                'label'         => 'メインカテゴリ',
                'name'          => 'main_category',
                'type'          => 'post_object',
                'post_type'     => ['category'],
                'allow_null'    => true,
                'multiple'      => false,
                'return_format' => 'id',  // IDだけ保存してメモリを節約
                'ui'            => true,
            ],
        ],
        'location' => [[['param' => 'post_type', 'operator' => '==', 'value' => 'post']]],
        'active'   => true,
    ]);
});

ステップ3:リレーションシップデータを正しく取得・表示する

// functions.php: Relationshipフィールドの値を取得してループ

// ① return_format='object'の場合(推奨:追加クエリ不要)
function get_related_posts_html(int $post_id): string {
    $related = get_field('related_posts', $post_id);
    if (!$related || !is_array($related)) {
        return '';
    }

    $html = '<ul class="related-posts">';
    foreach ($related as $post) {
        if (!($post instanceof WP_Post)) continue;
        setup_postdata($post);
        $html .= sprintf(
            '<li><a href="%s">%s</a></li>',
            esc_url(get_permalink($post)),
            esc_html($post->post_title)
        );
    }
    wp_reset_postdata();
    $html .= '</ul>';
    return $html;
}

// ② return_format='id'の場合(IDから投稿を取得)
function get_related_posts_by_ids(int $post_id): array {
    $ids = get_field('related_posts', $post_id);
    if (!$ids || !is_array($ids)) {
        return [];
    }

    // 一括でまとめて取得(N+1クエリを防ぐ)
    return get_posts([
        'post__in'       => $ids,
        'orderby'        => 'post__in',  // IDの順序を維持
        'numberposts'    => -1,
        'post_status'    => 'publish',
        'no_found_rows'  => true,  // ページネーション不要ならtrue
    ]);
}

ステップ4:双方向リレーションシップを実装する

// functions.php: 双方向リレーションシップの自動同期

add_action('acf/save_post', function(int $post_id): void {
    // 無限ループ防止
    if (defined('DOING_ACF_RELATIONSHIP_SYNC') && DOING_ACF_RELATIONSHIP_SYNC) return;
    define('DOING_ACF_RELATIONSHIP_SYNC', true);

    $related_ids = get_field('related_posts', $post_id, false); // IDで取得
    if (!is_array($related_ids)) $related_ids = [];

    // 以前のリレーションシップを取得(削除されたものを処理するため)
    $previous_ids = get_post_meta($post_id, '_previous_related_posts', true) ?: [];

    // 新しく追加されたID
    $added = array_diff($related_ids, $previous_ids);
    // 削除されたID
    $removed = array_diff($previous_ids, $related_ids);

    // 追加されたポストに逆方向のリレーションを追加
    foreach ($added as $related_id) {
        $reverse = get_field('related_posts', (int)$related_id, false) ?: [];
        if (!in_array($post_id, $reverse, true)) {
            $reverse[] = $post_id;
            update_field('related_posts', $reverse, (int)$related_id);
        }
    }

    // 削除されたポストから逆方向のリレーションを削除
    foreach ($removed as $related_id) {
        $reverse = get_field('related_posts', (int)$related_id, false) ?: [];
        $reverse = array_values(array_diff($reverse, [$post_id]));
        update_field('related_posts', $reverse, (int)$related_id);
    }

    // 現在のIDリストを保存(次回の差分計算用)
    update_post_meta($post_id, '_previous_related_posts', $related_ids);
}, 20);

ステップ5:パフォーマンスを最適化する

// functions.php: Relationshipフィールドの検索クエリを最適化

// ① 管理画面の検索対象を絞り込む(重い場合)
add_filter('acf/fields/relationship/query/name=related_posts', function(array $args): array {
    $args['posts_per_page'] = 20;  // 一度に取得する件数を制限
    $args['post_status']    = 'publish';
    $args['orderby']        = 'modified';
    $args['order']          = 'DESC';
    // カスタムメタが必要な場合は除外(重いmeta_queryを避ける)
    unset($args['meta_query']);
    return $args;
});

// ② フロントエンドでのキャッシュ(Transient利用)
function get_cached_related_posts(int $post_id): array {
    $cache_key = 'related_posts_' . $post_id;
    $cached = get_transient($cache_key);

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

    $related = get_field('related_posts', $post_id) ?: [];
    set_transient($cache_key, $related, HOUR_IN_SECONDS * 6);
    return $related;
}

// 投稿更新時にキャッシュを削除
add_action('acf/save_post', function(int $post_id): void {
    delete_transient('related_posts_' . $post_id);
}, 30);

注意事項

  • get_field()の戻り値の型はフィールド設定の「Return Format」によって変わります。object設定ならWP_Postオブジェクトの配列、id設定なら整数IDの配列が返ります。テンプレートで使う際は必ず型を確認してください
  • Relationshipフィールドで保存できる最大件数はフィールド設定のmaxで制限できます。制限なしにすると大量データの保存でDBが肥大化する恐れがあります
  • 双方向リレーションシップの自動同期はacf/save_post内で再帰的に実行されるため、必ずDOING_ACF_RELATIONSHIP_SYNCのような定数で無限ループを防いでください

まとめ

ACF Relationshipフィールドの修復は①get_post_meta()でrawデータを確認してreturn_format設定と一致させ、②acf_add_local_field_group()でフィールドをPHP定義に移行、③return_format='object'で追加クエリなし取得またはget_posts(['post__in'=>$ids])でN+1クエリを防ぎ、④acf/save_postフックで双方向リレーションを自動同期(無限ループ防止必須)、⑤acf/fields/relationship/queryフィルターで管理画面検索を最適化してTransientでフロント表示をキャッシュする手順で解決します。

お気軽にご相談ください

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