Un moyen plus rapide de wp_insert_post et add_post_meta en vrac

16

J'ai un fichier csv que je veux insérer qui se compose de ~ 1 500 lignes et 97 colonnes. Cela prend environ 2-3 heures pour faire une importation complète et j'aimerais améliorer cela s'il y a un moyen. Actuellement, pour chaque ligne, je fais un $ post_id = wp_insert_post puis un add_post_meta pour les 97 colonnes associées à chaque ligne. C'est assez inefficace ...

Existe-t-il une meilleure façon de procéder à ce sujet de manière à ce que a puisse obtenir un post_id pour conserver la relation entre post et ses valeurs post_meta?

En ce moment, j'essaie cela sur ma machine locale avec Wamp, mais je vais l'exécuter sur un VPS

Corey Rowell
la source
En plus des conseils WP ci-dessous, regardez également l'utilisation d'InnoDB dans MySQL et validez les transactions par lots, selon cette réponse .
Webaware

Réponses:

21

J'ai eu des problèmes similaires il y a quelque temps avec une importation CSV personnalisée, mais j'ai fini par utiliser du SQL personnalisé pour l'insertion en bloc. Mais je n'avais pas encore vu cette réponse:

Optimiser la post-insertion et la suppression pour les opérations en masse?

à utiliser wp_defer_term_counting()pour activer ou désactiver le comptage des termes.

De plus, si vous vérifiez la source du plugin d'import WordPress, vous verrez ces fonctions juste avant l'importation en masse:

wp_defer_term_counting( true );
wp_defer_comment_counting( true );

puis après l'insertion en vrac:

wp_defer_term_counting( false );
wp_defer_comment_counting( false );

Donc ça pourrait être quelque chose à essayer ;-)

L'importation de messages en tant que brouillon au lieu de les publier accélérera également les choses, car le lent processus de recherche d'un slug unique pour chacun est ignoré. On pourrait par exemple les publier plus tard en étapes plus petites, mais notez que ce type d'approche devrait marquer les articles importés d'une manière ou d'une autre, donc nous ne publions pas plus tard des brouillons! Cela nécessiterait une planification minutieuse et très probablement un codage personnalisé.

S'il y a par exemple beaucoup de titres de poste similaires (identiques post_name) à importer, alorswp_unique_post_slug() peut devenir lent, en raison de l'itération de la requête de boucle pour trouver un slug disponible. Cela peut éventuellement générer un grand nombre de requêtes db.

Depuis WordPress 5.1, le pre_wp_unique_post_slugfiltre est disponible pour éviter l'itération de boucle pour le slug. Voir ticket principal # 21112 . Voici un exemple:

add_filter( 'pre_wp_unique_post_slug', 
    function( $override_slug, $slug, $post_id, $post_status, $post_type, $post_parent ) {
        // Set a unique slug value to shortcircuit the slug iteration loop.
        // $override_slug = ...

        return $override_slug;
    }, 10, 6
);

Si l'on essaie par exemple $override_slug = _truncate_post_slug( $slug, 200 - ( strlen( $suffix ) + 1 ) ) . "-$suffix"avec $suffixas $post_id, alors nous noterons que $post_idc'est toujours 0pour les nouveaux messages, comme prévu. Il existe différentes manières de générer des nombres uniques en PHP, comme uniqid( '', true ). Mais utilisez ce filtre avec soin pour vous assurer d'avoir des limaces uniques. Nous pourrions par exemple exécuter une requête de comptage de groupe par la suite post_namepour être sûr.

Une autre option serait d'utiliser WP-CLI pour éviter le dépassement de délai. Voir par exemple ma réponse postée pour création de 20 000 messages ou pages à l'aide d'un fichier .csv?

Ensuite, nous pouvons exécuter notre script d'importation PHP personnalisé import.phpavec la commande WP-CLI:

wp eval-file import.php

Évitez également d'importer un grand nombre de types de publication hiérarchiques, car l'interface utilisateur wp-admin actuelle ne le gère pas bien. Voir par exemple le type de message personnalisé - liste des messages - écran blanc de la mort

Voici le bon conseil de @otto:

Avant les insertions en masse , désactivez le autocommitmode explicitement:

$wpdb->query( 'SET autocommit = 0;' );

Après les insertions en masse, exécutez:

$wpdb->query( 'COMMIT;' );

Je pense aussi que ce serait une bonne idée de faire un peu de ménage comme:

$wpdb->query( 'SET autocommit = 1;' );

Je n'ai pas testé cela sur MyISAM mais cela devrait fonctionner sur InnoDB .

Comme mentionné par @kovshenin, cette astuce ne fonctionnerait pas pour MyISAM .

