2026年7月22日

2026年7月22日

WordPressのカスタムデータベーステーブルを作成・管理する方法

はじめに

カスタム投稿タイプで対応できない大量データや複雑なリレーション構造が必要な場合、独自のデータベーステーブルを作成します。dbDelta()関数でWordPressのアップグレードと共存し、$wpdbのプレースホルダーでSQLインジェクションを防ぎます。

症状・原因

  • カスタム投稿タイプではパフォーマンスが出ない大量データを扱いたい
  • 複数テーブルのJOINが必要なデータ構造がある
  • ログ・統計データなど投稿とは異なる構造のデータを保存したい
  • プラグイン用の独立したテーブルを作成したい

解決手順

ステップ1:dbDelta でテーブルを作成する

// functions.php またはプラグインファイル

function create_custom_tables(): void {
    global $wpdb;
    $charset_collate = $wpdb->get_charset_collate();

    // テーブル名にプレフィックスを使用
    $table_name = $wpdb->prefix . 'access_log';

    $sql = "CREATE TABLE $table_name (
        id         BIGINT(20) UNSIGNED NOT NULL AUTO_INCREMENT,
        user_id    BIGINT(20) UNSIGNED NOT NULL DEFAULT 0,
        post_id    BIGINT(20) UNSIGNED NOT NULL DEFAULT 0,
        ip_address VARCHAR(45)  NOT NULL DEFAULT '',
        user_agent VARCHAR(255) NOT NULL DEFAULT '',
        accessed_at DATETIME    NOT NULL DEFAULT CURRENT_TIMESTAMP,
        PRIMARY KEY (id),
        KEY user_id (user_id),
        KEY post_id (post_id),
        KEY accessed_at (accessed_at)
    ) $charset_collate;";

    // dbDelta: テーブルが存在しない場合は作成、
    // 存在する場合は差分のみ適用
    require_once ABSPATH . 'wp-admin/includes/upgrade.php';
    dbDelta($sql);

    // テーブルバージョンを保存(後のアップグレードで使用)
    update_option('my_plugin_db_version', '1.0');
}

// プラグイン有効化時に実行
register_activation_hook(__FILE__, 'create_custom_tables');
// または init フック(サイト起動時に毎回チェック)
add_action('init', function(): void {
    if (get_option('my_plugin_db_version') !== '1.0') {
        create_custom_tables();
    }
});

ステップ2:wpdb でデータを挿入・更新・削除する

// functions.php: $wpdb を使った安全なCRUD操作

function log_page_access(int $post_id): void {
    global $wpdb;
    $table = $wpdb->prefix . 'access_log';

    // INSERT: プレースホルダーでSQLインジェクション対策
    $wpdb->insert(
        $table,
        [
            'user_id'    => get_current_user_id(),
            'post_id'    => $post_id,
            'ip_address' => $_SERVER['REMOTE_ADDR'] ?? '',
            'user_agent' => substr($_SERVER['HTTP_USER_AGENT'] ?? '', 0, 255),
        ],
        ['%d', '%d', '%s', '%s'] // データ型のフォーマット
    );
}

function update_access_count(int $id, int $count): bool {
    global $wpdb;
    return (bool) $wpdb->update(
        $wpdb->prefix . 'access_log',
        ['access_count' => $count],  // 更新データ
        ['id' => $id],               // WHERE 条件
        ['%d'],                      // 更新データのフォーマット
        ['%d']                       // WHERE のフォーマット
    );
}

function delete_old_logs(int $days = 30): int {
    global $wpdb;
    return (int) $wpdb->query(
        $wpdb->prepare(
            "DELETE FROM {$wpdb->prefix}access_log
             WHERE accessed_at < DATE_SUB(NOW(), INTERVAL %d DAY)",
            $days
        )
    );
}

ステップ3:安全な SELECT クエリを実装する

// $wpdb->prepare() でプレースホルダーを使う(必須)

