Aylık Arşiv: Kasım 2014

Doctrine 2 ile Optimistic Locking

Yoğun editör işlemlerinin olduğu projelerde aynı yazıyı içeriği güncelleme problemleri ile sıkça karşılaşılır.

Örnek senaryo;

  • 1. editör 1. yazıyı güncellemek için açtı.
  • 2. editör 1. yazıyı güncellemek için açtı.
  • 1. editör 1. yazıyı güncelledi.
  • 2. editör 1. yazıyı güncelledi(!).

Son değişikliği 2. editör yaptığı için 1. editörün yaptığı değişiklikler silindi. Bunu önlemek için 2. editöre “Senden önce 1. editör bu yazıyı düzenledi. Önce onun değişikliklerine bakmalısın.” demek gerek.

Peki bu uyarı sistemini ne ile kuracağız?
Optimistic Locking yöntemi ile.

Kısaca bu yöntemi şu şekilde çalışır:

Tablo ismimiz Post olsun. Post tablosuna “version” isminde bir sütun daha ekleyeceğiz. İlk insert işleminde version sütununa 1 yazılır. Her yazı güncellemesinde version sütunundaki sayı 1 arttırılır.

Kullanıcı içeriği güncellediğinde versiyon sayısı güncellemeden önceki sayı ile aynı değilse içerik daha önce birileri tarafından güncellenmiştir.

PHP’de bu işlemleri araya herhangi bir ORM koymadan halledebilirsiniz. Ancak sizin yerinize Doctrine 2 versiyonlama işlemini destekliyor.

Yapmanız gereken Post entity’nize bir @Version annotation’ı eklemeniz.

Örnek olarak hazırladığım Post ismindeki entity’e buraya tıklayarak ulaşabilirsiniz.

Entity sınıfında gerekli versiyonlama için düzenlemeyi yaptık.

Bu Post entity sınıfını kullanarak yazdığım örnek bir Symfony 2 controller’ı da şu şekilde:

namespace Acme\BlogBundle\Controller;

use Acme\BlogBundle\Entity\Post;
use Acme\BlogBundle\Form\PostType;
use Doctrine\DBAL\LockMode;
use Doctrine\ORM\OptimisticLockException;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Method;
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
use Symfony\Component\HttpFoundation\Request;

class DefaultController extends Controller
{
    /**
     * @Route("/post/{id}")
     * @Template()
     */
    public function showAction($id)
    {
        $em = $this->get('doctrine')->getManager();
        $entity = $em->find('Acme\BlogBundle\Entity\Post', $id, LockMode::OPTIMISTIC);
        $form = $this->createForm(new PostType(), $entity, ['action' => $this->generateUrl('update_action')]);
        return $this->render('AcmeBlogBundle:Default:show.html.twig', ['form' => $form->createView()]);

    }

    /**
     * @Route("/update", name="update_action")
     * @Method({"POST"})
     */
    public function updateAction(Request $request) {

        $post = new Post();

        $form = $this->createForm(new PostType(), $post);

        $form->handleRequest($request);

        if($form->isValid()) {
            $formData = $form->getData();
            $post->setId($formData->getId());
            $post->setTitle($formData->getTitle());
            $post->setContent($formData->getContent());
            $post->setVersion($formData->getVersion());

            try {
                $em = $this->getDoctrine()->getManager();
                $em->merge($post);
                $em->flush();
            } catch(OptimisticLockException $e) {
                return $this->render('AcmeBlogBundle:Default:locking.html.twig');
            }

            return $this->render('AcmeBlogBundle:Default:success.html.twig');

        }
            return $this->render('AcmeBlogBundle:Default:error.html.twig');

    }
}

22. satırda Optimistic Locking kullanarak find işlemini yapıyoruz. 51. satırda OptimisticLockException ismindeki Exception için bir kural yazılı. Doctrine 2 bizim yerimize version sütununu kontrol ediyor, eğer değer aynı değilse OptimisticLockException isminde bir Exception fırlatıyor.

Örneğin; /post/5 yolunu iki ayrı browser tabında açıp, ikisinde de güncelleme yapılsın. İlk güncelleme çalışacaktır ve 5 numaralı satırın version sütununu 2‘ye yükselecektir. İkinci tabdan güncelleme yapıldığında orada version bilgisi 1 olarak kaldığı için OptimisticLockException‘ı fırlayacaktır.

