[RESOLU] Dysfonctionnement des transactions via PDO avec PostgreSQL et avec PHP 7.x

plm
Invité n'ayant pas de compte PHPfrance

13 janv. 2019, 18:41

Bonjour à tous et bonne année,

comme nouvelle et bonne résolution, j'ai décidé de migrer mon application de 5.6.39 aux versions 7.x
J'ai testé la branche 7.1, la branche 7.2 et la branche 7.3.
Quelle que soit la branche 7.x, les transactions via "PDO" et avec PostgreSQL qui fonctionnaient depuis 4 ans en version 5.5.x et 5.6.x ne fonctionnent plus.
J'ai testé également les versions 9.5, 10 et 11 de PostgreSQL.

Dès que je passe en version PHP 7, j'ai systématiquement l'erreur ci-dessous :
array(3) { [0]=> string(5) "25P02" [1]=> int(7) [2]=> string(87) "ERROR: current transaction is aborted, commands ignored until end of transaction block" }
Résultat du "errorInfo()" de la requête.

Attention, je parle bien ici de "transaction", les simples requêtes, quant à elles, fonctionnent très bien.
En revanche, dès que je commence une transaction, soit par une instruction du type "PDO::beginTransaction()", cela plante à la première modification réalisée dans la transaction.

Quelqu'un a-t-il une idée ?
J'ai déjà fait des recherches dans "Google" et je ne trouve rien de probant.
Je suis très embêté car il s'agit d'une application qui fonctionne très bien dans sa version actuelle et avec PHP 5.6.39.

Par avance merci.

Avatar du membre
Administrateur PHPfrance
Administrateur PHPfrance | 8151 Messages

13 janv. 2019, 19:01

Étrange comme problème... #-o
Peux-tu nous donner le code de ta transaction ?
Quand tout le reste a échoué, lisez le mode d'emploi...

plm
Invité n'ayant pas de compte PHPfrance

13 janv. 2019, 22:30

Bonsoir,

merci de ta réponse, ci-dessous, le code du script "Class_HBL_Connecteur_BD_PDO.inc.php" qui est importé par le script encore plus bas :
<?php

include_once( 'Constants.inc.php' );

class HBL_Connecteur_BD extends PDO {
/**
* Cette classe gère les connexions à la base de données tout en offrant une couche d'abastraction.
* Effectivement, seule cette classe connait la localisation du fichier de paramètre externe.
*/

	public $LastInsertId; // Dernier ID créé

	public $RowCount; // Nombre d'occurrences modifiées

	
	public function __construct() {
	/**
	* Connexion à la base de données via le constructeur de PDO.
	*
	* @return Renvoi "true" en cas de succès de connexion à la base de données, sinon lève une exception.
	*/
		// Charge les différentes variables utiles à la connexion à la base de données.
		include( HBL_CONFIG_BD );

		$DSN = $_Driver . ':host=' . $_Host . ';port=' . $_Port . ';dbname=' . $_Base ;

		PDO::__construct( $DSN, $_User, $_Password, array( PDO::ATTR_PERSISTENT => TRUE	) );

		return TRUE;
	}


	protected $transactionBegin = FALSE;


	public function begin_Transaction() {
	/**
	* Mise en place d'une Transaction (permettant l'exécution de plusieurs requêtes SQL et de valider cet ensemble)
	*
	* @return Passe l'objet en mode Transaction
	*/
		//$this->transactionBegin = TRUE;
		$this->transactionBegin = TRUE;

		//$this->exec( 'BEGIN' );
		return $this->beginTransaction();
	}


	public function commit_Transaction(){
	/**
	* Valide l'ensemble des requêtes de mise à jour de la Transaction.
	*
	* @return Passe l'objet en mode "autocommit" à "off"
	*/
		if ( ! $this->transactionBegin ) return;

		$this->transactionBegin = FALSE;

		return $this->commit();
	}


	public function rollback_Transaction(){
	/**
	* Annule l'ensemble des requêtes de mise à jour de la Transaction.
	*
	* @return Conserve l'objet en mode Transaction
	*/
		if ( ! $this->transactionBegin ) return;

		$this->transactionBegin = FALSE;

		//$this->exec( 'ROLLBACK' );
		return $this->rollBack();
	}


