web-dev-qa-db-fra.com

vérifier l'entrée en double vs utiliser l'erreur PDO

J'ai une table MySQL qui a un champ pour les adresses e-mail qui est défini comme unique. Pour cet exemple, disons que tout mon formulaire permet à un utilisateur d'insérer son adresse e-mail dans le tableau.

Étant donné que le champ e-mail est unique, la requête devrait échouer s’ils essaient de saisir deux fois le même e-mail. Je suis curieux de savoir les compromis entre les deux scénarios:

1) Exécutez une instruction SELECT rapide avant d'effectuer l'insertion. Si la sélection renvoie des résultats, informez l'utilisateur et n'exécutez pas l'instruction INSERT.

2) Exécutez l'instruction INSERT et recherchez une erreur d'entrée en double

// snippet uses PDO
if (!$prep->execute($values))
{
    $err = $prep->errorInfo();
    if (isset($err[1]))
    {
        // 1062 - Duplicate entry
        if ($err[1] == 1062)
            echo 'This email already exists.';
    }
}

En outre, veuillez supposer une utilisation normale, ce qui signifie que les entrées en double doivent être minimales. Par conséquent, dans le premier scénario, vous devez évidemment exécuter une requête supplémentaire pour l'insertion every, tandis que dans le second, vous vous fiez à la gestion des erreurs.

De plus, je suis curieux d'entendre des réflexions sur le style de codage. Mon cœur dit: "Soyez un programmeur défensif! Vérifiez les données avant de les insérer! ' alors que mon cerveau dit 'Hmmm, il vaut peut-être mieux laisser MySQL se charger de vérifier les données pour vous'.

EDIT - Veuillez noter que ce n'est pas une question "Comment dois-je faire cela", mais plutôt une question "Pourquoi devrais-je le faire d'une manière particulière". Le petit extrait de code que j'ai inclus fonctionne, mais ce qui m'intéresse, c'est la meilleure façon de résoudre le problème.

34
JoshBramlett

INSERT + vérifier l'état devrait être une meilleure approche. Avec SELECT + INSERT vous pouvez demander à un autre thread d'insérer la même valeur entre le SELECT et le INSERT, ce qui signifie que vous devrez également envelopper ces deux dans un verrou de table.

Il est facile de pécher par excès de défense dans votre codage. Python a le dicton "il est plus facile de demander pardon que de demander la permission", et cette philosophie n'est pas vraiment spécifique à Python.

11
lanzz

Vous pourriez exécuter cela avec un bloc try catch:

try {
   $prep->execute($values);
   // do other things if successfully inserted
} catch (PDOException $e) {
   if ($e->errorInfo[1] == 1062) {
      // duplicate entry, do something else
   } else {
      // an error other than duplicate entry occurred
   }
}

Vous pouvez également rechercher des alternatives telles que "INSERT IGNORE" et "INSERT ... ON DUPLICATE KEY UPDATE" - bien que je pense que celles-ci sont spécifiques à MySQL et iraient à l'encontre de la portabilité de l'utilisation de PDO, si cela vous inquiète .

Edit: Pour répondre plus formellement à votre question, pour moi, la solution # 1 (le programmeur défensif) en pleine utilisation élimine efficacement le point de la contrainte unique en premier lieu. Je suis donc d'accord avec votre idée de laisser MySQL s'occuper de la vérification des données.

56
Dan LaManna

Sachez que si vous avez une colonne AUTO_INCREMENT et que vous utilisez InnoDB, un INSERT échoué incrémente la valeur "next auto-id", malgré qu'aucune nouvelle ligne ne soit ajoutée. Voir par exemple la documentation pour INSERT ... ON DUPLICATE KEY et AUTO_INCREMENT Handling in InnoDB . Cela entraîne des lacunes dans les ID AUTO_INCREMENT, ce qui peut être un problème si vous pensez que vous risquez de manquer d'ID.

Donc, si vous vous attendez à ce que la tentative d'insérer une ligne déjà existante soit courante et que vous souhaitiez éviter autant que possible les lacunes dans les identifiants AUTO_INCREMENT, vous pouvez effectuer à la fois une vérification préventive et une gestion des exceptions:

$already_exists = false;
$stmt = $pdo->prepare("SELECT id FROM emails WHERE email = :email");
$stmt->execute(array(':email' => $email));
if ($stmt->rowCount() > 0) {
    $already_exists = true;
}
else {
    try {
        $stmt = $pdo->prepare("INSERT INTO emails (email) VALUES (:email)");
        $stmt->execute(array(':email' => $email));
    } catch (PDOException $e) {
        if ($e->errorInfo[1] == 1062) {
            $already_exists = true;
        } else {
            throw $e;
        }
    }
}

La première requête garantit que nous n'essayons pas d'insérer l'e-mail si nous savons avec certitude qu'il existe déjà. La deuxième requête tente d'insérer l'e-mail s'il ne semble pas encore exister. Nous devons toujours vérifier les exceptions dans la deuxième requête car un doublon peut toujours se produire dans le cas peu probable d'une condition de concurrence critique (plusieurs clients ou threads exécutant l'extrait ci-dessus en parallèle).

Cette approche rend le code robuste, tout en évitant de créer des lacunes dans les identifiants AUTO_INCREMENT, sauf dans les rares cas de conditions de concurrence. C'est également l'approche la plus rapide si la tentative d'insertion d'un e-mail existant est plus courante que la tentative d'insertion d'un nouvel e-mail. Si la tentative d'insertion d'un e-mail existant est rare et que vous ne vous souciez pas des lacunes dans les identifiants AUTO_INCREMENT, la vérification préventive n'est pas nécessaire.

2
Boris Dalstein

Une façon plus simple qui fonctionne pour moi est de comparer le code d'erreur avec un tableau de codes d'état en double. De cette façon, vous n'avez pas à vous soucier du retour de PDO.

$MYSQL_DUPLICATE_CODES=array(1062,23000);
try {
   $prep->execute($values);
   // do other things if successfully inserted
} catch (PDOException $e) {
   if (in_array($e->getCode(),$MYSQL_DUPLICATE_CODES)) {
      // duplicate entry, do something else
   } else {
      // an error other than duplicate entry occurred
   }
}

Merci! :-)

0
ricwambugu