Problem mit MySQL-Transaktion / PHP + PDO

X-Coder

NGBler
Registriert
14 Juli 2013
Beiträge
118
Ich bin gerade mit meinem Latein am Ende und könnte etwas Hilfe gebrauchen.

Hat vielleicht jemand eine Idee warum es beim Commit in Zeile 50 nur gelegentlich zu dem PDO-Fehler: "There is no active transaction" kommt?
Ich starte diese Transaktion doch zuvor in Zeile 7, und beende diese dazwischen nicht mehr. Ich kann darin keinen Fehler entdecken.
Eingesetzt wird PHP 7.

Dieser Fehler tritt nur gelegentlich auf, ich konnte bis jetzt noch nicht nachstellen wann und warum. Er zeigt sich auch nur im Produktiv-System, nicht bei der Entwicklung.

Grundlegend versuche ich mehrere CSV-Dateien einzulesen, nehme vor dem Import in die Datenbank noch diverse Prüfungen vor, und importiere diese dann in eine MySQL-Datenbank.

Jede CSV-Datei wird als ein Auftragsjob behandelt, bei dem Datensätze mit Fehlern aber einfach übersprungen werden sollen und nur solche importiert werden sollen, welche auch allen Kriterien erfüllen und importierbar sind.

Ein Rollback soll nur durchgeführt werden, wenn gar keine Datensätze in die job_detail-Tablle importiert worden sind, damit der zuerst angelegt Job aus der job-Tabelle auch wieder gelöscht wird.


Könnte es sein, dass die Transaktion nicht durch mein Skript unterbrochen wird, sondern durch ein datenbankseitigen Timeout oder so etwas?
In dieser Zeit finden auch keine externen ALTER-Table Anweisungen statt, welche Transaktionen abbrechen lassen würden.

Der Code ist sehr gekürzt, ich habe weitgehendst nur die Kontrollstrukturen darin gelassen.
[src=php]<?php
try {
#$cfg->db // = Wrapper für PDO
$cntRowsInserted = 0; // Zähler für erfolgreiche Inserts

$autocommit = $cfg->db->get_autocommit(); // aktuellen Transaktionsmodus sichern (für verschachtelte Transaktionen)
$cfg->db->autocommit(false); // Autocommit deaktivieren und Transaktion starten

if ($hasError) {
throw new Exception('Fehlermeldung');
}

// Job anlegen
$stmt = $LEVEO->db->prepare('INSERT INTO jobs ...');
$stmt->execute();

// Zeilen einlesen und in Datenbank abspeichern
while ($row = ...) {
try {
if ($hasError) {
throw new Exception('Fehlermeldung');
}

// Query ausführen
try {
$stmt = $cfg->db->query('INSERT INTO job_details ...'); //löst Exception aus bei Fehlern
$cntRowsInserted++; //Wenn OK, dann erhöhen
}
catch (Exception $e) {
throw $e;
}
}
catch (Exception $ex) {
// - alle Exception abfangen, fehlerhaften Datensatz überspringen
// - mit anderen Datensätzen fortfahren
// - Fehlermeldung protokollieren
#fputcsv(...);
}
}

//Alles Datensätze des Jobs verarbeitet? - dann Job speichern
if ($cntRowsInserted > 0) { // Es wurde mindestens ein Datensatz erfolgreich verarbeitet
// ganzen Job zur Weiterverarbeitung freigeben
if ($autoFreigabe) {
$stmtJobFreigabe->bindValue(':job_id', $job_id);
$stmtJobFreigabe->execute();
}

// Auftragsjob speichern
$cfg->db->commit(); //<------------------ There is no active transaction!
$cfg->db->autocommit($autocommit); // Transaktionsmodus wiederherstellen
} else {
// Sollten keine Datensätze importiert worden sein, dann Job durch Rollback löschen
$cfg->db->rollback();
}
}
//Fehler?
catch (Exception $ex) {
// Sonstige unbehandelte Fehler speichern
$error = $ex->getMessage();

if (empty($DEBUG)) {
// Fehler-Details mailen
}

try {
// alle Änderungen an der Datenbank zurücknehmen
$cfg->db->rollback();
$cfg->db->autocommit($autocommit); // Transaktionsmodus wiederherstellen
}
catch (Exception $ex2) {
if (!empty($DEBUG)) {
// Fehlermeldung an Debugoutput anhängen
$error .= "\r\n" . $ex2->getMessage();
}
}
}
?>
[/src]
 
Zuletzt bearbeitet:
Da fehlt doch der Begin der Transaction, oder? Weil du setzt ja nur das Autocommit auf false. Lt. muss man aber dann noch mit START TRANSACTION bzw. BEGIN ne Transaction erzeugen.
 
  • Thread Starter Thread Starter
  • #3
Das fehlt nicht, du konntest es nur nicht sehen. Das wird im Wrapper aufgerufen und passiert zusammen mit dem autocommit(false)-Aufruf in Zeile 7, in dieser Funktion wird direkt $this->link->beginTransaction() der PDO-Klasse aufgerufen.

Das ist noch der Code vom Wrapper:
[src=php]
private $autocommit = true;
private $transactionLevel = 0;

function autocommit($b_mode){
if(!$b_mode && $this->transactionLevel == 0){
$transaction_started = $this->link->beginTransaction();
if($transaction_started){
$this->transactionLevel++;
$this->autocommit = false;
}
return $transaction_started;
}
}

function commit(){
$commited = false;
if($this->transactionLevel>0){
$this->transactionLevel--;
}
if($this->transactionLevel==0){
$commited = $this->link->commit();
if($commited) $this->autocommit = true;
}
return $commited;
}[/src]

Das Transaktions-Level sorgt dafür dass nur der letzte commit-Aufruf (wenn $transactionLevel==0) auch einen tatsächlichen commit auf Datenbankebene erzeugt. Dies benötige ich für die verschachtelten Transaktionen, da das ganze relativ modular aufgebaut ist.

EDIT: Mir ist da gerade etwas in der autocommit-Funktion aufgefallen, dass muss ich eben mal überprüfen...
 
Zuletzt bearbeitet:
MySQL kann keine "Nested Transcations", sobald transactionLevel > 1 und versucht wird eine "äußere" Transaktion zu commiten tritt vermutlich der Fehler auf, da jedes "START TRANSACTION" schwebende, also die vorherige, Transaktionen implizit commited.

Das geht eindeutig aus der (such nach pending) seit mindestens MySQL 5.5 hervor.

Abhängig vom exakten use case ließe sich das allerdings mittels umsetzen.
 
Zuletzt bearbeitet:
  • Thread Starter Thread Starter
  • #5
Genau das war mein Problem mit MySQL und den nicht unterstützten nested Transactions. Das ist quasi mein kleiner Workaround für das von dir beschriebene Problem.

Die "inneren" beginTransactions und commits sollten mit diesem Workaround tatsächlich auch nie auf Datenbankebene durchgeführt werden, sondern nur der commit auf der äußersten Ebene, wenn dass Transaktions-Level also wieder bei 0 ist.

Aber ich habe gerade vorhin bemerkt, dass ich das $transactionLevel bei mehrfachen Aufrufen von autocommit() gar nicht mehr das Level erhöhe, dass kann so dann auch nicht funktionieren. Eigentlich sollte jeder Aufruf von autocommit(false) dafür sorgen, dass das Level erhöht wird und bei jedem commit-Aufruf wieder abgezogen wird.
 
Zuletzt bearbeitet:
Zurück
Oben