Transactions et concurrence Symfony2

http://guidella.free.fr/General/symfony2TransactionsAndConcurrency.html


Table des matières


Référence

http://docs.doctrine-project.org/projects/doctrine-orm/en/2.1/reference/transactions-and-concurrency.html

La démarcation des transactions

La démarcation des transactions est la tâche de définir les limites de votre transaction. La démarcation des transactions correct est très importante car si elle n’est pas effectuée correctement, cela peut affecter négativement les performances de votre application. Beaucoup de bases de données et des couches d’abstraction de bases de données comme les PDE, fonctionne par défaut en mode auto-commit, ce qui signifie que chaque instruction SQL unique est enveloppée dans une petite transaction. Sans aucune démarcation des transactions explicites de votre côté, cela se traduit rapidement par de mauvaises performances car les transactions ne sont pas pas couteuses.

Pour la plupart, Doctrine2 prend déjà soin de la démarcation des transactions pour vous même. Toutes les opérations d’écriture (INSERT/UPDATE/DELETE) sont mis en attente jusqu’à ce que EntityManager#flush() soit invoqué qui enveloppe l’ensemble de ces modifications en une seule transaction.

Cependant, Doctrine2 permet aussi (et encourage) la prise en charge et le contrôle de démarcation des transactions par vous-même.

Ce sont deux façons de traiter les transactions lorsque vous utilisez l’ORM Doctrine qui sont maintenant décrits plus en détail.

Approche 1: Implicitement

La première approche est d’utiliser la gestion des transactions implicites fournies par l’EntityManager Doctrine ORM. Vu le code suivant, sans aucune démarcation des transactions explicites:

<?php
// $em instanceof EntityManager
$user = new User;
$user->setName('George');
$em->persist($user);
$em->flush();

Puisque nous ne faisons pas de la démarcation des transactions personnalisées dans le code ci-dessus, EntityManager#flush() commencera et commit/rollback une transaction. Ce comportement est rendu possible par le regroupement des opérations DML par l’ORM Doctrine et est suffisante si toutes les manipulations de données qui fait partie d’une unité de travail passe par le modèle du domaine et donc l’ORM.

Approche 2: Explicitement

L’alternative est d’utiliser explicitement l’API Doctrine\DBAL\Connection directement pour contrôler les limites de transaction. Le code ressemble alors à ceci:

<?php
// $em instanceof EntityManager
$em->getConnection()->beginTransaction(); // suspend auto-commit
try {
    //... do some work
    $user = new User;
    $user->setName('George');
    $em->persist($user);
    $em->flush();
    $em->getConnection()->commit();
} catch (Exception $e) {
    $em->getConnection()->rollback();
    $em->close();
    throw $e;
}

La démarcation des transactions explicite est nécessaire lorsque vous souhaitez inclure personnalisée opérations DBAL dans une unité de travail ou lorsque vous voulez faire usage de certaines méthodes de l’API EntityManager qui nécessitent une transaction active. Ces méthodes seront dans un throw avec une gestion d’exeception : TransactionRequiredException pour vous informer d’une erreur.

Une alternative plus pratique pour la démarcation des transactions explicite est l’utilisation d’abstractions de contrôle prévus dans le formulaire de connexion # transactionnelles (func $) et Connection#transactional($func). Lorsqu’ils sont utilisés, ces abstractions de contrôle vous assurent que vous n’oubliez jamais de rollback de la transaction ou fermer l’EntityManager, en dehors de la réduction du code évidente. Un exemple qui est fonctionnellement équivalent au code montré précédemment se présente comme suit:

<?php
// $em instanceof EntityManager
$em->transactional(function($em) {
    //... do some work
    $user = new User;
    $user->setName('George');
    $em->persist($user);
});

La différence entre la Connection#transactional($func) et EntityManager#transactional($func) est que les dernières abstraction “flushées” par l’EntityManager avant validation de la transaction ferme aussi l’EntityManager correctement quand une exception se produit (en plus de faire reculer la transaction).

1.3. Exception Handling

Lorsque vous utilisez la démarcation des transactions implicites et qu’une exception survienne durant EntityManager#flush(), la transaction est automatiquement annulée et l’EntityManager fermé.

