8828 sujets

Développement web côté serveur, CMS

Bonjour à tous
Je suis tombé sur le problème suivant:
J'ai une table Members dans laquelle je référence les membres de l'association
J'ai une table MemberGroups dans laquelle sont indiqués les groupes auxquels un membre appartient,
par exemple un membre peut être dans le groupe "actif" et dans le groupe "membre du CA".
Quand j'essaie de créer un membre puis de lui affecter un ou plusieurs groupes (par deux requêtes successives), tout se passe comme si la création du membre n'avait pas encore eu lieu au moment où je l'affecte à un groupe: le serveur MySQL répond qu'il y a une erreur dans les contraintes.
J'ai également d'autres cas moins nets qui me laissent penser que deux requêtes successives ne se déroulent pas exactement dans l'ordre où elles sont demandées, ou bien que les tables de contraintes ne sont pas immédiatement mises à jour.
Je n'ai rien trouvé sur ce sujet dans la documentation.
Quelqu'un pourrait il éclairer ma lanterne ?
Modérateur
Salut,

En reprenant cette modélisation (le MPD), tu dois faire en sorte que tes tables soient en innoDB et qu'elles aient cet aspect.


CREATE TABLE IF NOT EXISTS members (
    id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
    firstname VARCHAR(32) DEFAULT '',
    lastname VARCHAR(32) NOT NULL
) ENGINE=InnoDB;

CREATE TABLE IF NOT EXISTS groups (
    id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(32) NOT NULL
) ENGINE=InnoDB;

CREATE TABLE IF NOT EXISTS groups_members (
    id INT NOT NULL AUTO_INCREMENT PRIMARY KEY,
    members_id INT NOT NULL,
    groups_id INT NOT NULL,

    CONSTRAINT fk_gm_member FOREIGN KEY (members_id) REFERENCES members(id) ON UPDATE CASCADE ON DELETE CASCADE,
    CONSTRAINT fk_gm_group FOREIGN KEY (groups_id) REFERENCES groups(id) ON UPDATE CASCADE ON DELETE CASCADE
) ENGINE=InnoDB;


Comme tes tables sont déjà créées, tu peux vérifier avec cette commande :

SHOW TABLE STATUS WHERE Name IN ('members', 'groups', 'groups_members');


Si une table n'utilise pas le bon moteur, je t'invite à faire un alter table. exemple :

ALTER TABLE le_nom_de_ta_table ENGINE=InnoDB;


