Skip to content
Snippets Groups Projects
Commit 198b7de1 authored by Sean Madsen's avatar Sean Madsen
Browse files
parent 768083c1
No related branches found
No related tags found
1 merge request!287Migrate "Transaction Reference" wiki page
# Transaction Reference
<div class="panel"
style="background-color: #FFFFCE;border-color: #000;border-style: solid;border-width: 1px;">
<div class="panelHeader"
style="border-bottom-width: 1px;border-bottom-style: solid;border-bottom-color: #000;background-color: #F7D6C1;">
**Table of Contents**
</div>
<div class="panelContent" style="background-color: #FFFFCE;">
<div>
- [Background](#TransactionReference-Background)
- [Example: Wrapping code in a transaction (exception-safe helper;
recommended
for v4.6+)](#TransactionReference-Example:Wrappingcodeinatransaction(exception-safehelper;recommendedforv4.6+))
- [Example: Wrapping code in a transaction
(procedural style)](#TransactionReference-Example:Wrappingcodeinatransaction(proceduralstyle))
- [Example: Combining
transactions](#TransactionReference-Example:Combiningtransactions)
- [Example: Nesting
transactions (v4.6+)](#TransactionReference-Example:Nestingtransactions(v4.6+))
- [Example: Abnormal
termination](#TransactionReference-Example:Abnormaltermination)
- [Special Topics: TRUNCATE and ALTER force immediate
commit](#TransactionReference-SpecialTopics:TRUNCATEandALTERforceimmediatecommit)
- [Special Topics: Batching and
imports](#TransactionReference-SpecialTopics:Batchingandimports)
</div>
</div>
</div>
## Background
In online transaction processing, proper error-handling requires that
one demarcate the beginning and ending of each database transaction. If
you're unfamiliar with database transactions, then you may want to read
some background material like:
- <http://en.wikipedia.org/wiki/Database_transaction>
- <http://dev.mysql.com/doc/refman/5.1/en/commit.html>
## Example: Wrapping code in a transaction (exception-safe helper; recommended for v4.6+)
<div class="code panel" style="border-width: 1px;">
<div class="codeHeader panelHeader" style="border-bottom-width: 1px;">
**Using a Transaction (Exception-safe helper)**
</div>
<div class="codeContent panelContent">
<?php
/**
* @throws MyBusinessException
*/
function myBusinessOperation() {
CRM_Core_Transaction::create()->run(function($tx) {
CRM_Core_DAO::executeQuery( /* ... do some stuff ... */ );
if (/* ... received an error ... */) {
throw new MyBusinessException();
}
});
}
</div>
</div>
In this examplle, one never explicitly issues a BEGIN, ROLLBACK, or
COMMIT. When we instantiate the transaction with create(), it will issue
BEGIN automatically. If you throw an exception (or if an exception
otherwise bubbles up), then run() will issue a ROLLBACK; otherwise, it
will issue a COMMIT.
You can also trigger a rollback without throwing an exception using
$tx-&gt;rollback(), e.g.
<div class="code panel" style="border-width: 1px;">
<div class="codeHeader panelHeader" style="border-bottom-width: 1px;">
**Using a Transaction (Exception-safe helper)**
</div>
<div class="codeContent panelContent">
<?php
/**
* @return bool
*/
function myBusinessOperation() {
$result = NULL;
CRM_Core_Transaction::create()->run(function($tx) use (&$result) {
CRM_Core_DAO::executeQuery( /* ... do some stuff ... */ );
if (/* ... received an error ... */) {
$tx->rollback();
$result = FALSE;
} else {
$result = TRUE;
}
});
return $result;
}
</div>
</div>
## Example: Wrapping code in a transaction (procedural style)
General rules:
- Mark the beginning of a transaction with "$tx =
new CRM_Core_Transaction()".
- Mark the transaction as failed ("$tx-&gt;rollback()") when an error
is detected
- Continue reporting and handling errors (by returning error-codes,
throwing exceptions, etc)
<div class="code panel" style="border-width: 1px;">
<div class="codeHeader panelHeader" style="border-bottom-width: 1px;">
**Using a Transaction (Non-Exception Style)**
</div>
<div class="codeContent panelContent">
<?php
/**
* @return bool TRUE on success; FALSE on failure
*/
function myBusinessOperation() {
$tx = new CRM_Core_Transaction();
CRM_Core_DAO::executeQuery( /* ... do some stuff ... */ );
if (/* ... received an error ... */) {
$tx->rollback();
return FALSE;
} else {
return TRUE;
}
}
/**
* @throws MyBusinessException
*/
function myBusinessOperation() {
$tx = new CRM_Core_Transaction();
try {
/* ... do some stuff ... */
if (/* ... received an error ... */) {
throw new MyBusinessException();
}
} catch (Exception $e) {
$tx->rollback();
throw $e; // re-throw the exception
}
}
</div>
</div>
The first example explicitly marks the beginning of the transaction in
our function and (if there's an error) marks the transaction for
ROLLBACK. It **also** reports the error ("return FALSE") so that anyone
who calls myBusinessOperation() can perform their own cleanup.
Note that we never explicitly issue a BEGIN, ROLLBACK or COMMIT. When
instantiating CRM_Core_Transaction, it will issue a BEGIN
automatically. When the function terminates (or, more specifically, when
$tx destructs), it will issue a COMMIT or ROLLBACK.
## Example: Combining transactions
When writing business-logic, each business operation should generally be
executed within a transaction – for example, when a constituent fills in
a profile form, the business operation of "Create a new contact from
profile form" should be executed inside a transaction. Similarly, when a
constituent registers for an event, the business operation of "Register
for event" should be executed inside a transaction.
In practice, we often perform multiple operations at the same time – for
example, a new constituent may fill in a profile **and** register for an
event at the same time. When executed individually, each operation
should be its own transaction. When executed together, the two
operations should be in the same transaction. Civi accomplishes this by
requiring each operation to **declare** its own transaction; if
operations are combined or overlap, then the transactions will be
combined automatically. The following example illustrates the
programming style:
<div class="code panel" style="border-width: 1px;">
<div class="codeHeader panelHeader" style="border-bottom-width: 1px;">
**Combining transactions**
</div>
<div class="codeContent panelContent">
<?php
/**
* Create a contact using a profile form
*
* @return NULL|int contact ID, or NULL on error
*/
function createContactFromProfile($contactData) {
$tx = new CRM_Core_Transaction();
...
if (...error...) {
$tx->rollback();
return NULL;
} else {
return $contactID;
}
}
/**
* Register a contact for an event
* @return int|NULL participant registration ID, or NULL on error
*/
function registerForEvent($eventID, $contactID) {
$tx = new CRM_Core_Transaction();
...
if (...error...) {
$tx->rollback();
return NULL;
} else {
return $participantID;
}
}
/**
* Create a new contact and register for an event
*
* @return NULL|int participant ID, or NULL on error
*/
function registerNewContactForEvent($eventID, $contactData) {
$tx = new CRM_Core_Transaction();
$contactID = createContactFromProfile($contactData);
if ($contactID === NULL) {
return NULL;
}
$participantID = registerForEvent($eventID, $contactID)
if ($participantID === NULL) {
return NULL;
}
return $participantID;
}
</div>
</div>
This has a few important properties:
- If an error arises while creating the contact, the entire
transaction will be rolled back – leaving no contact records, no
participant records, etc – and an error will be returned.
- If an error arises while creating the registration, the entire
transaction will be rolled back – leaving no contact records, no
participant records, etc – and an error will be returned.
## Example: Nesting transactions (v4.6+)
In some cases, it may be appropriate to use a **nested** transaction or
[SAVEPOINT](http://dev.mysql.com/doc/refman/5.1/en/savepoint.html){.external-link}s.
With nested transactions, it is possible to rollback individual steps
(such as the contact-creation or the registration) while committing the
overall work. This is appropriate in cases where some errors are
recoverable, expected, or otherwise tolerated. For example, suppose you
have a bulk importer and want this policy: "We will tolerate errors as
long as they affect &lt;5 records." If there are &lt;5 errors, then all
the valid records should be allowed (and bad records should be skipped);
if there are &gt;=5 records, then the entire batch should be skipped.
One can create a nested transaction the same way as before – but one
must pass the argument "$nested==TRUE", e.g. "new
CRM_Core_Transaction(TRUE)" or "CRM_Core_Transaction::create(TRUE)".
<div class="code panel" style="border-width: 1px;">
<div class="codeHeader panelHeader" style="border-bottom-width: 1px;">
**Nested transactions**
</div>
<div class="codeContent panelContent">
<?php
/**
* Attempt to import a batch of contacts.
*
* @param array $contacts list of contact records to import
* @param int $maxErrors max number of contacts allowed to hit an error
* @param array $erroneousContacts a list of contacts that were skipped due to errors
* @return bool TRUE if the batch was committed
*/
function importBatchOfContacts($contacts, $maxErrors, &$erroneousContacts) {
$txMain = new CRM_Core_Transaction(TRUE);
$erroneousContacts = array();
foreach ($contacts as $contact) {
try {
$txNested = new CRM_Core_Transaction(TRUE); // NOTE: the "TRUE" makes for a nested transaction
if (!createContactFromProfile($contact)) {
$erroneousContacts[] = $contact;
}
} catch (Exception $e) {
$erroneousContacts[] = $contact;
$txNested->rollback();
}
$txNested = NULL; // finish the nested transaction
}
if (count($erroneousContacts) < $maxErrors) {
// batch was "good enough"; errors have been outputted to $erroneousContacts
return TRUE;
} else {
// too many errors; give up on the entire batch
$txMain->rollback();
$erroneousContacts = $contacts;
return FALSE;
}
}
</div>
</div>
<div class="panelMacro">
!!! warning{width="16" height="16"} Marking a transaction for rollback is different from sending the ROLLBACK command to the SQL server – the two may not happen at the same time.The transaction is marked for rollback when an error is first detected, but the ROLLBACK command is sent when all outstanding copies of CRM_Core_Transaction finish-up.
For example, suppose the sequence of events include:
- Someone calls *registerNewContactForEvent*
- *registerNewContactForEvent* creates $tx (the first copy of *CRM_Core_Transaction*)
- *registerNewContactForEvent* calls *registerForEvent*
- *registerForEvent* creates $tx (the second copy of *CRM_Core_Transaction*)
- *registerForEvent* encounters an error and **marks the transaction for rollback** (but the SQL ROLLBACK is **not** executed yet)
- *registerForEvent* terminates – and therefore $tx is destroyed (but the SQL ROLLBACK is **not** executed yet)
- *registerNewContactForEvent* terminates – and therefore $tx is destroyed, and **the SQL ROLLBACK is executed**
</div>
## Example: Abnormal termination
In some exceptional circumstances, program execution terminates
abnormally – which prevents the normal transaction logic from managing
the rollback or commit properly. For example, when Civi encounters a
fatal error, it calls PHP's exit() to abort processing. Of course, if
there's a fatal error, then any pending transactions shouldn't be
committed. CRM_Core_Error addresses this by calling
CRM_Core_Transaction immediately before exit:
<div class="code panel" style="border-width: 1px;">
<div class="codeHeader panelHeader" style="border-bottom-width: 1px;">
**Abnormal termination**
</div>
<div class="codeContent panelContent">
<?php
static function abend($code) {
// do a hard rollback of any pending transactions
// if we've come here, its because of some unexpected PEAR errors
CRM_Core_Transaction::forceRollbackIfEnabled();
CRM_Utils_System::civiExit($code);
}
</div>
</div>
## Special Topics: TRUNCATE and ALTER force immediate commit
In MySQL, changes to the schema will cause pending transactions to
commit immediately (regardless of what Civi would normally do for
transactions) – so installation of new extensions, modification of
custom-data, and cache-resets would be likely to interfere with
transaction management.
## Special Topics: Batching and imports
If you are serially processing a large number of 'rows', despite the
additional overhead it is normally more appropriate to put a transaction
around the logically linked operations for each row and NOT to put a
transaction around the loop through all the rows. Exceptions include
cases where a whole batch must succeed or fail together; this is often
something better done offline.
......@@ -75,6 +75,7 @@ pages:
- Overview: framework/database/index.md
- XML Schema definition: framework/database/schema-definition.md
- Schema Design: framework/database/schema-design.md
- Transaction Reference: framework/database/transactions.md
- Resources Reference: framework/resources.md
- UI Reference: framework/ui.md
- Region Reference: framework/region.md
......
......@@ -174,3 +174,4 @@ Smart+group+testing testing/manual/#smart-group
Tarball+installation+testing testing/manual/#tarball
Contributing+to+CiviCRM+using+GitHub tools/git/#github
Git+Commit+Messages+for+CiviCRM tools/git/#committing
Transaction+Reference framework/database/transactions.md
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment