Doctrine2 ORM のロック機構について

参考情報 : 9. Transactions and Concurrency — Doctrine 2 ORM 2.0.0 documentation

  • 楽観的ロック と 悲観的ロックがあります
    • 楽観的ロック : LockMode::OPTIMISTIC + version指定(任意)
    • 悲観的ロック : LockMode::PESSIMISTIC_WRITE
  • 楽観的ロックの場合、Entity と DBスキーマに、int型 または timestamp型の「version」カラムが必要です。

    Version numbers (not timestamps) should however be preferred as they can not potentially conflict in a highly concurrent environment, unlike timestamps where this is a possibility, depending on the resolution of the timestamp on the particular database platform.

  • 楽観的ロックで、versionカラムが無い場合は、OptimisticLockException(Cannot obtain optimistic lock on unversioned entity.)が発生します。
  • 楽観的ロックは、「処理の前後でversionが一致しているかどうか」をチェックし、競合した場合(versionが不一致)は「他の誰かによってEntityが更新された(versionが更新された)」ことを示すOptimisticLockExceptionが発生する、という仕組みです。
    → flush()によってEntityのversionのチェックとversionカラム値が更新されます。
doctrine.DEBUG: SELECT
                  t0.version AS version1, t0.id AS id2, t0.name AS name3,
                  t0.price AS price4, t0.description AS description5
                FROM
                  products t0
                WHERE
                  t0.id = ? [2]
doctrine.DEBUG: "START TRANSACTION"
doctrine.DEBUG: UPDATE
                  products
                SET
                  price = ?, version = version + 1
                WHERE
                  id = ? AND version = ? [800,2,2]
doctrine.DEBUG: SELECT version FROM products WHERE id = ? [2]
doctrine.DEBUG: "COMMIT"
  • 悲観的ロックを使う場合は、オートコミットを無効にして、明示的にトランザクションを開始する($em->beginTransaction())必要があります。
    → 明示的にトランザクションを開始しない場合、TransactionRequiredException(An open transaction is required for this operation.)が発生します。
  • 悲観的ロックには2つのモードが用意されています。
    • PESSIMISTIC_WRITE : 読み書きのために行ロックします。「select ... for update」が発行されます
    • PESSIMISTIC_READ

Entityのマッピング定義

use Doctrine\ORM\Mapping as ORM;

/**
 * @ORM\Entity
 * @ORM\Table(name="products")
 */
class Product
{
    /**
     * @ORM\Version
     * @ORM\Column(type="integer")
     */
    private $version;

    ...

検証用コード

use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Doctrine\DBAL\LockMode;
use Doctrine\ORM\OptimisticLockException;
use Xxxx\SampleBundle\Entity\Product;

class TestDoctrineOptimisticLockCommand extends ContainerAwareCommand
{

    protected function configure()
    {
        $this->setName('sample:doctrine-optimistic-lock');
    }

    protected function execute(InputInterface $input, OutputInterface $output)
    {
        try {

            $em = $this->getContainer()->get('doctrine')->getManager();

//             $em->beginTransaction();

            $repository = $em->getRepository('XxxxBundle:Product');

            $product = $repository->find(2, LockMode::OPTIMISTIC);
//             $product = $repository->find(2, LockMode::PESSIMISTIC_WRITE);

            $em->flush();

        } catch (OptimisticLockException $e){

            echo "caught OptimisticLockException -- " . $e->getMessage();

        } catch (\Exception $e) {

            echo 'caught Exception - ' . $e->getMessage();

        }

        $em->close();
    }
}

メモ

  • Symfony2 Consoleコマンド
    • $ php app/console doctrine:schema:update --force
  • MySQL操作
    • alter table products add version int;
    • alter table products drop column version;