Puisque tu développes en php, tu dois utiliser ceinture et bretelles pour ce genre de manipulation (try/catch et les transaction (d'où innoDB)). Pourquoi ? Parce que tu enchaînes plusieurs requêtes dépendantes !


<?php
try {
    // 1. Démarrer la transaction
    $pdo->beginTransaction();

    // 2. Insérer le nouveau membre
    $stmtMember = $pdo->prepare("
        INSERT INTO members (firstname, lastname) 
        VALUES (:firstname, :lastname)
    ");
    $stmtMember->execute([
        ':firstname' => 'Jean',
        ':lastname'  => 'Dupont'
    ]);

    // 3. Récupérer l'ID généré par l'AUTO_INCREMENT
    $memberId = $pdo->lastInsertId();

    // 4. Associer le membre à un ou plusieurs groupes
    $groupIds = [1, 2]; // Ex: "Actif" (1) et "Membre du CA" (2)

    $stmtGroup = $pdo->prepare("
        INSERT INTO groups_members (members_id, groups_id) 
        VALUES (:member_id, :group_id)
    ");

    foreach ($groupIds as $groupId) {
        $stmtGroup->execute([
            ':member_id' => $memberId,
            ':group_id'  => $groupId
        ]);
    }

    // 5. Valider définitivement les requêtes
    $pdo->commit();
    echo "Membre créé avec succès (ID : $memberId) et associé à ses groupes !";

} catch (PDOException $e) {
    // En cas d'erreur (ex: le groupe n'existe pas), annuler toutes les opérations
    if ($pdo->inTransaction()) {
        $pdo->rollBack();
    }
    echo "Erreur lors de la création : " . $e->getMessage();
}


ps : À noter que depuis MySQL 5.5 (sorti il y a plus de 10 ans), InnoDB est le moteur par défaut. Si tu n'as rien spécifié à la création initiale de tes tables, elles sont très probablement déjà en InnoDB.
Modifié par Niuxe (25 Aug 2026 - 18:40)
Merci pour cette réponse.

Comme je l'ai déjà dit, je n'ai pas beaucoup de connaissances dans le domaine des bases de données, ce qui n'est pas très grave, car la base de données de mon association est petite (le fichier de sauvegarde compressé fait moins de 300Ko), l'utilisation est peu fréquente, et la plupart des opérations impliquant une écriture sont réalisées par l'administrateur, c'est à dire moi-même.
Comme toute personne sensée peu compétente dans un domaine, je conserve bien entendu les paramètres par défaut de l'application, le moteur est donc bien InnoDB.

Pour l'écriture dans la base, j'ai pris l'approche suivante:
Un objet Database
Pour chaque table un objet DBtable qui possède une méthode updateItem

Cette méthode prend comme paramètre un objet dont les propriétés ont pour nom le nom de la colonne correspondante dans la table
La méthode updateItem fabrique une requête "INSERT INTO... ON DUPLICATE KEY UPDATE.. qui est préparée, puis exécutée avec les paramètres et valeurs nécessaires, y compris l'identifiant de la personne qui a lancé la mise à jour et la date et l'heure de la mise à jour.

Toutes les tables ont une colonne "id" en autoincrement, mais les index réellement parlant sont des chaînes de caractères significatives. Ainsi le membre de l'association "Jean Dupont" a bien un id automatiquement créé lors de la création de la ligne correspondante dans la table, mais il a également un identifiant unique (memberID) "jdupont" qui est utilisé dans la plupart des requêtes et définitions de contraintes.

Par exemple la table MemberGroups a deux contraintes: que le champ goupID existe dans la table Groups et que le champ memberID existe dans la table Members.

Dans le cas qui nous intéresse, le code (simplifié) de la mise à jour ou création d'un membre avec des groupes s'écrit

function dbUpdate() {
       $mbTable = getDBtable('Members');
       $mbTable -> updateItem($this);
       $groupTable = getDBTable('MemberGroups');
       foreach($this -> groupInfo as $group) {
             $info = (object) ['mbID' => $this -> mbID, 'groupID' => $group];
             $groupTable -> updateItem($info);
      }
}


Dans ce code, la table Members est d'abord mise à jour avec les informations du membre de l'association, puis pour chaque groupe dans l'array groupInfo la table MemberGroups est mise à jour avec les deux champs mbID et groupID pour chacun des groupes.

Je constate que ce code fonctionne sans problème si j'ajoute un groupe à un membre existant, mais que si je veux créer un nouveau membre et le mettre dans un groupe, je reçois un message d'erreur disant qu'il y a une violation de contrainte, ce qui me semble devoir signifier que l'utilisateur est bien créé mais que la mise à jour de la table MemberGroups prend en compte la table des membres avant création du nouveau membre.
Ayant été à la base développeur de système d'exploitation, je comprend très bien le pourquoi de la chose, mais ma question est "que faire pour assurer que la création du nouveau membre soit effectivement prise en compte avant la mise à jour de la table MemberGroups ?"
Peut-être faut il considérer la mise à jour de la table des membres comme un transaction et effectuer un commit avant de mettre à jour la table MemberGroups, plutôt que de considérer l'ensemble de ces mise à jours comme une seule transaction ?
Modifié par PapyJP (26 Aug 2026 - 11:29)
Modérateur
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)
Salut,

ce n'est pas forcement une bonne pratique d'utliser "jdupont" un peu partout dans tes tables et requêtes Smiley sweatdrop

C'est plus parlant quand tu regardes tes tables, mais ça peut avoir des impactes assez pénible. On a déjà eu des cas de femmes qui divorcent et pour qui du jour au lendemain il devient hors de question de taper un login qui contient le nom de famille de l'ex mari ... c'est une purge à modifier dans des tas de tables (dans pleins d'applications différente dans notre cas).