Transaktionen

Transaktionen

Die n2n API ermöglicht es dir, ressourcenübergreifende Transaktionen zu starten. Die API erlaubt zum Beispiel ein sicheres Arbeiten mit mehreren Datenbank-Verbindungen oder auch FileManagern. Zudem ist es wesentlich einfacher, die Transaktionen über die n2n API zu steuern, als zum Beispiel über ein Pdo-Objekt.

  1. Transaktionen im Controller
  2. Transaction Manager
  3. Verschachtelte Transaktionen
  4. Read-Only-Transaktion
  5. Extended / transactional EntityManager

Transaktionen im Controller

Am einfachsten ist es, Transaktionen im Controller über die Methoden ControllerAdapter::beginTransaction(), ControllerAdapter::commit() und ControllerAdapter::rollBack() zu steuern.

class ExampleController extends ControllerAdapter {  
    
    public function doOrm(ExampleDao $exampleDao) {
        $this->beginTransaction();
        
        $exampleDao->saveArticle(new Article('lorem ipsum'));
        
        $this->commit();
    }
}

Wird zwischen ControllerAdapter::beginTransaction() und ControllerAdapter::commit() eine Exceptions geworfen oder ein Fatal-Error "getriggert", so wird die Transaktion zurückgesetzt (Rollback).

    public function doOrm(ExampleDao $exampleDao) {
        $this->beginTransaction();
        
        $exampleDao->saveArticle(new Article('lorem ipsum'));
        
        throw new IllegalStateException('Something went wrong.'); // leads to rollback
        
        $this->commit();
    }

Der neue Artikel wird im oben stehenden Beispiel nicht gespeichert / zurückgesetzt (Rollback).

Die Ausnahme bilden Exceptions vom Typ n2n\http\StatusException (siehe Abschnitt StatusException im Artikel StatusException). Diese führen zu einem Commit.

    public function doArticle($id, ExampleDao $exampleDao) {
        $this->beginTransaction();
        
        $article = $exampleDao->getArticleById($id);
        if ($article === null) {
            $exampleDao->createNotFoundLogEntry($id);
            throw new PageNotFoundException();
        }
        
        $this->commit();
    }

Wird im oben stehenden Beispiel eine n2n\http\PageNotFoundException  (sie erweitert n2n\http\StatusException) geworfen, wird die Transaktion dennoch "committed". Wenn wir davon ausgehen, dass ExampleDao::createNotFoundLogEntry() einen Log-Eintrag in die Datenbank schreibt, so würde dieser erstellt.

Transaktionen, die mit ControllerAdapter::beginTransaction() gestartet, aber nicht beendet werden, werden ebenfalls "committed".

Transaction Manager

Ausserhalb von Controllern kannst du Transaktionen über den magischen Typ n2n\core\container\TransactionManager steuern. TransactionManager::createTransaction() beginnt eine Transaktion und gibt dir sie in Form eines Objekts n2n\core\container\Transaction zurück. Die Transaktion kannst du mit Transaction::commit() oder Transaction::rollBack() beenden.

class ExampleDao implements RequestScoped {
    private $em;
    private $tm;
    
    private function _init(EntityManager $em, TransactionManager $tm) {
        $this->em = $em;
        $this->tm = $tm;
    }

    public function saveArticle(Article $article) {
        $tx = $this->tm->createTransaction();
        $this->em->persist($article);
        $tx->commit();
    }
}

Wird während einer Transaktion, die über den TransactionManager gestartet wurde, eine Exception geworfen oder ein Fatal-Error "getriggert", so wird die Transaktion zurückgesetzt (Rollback). Beendest du die Transaktion nicht, so wird sie ebenfalls zurückgesetzt.

Verschachtelte Transaktionen

Ein weiterer Vorteil von n2n-Transaktionen ist, dass du sie verschachteln kannst. Der wirkliche Commit / Rollback auf den Ressourcen erfolgt erst, wenn die Root-Transaktion (erste geöffnete Transaktion) beendet wird.

class ExampleController extends ControllerAdapter {  
   
    public function doArticle($id, ExampleDao $exampleDao) {
        $this->beginTransaction();
        
        $exampleDao->saveArticle(new Article('test'));
        
        $this->commit();
    }
}
class ExampleDao implements RequestScoped {
    private $em;
    private $tm;
    
    private function _init(EntityManager $em, TransactionManager $tm) {
        $this->em = $em;
        $this->tm = $tm;
    }

    public function saveArticle(Article $article) {
        $tx = $this->tm->createTransaction();
        $this->em->persist($article);
        $tx->commit();
    }
}

