Si vous avez déjà vu une table “fantôme” (créée en dev, absente en prod) ou une colonne qui refuse d’apparaître malgré un déploiement propre, vous avez déjà rencontré les limites d’une utilisation approximative de dbDelta().
Le problème / Le besoin
WordPress 6.9.4 (avril 2026) reste fondamentalement orienté “contenu” : posts, metas, taxonomies. Dès que vous stockez des données volumineuses, relationnelles, ou à fort débit (logs applicatifs, événements, synchronisations, données e-commerce sur-mesure), les tables natives deviennent vite un mauvais compromis.
Le besoin typique : créer (et faire évoluer) une ou plusieurs tables SQL personnalisées, de façon reproductible, compatible multisite, et robuste lors des mises à jour du plugin. Vous voulez une migration idempotente : si le code est exécuté 1 fois ou 20 fois, l’état final est le même.
À la fin, vous saurez :
- créer des tables personnalisées via
dbDelta()correctement (charset/collation, index, primary key, engine), - gérer les évolutions de schéma (ajout de colonnes, index) sans casser les sites existants,
- structurer ça proprement avec une approche “service” (plus maintenable qu’un gros fichier procédural),
- diagnostiquer les erreurs silencieuses les plus fréquentes.
Résumé rapide
- On crée un mini-plugin “journal d’événements” avec 1 table custom
wp_bpcab_events. - La création/mise à jour du schéma est déclenchée à l’activation, et aussi au runtime si la version de schéma change.
- On utilise
dbDelta()(fichierwp-admin/includes/upgrade.php) avec un SQL formaté comme WordPress l’attend. - On stocke une version de schéma dans
wp_options(ouwp_sitemetaen multisite réseau). - On ajoute un diagnostic : on loggue
$wpdb->last_erroret on expose un outil WP-CLI optionnel. - Code compatible WordPress 6.9.4+ et PHP 8.1+.
Quand utiliser cette solution
- Données volumineuses : des centaines de milliers / millions de lignes (ex : événements, logs, tracking interne).
- Requêtes relationnelles : jointures, agrégations, index composites, contraintes de performance.
- Write-heavy : beaucoup d’insertions, où
postmetadeviendrait un goulot d’étranglement. - Schéma stable : vous contrôlez le modèle et vous acceptez une vraie discipline de migration.
- Besoin de contraintes d’unicité (index UNIQUE) ou d’index spécifiques introuvables avec les APIs WP natives.
Quand ne PAS utiliser cette solution
- Si vos données sont “contenu éditorial” : un
custom post type+postmeta+ taxonomies reste plus intégré (REST API, révisions, permissions, UI). - Si vous avez besoin de requêter “un peu” : un CPT bien indexé + meta queries limitées peut suffire.
- Si vous n’êtes pas prêt à gérer les migrations : une table custom sans process de versioning devient une dette technique.
- Si vous voulez des relations fortes : WordPress n’impose pas de clés étrangères. Vous pouvez en mettre, mais
dbDelta()les gère mal (et beaucoup d’hébergeurs les désactivent).
Alternative fréquente : stocker des structures en JSON dans une seule colonne (ou en option) et indexer seulement quelques champs. C’est parfois le meilleur compromis, surtout si vous restez sous ~50k lignes.
Prérequis / avant de commencer
- WordPress : 6.9.4+.
- PHP : 8.1+ (recommandé), idéalement 8.2/8.3 si votre stack suit.
- MySQL/MariaDB : version moderne (MySQL 8+ ou MariaDB 10.6+). Les collations/charsets et indexations varient sinon.
- Environnement : faites ça d’abord en staging. J’ai souvent vu des tables créées avec le mauvais préfixe sur des multisites parce que le dev a testé “vite fait” en prod.
- Sauvegarde : dump SQL + snapshot fichiers. Ne testez pas de migrations sans plan de rollback.
Docs officielles utiles :
- dbDelta() (WordPress Developer Resources)
- Classe wpdb
- Database API Handbook
- Hooks d’activation/désactivation
- WordPress Core Trac (recherches dbDelta/upgrade.php)
- PHP.net (référence PHP, pour contexte — WordPress utilise wpdb, pas PDO)
L’approche naïve (et pourquoi l’éviter)
Le code que je vois le plus souvent (et qui “marche sur ma machine”) :
<?php
// MAUVAISE PRATIQUE : exemple volontairement problématique.
global $wpdb;
// 1) Pas de charset/collation, 2) pas de dbDelta, 3) exécuté à chaque chargement.
$sql = "CREATE TABLE {$wpdb->prefix}bpcab_events (
id BIGINT NOT NULL AUTO_INCREMENT,
created_at DATETIME NOT NULL,
type VARCHAR(50) NOT NULL,
payload LONGTEXT NULL,
PRIMARY KEY (id)
)";
$wpdb->query($sql);
Pourquoi ça pose problème :
- Exécuté à chaque requête : c’est du CPU et des locks inutiles, et parfois des erreurs SQL en boucle.
- Charset/collation : vous finissez avec une table
latin1sur un site enutf8mb4, et des comparaisons qui deviennent lentes (ou fausses). - Évolutions impossibles : au prochain déploiement, vous ajoutez une colonne… et rien ne se met à jour proprement.
- Pas de contrôle multisite : création sur le mauvais blog, ou seulement sur le site principal.
- Erreurs silencieuses :
$wpdb->query()peut échouer sans que vous ne le voyiez si vous ne logguez pas.
La bonne approche — tutoriel pas à pas
Étape 1 — Créer un plugin minimal (structure)
Créez un dossier wp-content/plugins/bpcab-custom-tables puis un fichier bpcab-custom-tables.php. L’objectif : isoler la logique de schéma dans une classe dédiée, versionnée.
Étape 2 — Définir une version de schéma
Ne versionnez pas “la version du plugin”, versionnez le schéma. C’est ce qui pilote les migrations. Exemple : 1, puis 2 quand vous ajoutez un index, etc.
Étape 3 — Écrire le SQL “dbDelta-compatible”
dbDelta() est strict : espaces, backticks, et format des index comptent. Dans mon expérience, 80% des échecs viennent d’un SQL “presque bon”.
- Utilisez
CREATE TABLE ... ( ... )avec une clé primaire. - Terminez par
$charset_collate(issu de$wpdb->get_charset_collate()). - Gardez un style constant (dbDelta compare des chaînes).
Étape 4 — Appeler dbDelta au bon moment
Deux déclencheurs :
- Activation :
register_activation_hook(). - Runtime : sur
plugins_loaded(ouinit) mais seulement si la version stockée < version courante. Sinon, vous ne faites rien.
Étape 5 — Multisite : réseau vs site
Si votre plugin est “network-activated”, vous devez décider :
- table par site : utilisez
$wpdb->prefix, et créez la table pour chaque blog lors de l’activation réseau, - table réseau : utilisez
$wpdb->base_prefixet stockez la version danssitemeta.
Je pars ici sur table par site (cas le plus courant pour des données liées au site).
Étape 6 — Ajouter un diagnostic (indispensable)
dbDelta() retourne un tableau de changements, mais si votre SQL est mal formé, l’erreur peut être indirecte. On ajoute :
- log conditionnel quand
WP_DEBUGest actif, - commande WP-CLI optionnelle pour forcer la migration et afficher le retour.
Code complet
Copiez-collez le fichier ci-dessous dans wp-content/plugins/bpcab-custom-tables/bpcab-custom-tables.php, puis activez le plugin. Le code est autonome.
<?php
/**
* Plugin Name: BPCAB Custom Tables (dbDelta)
* Description: Exemple avancé de création et migration de tables personnalisées avec dbDelta() (WP 6.9.4+, PHP 8.1+).
* Version: 1.0.0
* Requires at least: 6.9
* Requires PHP: 8.1
* Author: BPCAB
*/
declare(strict_types=1);
if (!defined('ABSPATH')) {
exit;
}
final class BPCAB_Custom_Tables_Plugin {
/**
* Version du schéma SQL (pas la version du plugin).
* Incrémentez uniquement quand le CREATE TABLE change.
*/
private const SCHEMA_VERSION = 1;
/**
* Clé option pour stocker la version de schéma par site.
*/
private const OPTION_SCHEMA_VERSION = 'bpcab_events_schema_version';
/**
* Singleton simple (suffisant pour un plugin démo).
*/
private static ?self $instance = null;
public static function instance(): self {
if (self::$instance === null) {
self::$instance = new self();
}
return self::$instance;
}
private function __construct() {
// Runtime upgrade : on vérifie si une migration est nécessaire.
add_action('plugins_loaded', [$this, 'maybe_upgrade']);
// (Optionnel) WP-CLI.
if (defined('WP_CLI') && WP_CLI) {
$this->register_wp_cli();
}
}
/**
* Hook d'activation.
* Attention : en multisite, $network_wide peut être true.
*/
public static function activate(bool $network_wide): void {
$plugin = self::instance();
if (is_multisite() && $network_wide) {
// Activation réseau : on crée la table sur chaque site.
$site_ids = get_sites(['fields' => 'ids']);
foreach ($site_ids as $site_id) {
switch_to_blog((int) $site_id);
$plugin->upgrade_schema(true);
restore_current_blog();
}
return;
}
// Activation “site” classique.
$plugin->upgrade_schema(true);
}
/**
* Hook runtime : exécute la migration seulement si nécessaire.
*/
public function maybe_upgrade(): void {
$installed = (int) get_option(self::OPTION_SCHEMA_VERSION, 0);
if ($installed >= self::SCHEMA_VERSION) {
return;
}
$this->upgrade_schema(false);
}
/**
* Applique la migration du schéma (create/alter via dbDelta).
*
* @param bool $is_activation True si appelé pendant l'activation.
*/
private function upgrade_schema(bool $is_activation): void {
global $wpdb;
// Sécurité : on ne tente pas de migrer si la DB n'est pas accessible.
if (!($wpdb instanceof wpdb)) {
return;
}
require_once ABSPATH . 'wp-admin/includes/upgrade.php';
$table = $this->table_name($wpdb);
// Charset/collation du site courant.
$charset_collate = $wpdb->get_charset_collate();
/**
* SQL dbDelta-compatible :
* - Types explicites
* - PRIMARY KEY obligatoire
* - Index déclarés dans le CREATE TABLE
* - Pas de virgule finale avant la parenthèse fermante
*/
$sql = "CREATE TABLE {$table} (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
created_at DATETIME NOT NULL,
event_type VARCHAR(50) NOT NULL,
object_id BIGINT UNSIGNED NULL,
user_id BIGINT UNSIGNED NULL,
ip VARBINARY(16) NULL,
payload LONGTEXT NULL,
PRIMARY KEY (id),
KEY event_type (event_type),
KEY created_at (created_at),
KEY object_id (object_id),
KEY user_id (user_id)
) {$charset_collate};";
$results = dbDelta($sql);
// Mise à jour de la version uniquement si dbDelta s'est exécuté.
// dbDelta peut retourner un tableau vide si rien n'a changé.
update_option(self::OPTION_SCHEMA_VERSION, self::SCHEMA_VERSION, true);
// Diagnostic utile en debug : dbDelta n'est pas toujours bavard.
if (defined('WP_DEBUG') && WP_DEBUG) {
$context = $is_activation ? 'activation' : 'runtime';
error_log('[BPCAB dbDelta][' . $context . '] Table: ' . $table);
error_log('[BPCAB dbDelta][' . $context . '] Results: ' . wp_json_encode($results));
if (!empty($wpdb->last_error)) {
error_log('[BPCAB dbDelta][' . $context . '] wpdb last_error: ' . $wpdb->last_error);
}
}
}
/**
* Nom de table (préfixe par site).
*/
private function table_name(wpdb $wpdb): string {
// IMPORTANT : pas d'échappement manuel ici, dbDelta attend un identifiant SQL.
// Le préfixe WordPress est contrôlé côté config, pas une entrée utilisateur.
return $wpdb->prefix . 'bpcab_events';
}
/**
* WP-CLI : commande pour forcer la migration et afficher le résultat.
*/
private function register_wp_cli(): void {
WP_CLI::add_command('bpcab schema', function(array $args, array $assoc_args) {
$force = isset($assoc_args['force']) ? (bool) $assoc_args['force'] : false;
if ($force) {
update_option(self::OPTION_SCHEMA_VERSION, 0, true);
}
$installed = (int) get_option(self::OPTION_SCHEMA_VERSION, 0);
WP_CLI::log('Installed schema version: ' . $installed);
WP_CLI::log('Target schema version: ' . self::SCHEMA_VERSION);
$this->maybe_upgrade();
$installed_after = (int) get_option(self::OPTION_SCHEMA_VERSION, 0);
WP_CLI::success('Schema version after: ' . $installed_after);
}, [
'shortdesc' => 'Gère le schéma SQL (dbDelta) pour les tables BPCAB.',
'synopsis' => [
[
'type' => 'assoc',
'name' => 'force',
'optional' => true,
'description' => 'Force la migration en remettant la version à 0 avant exécution.',
],
],
]);
}
}
// Boot.
BPCAB_Custom_Tables_Plugin::instance();
// Activation.
register_activation_hook(__FILE__, [BPCAB_Custom_Tables_Plugin::class, 'activate']);
Explication du code
Pourquoi une version de schéma stockée en option
Le point clé : dbDelta() ne sait pas “quand” vous voulez migrer. Vous devez piloter. Stocker la version en option permet :
- d’éviter d’appeler
dbDelta()à chaque requête, - de déclencher la migration au moment opportun (activation ou mise à jour),
- de gérer des migrations multi-étapes si nécessaire.
Sur multisite, cette option est stockée par site (table wp_XX_options), ce qui colle au choix “table par site”.
Pourquoi plugins_loaded
J’utilise plugins_loaded pour la vérification runtime, parce que :
- les plugins sont chargés,
- la DB est disponible,
- vous évitez des effets de bord liés à
initquand certains plugins font déjà des requêtes.
Edge case réel : si un autre plugin essaie d’écrire dans votre table sur init avec une priorité très basse, vous voulez que votre schéma soit prêt avant.
Le SQL “dbDelta-compatible” et ses contraintes
dbDelta() (dans wp-admin/includes/upgrade.php) parse votre SQL, compare avec l’existant, puis exécute des ALTER TABLE. Ce parseur n’est pas un parseur SQL complet. Il est sensible à :
- la présence d’une PRIMARY KEY,
- le format des index (
KEY name (col)), - les types (ex :
BIGINT UNSIGNEDvsBIGINT(20) unsigned), - les variations de casse/espaces qui peuvent changer le diff.
Je garde volontairement un SQL “simple” : pas de clés étrangères, pas de contraintes CHECK, pas de colonnes générées. Vous pouvez en avoir besoin, mais dbDelta() ne les gère pas bien selon les versions de MySQL/MariaDB.
Pourquoi VARBINARY(16) pour l’IP
Stocker les IP en VARCHAR(45) est simple, mais lent à indexer et plus lourd. VARBINARY(16) permet IPv4/IPv6 en binaire. Si vous indexez un jour l’IP, vous serez content d’avoir ce format.
Attention : ça implique une conversion à l’écriture/lecture (ex : inet_pton/inet_ntop côté PHP). Je ne l’implémente pas ici pour rester focalisé sur dbDelta().
Activation multisite : switch_to_blog
Sur activation réseau, le piège classique : créer la table seulement pour le site courant. Ici, on itère get_sites(), puis switch_to_blog() pour que $wpdb->prefix pointe sur le bon site.
Race condition possible : si un site est créé pendant l’activation réseau (rare), il n’aura pas la table. Solution : ajouter un hook wpmu_new_blog pour créer la table à la création du site. Je le montre en variante.
Diagnostic : $wpdb->last_error et logs
Quand dbDelta() ne fait “rien”, vous devez savoir si :
- rien n’était à faire (OK),
- le SQL n’a pas été compris,
- un
ALTERa échoué.
En debug, je loggue le retour de dbDelta() et $wpdb->last_error. Sur des sites avec cache agressif et logs désactivés, c’est souvent la différence entre 5 minutes et 2 heures de “pourquoi la colonne n’existe pas”.
Variantes et cas d’usage
Variante 1 — Ajouter une colonne et un index (migration de schéma v2)
Cas réel : vous voulez ajouter request_id pour corréler des événements. Avec dbDelta(), vous modifiez le SQL, puis vous incrémentez SCHEMA_VERSION.
Extrait (à intégrer en remplaçant le SQL, et SCHEMA_VERSION = 2) :
// Exemple de schéma v2 : ajout de request_id + index.
$sql = "CREATE TABLE {$table} (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
created_at DATETIME NOT NULL,
event_type VARCHAR(50) NOT NULL,
request_id CHAR(36) NULL,
object_id BIGINT UNSIGNED NULL,
user_id BIGINT UNSIGNED NULL,
ip VARBINARY(16) NULL,
payload LONGTEXT NULL,
PRIMARY KEY (id),
KEY event_type (event_type),
KEY created_at (created_at),
KEY request_id (request_id),
KEY object_id (object_id),
KEY user_id (user_id)
) {$charset_collate};";
Note : dbDelta() gère assez bien l’ajout de colonnes/index. Les renommages et suppressions sont une autre histoire (voir variante 2).
Variante 2 — Renommer/supprimer une colonne (dbDelta ne suffit pas)
dbDelta() est mauvais pour les suppressions/renommages. Dans ce cas, je fais :
- un “plan” de migration explicite par version (ALTER TABLE manuels),
- puis je laisse
dbDelta()assurer l’état final (idempotence).
Exemple : en v3, vous remplacez event_type par type. Il faut exécuter un ALTER conditionnel :
<?php
// Exemple incomplet : à intégrer dans une routine de migration versionnée.
global $wpdb;
$table = $wpdb->prefix . 'bpcab_events';
// Vérifie si la colonne event_type existe (INFORMATION_SCHEMA).
$col = $wpdb->get_var(
$wpdb->prepare(
"SELECT COLUMN_NAME
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_SCHEMA = DATABASE()
AND TABLE_NAME = %s
AND COLUMN_NAME = %s",
$table,
'event_type'
)
);
if ($col === 'event_type') {
// Renommage explicite.
$wpdb->query("ALTER TABLE {$table} CHANGE event_type type VARCHAR(50) NOT NULL");
}
Ensuite, votre SQL final (pour dbDelta) doit déclarer type et non event_type. Sans cette étape, vous risquez de vous retrouver avec les deux colonnes (ou aucune, selon les versions DB).
Variante 3 — Multisite : créer la table à la création d’un nouveau site
Si votre plugin est network-activated et que de nouveaux sites apparaissent, ajoutez :
<?php
// À ajouter dans le constructeur : add_action('wpmu_new_blog', ...).
add_action('wpmu_new_blog', function(int $blog_id): void {
// Crée le schéma pour le nouveau site.
switch_to_blog($blog_id);
BPCAB_Custom_Tables_Plugin::instance()->maybe_upgrade();
restore_current_blog();
}, 10, 1);
J’ai croisé ce bug sur des multisites d’agences : tout marche pendant 6 mois, puis un nouveau site est créé, et votre plugin casse parce que la table n’existe pas.
Compatibilité Divi 5 / Elementor / Avada
dbDelta() et les tables personnalisées sont indépendants du builder. Les problèmes viennent plutôt de quand vous écrivez/lisez dans la table.
Divi 5
Divi déclenche beaucoup d’actions en édition visuelle. Évitez de faire des écritures en DB sur des hooks génériques comme wp_loaded sans garde-fous, sinon vous allez polluer votre table avec des événements d’édition.
- Si vous logguez des événements front, filtrez par contexte (
is_admin(), capabilities, et éventuellement un flag Divi si vous l’avez). - En pratique : ne migrez jamais le schéma pendant un rendu Divi Builder. Faites-le sur activation +
plugins_loadedcomme montré.
Elementor
Elementor fait des requêtes AJAX et des previews. Si vous écrivez dans votre table à chaque requête, vous allez générer des doublons. Le pattern : logguer uniquement sur des actions métier (soumission formulaire, commande, webhook), pas sur “page view”.
Avada (Fusion Builder)
Avada met souvent en cache des fragments. Si votre UI de debug affiche des données issues de la table, prévoyez l’invalidation (ou désactivez le cache sur cette page admin). Sinon vous allez croire que vos insertions “ne marchent pas”.
Vérifications après mise en place
Check-list rapide après activation :
- Dans phpMyAdmin/Adminer : la table
wp_bpcab_eventsexiste (ouwp_XX_bpcab_eventsen multisite). - La collation/charset correspond au site (souvent
utf8mb4). - L’option
bpcab_events_schema_versionvaut1(ou votre version). - Les index existent (event_type, created_at, etc.).
Tableau de diagnostic (symptômes réalistes) :
| Symptôme | Cause probable | Vérification | Solution |
|---|---|---|---|
| La table n’apparaît pas après activation | Plugin activé sur un site différent (multisite) ou hook d’activation non exécuté | Vérifiez “Extensions > Installées” et le contexte réseau / site | Activez au bon niveau, ou utilisez l’activation réseau + boucle get_sites() |
| La table existe mais il manque une colonne | Version de schéma non incrémentée, ou SQL dbDelta mal formé | Regardez l’option bpcab_events_schema_version et les logs debug |
Incrémentez SCHEMA_VERSION, corrigez le SQL, puis forcez via WP-CLI --force |
| Erreur SQL dans les logs (ALTER TABLE failed) | Type incompatible, index trop long, collation différente | $wpdb->last_error + logs MySQL |
Ajustez le schéma (taille d’index, type), harmonisez charset/collation |
| Ça marche en dev, pas en prod | Différences MySQL/MariaDB, droits DB, cache opcode, plugin de snippets | Comparez versions DB, vérifiez SHOW GRANTS, désactivez snippets |
Déployez via plugin versionné, purgez caches, corrigez compat DB |
Si ça ne marche pas
- Confirmez que le code est au bon endroit : un classique est de coller le code dans
functions.phppuis de chercher pourquoiregister_activation_hookne se déclenche pas. Ce hook est fait pour les plugins. - Activez WP_DEBUG en staging :
define('WP_DEBUG', true); define('WP_DEBUG_LOG', true); - Regardez
wp-content/debug.log: vous devriez voir les lignes[BPCAB dbDelta]. - Vérifiez la version PHP : si vous êtes sur une vieille 7.x, le plugin ne devrait pas être chargé (et vous aurez des erreurs fatales). PHP 8.1+ est requis.
- Forcez la migration (si WP-CLI est dispo) :
wp bpcab schema --force - Inspectez la table réellement ciblée : sur multisite,
$wpdb->prefixchange. Beaucoup de “table manquante” sont juste une confusion de préfixe. - Videz les caches : cache objet persistant (Redis/Memcached), cache page, OPcache. J’ai déjà vu un déploiement où l’ancien fichier plugin restait servi par OPcache sur un nœud.
Pièges et erreurs courantes
| Erreur | Cause | Solution |
|---|---|---|
Copier le code dans functions.php et attendre que l’activation crée la table |
register_activation_hook() ne fonctionne que pour les plugins |
Mettre le code dans un plugin, ou déclencher la migration autrement (moins recommandé) |
Call to undefined function dbDelta() |
upgrade.php n’est pas inclus |
Ajouter require_once ABSPATH . 'wp-admin/includes/upgrade.php'; avant l’appel |
| dbDelta ne crée pas l’index | Syntaxe d’index non conforme (nom, parenthèses, espaces) | Utiliser KEY index_name (column) dans le CREATE TABLE, format stable |
| Erreur 500 après avoir “modifié vite fait” le plugin | Point-virgule manquant, parenthèse oubliée, PHP 8.1 plus strict | Relire, activer logs, passer par un IDE, déployer via CI |
| La migration se lance en boucle | Option de version jamais mise à jour (ou stockée au mauvais endroit) | Vérifier update_option, multisite, et que SCHEMA_VERSION est bien un entier |
| Tester sur production sans sauvegarde | Pression / oubli | Staging obligatoire, dump DB, plan de rollback (et fenêtre de maintenance si ALTER lourd) |
| Conflit avec un plugin de snippets | Le snippet est désactivé / exécuté dans un ordre inattendu | Préférer un vrai plugin versionné pour les migrations de schéma |
| Code d’un ancien tutoriel incompatible | SQL dbDelta approximatif, charset absent, pratiques obsolètes | Repartir de $wpdb->get_charset_collate() et d’un schéma versionné |
Conseils sécurité, performance et maintenance
Sécurité
- Ne construisez pas de SQL de schéma à partir d’entrées utilisateur. Ici, seul le préfixe WP est utilisé (contrôlé par configuration).
- Pour toutes les requêtes applicatives (INSERT/SELECT), utilisez
$wpdb->prepare()et l’échappement adapté.dbDelta()n’a rien à voir avec l’injection SQL, mais les tables custom attirent souvent du SQL “fait à la main”. - Évitez de logguer des données sensibles dans
payload(tokens, emails, IP en clair). Si vous stockez des secrets, chiffrez côté application.
Performance
- ALTER TABLE peut locker la table selon l’opération et le moteur. Planifiez les migrations lourdes hors pics.
- Indexez selon vos requêtes réelles. Trop d’index ralentit les insertions.
- Si vous écrivez beaucoup : regroupez, utilisez des inserts batch, et évitez les transactions longues si votre hébergeur est fragile.
Maintenance
- Gardez un historique de schéma (même minimal) dans le code : quelles versions, quels changements.
- Ajoutez une commande WP-CLI (comme ici) : c’est un gain énorme en support.
- Sur multisite, décidez explicitement “table par site” vs “table réseau” dès le début. Migrer de l’un à l’autre est pénible.
Ressources
- dbDelta() — référence
- wpdb — référence
- Database API Handbook
- Activation/Deactivation Hooks
- Miroir GitHub de WordPress Core (wordpress-develop)
- Recherche Trac : dbDelta
- PHP.net — déclarations de types (utile avec PHP 8.1+)
FAQ
dbDelta() est-il “recommandé” en 2026 avec WordPress 6.9.4 ?
Oui, pour des plugins qui gèrent des tables custom. Ce n’est pas parfait, mais c’est l’outil “core” prévu pour ça. Si vous avez des migrations complexes, combinez-le avec des ALTER versionnés.
Pourquoi dbDelta() ne supprime pas une colonne quand je l’enlève du SQL ?
Parce que dbDelta() est orienté “ajout/ajustement”, pas “destruction”. Pour supprimer/renommer, faites des migrations explicites et prudentes (avec sauvegarde).
Dois-je utiliser InnoDB explicitement dans le SQL ?
En général non. WordPress laisse MySQL choisir le moteur par défaut. Si vous imposez ENGINE=InnoDB, testez sur des hébergements mutualisés : certains le bloquent, et dbDelta() peut devenir imprévisible.
Pourquoi ma table est en latin1 alors que le site est en utf8mb4 ?
Parce que vous n’avez pas ajouté $wpdb->get_charset_collate() à la fin du CREATE TABLE. C’est le détail qui fait toute la différence.
Est-ce que je peux mettre des clés étrangères ?
Techniquement oui en MySQL, mais dbDelta() ne les gère pas bien, et beaucoup de stacks WordPress ne les utilisent pas. Je préfère des contraintes applicatives + index, sauf besoin fort et environnement maîtrisé.
Comment tester proprement ces migrations ?
Le plus fiable :
- staging avec une copie de prod,
- activation/désactivation répétée,
- test de montée de version : installez v1, insérez des données, déployez v2, vérifiez schéma + données,
- WP-CLI pour forcer les migrations et obtenir des logs reproductibles.
dbDelta() peut-il casser des données existantes ?
Il peut échouer au milieu d’une migration (ALTER partiel) ou modifier un type de colonne si votre SQL change. D’où l’intérêt : versionner, tester, et éviter les changements destructifs sans plan.
Pourquoi utiliser une classe plutôt que des fonctions globales ?
Pour isoler le schéma, éviter les collisions de noms, et garder un point d’entrée unique. Sur des plugins réels, je pousse plus loin avec un conteneur de services, mais ici je reste volontairement simple et copiable.
Que faire si un autre plugin utilise le même nom de table ?
Préfixez avec un namespace unique (slug du plugin/éditeur) et évitez les noms génériques. bpcab_events est déjà plus sûr que events.
Est-ce compatible avec des plugins de cache objet persistant ?
Oui, mais vos vérifications (UI admin, widgets) peuvent afficher des données en cache. Purgez Redis/Memcached si vous suspectez un affichage obsolète. La création de table elle-même ne dépend pas du cache objet.
Comment gérer une table unique au réseau (multisite) ?
Utilisez $wpdb->base_prefix au lieu de $wpdb->prefix, stockez la version avec get_site_option/update_site_option, et ne faites la migration qu’une seule fois (site principal). Testez bien les permissions et l’activation réseau.