2010-10-19 28 views
43

C'è un modo semplice per verificare la presenza di chiavi duplicate con Doctrine 2 prima di eseguire un flush?Controllo di chiavi duplicate con Doctrine 2

+1

io in realtà non hanno una risposta, ma mi chiedo come il controllo prima un colore che è diverso che fare il colore e la gestione dell'errore (assumendo una chiave duplicata esiste). –

+0

Su un flush verranno generate eccezioni specifiche del database. – tom

+3

La maggior parte delle soluzioni presentate qui non tiene conto del fatto che * non è possibile * controllare preventivamente i duplicati, poiché non si tratta di un'operazione atomica e pertanto è possibile * avere * ancora valori duplicati, se altri inserimenti di thread nel tavolo, per esempio. Quindi le uniche soluzioni possibili nella mia mente sono la gestione manuale dell'errore o l'uso del blocco. Il primo è piuttosto brutto con Doctrine (dato che il EM si chiude), quest'ultimo può avere conseguenze terribili in termini di prestazioni, se non si presta attenzione. Mi piacerebbe vedere una buona risposta a questo me stesso. –

risposta

32

è possibile prendere il UniqueConstraintViolationException in quanto tale:

use Doctrine\DBAL\Exception\UniqueConstraintViolationException; 

// ... 

try { 
    // ... 
    $em->flush(); 
} 
catch (UniqueConstraintViolationException $e) { 
    // .... 
} 
+0

Questo è stato aggiunto nel 2014. Questo dovrebbe essere il modo in cui farlo ora. – tom

+0

Questo è disponibile dal momento che Doctrine DBAL 2.5 - UniqueConstraintViolationException eredita da ConstraintViolationException vedere: https://github.com/doctrine/dbal/blob/master/lib/Doctrine/DBAL/Exception/ConstraintViolationException.php # L27 –

+0

Per la versione corrente, prendi invece questo: \ Doctrine \ DBAL \ Exception \ UniqueConstraintViolationException – nicolallias

4

Mi sono imbattuto in questo problema qualche tempo fa. Il problema principale non sono eccezioni specifiche del database ma il fatto, quando viene lanciata una PDOException, viene chiuso EntityManager. Ciò significa che non puoi essere sicuro di cosa accadrà con i dati che desideri scaricare. Ma probabilmente non verrebbe salvato nel database perché penso che ciò avvenga all'interno di una transazione.

Così, quando stavo pensando a questo problema, mi è venuta in mente questa soluzione, ma non avevo ancora il tempo di scriverlo.

  1. È possibile utilizzare event listeners, in particolare l'evento onFlush. Questo evento viene richiamato prima che i dati vengano inviati al database (dopo che i changeset sono stati calcolati, in modo da sapere già quali entità sono state modificate).
  2. In questo listener di eventi dovresti sfogliare tutte le entità modificate per le loro chiavi (per le ricerche primarie verrà visualizzato nei metadati della classe per @Id).
  3. Quindi è necessario utilizzare un metodo di ricerca con i criteri delle chiavi. Se doveste trovare un risultato, avete la possibilità di lanciare la vostra eccezione, che non chiuderà EntityManager e sarete in grado di catturarlo nel vostro modello e apportare alcune correzioni ai dati prima di provare nuovamente il flush.

Il problema con questa soluzione potrebbe essere che potrebbe generare un sacco di query sul database, quindi richiederebbe un sacco di ottimizzazione. Se si desidera utilizzare tale cosa solo in alcuni punti, si consiglia di eseguire il controllo sul luogo in cui potrebbe verificarsi il duplicato. Così, per esempio, dove si desidera creare un'entità e salvarlo:

$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

Anche questo non è sicuro dalla concorrenza. Se lo implementate, potresti comunque ottenere duplicati di eccezioni su flush. –

18

Io uso questa strategia per verificare la presenza di vincoli univoci dopo flush(), non può essere quello che vuoi, ma potrebbe aiutare qualcun altro.


quando si chiama flush(), se un vincolo unico fallisce, un PDOException è gettato con il codice 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; 
} 

Se avete bisogno di ottenere il nome della colonna in mancanza:

Creare indici tabella con i nomi prefissati, ad esempio. 'Unique_'

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

non specificano le colonne come unico nella definizione @Column

Questo sembra ignorare il nome di indice con uno a caso ...

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

Dopo aver rigenerare il tavolo (potrebbe essere necessario farlo cadere & ricostruire), si dovrebbe essere in grado di estrarre il nome della colonna dal messaggio di eccezione.

// ... 
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; 

Non perfetto, ma funziona ...

+3

Immagino che Doctrine abbia cambiato la gestione delle eccezioni qualche tempo fa. Ricevo un PDOException all'interno di una \ Doctrine \ DBAL \ DBALException per questa situazione. Il codice sopra sarebbe qualcosa come catch (\ Doctrine \ DBAL \ DBALException $ e) {if ($ e-> getPrevious() -> getCode() === '23000') {/ * do stuff * /}}. È importante notare che la cattura di questa eccezione è l'unico modo per gestire alcune situazioni con concorrenza elevata. Una query selezionata per la convalida non è sufficiente –

