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.phpでDROP TABLEと関連オプションを完全削除します。