Birgire
la source
6
En plus de cela, vous pouvez également utiliser la fonction de requête pour désactiver la validation automatique avant, puis valider manuellement une fois les insertions effectuées. Cela accélère considérablement les opérations au niveau de la base de données lors des insertions en masse. Envoyez simplement un SET autocommit=0;avant les encarts, suivi d'un COMMIT;après.
Otto
Intéressant, merci pour ça! Je devrai le tester quand je rentrerai.
Corey Rowell
@Otto, merci pour le bon conseil. Nous pourrions donc faire $wpdb->query('SET autocommit = 0;');avant les insertions, mais pouvons-nous sauter $wpdb->query('START TRANSACTION;');dans ce cas? Je vais consulter le manuel MySQL pour en savoir plus à ce sujet ;-) cheers.
birgire
1
Bon point Mark. Si ce ne sont que des insertions et non des mises à jour, cela wp_suspend_cache_addition( true )devrait aider à NE PAS mettre de trucs dans le cache d'objets. @Birgire a également mentionné qu'ils n'avaient pas testé cela avec MyISAM - ne vous embêtez pas, le moteur de stockage ne prend pas en charge les transactions, donc la configuration de l'autocommit ou le démarrage d'une transaction n'aura aucun effet.
kovshenin
1
bon conseil @Otto. Plus tôt, ma requête prenait 38 secondes, il faut maintenant 1 seconde.
Annapurna
5

Vous devrez insérer le message pour obtenir votre ID, mais le $wpdb->postmetatableau est très simple dans sa structure. Vous pourriez probablement utiliser une INSERT INTOdéclaration simple , comme celle-ci dans les documents MySQL:INSERT INTO tbl_name (a,b,c) VALUES(1,2,3),(4,5,6),(7,8,9);

Dans ton cas...

$ID = 1; // from your wp_insert_post
$values = '($ID,2,3),($ID,5,6),($ID,8,9)'; // build from your 97 columns; I'd use a loop of some kind
$wpdb->query("INSERT INTO {$wpdb->postmeta} (post_id,meta_key,meta_value) VALUES {$values}");

Cela ne traitera pas de l'encodage, de la sérialisation, de l'échappement, de la vérification des erreurs, des duplications ou de quoi que ce soit d'autre, mais je m'attendrais à ce que ce soit plus rapide (même si je n'ai pas essayé).

Je ne ferais pas cela sur un site de production sans des tests approfondis, et si je n'avais qu'à le faire une ou deux fois, j'utiliserais les fonctions de base et prendrais un long déjeuner pendant que les choses importent.

s_ha_dum
la source
Pensez que je vais prendre un long déjeuner, plutôt que de ne pas insérer de données brutes dans mes tables et il n'y a aucun sens à réécrire ce que Wordpress fera déjà.
Corey Rowell
1
c'est comme ça que l'injection mysql se produit, donc ne l'utilisez pas.
OneOfOne
Tout est codé en dur, @OneOfOne. L'injection ne peut pas, par définition, ne pas se produire sans entrée fournie par l'utilisateur. Telle est la nature de "l'injection". L'OP importe des données à partir d'un fichier .csv qui est sous son contrôle en utilisant du code sous son contrôle. Il n'y a aucune opportunité pour qu'un tiers injecte quoi que ce soit. Veuillez prêter attention au contexte.
s_ha_dum
+1 de ma part, j'ai dû ajouter 20 valeurs de champs douaniers et c'était beaucoup plus rapide que "add_post_meta"
Zorox
1
Vous ne pouvez pas vous attendre à ce que l'OP vérifie soigneusement le fichier CSV avant de l'importer, et vous devez donc le traiter comme une entrée utilisateur, et au moins ->prepare()vos instructions SQL. Dans votre scénario, que se passerait-il si la colonne ID dans le CSV contenait quelque chose comme 1, 'foo', 'bar'); DROP TABLE wp_users; --? Quelque chose de mal probablement.
kovshenin
5

J'ai dû ajouter ceci:

    remove_action('do_pings', 'do_all_pings', 10, 1);

Gardez à l'esprit que cela sautera do_all_pings, qui traitera les pingbacks, les pièces jointes, les trackbacks et autres pings (lien: https://developer.wordpress.org/reference/functions/do_all_pings/ ). Ma compréhension de la lecture du code est que les pingbacks / trackbacks / enceintes en attente seront toujours traités après avoir supprimé cette remove_actionligne, mais je ne suis pas complètement sûr.

Mise à jour: j'ai également ajouté

    define( 'WP_IMPORTING', true );

Au-delà, j'utilise:

    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;' );

    /* Inserting 100,000 posts at a time
       including assigning a taxonomy term and adding meta keys
       (i.e. a `foreach` loop with each loop containing:
       `wp_insert_post`, `wp_set_object_terms`, `add_post_meta`.)
    */

    $wpdb->query( 'COMMIT;' );
    wp_defer_term_counting( false );
    wp_defer_comment_counting( false );
firasd
la source
1

Remarque importante sur 'SET autocommit = 0;'

après avoir défini autocommit = 0si le script arrête l'exécution (pour une raison quelconque, comme exitune erreur fatale ou etc ...), alors vos modifications NE SERONT PAS ENREGISTRÉES DANS DB!

$wpdb->query( 'SET autocommit = 0;' );

update_option("something", "value");     

exit; //lets say, here happens error or anything...

$wpdb->query( 'COMMIT;' );

Dans ce cas update_option, ne sera pas enregistré dans DB!

Ainsi, le meilleur conseil est de vous être COMMITenregistré en shutdownfonction en tant que précatuion (en cas de sortie inattendue).

register_shutdown_function( function(){
    $GLOBALS['wpdb']->query( 'COMMIT;' );
} );
T.Todua
la source