Быстрый способ массового добавления wp_insert_post и add_post_meta
У меня есть CSV-файл, который я хочу импортировать, содержащий около 1500 строк и 97 столбцов. Полный импорт занимает около 2-3 часов, и я хотел бы улучшить этот процесс, если это возможно. В настоящее время для каждой строки я выполняю $post_id = wp_insert_post
, а затем add_post_meta
для 97 связанных столбцов с каждой строкой. Это довольно неэффективно...
Есть ли лучший способ сделать это, чтобы можно было получить post_id и сохранить связь между записью и её значениями метаданных?
Сейчас я тестирую это на локальной машине с WAMP, но потом это будет работать на VPS.

У меня были похожие проблемы некоторое время назад при импорте пользовательских CSV, но в итоге я использовал пользовательский SQL для массовой вставки. Однако тогда я не видел этого ответа:
Оптимизация вставки и удаления записей для массовых операций?
где предлагается использовать wp_defer_term_counting()
для включения или отключения подсчета терминов.
Также, если вы посмотрите исходный код плагина WordPress Importer, вы увидите эти функции непосредственно перед массовым импортом:
wp_defer_term_counting( true );
wp_defer_comment_counting( true );
и после массовой вставки:
wp_defer_term_counting( false );
wp_defer_comment_counting( false );
Так что это стоит попробовать ;-)
Импорт записей как черновиков вместо опубликованных также ускорит процесс, так как пропускается медленный процесс поиска уникального слага для каждой записи. Позже их можно опубликовать небольшими партиями, но учтите, что такой подход требует маркировки импортированных записей, чтобы случайно не опубликовать другие черновики! Это требует тщательного планирования и, скорее всего, пользовательского кода.
Если импортируется много записей с похожими заголовками (одинаковый post_name
), то wp_unique_post_slug()
может работать медленно из-за циклических запросов для поиска доступного слага. Это может привести к огромному количеству запросов к базе данных.
Начиная с WordPress 5.1 доступен фильтр pre_wp_unique_post_slug
, чтобы избежать циклического поиска слага. См. тикет в ядре #21112. Вот пример:
add_filter( 'pre_wp_unique_post_slug',
function( $override_slug, $slug, $post_id, $post_status, $post_type, $post_parent ) {
// Установите уникальное значение слага, чтобы обойти цикл итераций.
// $override_slug = ...
return $override_slug;
}, 10, 6
);
Если попробовать, например, $override_slug = _truncate_post_slug( $slug, 200 - ( strlen( $suffix ) + 1 ) ) . "-$suffix"
с $suffix
в качестве $post_id
, то можно заметить, что $post_id
всегда равен 0
для новых записей, как и ожидалось. Однако в PHP есть различные способы генерации уникальных чисел, например uniqid( '', true )
. Но используйте этот фильтр осторожно, чтобы гарантировать уникальность слагов. Можно, например, выполнить групповой запрос подсчета по post_name
для проверки.
Еще один вариант — использовать WP-CLI для избежания таймаутов. См., например, мой ответ на вопрос Создание 20 000 записей или страниц с использованием CSV-файла?
Затем можно запустить пользовательский PHP-скрипт импорта import.php
с помощью команды WP-CLI:
wp eval-file import.php
Также избегайте импорта большого количества иерархических типов записей, так как текущий интерфейс wp-admin плохо с этим справляется. См., например, Пользовательский тип записи — список записей — белый экран смерти
Отличный совет от @otto:
Перед массовой вставкой явно отключите режим autocommit
:
$wpdb->query( 'SET autocommit = 0;' );
После массовой вставки выполните:
$wpdb->query( 'COMMIT;' );
Также, я думаю, будет хорошей идеей выполнить некоторые действия по обслуживанию:
$wpdb->query( 'SET autocommit = 1;' );
Я не тестировал это на MyISAM, но это должно работать на InnoDB.
Как упомянул @kovshenin, этот совет не сработает для MyISAM.

Кроме того, вы также можете использовать функцию запроса, чтобы отключить автокоммит перед операциями, а затем вручную выполнить коммит после завершения вставок. Это значительно ускоряет операции на уровне базы данных при массовых вставках. Просто отправьте SET autocommit=0;
перед вставками, а затем COMMIT;
после них.

Интересно, спасибо за совет! Нужно будет протестировать это, когда вернусь домой.

@Otto, спасибо за отличный совет. Значит, мы можем сделать $wpdb->query('SET autocommit = 0;');
перед вставками, но можем ли мы пропустить $wpdb->query('START TRANSACTION;');
в этом случае? Я изучу документацию MySQL, чтобы узнать больше об этом ;-) спасибо.

Вот полезная ссылка (MySQL 5.1) http://dev.mysql.com/doc/refman/5.1/en/commit.html по этой теме - по крайней мере для меня ;-)

Начало транзакции неявно отключает автокоммит до завершения транзакции коммитом. Однако для одноразовых скриптов импорта я считаю гораздо понятнее просто отключить автокоммит вручную, а затем выполнить COMMIT, когда это нужно. Транзакционная логика отлично подходит для выполнения множественных операций, но для единичного импорта проще использовать "грубую силу".

Если бы мне нужно было импортировать значительно больше, чем 1500 записей (например, 400к, как я делал раньше), я бы отключил автокоммит и настроил выполнение COMMIT каждые, скажем, 500 записей... Таким образом можно получить перезапускаемую логику с точки сбоя, сохраняя при этом высокую скорость.