Lorsque vous utilisez la démarcation des transactions explicites et qu’une exception se produise, la transaction devrait être annulées immédiatement et l’ EntityManager fermés en invoquant EntityManager#close() et rejetée ensuite, comme démontré dans l’exemple ci-dessus. Cela sera manipulé avec élégance par les abstractions de contrôle montré précédemment. Notez que lorsque la capture d’exception agit vous devrez généralement relever l’exception. Si vous avez l’intention de ne remettre que certaines exceptions, il faudra les attraper de façon explicite dans des blocs catch tôt (mais n’oubliez pas de rollback de la transaction et fermer l’EntityManager là aussi). Toutes les meilleures pratiques d’autres de gestion des exceptions s’appliquent par analogie (c’est à dire soit vous connecter ou de rejeter, pas les deux, etc.)

En conséquence de cette procédure, toutes les instances précédemment gérés ou supprimés de l’EntityManager se détachent. L’état des objets détachés sera l’état au moment où la transaction aura été annulée. L’état des objets n’est en aucune façon annulées et donc les objets sont maintenus hors de la synchronisation avec la base de données. L’application peut continuer à utiliser les objets détachés, sachant que leur état est potentiellement plus précis.

Si vous avez l’intention de commencer une autre unité de travail après qu’une exception sse soit produite, vous devriez le faire avec une nouvelle EntityManager.

Verrouillage de soutien

Doctrine2 offre un support pour les stratégies de verrouillage pessimistes et optimistes nativement. Cela permet de prendre un contrôle très fin sur ce type de verrouillage qui est requis pour vos entités dans votre application.

Verrouillage optimiste

Base de données des transactions sont très bien pour le contrôle d’opérations concurrente lors d’une demande unique. Cependant, une transaction ne doit pas s’étendre sur plusieurs demandes, le soi-disant «utilisateur pense que le temps”. Par conséquent, une «transaction commerciale» de longue durée qui s’étend sur plusieurs demandes doit impliquer plusieurs transactions BDD. Ainsi, les transactions de bases de données seul peuvent mieux contrôler la simultanéité que lors d’une telle transaction commerciale de longue durée. Le contrôle d’opérarions concurrentes deviennent partiellement la responsabilité de l’application elle-même.

La doctrine a un support intégré pour le verrouillage optimiste automatique via une simple requête. Dans cette approche toute entité qui doit être protégé contre les modifications concurrentes lors des transactions d’affaires de longue durée devient un champ de version qui est soit un nombre simple (type de mapping: entier) ou un timestamp (mappage de type: datetime). Lorsque des modifications d’une telle entité persistent à la fin d’une transaction de longue durée, la version de l’entité est comparée à la version de la base de données et si elles ne correspondent pas, une erreur d’exception OptimisticLockException est envoyé, ce qui indique que l’entité a été modifiée par quelqu’un d’autre déjà.

Vous désignez un champ de version dans une entité comme suit. Dans cet exemple nous allons utiliser un nombre entier.

<?php
class User
{
    // ...
    /** @Version @Column(type="integer") */
    private $version;
    // ...
}

Alternativement, un type datetime peut être utilisé (qui correspond à un timestamp ou datetime SQL):

<?php
class User
{
    // ...
    /** @Version @Column(type="datetime") */
    private $version;
    // ...
}

Les numéros de version (non horodatages) devraient toutefois être préférées, ils ne peuvent pas entrer en conflit potentiel dans un environnement hautement simultanées, contrairement à l’horodatage, selon la résolution de l’horodatage sur la plate-forme de bases de données particulier.

Quand un conflit de version est rencontré lors d’un EntityManager#flush(), une OptimisticLockException est levée et la transaction active annulées (ou marqués pour rollback). Cette exception peut être attrapés et manipulés. Les réponses possibles à une OptimisticLockException sont de présenter le conflit à l’utilisateur ou de rafraîchir ou recharger les objets dans une nouvelle transaction et ensuite retenter l’opération.

Avec PHP qui promeut une architecture à mémoire distribuée, le temps entre une mise à jour et la modification réelle l’entité peut dans le pire scénario être aussi long que votre délai de session des applications. Si des changements se produisent pour l’entité dans ce laps de temps, vous voulez savoir directement lors de la récupération de l’entité qui vous a envoyé une exception du verrouillage optimiste:

Vous pouvez toujours vérifier la version d’une entité au cours d’une demande, soit lors de l’appel EntityManager#find()

<?php
use Doctrine\DBAL\LockMode;
use Doctrine\ORM\OptimisticLockException;

$theEntityId = 1;
$expectedVersion = 184;

try {
    $entity = $em->find('User', $theEntityId, LockMode::OPTIMISTIC, $expectedVersion);

    // do the work

    $em->flush();
} catch(OptimisticLockException $e) {
    echo "Sorry, but someone else has already changed this entity. Please apply the changes again!";
}

Ou vous pouvez utiliser EntityManager#lock() à savoir:

