Введение

В этой статье мы с вами научимся работать напрямую с базой данных WordPress используя $wpdb. Начнем с простого примера для быстрого старта; далее рассмотрим основные методы и свойства; рассмотрим неочевидные, но нужные решения; научимся работать со сторонней БД; а также научимся создавать и удалять таблицы в БД и добавлять их для удобной работы в $wpdb.

Данную статью следует воспринимать больше как гайд, нежели документацию по $wpdb, т.к. здесь описаны не все методы и свойства и упор больше сделан на практическое применение.

Цель статьи: помочь разобраться во всех нюансах и научиться работать с $wpdb.

Для лучшего понимания статьи, нужно знать основы ООП (объектно-ориентированного программирования).

Что такое $wpdb

$wpdb — это глобальный объект в WordPress, который позволяет взаимодействовать с Базой данных сайта. Если точнее — то это глобальный экземпляр класса wpdb.

С помощью $wpdb можно работать как с основной БД сайта, данные которой указаны в wp-config.php, так и с другими БД, доступными с хоста сайта.

Быстрый старт

Задача: получить 10 записей (posts) из БД любого типа, в названии которых есть слово «Котик». По-сути, сделаем простой поиск по названию.

В обычной ситуации мы воспользовались-бы функцией get_posts() или классом WP_Query(), но для примера выполним эту задачу используя $wpdb.

Решение: добавим в файл functions.php активной темы следующий код:

/**
 * @param string $need_word
 * @param string $post_status
 *
 * @return array
 */
function feodoraxis_find_posts( $need_word, $post_status = 'publish' ) {
	//Объявим глобальную переменную для доступа к ней	
	global $wpdb;
	
	//Опишем SQL-запрос
	$sql = "SELECT * FROM `" . $wpdb->posts . "` WHERE `post_title` LIKE %s AND `post_status` = %s";
	
	//Очистим входящие данные для безопасного запроса с помощью метода prepare
	$query = $wpdb->prepare( 
		$sql,
		'%' . $wpdb->esc_like( $need_word ) . '%', //Первый параметр %s. Методом esc_like(), подготовим строку для работы с оператором LIKE
		$post_status
	);
	
	//Получим результат. Для других запросов (не select), вместо get_results следует использовать query()
	$result = $wpdb->get_results( $query );
	
	//Если получили массив, даже пустой - значит запрос отработал корректно, возвращаем результат
	if ( is_array( $result ) ) {
		return $result;
	}
	
	//В случае любой ошибки возвращаем пустой массив
	return [];
}

Теперь с помощью этой функции мы можем найти нужную запись по части её названия.

Предостережения

Потенциальные проблемы

При разработке лучше использовать функции и методы специально предназначенные для выборки, записи, обновления и удаления записей (далее CRUD — Create, Read, Update, Delete). Во встроенных методах и функциях реализованы оптимизированные запросы к БД, кеширование, а также хуки для встраивания функционала плагинов и тем. Например, если вы захотите использовать плагин «Redis объектный кеш», то данные, получаемые через функцию get_option() или get_posts() будут закешированы. А при использовании $wpdb этот момент придется реализовывать самостоятельно.

Также при следующем обновлении WordPress, структура БД может измениться. Разработчики ответственно подходят к продукту, поэтому встроенные функции сразу будут адаптированы к новой структуре БД и ваш функционал продолжит работать как и раньше. При использовании $wpdb, в случае такого обновления, ваш функционал может сломаться. Согласитесь, ситуация плохая, особенно для коммерческих проектов.

Когда можно использовать

Ситуация 1.

Если вам нужно реализовать запрос в БД, который не могут дать стандартные функции. Например, вы хотите вывести самую высокую и самую низкую стоимость товаров в каталоге. Перебирать все позиции просто глупо и неэффективно, когда можно написать один запрос. В этом случае отключите автообновления WordPress на боевом сервере, чтобы избежать ситуации, когда при обновлении изменилась БД. Сам дистрибутив в этом случае следует обновлять самостоятельно и регулярно, с предварительным созданием бекапов.

О создании бекапов и переносе WordPress можете почитать в статье «Перенос сайта WordPress на другой домен«

Ситуация 2.

Если потребовалось добавить свои таблицы в БД. В этом случае у вас просто нет других вариантов, т.к стандартные функции не умеют работать со сторонними таблицами.