0

Vorrei aggiungere a questo specifico riguardo PDOExceptions--

Il codice di errore è 23000 codice coperta per una famiglia di Violazioni di vincoli di integrità che MySQL può restituire.

Pertanto, la gestione del codice di errore 23000 non è abbastanza specifica per alcuni casi d'uso.

Ad esempio, è possibile che si desideri reagire in modo diverso a una violazione di record duplicato piuttosto che a una violazione di chiave esterna mancante.

Ecco un esempio di come trattare con questo:

try { 
    $pdo -> executeDoomedToFailQuery(); 
} catch(\PDOException $e) { 
    // log the actual exception here 
    $code = PDOCode::get($e); 
    // Decide what to do next based on meaningful MySQL code 
} 

// ... The PDOCode::get function 

public static function get(\PDOException $e) { 
    $message = $e -> getMessage(); 
    $matches = array(); 
    $code = preg_match('/ (\d\d\d\d)/', $message, $matches); 
    return $code; 
} 

mi rendo conto che questo non è il più dettagliato la questione è stato chiesto, ma ho trovato questo è molto utile in molti casi, e non è Doctrine2 specifica .

0

Il modo più semplice dovrebbe essere questo:

$product = $entityManager->getRepository("\Api\Product\Entity\Product")->findBy(array('productName' => $data['product_name'])); 
if(!empty($product)){ 
// duplicate 
} 
+0

Questo non è molto sicuro in un ambiente ad alta concorrenza, come controllare se i nomi utente sono già stati registrati in un sito web popolare. – Andrew

0

ho usato questo e sembra funzionare. Restituisce il numero di errore specifico di MySQL, ad esempio 1062 per una voce duplicata, pronto per essere gestito come desideri.

try 
{ 
    $em->flush(); 
} 
catch(\PDOException $e) 
{ 
    $code = $e->errorInfo[1]; 
    // Do stuff with error code 
    echo $code; 
} 

Ho testato questo con pochi altri scenari e rinvierà altri codici anche come 1146 (Tabella inesistente) e 1054 (Unknown column).

2

Se si desidera rilevare errori duplicati. Non si deve solo controllare il numero di codice

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

perché questo prenderà altri errori come 'utente' campo non può essere vuoto. La mia soluzione è quello di verificare il messaggio di errore, se contiene il testo 'voce duplicata'

   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 { 
         //.... 
        } 
       } 
2

In Symfony 2, in realtà genera un \ Exception, non un \ PDOException

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

} 

$ e- > getMessage() Echos qualcosa come segue:

è verificata un'eccezione durante l'esecuzione di 'INSERT INTO (...) VALORI (,,,????)' con params [...]:

SQLSTATE [23000]: integrità violazione del vincolo: 1.062 voce duplicata '...' per la chiave 'primaria'

3

Se stai usando Symfony2 è possibile utilizzare UniqueEntity(…) con form->isValid() per catturare i duplicati prima di lavare().

Sono sulla recinzione che posta questa risposta qui ma sembra preziosa dal un sacco di di utenti Doctrine useranno anche Symfony2. Per essere chiari: utilizza la classe di convalida di Symfony che sotto il cofano sta usando un repository di entità per verificare (è configurabile ma il valore predefinito è findBy).

Sul entità è possibile aggiungere l'annotazione:

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

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

Poi nel controller, dopo aver consegnato la richiesta al modulo è possibile controllare le vostre convalide.

$form->handleRequest($request); 

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

Mi raccomando che combina questo con la risposta di Pietro, dal momento che lo schema del database dovrebbe valere anche unicità:

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

Se l'esecuzione di una query SELECT prima l'inserto non è ciò che si vuole, si può esegue solo flush() e cattura l'eccezione.

In Dottrina DBAL 2.3, si può capire in modo sicuro un errore di vincolo univoco, cercando in codice di errore di eccezione DOP (23000 per MySQL, 23050 per Postgres), che è avvolto dalla Dottrina DBALException:

 try { 
      $em->flush($user); 
     } catch (\Doctrine\DBAL\DBALException $e) { 
      if ($e->getPrevious() && 0 === strpos($e->getPrevious()->getCode(), '23')) { 
       throw new YourCustomException(); 
      } 
     } 
+0

In realtà il codice 23000 non è specifico per errori di vincoli univoci. Esempio tratto dal [documento MySQL] (https://dev.mysql.com/doc/refman/5.5/en/error-messages-server.html): Errore: 1048 SQLSTATE: 23000 (ER_BAD_NULL_ERROR) Messaggio: colonna '% s' non può essere nullo – Emilie

+0

@emile fa quell'errore restituisce un'eccezione con il codice 23000, o è solo un numero interno di MySQL? –