<?php
use Doctrine\DBAL\LockMode;
use Doctrine\ORM\OptimisticLockException;

$theEntityId = 1;
$expectedVersion = 184;

$entity = $em->find('User', $theEntityId);

try {
    // assert version
    $em->lock($entity, LockMode::OPTIMISTIC, $expectedVersion);

} catch(OptimisticLockException $e) {
    echo "Sorry, but someone else has already changed this entity. Please apply the changes again!";
}

Notes importante implémentation

Vous pouvez facilement obtenir un workflow de verrouillage optimiste incorrect si vous comparez les versions erronées. Disons que vous avez Alice et Bob qui accédent à un compte bancaire hypothétique:

  • Alice lit le titre de l’article de blog étant «Foo», en version verrou optimiste 1 (Requête GET)
  • Bob lit le titre de l’article de blog étant «Foo», en version verrou optimiste 1 (Requête GET)
  • Bob met à jour le titre de «Bar», la mise à niveau la version verrou optimiste à 2 (demande POST de formulaire)
  • Alice met à jour le titre de «Baz», … (Demande POST de formulaire)

Maintenant à la dernière étape de ce scénario, le blog doit être lu à nouveau dans la base afin de permettre que l’update d’Alice peut être appliquée. À ce stade, vous voulez vérifier si le blog est encore à la version 1 (ce qui n’est pas dans ce scénario).

Utiliser le verrouillage optimiste correctement, vous devez ajouter la version comme un champ supplémentaire caché (ou dans la session pour plus de sécurité). Sinon, vous ne pouvez pas vérifier si la version est toujours celle qui est à l’origine lu dans la base quand Alice fait son requête GET pour le billet de blog. Si cela vous arrive, vous pourriez voir les mises à jour perdues, vous avez voulu prévenir avec verrou optimiste.

Voir l’exemple de code, la forme (Requête GET):

<?php
$post = $em->find('BlogPost', 123456);

echo '<input type="hidden" name="id" value="' . $post->getId() . '" />';
echo '<input type="hidden" name="version" value="' . $post->getCurrentVersion() . '" />';

Et l’action headline de changement (requête POST):

<?php
$postId = (int)$_GET['id'];
$postVersion = (int)$_GET['version'];

$post = $em->find('BlogPost', $postId, \Doctrine\DBAL\LockMode::OPTIMISTIC, $postVersion);

Verrouillage pessimiste

Doctrine2 supporte le verrouillage pessimiste au niveau base de données. Aucune tentative n’est faite pour mettre en œuvre le verrouillage pessimiste intérieur de doctrine, plutôt spécifiques au fournisseur et les commandes ANSI-SQL sont utilisées pour acquérir les verrous de niveau ligne. Chaque entité peut faire partie d’une serrure pessimiste, aucune métadonnées particulière n’est requisent pour utiliser cette fonctionnalité.

Toutefois, pour travailler avec le verrouillage pessimiste, vous devez désactiver le mode auto-commit de votre base de données et commencer une transaction autour de votre verrouillage pessimistes en utilisant “Approach 2: Explicit Transaction Demarcation” décrit ci-dessus. Doctrine2 va lever une exception si vous essayez d’acquérir un verrou pessimiste et qu’aucune transaction n’est en cours.

Doctrine2 soutient actuellement deux modes de verrouillage pessimiste:

  • Ecriture pessimiste (Doctrine\DBAL\LockMode::PESSIMISTIC_WRITE), verrouille les lignes de bases de données sous-jacente pour lire et écrire les opérations simultanées.
  • Lecture pessimiste (Doctrine\DBAL\LockMode::PESSIMISTIC_READ), locks les autres verrous de demandes simultanées qui tentent de mettre à jour ou de verrouiller des lignes en mode écriture.

Vous pouvez utiliser les verrous pessimistes dans trois scénarios différents:

  • Using
    • EntityManager#find($className, $id,\Doctrine\DBAL\LockMode::PESSIMISTIC_WRITE)
    • EntityManager#find($className, $id, \Doctrine\DBAL\LockMode::PESSIMISTIC_READ)
  • Using
    • EntityManager#lock($entity, \Doctrine\DBAL\LockMode::PESSIMISTIC_WRITE)
    • EntityManager#lock($entity, \Doctrine\DBAL\LockMode::PESSIMISTIC_READ)
  • Using
    • Query#setLockMode(\Doctrine\DBAL\LockMode::PESSIMISTIC_WRITE)
    • Query#setLockMode(\Doctrine\DBAL\LockMode::PESSIMISTIC_READ)