За пределами wp_posts: когда и как создавать собственные таблицы в базе данных WordPress

Вы разрабатываете плагин для бронирования, сложного каталога или системы аналитики? Вам нужно хранить данные, которые плохо ложатся на структуру wp_posts и wp_postmeta: временные ряды, отношения «многие-ко-многим», массивы с быстрым поиском? Стандартные Custom Post Types и произвольные поля — не всегда решение. Иногда нужно идти глубже и создавать собственные таблицы в базе данных WordPress. Это звучит страшно, но именно так создаются профессиональные, высокопроизводительные плагины. Разберём, когда это действительно нужно, как сделать это правильно и безопасно, и как работать с кастомными таблицами, не сломав WordPress.


Часть 1: Когда CPT и Postmeta недостаточно? 4 сценария для кастомных таблиц

  1. Высокая производительность и сложные запросы:

    • Пример: Таблица статистики с миллионами записей. Вам нужны быстрые GROUP BYJOIN, агрегатные функции по датам.

    • Проблема с postmeta: Метаполя хранятся в длинной таблице wp_postmeta в формате ключ-значение, запросы с сортировкой или фильтрацией по ним — очень медленные.

  2. Строгая структура данных (валидация, типы):

    • Пример: Таблица заказов сервиса, где каждое поле имеет конкретный тип (INTDECIMAL(10,2)DATETIME), ограничения (NOT NULL) и индексы.

    • Проблема с postmeta: Все значения хранятся как LONGTEXT, нет типизации, легко сохранить строку вместо числа.

  3. Отношения «многие-ко-многим»:

    • Пример: Система тестирования, где один вопрос (вопрос_id) может быть в нескольких тестах (тест_id), а один тест содержит много вопросов.

    • Решение: Отдельная таблица-связка с двумя колонками: question_id и test_id.

  4. Временные или служебные данные:

    • Пример: Очередь задач (queue), кеш сложных вычислений, сырые логи перед обработкой.

    • Эти данные не являются «контентом» в понимании WordPress, и хранить их среди постов — архитектурная ошибка.

Правило: Если ваши данные — это объекты сайта (товары, статьи, отзывы) — используйте CPT. Если это транзакции, логи, связи или служебные структуры — скорее всего, нужна своя таблица.

Часть 2: Создание таблиц правильно: хуки, dbDelta и безопасность

Никогда не создавайте таблицы прямым SQL-запросом! Используйте встроенный класс dbDelta().

Шаг 1: Хук активации плагина
Создавать таблицы нужно при активации плагина. Но помните: хук register_activation_hook срабатывает только при первой активации. Если пользователь деактивирует и снова активирует плагин — код не выполнится. Поэтому используем проверку на существование таблицы.

php
register_activation_hook( __FILE__, 'myplugin_create_custom_table' ); function myplugin_create_custom_table() { global $wpdb; $table_name = $wpdb->prefix . 'myplugin_orders'; // Всегда используйте префикс! $charset_collate = $wpdb->get_charset_collate(); $sql = "CREATE TABLE $table_name (
        id bigint(20) NOT NULL AUTO_INCREMENT,
        user_id bigint(20) NOT NULL,
        service_name varchar(200) NOT NULL,
        amount decimal(10,2) DEFAULT 0.00,
        status varchar(20) DEFAULT 'pending',
        created_at datetime DEFAULT CURRENT_TIMESTAMP,
        PRIMARY KEY (id),
        KEY user_id (user_id),
        KEY status (status),
        KEY created_at (created_at)
    ) $charset_collate;"; require_once( ABSPATH . 'wp-admin/includes/upgrade.php' ); dbDelta( $sql ); // Проверка на ошибки (для логов) if ( ! empty( $wpdb->last_error ) ) { // Запишите ошибку в логи или отладочный файл error_log( 'Ошибка создания таблицы: ' . $wpdb->last_error ); } }

Важные особенности dbDelta():

  • Он сравнивает существующую структуру с вашим SQL и вносит только нужные изменения (без потери данных!).

  • Требует очень строгого SQL-синтаксиса: каждый столбец на новой строке, запятые правильные, ключевые слова в верхнем регистре.

  • Не удаляет столбцы, которых нет в новом запросе. Для удаления нужен отдельный ALTER.

Шаг 2: Проверка и обновление структуры при обновлении плагина
При обновлении плагина тоже может потребоваться изменить таблицу. Используйте хук plugins_loaded и проверку версии.