	public function prepareSQL( $sql ) {
	/**
	* Automatise la préparation d'une requète en ajoutant la gestion des exceptions
	*
	* @param[in] $sql La requête à préparer
	*
	* @return Renvoi la requête préparée
	*/
		// évite les espaces insécables dans la chaîne de caractère :s
		$sql = str_replace(" ", " ", $sql);

		if ( ! $Query = $this->prepare( $sql ) ) {//print('Error : '.$sql);
			$Error = $Query->errorInfo();

			if ( $this->transactionBegin == TRUE ) $this->rollback_Transaction();

			throw new Exception( $Error[ 2 ], $Error[ 1 ] );
		}

		return $Query;
	}


	public function bindSQL( $Query, $Reference, $Value, $Type, $Length = 10 ){
	/**
	* Automatise l'association des paramètres sur une requète SQL.
	*
	* @param[in] $Query La requète modifier, passé en référence
	* @param[in] $Reference la chaine de caractère référence à remplacer dans la requête
	* @param[in] $Value la valeur à mettre à la place de la référence
	* @param[in] $Type le type de variable à remplacer. Pour l'instant ne sont géré que les entiers et les chaines de caractères
	* @param[in] $Length la longueur maximal de la chaine de caractère à remplacer
	*
	*/
		// Si le type est un "Numérique".
		if( $Type === PDO::PARAM_INT || $Type === PDO::PARAM_BOOL || $Type === PDO::PARAM_LOB ) {
			if ( ! $Query->bindParam( $Reference, $Value, $Type ) ) {
				$Error = $Query->errorInfo();

				if ( $this->transactionBegin == TRUE ) $this->rollback_Transaction();

				throw new Exception( $Error[ 2 ], $Error[ 1 ] );
			}
		}
		// Si le type est une "chaine de caractères".
		elseif($Type === PDO::PARAM_STR){
			if ( ! $Query->bindParam( $Reference, $Value, $Type, $Length ) ) {
				$Error = $Query->errorInfo();

				if ( $this->transactionBegin == TRUE ) $this->rollback_Transaction();

				throw new Exception( $Error[ 2 ], $Error[ 1 ] );
			}
		}
		else {
			if ( $this->transactionBegin == TRUE ) $this->rollback_Transaction();

			throw new Exception( "bindSQL - Format de donnée non géré");
		}

		// permet les appels en cascade 
		return $this;
	}


	public function executeSQL( $Query ){
	/**
	* Automatise l'exécution d'une requête
	*
	* @param[in] $Query La requète à executer, passé en référence
	*
	*/	
		$Status = $Query->execute();
		if ( $Status === FALSE ) {
			$Error = $Query->errorInfo();
var_dump($Error);
			if ( $Error[ 0 ] == 23505 ) { // Gestion des doublons.
				//throw new Exception("Application Error (".$Error[0].', '.$Error[2].')', $Error[ 0 ]);
				throw new Exception("Application Error", $Error[ 0 ]);
			}

			if ( $this->transactionBegin == TRUE ) $this->rollback_Transaction();

			$message = $Error[ 2 ] . ' (SQL: ' . $Error[ 0 ] . ')'; //(' . $Query->queryString . ')';

			throw new Exception( $message, $Error[ 1 ] );
		}

		$this->LastInsertId = $this->lastInsertId();
		$this->RowCount = $Query->rowCount();

		
		// permet les appels en cascade
		return $Query;
	}

}
Et voici le script d'exemple appelant :
<?php
include( 'Librairies/Class_HBL_Connecteur_BD_PDO.inc.php' );

const CODE_TYPE = PDO::PARAM_STR;
const CODE_LENGTH = 10;

const LANGUE_TYPE = PDO::PARAM_STR;
const LANGUE_LENGTH = 2;

const LIBELLE_TYPE = PDO::PARAM_STR;
const LIBELLE_LENGTH = 20;

$cnxBD = new HBL_Connecteur_BD();


$cnxBD->begin_Transaction();
print('BEGIN<br>');

