Быстрый способ массового добавления wp_insert_post и add_post_meta

8 июн. 2013 г., 19:37:53
Просмотры: 16.2K
Голосов: 21

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

Есть ли лучший способ сделать это, чтобы можно было получить post_id и сохранить связь между записью и её значениями метаданных?

Сейчас я тестирую это на локальной машине с WAMP, но потом это будет работать на VPS.

1
Комментарии

Помимо советов по WP, приведённых ниже, также рассмотрите использование InnoDB в MySQL и фиксацию транзакций пакетами, согласно этому ответу.

webaware webaware
9 июн. 2013 г. 01:20:00
Все ответы на вопрос 4
14
32

У меня были похожие проблемы некоторое время назад при импорте пользовательских 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.

8 июн. 2013 г. 22:36:03
Комментарии

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

Otto Otto
8 июн. 2013 г. 23:27:00

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

Corey Rowell Corey Rowell
8 июн. 2013 г. 23:31:19

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

birgire birgire
9 июн. 2013 г. 00:05:30

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

birgire birgire
9 июн. 2013 г. 00:15:43

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

Otto Otto
9 июн. 2013 г. 00:46:58

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

Otto Otto
9 июн. 2013 г. 00:48:13

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

birgire birgire
9 июн. 2013 г. 12:23:28

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

Otto Otto
10 июн. 2013 г. 19:26:09

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

yeahman yeahman
6 мая 2014 г. 20:27:25

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

birgire birgire
6 мая 2014 г. 20:36:19

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

Mark Kaplun Mark Kaplun
15 дек. 2014 г. 07:30:16

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

kovshenin kovshenin
2 авг. 2016 г. 03:01:42

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

Annapurna Annapurna
22 авг. 2017 г. 13:47:20

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

T.Todua T.Todua
14 июн. 2021 г. 16:35:24
Показать остальные 9 комментариев
0

Мне пришлось добавить это:

    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 );
2 авг. 2016 г. 00:41:38
6

Вам нужно будет вставить запись, чтобы получить 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}");

Это не учитывает кодировку, сериализацию, экранирование, проверку ошибок, дублирование или другие аспекты, но я ожидаю, что это будет быстрее (хотя сам не проверял).

Я бы не стал делать это на рабочем сайте без тщательного тестирования, и если бы мне нужно было выполнить это один или два раза, я бы использовал основные функции и ушел на долгий обед во время импорта.

8 июн. 2013 г. 20:13:34
Комментарии

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

Corey Rowell Corey Rowell
8 июн. 2013 г. 21:27:24

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

OneOfOne OneOfOne
9 мар. 2015 г. 19:04:46

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

s_ha_dum s_ha_dum
10 апр. 2015 г. 03:00:16

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

Zorox Zorox
5 окт. 2015 г. 15:48:10

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

kovshenin kovshenin
2 авг. 2016 г. 02:58:08

эта функция пропустит все внешние хуки (если есть какие-либо действия, подключенные к update_post_meta)

T.Todua T.Todua
26 апр. 2018 г. 00:13:57
Показать остальные 1 комментариев
0

Важное примечание о '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;' );    } );
14 сент. 2018 г. 20:04:11