Wird während einer aktiven Kind-Transaktion eine Exception geworfen oder ein Fatal-Error "getriggert", so wird die Root- mit all ihren Kind-Transaktion zurückgesetzt. Wird eine Kind-Transaktion aktiv mit Transaction::rollBack() zurückgesetzt, so müssen auch all ihre Eltern-Transaktionen (inklusive Root-Transaktionen) mit Transaction::rollBack() beendet werden, oder es wird eine n2n\core\container\TransactionStateException geworfen.

Read-Only-Transaktion

Übergibst du als ersten Parameter von ControllerAdapter::beginTransaction() oder TransactionManager::createTransaction()  true, so startest du eine Read-Only-Transaktion. Dies bedeutet, dass die Ressourcen während einer solchen Transaktion nicht beschrieben werden können. Dies verbessert die Performance, da zum Beispiel ein EntityManager bei Transaktions-Ende nicht überprüfen muss, ob Entities verändert wurden.

    public function doArticle($id, ExampleDao $exampleDao) {
        $this->beginTransaction(true);
        
        $article = $exampleDao->getArticleById($id);
        if ($article === null) {
            throw new PageNotFoundException();
        }
        
        $this->commit();
        
        $this->forward('view\article.html', array('article' => $article));
    }
Verwende also immer, wenn du nichts schreiben willst, eine Read-Only-Transaktion.

Grundsätzlich ist es eine gute Idee, Transaktionen kurz zu halten und vor Ausgabe der View zu beenden. Allerdings kann das zu Problemen führen, wenn du zum Beispiel der View Entities mit Beziehungs-Eigenschaften übergibst, für die der FetchType::Lazy gilt und noch nicht geladen wurden. Greifst du in der View auf diese Eigenschaften zu, so werden sie erst dann und somit ausserhalb der Transaktion geladen (führt beim "transactional" EntityManager zu einer Exception). In diesen Fällen macht es Sinn, die Transaktion erst nach ControllerAdapter::forward() zu beenden.

Ist bei verschachtelten Transaktionen die Root-Transaktion Read-Only, so müssen auch all ihre Kind-Transaktionen Read-Only sein.

Extended / transactional EntityManager

Die zwei EntityManager-Typen "extended" und "transactional" unterscheiden sich durch ihre Lebensdauer voneinander. Im Gegensatz zum "extended" ist der "transactional" EntityManager an eine Transaktion gebunden. Er kann auch nur innerhalb einer Transaktion angefordert werden und schliesst (EntityManager::close()) automatisch, sobald die Transaktion beendet wird. Du erhältst also für jede Transaktion einen neuen EntityManager und somit auch einen neuen Persistence Context. Das bedeutet auch, dass Beziehungs-Eigenschaften, für die der FetchType::LAZY gilt, nach Ende der Transaktion nicht mehr geladen werden können (eine Exception wird in diesem Fall geworfen).

Der "extended" EntityManager kann nach Ende der Transaktion beliebig weiter verwendet werden. Entities, die in der beendeten Transaktionen geladen wurden, haben weiterhin den Status MANAGED. Den "extended" EntityManager kannst du magischen Methoden übergeben lassen oder über EntityManagerFactory::getExtended() anfordern.

    private function _init(EntityManager $em) {
        $this->em = $em;
    }

Den "transactional" EntityManager erhältst du nur über EntityManagerFactory::getTransactional(). Ist keine Transaktion aktiv, so wirft diese Methode eine Exception.

class ExampleDao implements RequestScoped {
    private $emf;
    
    private function _init(EntityManagerFactory $emf) {
        $this->emf = $emf;
    }
    /**
     * @param int $id
     * @return atusch\bo\Article
     */
    public function getArticleById($id) {
        return $this->emf->getTransactional()->find(Article::getClass(), $id);
    }
}

Im oben stehenden Beispiel wissen wir nicht, ob beim Aufruf von _init() eine Transaktion aktiv ist. Weiter kann ExampleDao::getArticleById() mehrmals in verschiedenen Transaktionen aufgerufen werden. Deshalb laden wir nur die EntityManagerFactory magisch und holen erst beim Aufruf von ExampleDao::getArticleById() den "transactional" EntityManager der aktiven Transaktion.

Der "transactional" EntityManager reduziert die Gefahr vor Fehlern, die durch fehlerhaftes Anwenden von Transaktionen entstehen, beträchtlich. Für Applikationen, bei denen solche Fehler (z. B. korrupte Daten in der Datenbank) kritisch sein können, ist zu empfehlen, immer den "transactional" EntityManager zu verwenden. Dies wäre zum Beispiel bei einem Web-Shop der Fall, über den auch Bezahlungen abgewickelt werden.
« Lifecycle Beziehungen »

Kommentare

Du musst eingeloggt sein, damit du Beiträge erstellen kannst.

Fragen