$SQL = 'SELECT COUNT(lbr_code) AS "total" FROM lbr_libelles_referentiel ' .
	'WHERE lbr_code = :code ' .
	'AND lng_id = :langue ';
print($SQL.'<br>');

$Requete = $cnxBD->prepareSQL( $SQL );

$cnxBD->bindSQL( $Requete, ':code', 'XXX_XX', CODE_TYPE, CODE_LENGTH )
	->bindSQL( $Requete, ':langue', 'fr', LANGUE_TYPE, LANGUE_LENGTH );

$cnxBD->executeSQL( $Requete );


$Resultat = $Requete->fetch( PDO::FETCH_OBJ )->total;
print($Resultat.'<br>');

$SQL = 'SELECT COUNT(lbr_code) AS "total" FROM lbr_libelles_referentiel ' .
	'WHERE lbr_code = :code ' .
	'AND lng_id = :langue ';
print($SQL.'<br>');

$Requete = $cnxBD->prepareSQL( $SQL );

$cnxBD->bindSQL( $Requete, ':code', 'XXX_XX', CODE_TYPE, CODE_LENGTH )
	->bindSQL( $Requete, ':langue', 'fr', LANGUE_TYPE, LANGUE_LENGTH );

$cnxBD->executeSQL( $Requete );

$Resultat = $Requete->fetch( PDO::FETCH_OBJ )->total;
print($Resultat.'<br>');


if ( $Resultat > 0 ) {
	$SQL = 'DELETE FROM lbr_libelles_referentiel ' .
		'WHERE lbr_code = :code ' .
		'AND lng_id = :langue ';
print($SQL.'<br>');
	
	$Requete = $cnxBD->prepareSQL( $SQL );
	
	$cnxBD->bindSQL( $Requete, ':code', 'XXX_XX', CODE_TYPE, CODE_LENGTH );
	$cnxBD->bindSQL( $Requete, ':langue', 'fr', LANGUE_TYPE, LANGUE_LENGTH );
	
	$cnxBD->executeSQL($Requete);
}


$Requete = $cnxBD->prepareSQL(
	'INSERT INTO lbr_libelles_referentiel '.
	'(lbr_code, lng_id, lbr_libelle) ' .
	'VALUES ' .
	'(:Code, :Langue, :Libelle)'
	);
print($SQL.'<br>');


$cnxBD->bindSQL($Requete, ':Code', 'YYYY_YY', CODE_TYPE, CODE_LENGTH);
$cnxBD->bindSQL($Requete, ':Langue', 'fr', LANGUE_TYPE, LANGUE_LENGTH);
$cnxBD->bindSQL($Requete, ':Libelle', 'xxxxxxxxxxxxxx', LIBELLE_TYPE, LIBELLE_LENGTH);

$cnxBD->executeSQL($Requete);

$cnxBD->commit_Transaction();
print('COMMIT<br>');
?>
J'obtiens l'erreur ci-dessous (erreur + résultats des "print") :

Code : Tout sélectionner

BEGIN SELECT COUNT(lbr_code) AS "total" FROM lbr_libelles_referentiel WHERE lbr_code = :code AND lng_id = :langue 1 SELECT COUNT(lbr_code) AS "total" FROM lbr_libelles_referentiel WHERE lbr_code = :code AND lng_id = :langue array(3) { [0]=> string(5) "25P02" [1]=> int(7) [2]=> string(87) "ERROR: current transaction is aborted, commands ignored until end of transaction block" } Fatal error: Uncaught Exception: ERROR: current transaction is aborted, commands ignored until end of transaction block (SQL: 25P02) in /Applications/XAMPP/xamppfiles/htdocs/Loxense_v1/Librairies/Class_HBL_Connecteur_BD_PDO.inc.php:200 Stack trace: #0 /Applications/XAMPP/xamppfiles/htdocs/Loxense_v1/PHPTest.php(45): HBL_Connecteur_BD->executeSQL(Object(PDOStatement)) #1 {main} thrown in /Applications/XAMPP/xamppfiles/htdocs/Loxense_v1/Librairies/Class_HBL_Connecteur_BD_PDO.inc.php on line 200
On note que l'erreur est simplement le deuxième "SELECT", cela ne plante même pas sur une mise à jour.

