WordPress как на ладони
Официальная конференция по WordPress в Moскве 18-19 августа, начало в 9:00 wordpress jino

Метаданные в WordPress

Под словом «Метаданные» в WordPress скрывается огромная работа и потрясающая логика, которую можно ругать и хвалить одновременно, но одного у метаданных не отнять - они очень удобны. При этом разобраться в принципе их работы довольно просто.

Читайте также, как устроены таксономии в WordPress
Читайте также, как устроенны записи в WordPress

Что такое метаданные?

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

Например:

  • у записи (поста) есть основные данные: контент, заголовок, ... и могут быть дополнительные данные (метаданные), например число просмотров записи, ID миниатюры, ID редактировавшего юзера, и т.д.

  • у пользователя есть основные данные - это данные из таблицы wp_users: логин, ссылка на сайт, email, ... и метаданные: биография, ссылка на соц. профиль, настройки админ панели.

Метаданные могут быть у:

  • Записей - таблица wp_postmeta для wp_posts.
  • Пользователей - таблица wp_usermeta для wp_users.
  • Комментариев - таблица wp_commentmeta для wp_comments.
  • Терминов (элементов таксономий) - таблица wp_termmeta для wp_terms.
  • Сайтов (главный сайт сети в мультисайт) - таблица wp_sitemeta для wp_site.
  • Также можно добавить метаданные к любой произвольной таблице (см. ниже).

Связь таблиц метаданных с их основными таблицами

Все метаданные имеют одинаковую логику и таблицы одинаковой структуры в базе данных.

Как можно видеть, структура всех таблиц одинаковая, разница только в названии основных полей. Связываются они с основной таблицей через второе поле (оно у всех таблиц называется по-разному: post_id, user_id, comment_id, term_id). Основной ключ (желтый) обычно не используется и нужен только для точной идентификации конкретной строки в таблице (это иногда нужно, потому что удобно). Запросы как правило работают по трем полям: 2, 3, 4.

По умолчанию WordPress активно использует таблицы метаданных для записей и для пользователей. Менее активно используется таблица метаданных для комментариев - туда при помещении комментария в корзину записывается время удаления (если корзина отключена, то таблица не используется). И совсем не используется таблица метаданных для терминов.

Все таблицы метаданных отлично подходят для расширения возможностей движка. И их активно используют разработчики плагинов и тем.

Таблица метаданных для терминов, была добавлена только в версии 4.4 специально для того, чтобы разработчикам было куда сохранять нужные данные этих самых терминов.

Скрытые (защищенные) метаполя

В WordPress есть такое понятие как скрытые метаполя. Это поля, название которых (значение meta_key) начинается с нижнего подчеркивания _. Так принято называть метаполя, которые используются для нужд кода и не должны изменяться вручную.

В админке, для постов в блоке «Произвольные поля» скрытые метаполя не отображаются, а значит их нельзя изменить. Так, например, при редактировании записи в метаполе _edit_lock записывается метка времени и ID пользователя, который редактирует запись. Благодаря этому мы можем видеть что запись в текущий момент редактируется другим пользователем. Или другой пример, ID картинки-вложения, которая устанавливается как миниатюра записи, сохраняется в метаполе _thumbnail_id.

Для выяснения является ли метаполе скрытым есть специальная функция: is_protected_meta( $meta_key, $meta_type ).

Функции метаданных

Почти все функции метаданных работают на основе четырех базовых функций. На основе этих четырех функций по сути построено API работы со всеми метаданными в WordPress.

Для записей:

Для пользователей:

Для комментариев:

Для таксономий (терминов):

Получить значения всех метаполей объекта можно с помощью функции get_***_meta(). Для этого нужно указать только первый параметр: id объекта:

$metas = get_post_meta( 76 );
/*
Array
(
	[_edit_lock] => Array
		(
			[0] => 1517175359:1
		)

	[_edit_last] => Array
		(
			[0] => 1
		)

	[views] => Array
		(
			[0] => 10164
		)

	[_thumbnail_id] => Array
		(
			[0] => 9556
		)

	[photo] => Array
		(
			[0] => https://wp-kama.ru/wp-content/uploads/2010/03/Quicktags-API.png
			[1] => https://wp-kama.ru/wp-content/uploads/2017/07/image.png
		)

)
*/