Основные свойства

Таблицы $wpdb

В быстром старте для указания таблицы wp_posts, я использовал свойство $wpdb->posts. Аналогично можно обращаться ко всем стандартным таблицам WordPress.

Например: $wpdb->posts, $wpdb->postsmeta, $wpdb->options и так далее.

wp_ в данном случае — префикс названия таблицы; postsназвание таблицы без префикса.

В разных БД для WordPress могут быть разные префиксы, поэтому для универсальности лучше вызывать название таблицы используя одноименные с таблицами БД, свойства класса wpdb.

Есть альтернативный способ обращения к таблице БД, с учётом префикса: можно использовать свойство $wpdb->prefix. Такой способ подойдет, если вам нужно обратиться к таблице, которую создал, например, плагин.

Пример со стандартной таблицей posts: $wpdb->prefix . "posts".

Пример с таблицей WooCommerce: $wpdb->prefix . "woocommerce_order_items"

Другие свойства

У класса wpdb есть другие полезные свойства. Рассмотрим некоторых из них вкратце:

  • show_errors — определяет, нужно показывать ошибки в SQL-запросах или нет. Стандартно включается тогда, когда в конфигурации WordPress константы WP_DEBUG и WP_DEBUG_DISPLAY имеют значение true.
  • last_error — хранит в себе ошибку, возникшую во время последнего запроса
  • num_queries — сколько запросов было сделано
  • num_rows — количество строк, полученных при последнем запросе
  • rows_affecterd — количество строк, затронутых при последнем запросе
  • insert_id — ID, сгенерированный колонкой с AUTO_INCREMENT во время последнего запроса. Обычно используется при запросах INSERT.
  • last_query — последний сделанный запрос
  • last_result — результат последнего выполненного запроса
  • queries — журнал запросов для дебага
  • ready — готова-ли БД выполнять запросы
  • error — последняя обнаруженная ошибка SQL

Методы

query() — универсальный запрос

С его помощью можно делать любые запросы к активной БД. Исключение — SELECT — для него есть другие методы. Например, wpdb::get_results().

Как и для других методов wpdb, запрос необходимо очистить для защиты от sql-инъекций. Сделать это можно двумя способами: используя метод wpdb::prepare() или функцию esc_sql().

Пример: вставить сразу 3 записи одним запросом (метод wpdb::insert() умеет только одну запись за запрос)

<?php
global $wpdb;

$content = [
	[
		'post_title' => 'Название записи 1',
		'post_content' => 'Контент записи 1',
		'post_status' => 'publish',
	],
	[
		'post_title' => 'Название записи 2',
		'post_content' => 'Контент записи 2',
		'post_status' => 'publish',
	],
	[
		'post_title' => 'Название записи 3',
		'post_content' => 'Контент записи 3',
		'post_status' => 'draft',
	]
];
$values = $inserts = [];

$sql = "INSERT INTO `" . $wpdb->posts . "` (`post_title`, `post_content`, `post_status`) VALUES ";

foreach ( $content as $item ) {
	$inserts[] = '(%s, %s, %s)';
	
	$values = array_merge(
		[
			$item['post_title'],
			$item['post_content'],
			$item['post_status']
		],
		$values
	);
}

$sql .= implode( ', ', $inserts );

$query = $wpdb->prepare( $sql, $values );
$result = $wpdb->query( $query );

get_results() — получение данных

Работает аналогично методу wpdb::query(), но предназначен только для SELECT-запросов.

Пример: повторим решение из быстрого старта, но разовьём его и научимся искать записи по содержимому контента. Искать при этом будем только по товарам WooCommerce.

/**
 * @param string $need_word
 * @param string $post_status
 *
 * @return array
 */