спасибо, я учту это. Я искал "autocommit" на svn.wp-plugins.org, но не нашел много (только один результат для тестового случая), так что это может быть хорошей идеей в качестве опции для плагинов импорта ;-)

Я даже не знал, что svn.wp-plugins.org все еще работает. Официальное название теперь plugins.svn.wordpress.org. :)

то есть я ставлю $wpdb->query('SET autocommit = 0;');
перед своими вставками, а после вставок выполняю $wpdb->query('COMMIT');
и все?

да, думаю, это должно сработать, и, возможно, стоит добавить $wpdb->query('SET autocommit = 1;');
снова после выполнения.

При использовании кеша объектов логика транзакций может привести к странным результатам, особенно если код завершится с ошибкой до commit, так как в кеше окажутся данные, которых нет в БД, что может вызвать очень сложные для отладки баги.

Хорошее замечание, Mark. Если это только вставки, а не обновления, то wp_suspend_cache_addition( true )
поможет НЕ помещать данные в кеш объектов. Также @birgire упомянул, что они не тестировали это с MyISAM — даже не стоит пытаться, этот движок хранения не поддерживает транзакции, поэтому установка autocommit или начало транзакции не дадут никакого эффекта.

отличный совет, @Otto. Мой запрос раньше занимал 38 секунд, теперь всего 1 секунду.

MyISAM и InnoDB используют разные подходы. https://stackoverflow.com/a/32913817/2377343

Мне пришлось добавить это:
remove_action('do_pings', 'do_all_pings', 10, 1);
Имейте в виду, что это пропустит do_all_pings
, который обрабатывает пингбеки, вложения, трекбеки и другие пинги (ссылка: https://developer.wordpress.org/reference/functions/do_all_pings/). Насколько я понял из кода, ожидающие пингбеки/трекбеки/вложения всё равно будут обработаны после удаления этой строки remove_action
, но я не полностью уверен.
Обновление: Я также добавил
define( 'WP_IMPORTING', true );
Кроме того, я использую:
ini_set("memory_limit",-1);
set_time_limit(0);
ignore_user_abort(true);
wp_defer_term_counting( true );
wp_defer_comment_counting( true );
$wpdb->query( 'SET autocommit = 0;' );
/* Вставка 100 000 записей за раз
включая назначение таксономического термина и добавление мета-ключей
(т.е. цикл `foreach`, где каждая итерация содержит:
`wp_insert_post`, `wp_set_object_terms`, `add_post_meta`.)
*/
$wpdb->query( 'COMMIT;' );
wp_defer_term_counting( false );
wp_defer_comment_counting( false );

Вам нужно будет вставить запись, чтобы получить ID, но таблица $wpdb->postmeta
имеет очень простую структуру. Вероятно, можно использовать прямой оператор INSERT INTO
, как показано в документации MySQL: INSERT INTO tbl_name (a,b,c) VALUES(1,2,3),(4,5,6),(7,8,9);
В вашем случае...
$ID = 1; // полученный из wp_insert_post
$values = '($ID,2,3),($ID,5,6),($ID,8,9)'; // формируется из ваших 97 колонок; я бы использовал какой-нибудь цикл
$wpdb->query("INSERT INTO {$wpdb->postmeta} (post_id,meta_key,meta_value) VALUES {$values}");
Это не учитывает кодировку, сериализацию, экранирование, проверку ошибок, дублирование или другие аспекты, но я ожидаю, что это будет быстрее (хотя сам не проверял).
Я бы не стал делать это на рабочем сайте без тщательного тестирования, и если бы мне нужно было выполнить это один или два раза, я бы использовал основные функции и ушел на долгий обед во время импорта.

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

вот так происходит инъекция в MySQL, пожалуйста, не используйте это.

Всё жёстко прописано в коде, @OneOfOne. Инъекция не может — по определению — произойти без пользовательского ввода. В этом суть "инъекции". Автор импортирует данные из .csv файла, который находится под его контролем, используя код под его контролем. Нет возможности для третьей стороны что-то инжектировать. Пожалуйста, обращайте внимание на контекст.

+1 от меня, мне нужно было добавить 20 пользовательских полей, и это было намного быстрее, чем "add_post_meta"

Нельзя ожидать, что автор вопроса тщательно проверит CSV-файл перед импортом, поэтому следует рассматривать его как пользовательский ввод и хотя бы использовать ->prepare()
для SQL-запросов. В вашем сценарии, что произойдет, если столбец ID в CSV будет содержать что-то вроде 1, 'foo', 'bar'); DROP TABLE wp_users; --
? Скорее всего, ничего хорошего.

Важное примечание о 'SET autocommit = 0;'
После установки autocommit = 0
, если выполнение скрипта остановится (по любой причине, такой как exit
, фатальная ошибка и т.д.), то ваши изменения НЕ СОХРАНЯТСЯ В БАЗЕ ДАННЫХ!
$wpdb->query( 'SET autocommit = 0;' );
update_option("something", "value");
exit; //допустим, здесь произошла ошибка или что-то еще...
$wpdb->query( 'COMMIT;' );
В этом случае update_option
не сохранится в базе данных!
Поэтому лучший совет - зарегистрировать COMMIT
в функции shutdown
в качестве предосторожности (на случай непредвиденного завершения скрипта).
register_shutdown_function( function(){ $GLOBALS['wpdb']->query( 'COMMIT;' ); } );