Очистка значений метаполей при сохранении

Значение любого метаполя можно очистить через фильтр: sanitize_(type)_meta_(meta_key).

Этот фильтр срабатывает всегда при добавлении или обновлении метаполя.

Все варианты фильтра, если указать первый изменяемый параметр:

  • sanitize_post_meta_(meta_key)
  • sanitize_user_meta_(meta_key)
  • sanitize_comment_meta_(meta_key)
  • sanitize_term_meta_(meta_key)
Пример использования фильтра

Допустим, у нас есть метаполе пользователя my_history (моя история). В это поле пользователь может писать какой-то текст, но в нем нельзя использовать HTML теги. Чтобы быть уверенным наверняка, что туда не попадут эти самые теги, лучше всего очистить значение поля перед сохранением его в базу данных:

add_filter( 'sanitize_user_meta_'.'my_history', function( $meta_value ){
	return wp_strip_all_tags( $meta_value );
});

Регистрация метаполей

С версии 4.6 в WordPress появилась возможность дополнительно описывать каждое метаполе. Делается это через функцию register_meta().

Регистрация метаполя, нужна для использования его в разных API, например для REST API (полная поддержка REST API ожидается с версии WP 5.0).

Поведение register_meta() чем-то походе на register_post_type() - данные аналогично сохраняются в глобальную переменную $wp_meta_keys. Это позволяет получать данные метаполя в любое время при написании кода.

Пример регистрация метаполя

Зарегистрируем метаполе для поста с функциями доступа и очистки:

register_meta( 'post', 'bookname', [
	'type'              => 'string',
	'description'       => 'Название книги',
	'single'            => true,
	'sanitize_callback' => function( $meta_value, $meta_key, $object_type ){
		return wp_strip_all_tags( $meta_value ); // удалим html теги
	},
	'auth_callback'     => function( $false, $meta_key, $postID, $user_id, $cap, $caps ){
		// запретим создание и редактирование этого метаполя для всех кроме админа
		return current_user_can('manage_options');
	},
	'show_in_rest'      => false,
] );

В результате, если зайти на страницу редактирования записи и попробовать там создать метаполе bookname:

  • Если вы вошли как админ - метаполе создастся.
  • Если как редактор, автор и т.д. - вы не сможете создать метаполе.

За это отвечает параметр auth_callback

Далее, если в значении указать строку с HTML тегами, при обновлении они будут вырезаны.

За это отвечает параметр sanitize_callback

Параметры: type, description, single, show_in_rest в версии WP 4.6, носят только информационный характер и пока нигде не используются... Ну и в общем, я пока не встречал логику регистрации метаполей в плагинах или где-либо еще. Идея заложена, но по факту пока не используется (полная поддержка ожидается с версии WP 5.0).

Остальные функции связанные с регистрацией метаполей:

Метаполя для свой таблицы

API метаполей позволяет создать свою таблицу метаполей для любой таблицы. Рассмотрим на примере.

Допустим у нас есть таблица my_books:

Создаем для нее таблицу метаданных my_bookmeta:

Для создания таблицы метаданных, нужно один раз запустить написанную нами функцию create_book_meta_table(). Код функции:

## register_activation_hook( __FILE__, 'create_book_meta_table');
## Функция создания таблицы метаданных. Нужно запустить один раз. Можно повесить на register_activation_hook()
function create_book_meta_table(){
	global $wpdb;

	$collate = '';
	if ( ! empty($wpdb->charset) ) $collate  = "DEFAULT CHARACTER SET $wpdb->charset";
	if ( ! empty($wpdb->collate) ) $collate .= " COLLATE $wpdb->collate";

	/*
	 * Indexes have a maximum size of 767 bytes. Historically, we haven't need to be concerned about that.
	 * As of 4.2, however, we moved to utf8mb4, which uses 4 bytes per character. This means that an index which
	 * used to have room for floor(767/3) = 255 characters, now only has room for floor(767/4) = 191 characters.
	 */
	$max_index_length = 191;

	$main_field = 'book_id'; // название главной колонки, должно выглядеть как: $meta_type . '_id'
	$table_name = 'my_bookmeta';

	$wpdb->query(
		"CREATE TABLE $table_name (
			meta_id      bigint(20)   unsigned NOT NULL auto_increment,
			$main_field  bigint(20)   unsigned NOT NULL default '0',
			meta_key     varchar(255)                   default NULL,
			meta_value   longtext,
			PRIMARY KEY  (meta_id),
			KEY $main_field ($main_field),
			KEY meta_key (meta_key($max_index_length))
		) $collate;"
	);
}

Название главной колонки, должно выглядеть как: $meta_type . '_id'. Так оно собирается в функциях: ***_metadata().

Зарегистрируем функции для работы с метаданными book:

function add_book_meta( $id, $meta_key, $meta_value, $unique = false ) {
	return add_metadata( 'book', $id, $meta_key, $meta_value, $unique );
}

function delete_book_meta( $id, $meta_key, $meta_value = '' ) {
	return delete_metadata( 'book', $id, $meta_key, $meta_value );
}

function get_book_meta( $id, $meta_key = '', $single = false ) {
	return get_metadata( 'book', $id, $meta_key, $single );
}

function update_book_meta( $id, $meta_key, $meta_value, $prev_value = '' ){
	return update_metadata( 'book', $id, $meta_key, $meta_value, $prev_value );
}

Все!

Теперь мы можем использовать функции для управления метаданными, которые будут работать в точности, как работают метаданные в WordPress. Сюда входит и кэширование метаданных, и всевозможные хуки...

Например:

// добавим данные в таблицу метаданных
update_book_meta( 12, 'author_name', 'Циркон' );

// получим значение метаполя 
get_book_meta( 12, 'author_name', 1 ); //> Циркон

// получим значения всех метполей
get_book_meta( 12 ); //> вернет массив
Построение запроса с использованием метаданных

В WordPress очень удобно можно выбирать или сортировать строки основной таблицы, на основе параметров метаданных. Например, в WP_query() это делается через параметр meta_query.

Прикрутить такой же функционал к нашей таблице можно с помощью класса WP_Meta_Query{}. Напишу, для примера функцию получения книг, с возможностью выборки по метаданным:

// установим таблицы в $wpdb
global $wpdb;
$wpdb->books    = "my_books";
$wpdb->bookmeta = "my_bookmeta";

## Пример функции для получения книг, с возможностью выборки по метаданным
function get_books( $args = array() ){
	global $wpdb;

	$default = [
		'book_id'        => 0,
		'name'           => '',
		'content_search' => '',
		// понимаемые мета-параметры
		'meta_key'       => '',
		'meta_value'     => '',
		'meta_value_num' => '',
		'meta_compare'   => '',
		'meta_query'     => array(),
	];

	$args = array_merge( $default, $args );

	$WHERE = array();
	$JOIN = $ORDER_BY = $LIMIT = '';

	if( $args['book_id'] ){
		// 'my_books.' нужно потому что поле назвается одинаково у главной и у мета таблицы
		$WHERE[] = $wpdb->prepare('my_books.book_id = %d', $args['book_id'] );
	}
	if( $args['name'] ){
		$WHERE[] = $wpdb->prepare('name = %s', $args['name'] );
	}
	if( $args['content_search'] ){
		$WHERE[] = $wpdb->prepare('content LIKE %s', '%'. $wpdb->esc_like( $args['content_search'] ) .'%' );
	}

	// мета запрос
	if( $args['meta_query'] || $args['meta_key'] ){
		$metaq = new WP_Meta_Query();
		$metaq->parse_query_vars( $args ); // парсим возможные мета-параметры из параметров $args

		// первый параметр 'book' должен быть началом свойства $wpdb->bookmeta без суффика 'meta'
		// Т.е. мы указываем 'book' к нему добавляется 'meta' и свойство 'bookmeta' должно существовать в $wpdb
		// см. https://wp-kama.ru/function/_get_meta_table
		$mq_sql = $metaq->get_sql( 'book', $wpdb->books, 'book_id' );

		$JOIN    = $mq_sql['join'];  // INNER JOIN my_bookmeta ON ( my_books.book_id = my_bookmeta.book_id )
		$WHERE[] = $mq_sql['where']; // AND ( ( my_bookmeta.meta_key = 'author_name' AND my_bookmeta.meta_value = 'Циркон' ) )
	}

	$WHERE = 'WHERE '. implode( ' AND ', $WHERE );

	/*
	для сортировки по метаполям понадобится $metaq->get_clauses()
	Array(
		[metasort] => Array(
				[key]     => author_name
				[value]   => Циркон
				[compare] => =
				[alias]   => my_bookmeta
				[cast]    => CHAR
			)
	)
	пример смотрите в: https://wp-kama.ru/function/WP_Query::parse_orderby
	*/
	$ORDER_BY = 'ORDER BY name ASC';

	$res = $wpdb->get_results( "SELECT * FROM $wpdb->books $JOIN $WHERE $ORDER_BY $LIMIT" );

	return $res;
}