php
add_action( 'plugins_loaded', 'myplugin_check_db_version' ); function myplugin_check_db_version() { $current_version = get_option( 'myplugin_db_version', '1.0' ); if ( version_compare( $current_version, '1.1', '<' ) ) { myplugin_update_table_to_v1_1(); update_option( 'myplugin_db_version', '1.1' ); } }

Часть 3: CRUD операции: как работать с таблицей безопасно

Никаких прямых SQL-запросов через $wpdb->query() без подготовки!

  1. Вставка (Create):

    php
    global $wpdb; $wpdb->insert( $wpdb->prefix . 'myplugin_orders', array( 'user_id' => get_current_user_id(), 'service_name' => 'Премиум-подписка', 'amount' => 2999.00, 'status' => 'completed', ), array( '%d', '%s', '%f', '%s' ) // Форматы: decimal - %f, integer - %d, string - %s ); $order_id = $wpdb->insert_id;
  2. Чтение (Read):

    php
    $results = $wpdb->get_results( $wpdb->prepare( "SELECT * FROM {$wpdb->prefix}myplugin_orders WHERE user_id = %d AND status = %s ORDER BY created_at DESC", get_current_user_id(), 'pending' ) );
  3. Обновление (Update):

    php
    $wpdb->update( $wpdb->prefix . 'myplugin_orders', array( 'status' => 'cancelled' ), array( 'id' => $order_id ), array( '%s' ), array( '%d' ) );
  4. Удаление (Delete):

    php
    $wpdb->delete( $wpdb->prefix . 'myplugin_orders', array( 'id' => $order_id ), array( '%d' ) );

Преимущества: Автоматическое экранирование, защита от SQL-инъекций, правильная работа с типами данных.

Часть 4: Интеграция с WordPress: хуки, страницы админки, CLI

  1. Добавление страницы в админку: Используйте add_menu_page() и вывод таблицы с классами WordPress (WP_List_Table) для красивого и функционального интерфейса управления данными.

  2. Создание команд WP-CLI: Пользователи смогут импортировать/экспортировать данные или чистить таблицы через консоль.

    php
    if ( defined( 'WP_CLI' ) && WP_CLI ) { WP_CLI::add_command( 'myplugin cleanup', 'myplugin_cli_cleanup' ); } function myplugin_cli_cleanup( $args ) { // Логика очистки WP_CLI::success( 'Таблица очищена!' ); }
  3. Вешайте хуки на свои действия: Это позволит другим разработчикам расширять функционал вашего плагина.

    php
    do_action( 'myplugin_order_completed', $order_id, $order_data );

Часть 5: Миграции и откат изменений

Для профессиональных плагинов нужна система миграций:

  1. Храните историю SQL-изменений в отдельных файлах.

  2. При обновлении плагина последовательно применяйте миграции.

  3. Всегда предусматривайте возможность отката. Пишите обратные миграции (down-миграции).

  4. Используйте готовые библиотеки (например, Phinx), адаптированные для WordPress.

Часть 6: Главные «подводные камни»

  1. Резервное копирование: Пользователи делают бэкапы через стандартные плагины, которые не знают о ваших таблицах. Предусмотрите в своём плагине функцию экспорта или уведомите пользователей о необходимости полного бэкапа БД.

  2. Мультисайт (Multisite): В режиме сети каждая подсайт имеет свои таблицы? Или одна общая таблица с полем blog_id? Решите это на этапе проектирования.

  3. Производительность: Создавайте индексы на поля, по которым часто идёт поиск и сортировка. Но не переусердствуйте — каждый индекс замедляет вставку.

  4. Удаление данных при удалении плагина: Дайте пользователю выбор — удалять таблицы или оставить данные. Используйте хук uninstall.php.


Заключение

Создание собственных таблиц — это переход на новый уровень разработки под WordPress. Это требует большей ответственности, но даёт неограниченную гибкость и производительность.

Прежде чем создавать таблицу, спросите себя:

  1. Эти данные действительно не вписываются в wp_posts/wp_postmeta?

  2. Готов ли я поддерживать миграции схемы?

  3. Понятно ли пользователям, как делать бэкап этих данных?

Если ответы «да» — смело создавайте. Используйте dbDelta(), работайте через методы $wpdb, не забывайте об индексах. И ваш плагин сможет обрабатывать миллионы записей так же легко, как WordPress обрабатывает посты.