Bazı kaynaklarda versiyonlama sütunlarını date time veya timestamp olarak da tutulmasından bahsedilebilir. Ancak olası zaman kaymaları için bu yöntem önerilmez.

Ayrıca Bkz.: Dirty read

Not: Symfony 2’de Doctrine 2 varsayılan olarak geldiği için Symfony 2 controller örneği verdim. Doctrine 2’yi başka frameworklerde de kullanabilirsiniz.

 
11 Kudos
Don't move

PHP – Identity Map Pattern

$user1 = User::find(1);
$user  = new User();
$user1 = $user->find(1);

Bunlar ve buna benzer kullanımlar PHP içerisinde sıkça görebileceğiniz kullanıcı çekme yöntemleridir. User sınıfındaki find() metotu size bir UserRepository (ismi salladım) nesnesi döndürür oradan işlem yaparsınız.

Örneğin; Runtime’da iki alakasız yerde 1 numaralı kullanıcının veritabanındaki bilgilerine ihtiyacınız var.

Birinci yerde User::find(1) yaptınız ve SELECT sorgusu çalıştırdınız. Kodun farklı bir noktasında tekrar User::find(1) yaptınız ve tekrar SELECT sorgusu işlendi.

Ama daha önce 1 numaralı kullanıcı veritabanından çekilmişti. Tekrar SELECT yapmaya gerek var mı?

veya…

X metotu içinde User::find(1) yaptınız kullanıcıyı çektiniz ve kullanıcı adı Ali.

Sonra Y metotunda tekrar User::find(1) yaptınız. Ama Y metotunun içinde şöyle bir if koşulu var: “Eğer id 1 ise kullanıcı adını Veli yap”.

Y metotundaki kullanıcı adı Veli oldu. Ama geri X metotuna döndüğümde kullanıcı adı hâlen Ali kaldı.

Böyle birçok farklı senaryo düşünülebilir.

Buradaki temel problem her find yapıldığında yeni bir UserRepository objesinin geriye dönmesinden kaynaklanıyor.

Sistem geneli 1 numaralı kullanıcı için hep aynı nesneyi kullansa tekrar SELECT‘e gerek kalmayacak ve bir metotta kullanıcı adı setlendiğinde başka metotta da bu görülebilecek.

Peki bu nasıl sağlanacak? Identity Map Pattern ile.

Runtime’da objeleri cacheleyeceğiz.

User::find(1) işlemi için metotu hazırlayalım.

User.php

class User {
    public static function find($id) {
        return (new UserMapper)->init($id);
    }
}

Kullanıcı bilgilerinin bulunduğu UserRepository ile User sınıfının arasındaki bağlantıyı sağlayacak Mapper.

UserMapper.php

class UserMapper {
    
    private static $object;
    
    public function init($id) {
        if( ! isset(self::$object[$id])) {
            self::$object[$id] = (new UserRepository)->fetch($id);
        }

        return self::$object[$id];
        
    }
    
}

Son olarak da kullanıcı bilgilerini barındıran UserRepository sınıfı

UserRepository.php

class UserRepository {

    public $users = [['name' => 'Ali'], ['name' => 'Veli']];

    private $name;

    public function fetch($id) {
        if( ! isset($this->users[$id])) {
            throw new InvalidArgumentException;
        }

        $userRepository = new self; 
        $userRepository->setName($this->users[$id]['name']);

        return $userRepository;
    }

    public function getName() {
        return $this->name;
    }

    public function setName($name) {
        $this->name = $name;
    }
}

Burada bir de DAO işlemleri için ekstra sınıflar gerekiyor. Ancak örnek olması için veritabanı olarak bir basit array kullandım.

Örnek işleme bakalım:

test.php


$user1 = User::find(1);
$user2 = User::find(1);

$user1->setName('Emre');
echo $user2->getName(); // $user2 objesi de Emre oldu

UserMapper sınıfında nesneler cachelenmeseydi ve her defasında return (new UserRepository)->fetch($id); yapılmış olsaydı $user2 nesnesinin getName metotu Veli sonucunu döndürecekti.

Ayrıca cachelendiği için UserRepository sınıfındaki fetch metotu da 1 defa çalıştı. Buradaki isset işlemini SELECT sorgusu olarak düşünebilirsiniz.

Ayrıca Bkz.: Optimistic Offline Lock

 
5 Kudos
Don't move