Проверим функцию, сделаем запрос:

// запрос на получение книг
$books = get_books([
	'meta_key'   => 'author_name',
	'meta_value' => 'Циркон',
]);

// или так
$books = get_books([
	'meta_query' =>[
		'metasort' => [
			'key'   => 'author_name',
			'value' => 'Циркон',
		]
	]
]);

print_r( $books );
/*
Получим:
Array(
		[0] => stdClass Object(
			[book_id] => 12
			[name] => Вишневый сад
			[content] => Содержание книги ...
			[meta_id] => 2
			[meta_key] => author_name
			[meta_value] => Циркон
		)
)
*/

Повторюсь, теперь выборку по метаполям можно делать любой сложности, все что позволяет делать WP_Meta_Query{}, оно же meta_query в WP_Query.

Пример выборки посложнее:

$args = array(
	'meta_query'   => array(
		'relation' => 'AND',
		array(
			'key'     => 'author_name',
			'value'   => 'алекс',
			'compare' => 'LIKE'
		),
		array(
			'key'     => 'price',
			'value'   => array( 20, 100 ),
			'type'    => 'numeric',
			'compare' => 'BETWEEN'
		)
	)
);
$books = get_books( $args );

Производительность и метаданные

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

Слабым звеном в метаданных является поле meta_value любой таблицы метаданных (например таблицы wp_postmeta). У meta_value нет и не может быть индекса, потому что поле имеет тип LONGTEXT, чтобы можно было хранить в ней любые данные: числа, тексты любой длинны, сериализованные массивы и т.д. Также индексирование невозможно, потому что при построении запроса, например, сортировка по метаполю, где хранятся числа, значения превращаются из строк в числа функцией CAST() и только потом сортируются. Такой подход «убил» бы индекс, даже если бы он там был.

Из-за вышеописанных особенностей не всегда правильно использовать метаполя для хранения данных, по которым затем будет выборка или сортировка, хотя в большинстве случаев они подходят хорошо.

Всегда проверяйте запросы в реальных условиях, смотрите на время выполнения и то, как меняется это время от количества данных и параметров запроса. Если запросы стали медленными, то пришло время как-то их кэшировать или подключать внешнюю систему индексирования, например Sphinx или Elasticsearch.

Если ожидается хранить большие объемы данных, по которым нужна будет выборка и сортировка, то возможно стоит создавать специальную таблицу под такие данные. Под большими объемами данных я имею ввиду количество значений одного мета-ключа от 20000 и более. С таким объемом данных в метаполях запросы уже будут заметно тормозить и без кэширования (которое не всегда подходит) или других надстроек уже будет не обойтись.

-

Читайте также:

3 коммента
Здравствуйте, !     Войти . Зарегистрироваться