Premessa
In questo breve articolo, anciamo ad analizzare come velocizzare una importazione massiva di dati utilizzando drupal 7. Tanto per iniziare diciamo che la classica funzione node_save con Drupal 7 aggiorna i nostri nodi ha al suo interno
$transaction = db_transaction();
la quale fa iniziare la transazione e al termine della node save come il sistema esce dalla funzione viene eseguito immediatamente un commit.
Il punto di vista di PostgreSQL
Ora PostgreSQL dal canto suo per rendere al meglio in fase di inserminto massivo predilige avere qualcosa del tipo :
begin transaction;
insert .....
insert .....
insert .....
.................
commit transaction;
al posto di qualcosa del tipo :
begin transaction;
insert .....
commit transaction;
begin transaction;
insert .....
commit transaction;
....................................
begin transaction;
insert .....
commit transaction;
Cioè, dal punto di vista delle perfomance è meglio avere tante insert tra un begin e un commit finale al posto di avere tante transazioni con una sola insert all'interno. E' chiaro che se da una parte in questo modo diminuiamo l'overhead dovuto alle n transazioni aumento la velocità di inserimento del batch dall'altra con la soluzione un begin,commit ed n insert, se una sola delle n insert non va a buon fine ecco che tutta la procedura di inserimento va in rollback.
Il punto di vista di DRUPAL 7
Per poter andare ad ottenere quello che deisderiamo, andiamo a modificare la funzione
node_save($node) di drupal e scriviamo la funzione
batch_node_save($node,$transaction) :
function batch_node_save($node,$transaction) {
try {
// Load the stored entity, if any.
if (!empty($node->nid) && !isset($node->original)) {
$node->original = entity_load_unchanged('node', $node->nid);
}
field_attach_presave('node', $node);
global $user;
// Determine if we will be inserting a new node.
if (!isset($node->is_new)) {
$node->is_new = empty($node->nid);
}
// Set the timestamp fields.
if (empty($node->created)) {
$node->created = REQUEST_TIME;
}
// The changed timestamp is always updated for bookkeeping purposes,
// for example: revisions, searching, etc.
$node->changed = REQUEST_TIME;
$node->timestamp = REQUEST_TIME;
$update_node = TRUE;
// Let modules modify the node before it is saved to the database.
module_invoke_all('node_presave', $node);
module_invoke_all('entity_presave', $node, 'node');
if ($node->is_new || !empty($node->revision)) {
// When inserting either a new node or a new node revision, $node->log
// must be set because {node_revision}.log is a text column and therefore
// cannot have a default value. However, it might not be set at this
// point (for example, if the user submitting a node form does not have
// permission to create revisions), so we ensure that it is at least an
// empty string in that case.
// @todo: Make the {node_revision}.log column nullable so that we can
// remove this check.
if (!isset($node->log)) {
$node->log = '';
}
}
elseif (!isset($node->log) || $node->log === '') {
// If we are updating an existing node without adding a new revision, we
// need to make sure $node->log is unset whenever it is empty. As long as
// $node->log is unset, drupal_write_record() will not attempt to update
// the existing database column when re-saving the revision; therefore,
// this code allows us to avoid clobbering an existing log entry with an
// empty one.
unset($node->log);
}
// When saving a new node revision, unset any existing $node->vid so as to
// ensure that a new revision will actually be created, then store the old
// revision ID in a separate property for use by node hook implementations.
if (!$node->is_new && !empty($node->revision) && $node->vid) {
$node->old_vid = $node->vid;
unset($node->vid);
}
// Save the node and node revision.
if ($node->is_new) {
// For new nodes, save new records for both the node itself and the node
// revision.
drupal_write_record('node', $node);
_node_save_revision($node, $user->uid);
$op = 'insert';
}
else {
// For existing nodes, update the node record which matches the value of
// $node->nid.
drupal_write_record('node', $node, 'nid');
// Then, if a new node revision was requested, save a new record for
// that; otherwise, update the node revision record which matches the
// value of $node->vid.
if (!empty($node->revision)) {
_node_save_revision($node, $user->uid);
}
else {
_node_save_revision($node, $user->uid, 'vid');
$update_node = FALSE;
}
$op = 'update';
}
if ($update_node) {
db_update('node')
->fields(array('vid' => $node->vid))
->condition('nid', $node->nid)
->execute();
}
// Call the node specific callback (if any). This can be
// node_invoke($node, 'insert') or
// node_invoke($node, 'update').
node_invoke($node, $op);
// Save fields.
$function = "field_attach_$op";
$function('node', $node);
module_invoke_all('node_' . $op, $node);
module_invoke_all('entity_' . $op, $node, 'node');
// Update the node access table for this node. There's no need to delete
// existing records if the node is new.
$delete = $op == 'update';
node_access_acquire_grants($node, $delete);
// Clear internal properties.
unset($node->is_new);
unset($node->original);
// Clear the static loading cache.
entity_get_controller('node')->resetCache(array($node->nid));
// Ignore slave server temporarily to give time for the
// saved node to be propagated to the slave.
db_ignore_slave();
}
catch (Exception $e) {
$transaction->rollback();
watchdog_exception('node', $e);
throw $e;
}
}
Il nostro programma
A questo punto basta richiamare poi dal nostro codice la funzione nel seguente modo :
$transaction = db_transaction();
Inizio ciclo di inserimento nodi..
if ($node = node_submit($node))
batch_node_save($node,$transaction);
fine ciclo inserimento nodi
Il commit finale verrà eseguito automaticamente all'uscita della nostra funzione