Ton explication permet de bien cibler l'origine du bug/problème ! Et c'est bien se dont je te parlais au début de notre discussion.
Ce n'est pas un souci d'asynchronisme ou de délai d'exécution de MySQL (MySQL exécute les requêtes de manière strictement séquentielle sur une même connexion), mais un problème de flux de données dans le code PHP. Ton code php n'a pas de garde fou. En général, le mode Yolo/bourrin/naïf, c'est casse gueule, n'est ce pas ?
D'où vient l'erreur de contrainte ?
Quand tu crées un nouveau membre :
1. Tu exécutes $mbTable->updateItem($this);.
2. Juste après, tu boucles sur $this->groupInfo en passant $this->mbID.
Si $this->mbID est généré ou vérifié uniquement après l'insertion en base, ou si la valeur contenue dans $this->mbID n'est pas encore enregistrée dans la table Members au moment de la vérification de la contrainte par InnoDB, la requête sur MemberGroups échoue immédiatement avec une erreur de clé étrangère (Foreign Key Constraint Violation).
Si la contrainte cible une colonne textuelle (memberID), il faut s'assurer à 100 % que cette valeur exacte est déjà présente et validée dans la table Members avant d'insérer dans MemberGroups.
Deux transactions, ça n'a pas de sens et c'est même à proscrire
Tu demandes s'il faut faire un commit après la création du membre, puis un autre après les groupes.
Le principe d'une transaction SQL est d'être
atomique (le « A » de
ACID) : soit le membre
ET ses groupes sont enregistrés, soit rien du tout. Si tu découpes en deux transactions et que l'insertion des groupes échoue (erreur réseau, contrainte de groupe inexistant, etc.), tu te retrouves avec un membre « fantôme » créé en base sans ses groupes.
La solution est d'encapsuler dans une transaction PDO
Même pour une petite base de données associative, l'
utilisation des transactions et des blocs try/catch est de rigueur dès qu'une
action métier nécessite plus d'une requête d'écriture.
Pour ce faire :
1. Dans ton objet Database, ajoute des méthodes pour gérer le transactionnel :
<?php
class Database{
// ...
public function beginTransaction() {
return $this->pdo->beginTransaction();
}
public function commit() {
return $this->pdo->commit();
}
public function rollBack() {
if ($this->pdo->inTransaction()) {
$this->pdo->rollBack();
}
}
}
2. Dans ton code métier :
try {
// Démarrage de la transaction globale
$db->beginTransaction();
// 1. Mise à jour / Création du membre
$mbTable = getDBtable('Members');
$mbTable->updateItem($this);
// S'assurer ici que $this->mbID contient bien la valeur fraîchement insérée !
if (empty($this->mbID)) {
$this->mbID = $pdo->lastInsertId();
}
// Sécurité : si mbID est toujours vide après ça, on stoppe tout
if (empty($this->mbID)) {
throw new Exception("Impossible de récupérer l'identifiant du membre inséré.");
}
// 2. Association aux groupes
$groupTable = getDBtable('MemberGroups');
foreach ($this->groupInfo as $group) {$info = (object) [
'mbID' => $this->mbID,
'groupID' => $group
];
$groupTable->updateItem($info);
}
// Si tout s'est bien passé, validation de la transaction
$db->commit();
} catch (Exception $e) {
// Si erreur sur une requête, annule tout
$db->rollBack();
// Journaliser ou affichage erreur
error_log("Erreur lors de la mise à jour du membre : " . $e->getMessage());
throw $e; // Re-jeter l'exception pour la traiter plus haut si besoin
}
On peut aller encore plus loin. Souvent, l'utilisation des transactions est de rigueur. Et si tu ajoutes une closure, ça te permet de factoriser tes transactions.
class Database {
private PDO $pdo;
/**
* Exécute un callback dans une transaction sécurisée.
*/
public function transaction(callable $callback) {
try {
$this->pdo->beginTransaction();
// Exécution de la logique métier
$result = $callback($this->pdo);
$this->pdo->commit();
return $result;
} catch (Throwable $e) {
if ($this->pdo->inTransaction()) {
$this->pdo->rollBack();
}
error_log("Erreur de transaction : " . $e->getMessage());
throw $e; // Re-jeter l'exception pour la traiter plus haut si besoin
}
}
}
ton code métier :
public function dbUpdate() {
$db = getDB(); // Instance de Database
$db->transaction(function(PDO $pdo) {
// 1. Mise à jour / Création du membre
$mbTable = getDBtable('Members');
$mbTable->updateItem($this);
// Récupération de l'ID généré si nouveau membre
if (empty($this->mbID)) {
$this->mbID = $pdo->lastInsertId();
}
// Garde-fou
if (empty($this->mbID)) {
throw new Exception("Impossible de récupérer l'identifiant du membre inséré.");
}
// 2. Association aux groupes
$groupTable = getDBtable('MemberGroups');
foreach ($this->groupInfo as $group) {
$info = (object) [
'mbID' => $this->mbID,
'groupID' => $group
];
$groupTable->updateItem($info);
}
});
}
Un dernier point d'attention sur le INSERT ... ON DUPLICATE KEY UPDATE
Utiliser cette commande pour la création/mise à jour d'un membre est une très mauvaise idée.
1.
Risque de collision : Si deux colonnes ont des contraintes d'unicité différentes, le comportement devient imprévisible
2.
Masquage des erreurs : Les vraies erreurs (contraintes violées, types incorrects) sont silencieusement transformées en UPDATE
3.
Une clef strictement unique obligatoire : La commande ne fonctionne correctement qu'avec une clé unique bien définie
Il est bien plus robuste de séparer explicitement :
- `INSERT INTO` pour la création
- `UPDATE ... WHERE` pour la mise à jour
Modifié par Niuxe (26 Aug 2026 - 17:26)