function feodoraxis_find_products( $need_word, $post_status = 'publish' ) {
	//Объявим глобальную переменную для доступа к ней
	global $wpdb;
	
	/**
	 * Опишем SQL-запрос.
	 * 
	 * Т.к. в запросе строкой указываем нужный тип записи без переменных и внешних источников - то конкретно это условие можно не очищать
	 */
	$sql = "SELECT * FROM `" . $wpdb->posts . "` WHERE `post_title` LIKE %s AND `post_content` LIKE %s AND `post_status` = %s AND `post_type` = 'product'";
	
	//Первый и второй параметр %s. Методом esc_like(), подготовим строку для работы с оператором LIKE
	$need_word_prepared = '%' . $wpdb->esc_like( $need_word ) . '%';

	//Очистим входящие данные для безопасного запроса с помощью метода prepare
	$query = $wpdb->prepare(
		$sql,
		$need_word_prepared,
		$need_word_prepared,
		$post_status
	);

	//Получим результат
	$result = $wpdb->get_results( $query );

	//Если получили массив, даже пустой - значит запрос отработал корректно, возвращаем результат
	if ( is_array( $result ) ) {
		return $result;
	}

	//В случае любой ошибки возвращаем пустой массив
	return [];
}

esk_like() — подготовка строки для LIKE

Подготавливает строку для работы с оператором LIKE в SQL-запросе. Он экранирует в строке специальные символы «_» и «%».

Данный метод следует использовать только для метода wpdb::prepare() и функции esc_sql().

Пример:

<?php
$find = "котики спят 70% своей жизни";
$like = "%" . $wpdb->esc_like( $find ) . "%";
$sql = $wpdb->prepare( "SELECT * FROM `" . $wpdb->posts . "` WHERE `post_content` LIKE %s", $like );

insert() — вставка

Позволяет вставить одну строку в таблицу. Входящие данные можно не очищать.

Если нужно вставить сразу несколько строк — то лучше воспользоваться методом wpdb::query().

Агрумуенты метода: wpdb::insert( string $table, array $data, array|null $format = null );

Рассмотрим каждый аргумент:

  • $table — название таблицы, в которую мы хотим добавить строку
  • $data — массив данных, которые мы хотим добавить. Используется формат array('колонка' => 'данные')
  • $format — массив определяющий формат входящих данные аргумента $data. Для определения формата используются следующие флаги: '%d', '%f', '%s' (integer, float и string соответственно)

Возвращает количество вставленных строк при успехе; а при ошибке возвращает false.

Чтобы узнать ID вставленной строки, колонки с AUTO_INCREMENT, после выполнения метода можно обратиться к свойству wpdb::insert_id. По нему-же можно определить, удачно отработал последний метод или нет — при неудаче возвращает значение 0.

Пример использования:

<?php
function feodoraxis_insert_post( string $title, string $content, string $status = 'publish', int $parent_id = 0 ):int {
	global $wpdb;

	$result = $wpdb->insert(
		//Название таблицы
		$wpdb->posts, 
		
		//Данные, которые мы хотим добавить в таблицу
		[
			'post_title' => $title,
			'post_content' => $content,
			'post_status' => $status,
			'post_parent' => $parent_id
		],

		//Формат вводимых данных
		[
			'%s',
			'%s',
			'%s',
			'%d' //т.к. $parent_id имеет формат integer, то и указываем значение '%d'
		],
	);

	//Если метод вернул не ложь - значит все сработало корректно
	if ( $result !== false && $wpdb->insert_id > 0 ) {
		return $wpdb->insert_id;
	}

	//Если метод вернул ложь - значит случилась ошибка. Вернём ноль.
	return 0;
}

update() — обновление

Обновляет строки. Входящие данные можно не очищать.

Аргументы метода: wpdb::update( string $table, array $data, array $where, array|string|null $format = null, array|string|null $where_format = null )

Рассмотрим каждый аргумент:

  • $table — название таблицы
  • $data — данные на обновление. Формат array('колонка' => 'значение')
  • $where — именованный массив для условий, согласно которым следует найти строки на обновление. Формат array('колонка' => 'значение'). Если указать несколько колонок — то будут учитываться условия всех колонок — т.е. использован оператор AND между условиями. Сравнение между колонкой и значением всегда ‘=‘. Если указать, например, array('колонка' => null) — то будет использована проверка IS NULL.
  • $format — массив, определяющий формат входящих данных аргумента $data. Для определения формата используются следующие флаги: '%d', '%f', '%s' (integer, float и string соответственно)
  • $where_format — работает аналогично аргументу $format, но определяет формат для аргумента $where.

Пример использования:

/**
 * Опубликуем все товары со статусом "draft"
 *
 * @return bool
 */