Avatar du membre
Administrateur PHPfrance
Administrateur PHPfrance | 8151 Messages

13 janv. 2019, 22:55

Vérifie ton premier SELECT, à priori je pense que ta 1ère requête échoue et du coup Postgres abandonne ta transaction.
https://stackoverflow.com/a/2979389
Quand tout le reste a échoué, lisez le mode d'emploi...

plm
Invité n'ayant pas de compte PHPfrance

14 janv. 2019, 00:06

Tu vois bien que non, puisque dans ma trace, juste après le 1er "SELECT", la ligne "1", ce qui veut dire que le "SELECT" à bien trouvé une occurrence. Le plus drôle est que le 2ème "SELECT" est strictement identique.
Sinon, as-tu réussi à faire des transactions depuis que PHP est en version "7.x" ?

Avatar du membre
Administrateur PHPfrance
Administrateur PHPfrance | 8151 Messages

14 janv. 2019, 00:45

Tu vois bien que non, puisque dans ma trace, juste après le 1er "SELECT", la ligne "1", ce qui veut dire que le "SELECT" à bien trouvé une occurrence. Le plus drôle est que le 2ème "SELECT" est strictement identique.
Mouais, enfin moi je chercherai du côté dans la dans ce coin car Postgres a clôturé la transaction.
Donc exécute tes requêtes directement dans Postgres sans passer par PHP pour voir si ça coince à un moment
Sinon, as-tu réussi à faire des transactions depuis que PHP est en version "7.x" ?
Oui sans aucun problème.
Avec MySQL toutefois, car j'ai pas de Postgres mais il n'y a aucune raison pour que ça ne fonctionne pas, et si c'était le cas il y aurait une note dans la doc.
Quand tout le reste a échoué, lisez le mode d'emploi...

plm
Invité n'ayant pas de compte PHPfrance

14 janv. 2019, 02:18

Encore une fois, ce qui m'ennuie est que ce même code fonctionne en PHP 5.6.x et avec la même version de PostgreSQL.
La seule chose qui change dans mon cas, c'est la version de PHP, ce n'est ni le code, ni la version de PostgreSQL.

Avatar du membre
Administrateur PHPfrance
Administrateur PHPfrance | 8151 Messages

14 janv. 2019, 11:09

Quand c'est comme cela, la solution la plus efficace pour debuguer c'est de repartir d'une page blanche et de construire un script le + minimaliste possible en s'inspirant des exemples de la doc.
Si le code le + minimaliste fonctionne, alors il faudra construire progressivement en ajoutant le reste et en testant à chaque étape pour voir à quel moment ça coince.
Quand tout le reste a échoué, lisez le mode d'emploi...

Mammouth du PHP | 1005 Messages

14 janv. 2019, 13:10

De ce qu'on trouve sur ce problème, c'est que c'est un problème de commit.
La m"thode begin_transaction désactive l'auto commit, normale pour une transaction, mais peux peut-être faut commité pour les select....qui de toutes façon n'impacte pas le rollback
L'expérience est la somme de toutes nos erreurs.

plm
Invité n'ayant pas de compte PHPfrance

14 janv. 2019, 17:34

Youhou, je viens de trouver.
En fait, dans ma méthode "executeSQL", j'essayais de récupérer systématiquement le dernier "$this->lastInsertId()".
Autant, en PHP 5, les erreurs ne remontent pas systématiquement des exceptions, autant en PHP 7, cela n'est plus vrai.
Dans mon cas, et donc avec "PostgreSQL", il aurait fallu que je renseigne le nom de la "séquence" dans l'appel, par exemple "$this->lastInsertId( 'masequence')". Comme je ne le faisais pas, cela levé une erreur, et donc cela fermée ma transaction.
Désolé, pour le dérangement, merci "@rthur" et bonne continuation à tous.

Avatar du membre
Administrateur PHPfrance
Administrateur PHPfrance | 8151 Messages

14 janv. 2019, 18:47

Bonne nouvelle !
A bientôt :-D
Quand tout le reste a échoué, lisez le mode d'emploi...