diff --git a/core/cmdbsource.class.inc.php b/core/cmdbsource.class.inc.php index 24c8c4d44..7b4568e76 100644 --- a/core/cmdbsource.class.inc.php +++ b/core/cmdbsource.class.inc.php @@ -99,6 +99,14 @@ class MySQLHasGoneAwayException extends MySQLException } } +/** + * @since 2.7.0 N°679 + */ +class MySQLNoTransactionException extends MySQLException +{ + +} + /** * CMDBSource @@ -130,6 +138,12 @@ class CMDBSource /** @var mysqli $m_oMysqli */ protected static $m_oMysqli; + /** + * @var int number of level for nested transactions : 0 if no transaction was ever opened, +1 for each 'START TRANSACTION' sent + * @since 2.7.0 N°679 + */ + protected static $m_iTransactionLevel = 0; + /** * SQL charset & collation declaration for text columns * @@ -570,25 +584,68 @@ class CMDBSource /** * @param string $sSQLQuery * - * @return \mysqli_result + * @return \mysqli_result|null * @throws \MySQLException * @throws \MySQLHasGoneAwayException + * @throws \CoreException + * + * @since 2.7.0 N°679 handles nested transactions */ public static function Query($sSQLQuery) + { + if (preg_match('/^START TRANSACTION;?$/i', $sSQLQuery)) + { + self::StartTransaction(); + + return null; + } + if (preg_match('/^COMMIT;?$/i', $sSQLQuery)) + { + self::Commit(); + + return null; + } + if (preg_match('/^ROLLBACK;?$/i', $sSQLQuery)) + { + self::Rollback(); + + return null; + } + + + return self::DBQuery($sSQLQuery); + } + + /** + * Send the query directly to the DB. **Be extra cautious with this !** + * + * Use {@link Query} if you're not sure. + * + * @internal + * + * @param string $sSql + * + * @return bool|\mysqli_result + * @throws \MySQLHasGoneAwayException + * @throws \MySQLException + * + * @since 2.7.0 N°679 + */ + private static function DBQuery($sSql) { $oKPI = new ExecutionKPI(); try { - $oResult = self::$m_oMysqli->query($sSQLQuery); + $oResult = self::$m_oMysqli->query($sSql); } - catch(mysqli_sql_exception $e) + catch (mysqli_sql_exception $e) { - throw new MySQLException('Failed to issue SQL query', array('query' => $sSQLQuery, $e)); + throw new MySQLException('Failed to issue SQL query', array('query' => $sSql, $e)); } - $oKPI->ComputeStats('Query exec (mySQL)', $sSQLQuery); + $oKPI->ComputeStats('Query exec (mySQL)', $sSql); if ($oResult === false) { - $aContext = array('query' => $sSQLQuery); + $aContext = array('query' => $sSql); $iMySqlErrorNo = self::$m_oMysqli->errno; $aMySqlHasGoneAwayErrorCodes = MySQLHasGoneAwayException::getErrorCodes(); @@ -599,10 +656,134 @@ class CMDBSource throw new MySQLException('Failed to issue SQL query', $aContext); } - + return $oResult; } + /** + * If nested transaction, we are not starting a new one : only one global transaction will exist. + * + * Indeed [the official documentation](https://dev.mysql.com/doc/refman/5.6/en/commit.html) states : + * + * > Beginning a transaction causes any pending transaction to be committed + * + * @internal + * @see m_iTransactionLevel + * @since 2.7.0 N°679 + */ + private static function StartTransaction() + { + $bHasExistingTransactions = self::IsInsideTransaction(); + if (!$bHasExistingTransactions) + { + self::DBQuery('START TRANSACTION'); + } + + self::AddTransactionLevel(); + } + + /** + * Sends the COMMIT to the db only if we are at the root transaction level + * + * @internal + * @see m_iTransactionLevel + * @throws \MySQLException + * @throws \MySQLHasGoneAwayException + * @throws \MySQLNoTransactionException if called with no opened transaction + * @since 2.7.0 N°679 + */ + private static function Commit() + { + if (!self::IsInsideTransaction()) + { + // should not happen ! + throw new MySQLNoTransactionException('Trying to commit transaction whereas none have been started !', null); + } + + self::RemoveLastTransactionLevel(); + + if (self::IsInsideTransaction()) + { + return; + } + self::DBQuery('COMMIT'); + } + + /** + * Sends the ROLLBACK to the db only if we are at the root transaction level + * + * The parameter allows to send a ROLLBACK whatever the current transaction level is + * + * @internal + * @see m_iTransactionLevel + * + * @throws \MySQLNoTransactionException if called with no opened transaction + * @throws \MySQLException + * @throws \MySQLHasGoneAwayException + * @since 2.7.0 N°679 + */ + private static function Rollback() + { + if (!self::IsInsideTransaction()) + { + // should not happen ! + throw new MySQLNoTransactionException('Trying to commit transaction whereas none have been started !', null); + } + self::RemoveLastTransactionLevel(); + if (self::IsInsideTransaction()) + { + return; + } + + self::DBQuery('ROLLBACK'); + } + + /** + * @api + * @see m_iTransactionLevel + * @return bool true if there is one transaction opened, false otherwise (not a single 'START TRANSACTION' sent) + * @since 2.7.0 N°679 + */ + public static function IsInsideTransaction() + { + return (self::$m_iTransactionLevel > 0); + } + + /** + * @internal + * @see m_iTransactionLevel + * @since 2.7.0 N°679 + */ + private static function AddTransactionLevel() + { + ++self::$m_iTransactionLevel; + } + + /** + * @internal + * @see m_iTransactionLevel + * @since 2.7.0 N°679 + */ + private static function RemoveLastTransactionLevel() + { + if (self::$m_iTransactionLevel === 0) + { + return; + } + + --self::$m_iTransactionLevel; + } + + /** + * @internal + * @see m_iTransactionLevel + * @since 2.7.0 N°679 + */ + private static function RemoveAllTransactionLevels() + { + self::$m_iTransactionLevel = 0; + } + /** * @param string $sTable * diff --git a/core/config.class.inc.php b/core/config.class.inc.php index 3d2a59467..07220c373 100644 --- a/core/config.class.inc.php +++ b/core/config.class.inc.php @@ -174,6 +174,14 @@ class Config 'source_of_value' => '', 'show_in_conf_sample' => false, ), + 'db_core_transactions_enabled' => array( + 'type' => 'bool', + 'description' => 'If true, CRUD transactions in iTop core will be enabled', + 'default' => true, + 'value' => true, + 'source_of_value' => '', + 'show_in_conf_sample' => false, + ), 'skip_check_to_write' => array( 'type' => 'bool', 'description' => 'Disable data format and integrity checks to boost up data load (insert or update)', diff --git a/core/dbobject.class.php b/core/dbobject.class.php index 15f3d5d34..5b617494c 100644 --- a/core/dbobject.class.php +++ b/core/dbobject.class.php @@ -2700,9 +2700,13 @@ abstract class DBObject implements iDisplay } } + $bIsTransactionEnabled = MetaModel::GetConfig()->Get('db_core_transactions_enabled'); try { - CMDBSource::Query('START TRANSACTION'); + if ($bIsTransactionEnabled) + { + CMDBSource::Query('START TRANSACTION'); + } // First query built upon on the root class, because the ID must be created first $this->m_iKey = $this->DBInsertSingleTable($sRootClass); @@ -2723,11 +2727,17 @@ abstract class DBObject implements iDisplay $this->DBInsertSingleTable($sParentClass); } - CMDBSource::Query('COMMIT'); + if ($bIsTransactionEnabled) + { + CMDBSource::Query('COMMIT'); + } } catch (Exception $e) { - CMDBSource::Query('ROLLBACK'); + if ($bIsTransactionEnabled) + { + CMDBSource::Query('ROLLBACK'); + } throw $e; } diff --git a/datamodels/2.x/itop-portal-base/portal/src/Form/PreferencesFormManager.php b/datamodels/2.x/itop-portal-base/portal/src/Form/PreferencesFormManager.php index a5a64fb54..9ad9805a0 100644 --- a/datamodels/2.x/itop-portal-base/portal/src/Form/PreferencesFormManager.php +++ b/datamodels/2.x/itop-portal-base/portal/src/Form/PreferencesFormManager.php @@ -22,15 +22,15 @@ namespace Combodo\iTop\Portal\Form; -use Exception; -use IssueLog; use CMDBSource; -use Dict; -use UserRights; -use Combodo\iTop\Form\FormManager; -use Combodo\iTop\Form\Form; use Combodo\iTop\Form\Field\HiddenField; use Combodo\iTop\Form\Field\SelectField; +use Combodo\iTop\Form\Form; +use Combodo\iTop\Form\FormManager; +use Dict; +use Exception; +use IssueLog; +use UserRights; /** * Description of PreferencesFormManager diff --git a/webservices/backoffice.dataloader.php b/webservices/backoffice.dataloader.php index 37ff476f1..99f6cb63a 100644 --- a/webservices/backoffice.dataloader.php +++ b/webservices/backoffice.dataloader.php @@ -151,7 +151,7 @@ try catch(Exception $e) { $oP->p("An error happened while loading the data: ".$e->getMessage()); - $oP->p("Aborting (no data written)..."); + $oP->p("Aborting (no data written)..."); CMDBSource::Query('ROLLBACK'); } @@ -161,4 +161,3 @@ if (function_exists('memory_get_peak_usage')) } $oP->Output(); -?>