function feodoraxis_to_publish_products():bool {
	global $wpdb;

	$result = $wpdb->update( 
		//Название таблицы
		$wpdb->posts,

		//Данные, которые мы хотим изменить
		[
			'post_status' => 'publish',
		],

		//Найдем нужные записи (условия)
		[
			'post_type' => 'product',
			'post_status' => 'draft',
		],
		
		//Формат для данных (data)
		[
			'%s'
		],
		
		//Формат для условий (where)
		[
			'%s',
			'%s'
		]
	);
	
	//Если все хорошо - вернем истину
	return $result !== false;
}

delete() — удаление

Удаляет строки. Входящие данные можно не очищать.

Аргументы метода: wpdb::delete( string $table, array $where, array|string|null $where_format = null )

Рассмотрим каждый аргумент:

  • $table — название таблицы
  • $where — именованный массив для условий, согласно которым следует найти строки на обновление. Формат array('колонка' => 'значение'). Если указать несколько колонок — то будут учитываться условия всех колонок — т.е. использован оператор AND между условиями. Сравнение между колонкой и значением всегда ‘=‘. Если указать, например, array('колонка' => null) — то будет использована проверка IS NULL.
  • $where_format — массив, определяющий формат входящих данных аргумента $where. Для определения формата используются следующие флаги: '%d', '%f', '%s' (integer, float и string соответственно)

Пример использования:

/**
 * Удалим все товары со статусом "draft"
 *
 * @return bool
 */
function feodoraxis_delete_draft_products():bool {
	global $wpdb;
	
	$result = $wpdb->delete(
		//Название таблицы
		$wpdb->posts,

		//Найдем нужные записи
		[
			'post_type' => 'product',
			'post_status' => 'draft',
		],

		//Формат для where
		[
			'%s',
			'%s'
		]
	);
	
	//Если все хорошо - вернем истину
	return $result !== false;
}

prepare() — очистка строки

Подготавливает строку с SQL-запросом для безопасного исполнения. Принимает строку с SQL и аргументы для нее. Возвращает подготовленную строку с SQL, которую можно смело использовать для методов wpdb::query() и wpdb::get_results().

Синтаксис: wpdb::prepare( string $query, array|mixed ...$args )

Рассмотрим аргументы:

  • $query — принимает в себя строку с SQL-запросом с плейсхолдерами как для функции sprintf()
  • ...$args — принимает значения для SQL-запроса. Можно передавать значения как аргументы — последовательно — так и в виде массива.

Пример с массивом:

<?php
$wpdb->prepare( 
	"SELECT * FROM `table` WHERE `column` = %s AND `field` = %d OR `second_field` LIKE %s", 
	array( 
		'foo', 
		1337, 
		'%bar' 
	) 
);

Пример с последовательной передачей аргументов:

<?php
$wpdb->prepare( 
	"SELECT * FROM `table` WHERE `column` = %s AND `field` = %d OR `second_field` LIKE %s", 
	'foo', 
	1337, 
	'%bar' 
);

Оба примера будут работать одинаково.

Функция esc_sql()

Очищает входящие данные и возвращает очищенную строку. Принимает как массивы, так и строку. Возвращает те-же данные, которые были переданы.

Данные, очищенные этой функцией можно передавать в SQL-запрос без использования метода wpdb::prepare().

Данная функция является обёрткой над методом wpdb::_escape()

Синтаксис: esc_sql( array|string $data ):array|string

Пример использования:

<?php
$foo = "foo";
$bar = '%bar';
$wpdb->get_results( "SELECT * FROM `table` WHERE `column` = '" . esc_sql( $foo ) . "' AND `field` = '" . esc_sql( $bar ) . "'" );

Очистка аргументов для параметра IN

В рамках класса wpdb на момент написания статьи нет встроенных механизмов очистки данных для параметра IN. Поэтому тут нужна смекалка. Или умение гуглить 🙂

Пример решения:

<?php
function handle_in( array $values ):string  {
	$cites_list = array_map( function( $value ) {
		return "'" . esc_sql( $value ) . "'";
	}, $values );	
	
	return implode( ',', $cites_list );
}

$cites_list = ["Moscow", "Minsk", "Beijing"];
$result = $wpdb->get_results( "SELECT * FROM `" . $wpdb->posts . "` WHERE `post_title` IN (" . handle_in( $cites_list ) . ")" );