function get_post_access_logs(int $post_id, int $limit = 100): array {
    global $wpdb;

    // 単一レコード取得
    return $wpdb->get_results(
        $wpdb->prepare(
            "SELECT id, user_id, ip_address, accessed_at
             FROM {$wpdb->prefix}access_log
             WHERE post_id = %d
             ORDER BY accessed_at DESC
             LIMIT %d",
            $post_id,
            $limit
        ),
        ARRAY_A
    );
}

function get_access_count_by_post(): array {
    global $wpdb;

    // 集計クエリ
    return $wpdb->get_results(
        "SELECT post_id,
            COUNT(*) AS access_count,
            MAX(accessed_at) AS last_access
         FROM {$wpdb->prefix}access_log
         GROUP BY post_id
         ORDER BY access_count DESC
         LIMIT 50",
        ARRAY_A
    );
}

// 単一値を取得
function get_total_access_count(): int {
    global $wpdb;
    return (int) $wpdb->get_var(
        "SELECT COUNT(*) FROM {$wpdb->prefix}access_log"
    );
}

ステップ4:テーブルのバージョンアップグレードを管理する

// テーブル構造の変更をバージョン管理する

function upgrade_custom_tables(): void {
    global $wpdb;
    $current_version = get_option('my_plugin_db_version', '0');

    if (version_compare($current_version, '1.1', '<')) {
        // v1.1: access_count カラムを追加
        $table = $wpdb->prefix . 'access_log';
        $wpdb->query(
            "ALTER TABLE $table
             ADD COLUMN access_count INT UNSIGNED NOT NULL DEFAULT 1
             AFTER user_agent"
        );
        update_option('my_plugin_db_version', '1.1');
    }

    if (version_compare($current_version, '1.2', '<')) {
        // v1.2: referer カラムを追加
        $wpdb->query(
            "ALTER TABLE {$wpdb->prefix}access_log
             ADD COLUMN referer VARCHAR(500) NOT NULL DEFAULT ''
             AFTER user_agent"
        );
        update_option('my_plugin_db_version', '1.2');
    }
}
add_action('plugins_loaded', 'upgrade_custom_tables');

ステップ5:プラグイン無効化時にテーブルを削除する

// プラグイン削除時(uninstall.php)にテーブルを削除

// uninstall.php:
if (!defined('WP_UNINSTALL_PLUGIN')) exit;

global $wpdb;

// テーブルを削除
$wpdb->query("DROP TABLE IF EXISTS {$wpdb->prefix}access_log");

// 関連オプションを削除
delete_option('my_plugin_db_version');
delete_option('my_plugin_settings');

// マルチサイトの全サブサイトからも削除
if (is_multisite()) {
    $blog_ids = $wpdb->get_col("SELECT blog_id FROM {$wpdb->blogs}");
    foreach ($blog_ids as $blog_id) {
        switch_to_blog($blog_id);
        $wpdb->query("DROP TABLE IF EXISTS {$wpdb->prefix}access_log");
        delete_option('my_plugin_db_version');
        restore_current_blog();
    }
}

注意事項

  • dbDelta()はSQLの書式に厳格です。各カラム定義の後にカンマが必要で、PRIMARY KEYは別行に書く必要があります。書式が正しくないと既存テーブルが破壊される可能性があります
  • $wpdb->prepare()%d(整数)・%s(文字列)・%f(浮動小数点)のプレースホルダーをサポートします。ユーザー入力を直接SQLに埋め込むことは絶対に避けてください
  • テーブル名は必ず$wpdb->prefixを付けてください。マルチサイト環境では$wpdb->prefixがサブサイトごとに異なります

まとめ

カスタムテーブル管理は①dbDelta()CREATE TABLEを冪等に実行・update_option('db_version')でバージョン追跡、②$wpdb->insert()update()query($wpdb->prepare())でプレースホルダーを必須化しSQLインジェクションを防止、③get_results()get_var()で型を明示した安全なSELECT、④version_compare()でマイグレーションを管理しカラム追加を安全に適用、⑤uninstall.phpDROP TABLEと関連オプションを完全削除します。

お気軽にご相談ください

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