# « Requête $wpdb directe ou WP_Query pour une route REST maison : quand déroger »

> Comparer les deux approches pour un endpoint de reporting agrégé, avec les risques de sécurité d'un SQL direct mal préparé face à la sécurité native de WP_Query.

- Auteur : Clément Hadrot
- Publié le : 2026-02-05
- Mis à jour le : 2026-02-05
- Catégorie : Headless &amp; API
- URL : https://wpmoderne.dev.wordpress-developpement.fr/headless/wpdb-direct-wp-query-route-rest-maison/

## L’essentiel

- WP_Query protège par défaut, $wpdb non
- Les agrégations complexes justifient parfois $wpdb
- prepare() reste obligatoire sans exception

« WordPress database error: You have an error in your SQL syntax » — ce message, visible dans les journaux d'erreur d'un serveur, est souvent le premier signe qu'une requête `$wpdb` construite à la main a été mal préparée, parfois avec une faille d'injection SQL en prime. Pour une route REST personnalisée, le choix entre `WP_Query` et une requête `$wpdb` directe engage donc bien plus qu'une question de performance : il engage la sécurité même de l'endpoint.

## WP_Query : la voie sûre par défaut

`WP_Query` échappe automatiquement les valeurs qu'on lui transmet via ses paramètres structurés (`meta_query`, `tax_query`, `post__in`, etc.), et respecte nativement le système de capacités de WordPress pour filtrer les contenus visibles selon le rôle de l'utilisateur courant. Pour la quasi-totalité des routes REST qui listent, filtrent ou trient des contenus déjà représentés dans `wp_posts`, elle reste le choix par défaut le plus sûr et le plus maintenable.

```
$requete = new WP_Query( array(
    'post_type'      => 'produit',
    'meta_query'     => array(
        array(
            'key'     => 'stock_disponible',
            'value'   => 0,
            'compare' => '>',
            'type'    => 'NUMERIC',
        ),
    ),
    'posts_per_page' => 20,
) );
```

## Là où WP_Query montre ses limites

Un endpoint de reporting agrégé — par exemple le chiffre d'affaires total par catégorie de produit sur les trente derniers jours — dépasse ce que `WP_Query` peut exprimer nativement. Les agrégations SQL (`SUM()`, `GROUP BY`, jointures entre plusieurs tables meta) n'ont pas d'équivalent direct dans l'API de `WP_Query`, qui reste conçue pour récupérer des objets `WP_Post` complets, pas des résultats statistiques agrégés.

> L'essentiel à retenir : WP_Query protège par défaut, $wpdb non ; Les agrégations complexes justifient parfois $wpdb ; prepare() reste obligatoire sans exception

## Comparatif sur un cas concret de reporting

| Critère | WP_Query | $wpdb direct |
| --- | --- | --- |
| Échappement automatique | Oui, natif | Non, à la charge du développeur |
| Respect des capacités utilisateur | Oui, via les filtres cœur | Non, à réimplémenter manuellement |
| Agrégations SQL (SUM, GROUP BY) | Impossible nativement | Possible directement |
| Performance sur gros volumes | Correcte, avec des index adaptés | Souvent meilleure, requête ciblée |
| Lisibilité et maintenance | Élevée, code déclaratif | Plus faible, SQL brut à documenter |

## Utiliser $wpdb sans ouvrir de faille

Lorsque l'agrégation impose de déroger à `WP_Query`, la méthode `prepare()` de la classe `$wpdb` n'est jamais optionnelle. Elle échappe correctement les valeurs insérées dans la requête, à condition d'utiliser des espaces réservés (`%d`, `%s`, `%f`) plutôt que de concaténer directement des variables dans la chaîne SQL :

```
global $wpdb;

$jours = 30;

$resultats = $wpdb->get_results( $wpdb->prepare(
    "SELECT t.name AS categorie, SUM(pm.meta_value) AS total
     FROM {$wpdb->posts} p
     JOIN {$wpdb->postmeta} pm ON pm.post_id = p.ID AND pm.meta_key = 'montant_commande'
     JOIN {$wpdb->term_relationships} tr ON tr.object_id = p.ID
     JOIN {$wpdb->term_taxonomy} tt ON tt.term_taxonomy_id = tr.term_taxonomy_id
     JOIN {$wpdb->terms} t ON t.term_id = tt.term_id
     WHERE p.post_date >= DATE_SUB(NOW(), INTERVAL %d DAY)
     GROUP BY t.name",
    $jours
) );
```

Chaque valeur variable de la requête, ici le nombre de jours, passe par un espace réservé `%d`, jamais par une concaténation directe de chaîne. C'est cette discipline, et non le choix de `$wpdb` en lui-même, qui détermine si la route reste sûre.

## Ne pas oublier le contrôle d'accès

Contrairement à `WP_Query`, une requête `$wpdb` directe ne filtre jamais automatiquement selon les capacités de l'utilisateur courant. Pour un endpoint de reporting, cela signifie ajouter explicitement une vérification de capacité dans le `permission_callback` de la route :

```
'permission_callback' => function() {
    return current_user_can( 'manage_woocommerce' );
},
```

## Verdict argumenté

`WP_Query` reste le choix par défaut pour toute route qui liste ou filtre des contenus déjà structurés comme des articles WordPress : elle protège nativement contre l'injection et respecte les capacités utilisateur sans effort supplémentaire. Déroger vers une requête `$wpdb` directe se justifie uniquement pour des agrégations que `WP_Query` ne sait pas exprimer, et implique alors une responsabilité accrue : préparation systématique des requêtes et vérification manuelle des permissions, deux disciplines que `WP_Query` offrait gratuitement.