Подключение к сторонней БД

Стандартно в глобальной переменной $wpdb происходит взаимодействие с БД, указанной в файле wp-config.php. Но данный класс можно также использовать для подключения к сторонней БД, к которой есть доступ с вашего хоста. Для этого нужно создать отдельный экземпляр класса wpdb, в котором следует указать параметры подключения к другой БД.

Пример:

<?php
$wpdb2 = new wpdb( "db_user", "password", "db_name", "db_host" );
if ( ! empty( $wpdb2->error ) ) {
	wp_die( $wpdb2->error );
}

Теперь, используя переменную $wpdb2 с отдельным экземпляром класса wpdb мы, используя те-же методы можем взаимодействовать с указанной БД.

Создание и удаление своей таблицы в БД, а также взаимодействие с ней

При разработке можно создавать сторонние таблицы БД для разных нужд. Но делать это следует с умом. Метод или функция, создающие таблицу должны срабатывать строго один раз — когда активируется ваша тема или плагин. При отключении плагина или темы, рекомендуется удалять такие таблицы. Но лучше перед отключением плагина давать пользователю выбор: сохранить данные или удалить.

Например WooCommerce оставляет свои таблицы и данные в них при отключении. И это хорошо. Потому что плагин может быть отключен из-за ошибки на сервере, может быть отключен случайно или для тестирования админом. Зато при повторном включении, все настройки и данные сохраняются, что удобно.

Примеры для плагина

Пример создания таблицы при активации плагина:

<?php

function table_carts_create():void {
	global $wpdb; 
	$table_name = $wpdb->prefix . "carts"; // название таблицы
	$charset_collate = $wpdb->get_charset_collate();

	//Проверим, существует-ли таблица. Создаем только если ее нет.
	if ( $wpdb->get_var( "show tables like `" . $table_name . "`" ) == $table_name ) {
		return;
	}

	$sql = "CREATE TABLE `" . $table_name . "` (
		id int(11) NOT NULL AUTO_INCTEMENT,
		user_id int(11) NOT NULL,
		createtime datetime NOT NULL,
		UNIQUE KEY id (id)
		) $charset_collate;";

	require_once( ABSPATH . "wp-admin/includes/upgrade.php" );
	dbDelta( $sql );
}

register_activation_hook( __FILE__, "table_carts_create" );

Удаление таблицы при деактивации плагина:

function table_carts_remove():void {
	global $wpdb;
	$table_name = $wpdb->prefix . "carts";
	$sql = "DROP TABLE IF EXISTS `" . $table_name . "`";
	$wpdb->query( $sql );
}
register_uninstall_hook( __FILE__, "table_carts_remove" );

Примеры для темы

Создание таблицы при активации темы:

function table_carts_create():void {  
	global $wpdb; 
	$table_name = $wpdb->prefix . "carts"; // название таблицы
	$charset_collate = $wpdb->get_charset_collate();

	//Проверим, существует-ли таблица. Создаем только если ее нет.
	if ( $wpdb->get_var( "show tables like `" . $table_name . "`" ) == $table_name ) {
		return;
	}

	$sql = "CREATE TABLE `" . $table_name . "` (
		id int(11) NOT NULL AUTO_INCTEMENT,
		user_id int(11) NOT NULL,
		createtime datetime NOT NULL,
		UNIQUE KEY id (id)
		) $charset_collate;";

	require_once( ABSPATH . "wp-admin/includes/upgrade.php" );
	dbDelta( $sql );
}
add_action( "after_setup_theme", "table_carts_create" );

Удаление таблицы при деактивации темы:

<?php
function table_carts_remove():void {
	global $wpdb;
	$table_name = $wpdb->prefix . "carts";
	$sql = "DROP TABLE IF EXISTS `" . $table_name . "`";
	$wpdb->query( $sql );
}
add_action( "after_switch_theme", "table_carts_remove" );

Заключение

Надеюсь, статья была вам полезна. Если есть вопросы или замечания — пишите о них в ВК. Всё разберём 🙂

Успехов!

Андрей Смородин

С 2013 года занимаюсь веб-разработкой, а с 2015 ушел во фриланс.

Пишите в телеграм, если нужна помощь с WordPress, Laravel,
HTML-вёрсткой или разработка с нуля - @feodoraxis

Поделиться