53 votes

Vérification des clés dupliquées avec Doctrine 2

Existe-t-il un moyen simple de vérifier s'il y a des clés en double avec Doctrine 2 avant de faire un flush ?

1 votes

Je n'ai pas vraiment de réponse, mais je me demande en quoi le fait de vérifier avant un vidage est si différent que de faire le vidage et de traiter l'erreur (en supposant qu'une clé dupliquée existe).

0 votes

Lors d'une purge, des exceptions spécifiques à la base de données seront lancées.

5 votes

La plupart des solutions présentées ici ne prennent pas en compte le fait que vous venez de ne peut pas vérifier les doublons au préalable, car il ne s'agit pas d'une opération atomique et, par conséquent, vous pouvez todavía ont des valeurs dupliquées, si un autre thread insère dans la table, par exemple. Les seules solutions possibles à mon avis sont soit de gérer l'échec manuellement, soit d'utiliser le verrouillage. La première solution est plutôt moche avec Doctrine (car l'EM est fermé), la seconde peut avoir des conséquences désastreuses sur les performances, si vous n'êtes pas prudent. J'aimerais moi-même trouver une bonne réponse à cette question.

21voto

Peter Johnson Points 1156

J'utilise cette stratégie pour vérifier les contraintes uniques après que flush() Ce n'est peut-être pas ce que vous voulez, mais ça pourrait aider quelqu'un d'autre.


Lorsque vous appelez flush() si une contrainte unique échoue, une PDOException est lancé avec le code 23000 .

try {
    // ...
    $em->flush();
}
catch( \PDOException $e )
{
    if( $e->getCode() === '23000' )
    {
        echo $e->getMessage();

        // Will output an SQLSTATE[23000] message, similar to:
        // Integrity constraint violation: 1062 Duplicate entry 'x'
        // ... for key 'UNIQ_BB4A8E30E7927C74'
    }

    else throw $e;
}

Si vous avez besoin d'obtenir le nom de la colonne défaillante :

Créer des indices de table avec des noms préfixés, par exemple 'unique_'.

 * @Entity
 * @Table(name="table_name",
 *      uniqueConstraints={
 *          @UniqueConstraint(name="unique_name",columns={"name"}),
 *          @UniqueConstraint(name="unique_email",columns={"email"})
 *      })

NE PAS spécifier que vos colonnes sont uniques dans la définition de @Column.

Cela semble remplacer le nom de l'index par un nom aléatoire...

 **ie.** Do not have 'unique=true' in your @Column definition

Après avoir régénéré votre table (vous devrez peut-être l'abandonner et la reconstruire), vous devriez être en mesure d'extraire le nom de la colonne à partir du message d'exception.

// ...
if( $e->getCode() === '23000' )
{
    if( \preg_match( "%key 'unique_(?P<key>.+)'%", $e->getMessage(), $match ) )
    {
        echo 'Unique constraint failed for key "' . $match[ 'key' ] . '"';
    }

    else throw $e;
}

else throw $e;

Pas parfait, mais ça marche...

4 votes

Je suppose que Doctrine a modifié le traitement des exceptions il y a quelque temps. Je reçois une PDOException à l'intérieur d'un \Doctrine\DBAL\DBALException pour cette situation. Le code ci-dessus serait quelque chose comme catch( \Doctrine\DBAL\DBALException $e ){ if( $e->getPrevious()->getCode() === '23000' ) { /* faire des choses */ } }. Il est important de noter que la capture de cette exception est le seul moyen de faire face à certaines situations de forte concurrence. Une requête de sélection pour la validation n'est tout simplement pas suffisante.

4voto

Vašek Purchart Points 21

J'ai également rencontré ce problème il y a quelque temps. Le problème principal n'est pas les exceptions spécifiques à la base de données, mais le fait que lorsqu'une PDOException est levée, l'EntityManager est fermé. Cela signifie que vous ne pouvez pas être sûr de ce qui va se passer avec les données que vous voulez vider. Mais il est probable qu'elles ne seront pas sauvegardées dans la base de données car je pense que cela se fait dans le cadre d'une transaction.

En réfléchissant à ce problème, j'ai donc trouvé une solution, mais je n'ai pas encore eu le temps de l'écrire.

  1. On peut le faire en utilisant auditeurs d'événements en particulier l'événement onFlush. Cet événement est invoqué avant que les données ne soient envoyées à la base de données (après que les ensembles de modifications aient été calculés - vous savez donc déjà quelles entités ont été modifiées).
  2. Dans cet écouteur d'événements, vous devrez rechercher les clés de toutes les entités modifiées (pour les primaires, vous devrez rechercher @Id dans les métadonnées de la classe).
  3. Ensuite, vous devrez utiliser une méthode de recherche avec les critères de vos clés. Si vous trouvez un résultat, vous avez la possibilité de lancer votre propre exception, qui ne fermera pas l'EntityManager et vous pouvez l'attraper dans votre modèle et faire quelques corrections aux données avant de réessayer le flush.

Le problème de cette solution serait qu'elle pourrait générer un grand nombre de requêtes à la base de données, ce qui nécessiterait une optimisation importante. Si vous voulez utiliser ce genre de chose seulement à quelques endroits, je vous recommande de faire la vérification à l'endroit où le duplicata pourrait se produire. Par exemple, lorsque vous voulez créer une entité et l'enregistrer :

$user = new User('login');
$presentUsers = $em->getRepository('MyProject\Domain\User')->findBy(array('login' => 'login'));
if (count($presentUsers)>0) {
    // this login is already taken (throw exception)
}

0 votes

Cette méthode n'est pas non plus sûre du point de vue de la concurrence. Si vous l'implémentez, vous risquez d'obtenir des exceptions en double lors de la purge.

4voto

Mark Fox Points 1665

Si vous utilisez Symfony2, vous pouvez utiliser UniqueEntity( ) con form->isValid() pour attraper les doublons avant flush().

J'hésite à poster cette réponse ici, mais elle semble précieuse puisque beaucoup des utilisateurs de Doctrine utiliseront également Symfony2. Pour être clair : ceci utilise la classe de validations de Symfony qui, sous le capot, utilise un référentiel d'entités pour vérifier (c'est configurable mais la valeur par défaut est findBy ).

Sur votre entité, vous pouvez ajouter l'annotation :

use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;

/**
 * @UniqueEntity("email")
 */
class YourEntity {

Ensuite, dans votre contrôleur, après avoir transmis la requête au formulaire, vous pouvez vérifier vos validations.

$form->handleRequest($request);

if ( ! $form->isValid())
{
    if ($email_errors = $form['email']->getErrors())
    {
        foreach($email_errors as $error) {
           // all validation errors related to email
        }
    }
…

Je vous recommande de combiner cette réponse avec celle de Peter, puisque le schéma de votre base de données devrait également imposer l'unicité :

/**
 * @UniqueEntity('email')
 * @Orm\Entity()
 * @Orm\Table(name="table_name",
 *      uniqueConstraints={
 *          @UniqueConstraint(name="unique_email",columns={"email"})
 * })
 */

2voto

Si vous voulez juste attraper les erreurs dupliquées. Vous ne devez pas seulement vérifier le numéro de code

$e->getCode() === '23000'

car cela permettrait de détecter d'autres erreurs, comme le fait que le champ 'user' ne peut être vide. Ma solution est de vérifier le message d'erreur, s'il contient le texte 'Duplicate entry'.

                try {
                    $em->flush();
                } catch (\Doctrine\DBAL\DBALException $e) {

                    if (is_int(strpos($e->getPrevious()->getMessage(), 'Duplicate entry'))) {
                        $error = 'The name of the site must be a unique name!';
                    } else {
                        //....
                    }
                }

2voto

Aris Points 1407

Dans Symfony 2, il jette en fait un \Exception et non un \PDOException

try {
    // ...
    $em->flush();
}
catch( \Exception $e )
{
   echo $e->getMessage();
   echo  $e->getCode(); //shows '0'
   ### handle ###

}

$e->getMessage() renvoie un message du type suivant :

Une exception s'est produite lors de l'exécution de 'INSERT INTO (...) VALUES ( ?, ?, ?, ?)' avec les paramètres [...] :

SQLSTATE [23000] : Violation de la contrainte d'intégrité : 1062 Duplicate entry '...' for key 'PRIMARY' (doublon de l'entrée '...')

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X