第7章 Batch API(批处理)

作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com

我们做开发的时候,经常会遇到批量处理数据的情况,如果一次处理10条或者上百条数据,我们一次性的处理完成就可以了。当需要处理上千条、上万条的数据时,我们不可能把所有的数据,一股脑的都加载进来,逐一处理,这是行不通的,很容易就超出了PHP的各种限制,比如内存限制、执行时间的限制。这个时候,就需要批处理这种方式,Drupal提供了一套批处理API,方便我们做这件事情。

什么是批处理呢?批处理,顾名思义,就是一批一批的处理,假如有一万条数据需要处理,我们一批处理20条,当处理完这20条后,批处理API会再次发送一个HTTP请求,系统接着处理下面的20条,处理完以后,再发送HTTP请求,这样循环下去,直到这一万条数据处理完为止。注意,这里发送的HTTP 请求,是由Drupal自动完成的,在我们看起来,Drupal一次性的把所有的数据都处理了,实际后台方面,Drupal是把这些数据分成若干批分别处理。

什么地方用到了批处理?Drupal里面使用批处理的地方太多了,比如我们安装Drupal的时候,模块的安装,使用的就是批处理,这个是我们再熟悉不过的了。我们使用Apachesolr的时候,当需要重建所有索引的时候,而数据量又比较大,这个时候就可以使用批处理进行索引。批量修改特定字段的值时,如果涉及到的实体数量比较大时,也需要使用批处理。Feeds模块,使用单独表单提交后,导入数据时,用的就是批处理。Drupal系统中,用到的地方还有很多。


Drupal版本:

1使用Batch API批量修改各种会员价格

作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com

我们在为不同角色设置不同价格时,曾经用到过一个uc_batch_price模块,它能够根据定义好的折扣,一次性的批量修改所有商品的会员价格。具体配置可以参看第三集,这里我们主要讲解一下代码,首先是info文件:

name = Ubercart Batch Price

description = Ubercart Batch Price.

dependencies[] = uc_price_per_role

package = Ubercart - extra

core = 7.x

这里面的内容我们都比较熟悉了,我在编写程序的时候,如果这个模块具有通用性,我尽量都采用英文的形式,这样更便于其它用户接受。这里我们依赖于uc_price_per_role模块。

uc_batch_price.module文件中,我们实现了hook_menu,这里定义了两个菜单项:

/**

 * 实现钩子hook_menu().

 */

function uc_batch_price_menu() {

  $items['admin/store/settings/price_per_role/default'] = array(

    'title' => 'Default setting',

    'description' => 'Configure price per role settings.',

    'access arguments' => array('administer store'),

    'page callback' => 'drupal_get_form',

    'page arguments' => array('uc_batch_price_default_form'),

    'type' => MENU_LOCAL_TASK,

'file' => 'uc_batch_price.admin.inc',

  );

 

  $items['admin/store/settings/price_per_role/batch'] = array(

    'title'            => 'Batch process price',

    'page callback'    => 'drupal_get_form',

    'page arguments'    =>  array('uc_batch_price_batch_form'),

    'access arguments' => array('administer store'),

    'weight'           => 10,

    'type'             => MENU_LOCAL_TASK,

'file' => 'uc_batch_price.admin.inc',

  );

  return $items;

}

admin/store/settings/price_per_role/default,这是是定义配置表单的,对应的表单定义位于uc_batch_price.admin.inc'admin/store/settings/price_per_role/batch,用来定义批处理所在的表单页面的,对应的表单也位于uc_batch_price.admin.inc

我们先来看一下,不同角色的默认折扣配置页面的实现代码:

function uc_batch_price_default_form(){

  $enabled = variable_get('uc_price_per_role_enabled', array());

  $enabled_roles = array_filter($enabled);

  $uc_price_per_role_default = variable_get('uc_price_per_role_default', array());

  $roles = user_roles();

  //$rid = 1;

  //debug($roles);

  foreach($enabled_roles as $rid => $enabled){

//$rid++;

    $form['role'][$rid] = array(

      '#type' => 'fieldset',

      '#collapsible' => TRUE,

      '#collapsed' => FALSE,

      '#title' => $roles[$rid],

    );

    $form['role'][$rid]['multiplicator_'.$rid] = array(

      '#type' => 'textfield',

      '#title' => t('乘数'),

      '#description' => t('大于0小于等于1的小数'),

      '#default_value' => isset($uc_price_per_role_default[$rid]) ? $uc_price_per_role_default[$rid] : 0.8,

      '#size' => 16,

    );

  }

  $form['submit'] = array(

    '#type' =>'submit',

    '#value' => t('Submit'),

  );

  return $form;

}

 

function uc_batch_price_default_form_validate($form, &$form_state) {

  $enabled = variable_get('uc_price_per_role_enabled', array());

  $enabled_roles = array_filter($enabled);

  foreach($enabled_roles as $rid => $enabled){

    $multiplicator = $form_state['values']['multiplicator_'.$rid];

    if (!empty($multiplicator) && !is_numeric($multiplicator)) {

      form_set_error('multiplicator_'.$rid, t('乘数必须是一个数字.'));

    }

    elseif (!empty($multiplicator)) {

      if(($multiplicator > 1) || ($multiplicator < 0)){

         form_set_error('multiplicator_'.$rid, t('乘数必须是一个大于0小于等于1的数字.'));

  }

    }

  }

}

 

function uc_batch_price_default_form_submit($form, &$form_state) {

  $enabled = variable_get('uc_price_per_role_enabled', array());

  $enabled_roles = array_filter($enabled);

  $uc_price_per_role_default = array();

  foreach($enabled_roles as $rid => $enabled){

    $uc_price_per_role_default[$rid] = $form_state['values']['multiplicator_'.$rid];

  }

  variable_set('uc_price_per_role_default', $uc_price_per_role_default);

}

 

    在uc_batch_price_default_form里面,我们为每个与积分相关的会员角色,定义了一个表单元素,用来设置折扣值,比如8折,就是输入0.8就可以了。在表单的验证函数里面,我们对输入的折扣进行了验证,确保它是一个从01的小数。在提交函数里面,我们把每个角色的折扣值,都保存到了变量$uc_price_per_role_default里面,并通过variable_set,将它保存到数据库中。这里面的逻辑没有什么复杂的。

我们来看批处理的代码部分:

function uc_batch_price_batch_form(){

  $product_types = uc_product_types();

  $types = node_type_get_types();

  $options = array();

  //debug($product_types);

  //debug($types);

  foreach ($product_types as $key => $value) {

    $options[$value] = $types[$value]->name;

  }


  $form['types'] = array(

    '#title' => t('Content types to be processed'),

    '#type' =>'checkboxes',

    '#description' =>t('All nodes of these selected types will be processed'),

    '#options' => $options,

  );

  $form['submit'] = array(

    '#type' =>'submit',

    '#value' => t('Submit'),

  );

  return $form;

}

 

function uc_batch_price_batch_form_submit($form,&$form_state){

  $types = array_filter($form_state['values']['types']);

  //drupal_set_message('1234567');

  $batch = array(

    'operations' => array(

      array('uc_batch_price_price_process',array($types)),

    ),

    'finished' => 'uc_batch_price_batch_finished',

    'title' => t('批量处理价格'),

    'init_message' => t('开始批量处理价格.'),

    //'progress_message' => t('Reindexed @current out of @total.'),

    'error_message' => t('批量处理价格遇到错误.'),

'file' => drupal_get_path('module', 'uc_batch_price') . '/uc_batch_price.admin.inc',

  );

  batch_set($batch);

}

 

function uc_batch_price_price_process($types, &$context){

  //drupal_set_message('123456');

  //debug($types);

  $size = 20;

  //drupal_set_message('123456');

  if(!isset($context['sandbox']['progress'])){

    $context['sandbox']['progress'] = 0;

    $context['sandbox']['last_nid'] = 0;

    $context['sandbox']['max'] = db_select('node', 'n')

      ->condition('n.type', $types, 'IN')

      ->countQuery()

      ->execute()

      ->fetchField();

  }

  $enabled = variable_get('uc_price_per_role_enabled', array());

  $enabled_roles = array_filter($enabled);

  $uc_price_per_role_default = variable_get('uc_price_per_role_default', array());

  //drupal_set_message('123456');

  $query = db_select('node', 'n')

    ->fields('n', array('nid'))

    ->condition('n.type', $types, 'IN')

->condition('n.nid', $context['sandbox']['last_nid'], '>')

->orderBy('n.nid', 'ASC')

    ->range(0, $size);

  $result = $query->execute();

  foreach ($result as $record) {

    //drupal_set_message('123456');

    $product = node_load($record->nid);

$list_price = $product->list_price;

    foreach($enabled_roles as $rid => $enabled){

      if(!empty($uc_price_per_role_default[$rid])){

      //if(empty($product->role_prices[$rid]) && !empty($uc_price_per_role_default[$rid])){

    $product->role_prices[$rid] = $list_price * $uc_price_per_role_default[$rid];

  }

}

    node_save($product);

    $context['sandbox']['progress']++;

    $context['sandbox']['last_nid'] = $product->nid;

  }

  if($context['sandbox']['progress'] ==$context['sandbox']['max']){

    $context['finished'] = 1;

  }else{

    $context['finished'] = $context['sandbox']['progress']/$context['sandbox']['max'];

  }

}

 

function uc_batch_price_batch_finished($success, $results, $operations){

  if ($success) {

    // Here we do something meaningful with the results.

    $message = t('批量处理价格完成');

  }

  else {

    // An error occurred.

    // $operations contains the operations that remained unprocessed.

    $error_operation = reset($operations);

    $message = '在批量处理价格时出现一个错误'. $error_operation[0] .' 其参数为 :'. print_r($error_operation[0], TRUE);

  }

  drupal_set_message($message);

}

这里面的代码,包括四部分,表单定义、表单的提交处理函数、批处理函数、批处理的完成函数。我们的批处理程序,通常都是由这四部分组成的。

在表单定义函数uc_batch_price_batch_form里面,我们使用uc_product_types获取了所有的产品类型,把它作为选项,赋值给复选框。注意,这里的node_type_get_types,没有什么用,这个程序是同别的地方复制过来的。

在表单提交函数uc_batch_price_batch_form_submit里面,我们首先获取所有需要处理的产品类型,注意这里是如何提取复选框里面的数据的,我们使用了array_filter

$types = array_filter($form_state['values']['types']);

接着,我们定义了一个批处理:

  $batch = array(

    'operations' => array(

      array('uc_batch_price_price_process',array($types)),

    ),

    'finished' => 'uc_batch_price_batch_finished',

    'title' => t('批量处理价格'),

    'init_message' => t('开始批量处理价格.'),

    //'progress_message' => t('Reindexed @current out of @total.'),

    'error_message' => t('批量处理价格遇到错误.'),

'file' => drupal_get_path('module', 'uc_batch_price') . '/uc_batch_price.admin.inc',

  );

我们看到,批处理就是一个数组,里面包含几个键:operationsfinishedtitleinit_messageprogress_messageerror_messagefile。这里面的titleinit_messageerror_message都比较好理解,分别用来设置标题、初始化消息、错误消息;progress_message,用来设置批处理过程中的消息,提示用户完成了多少,这个我一般没有用过,一直都没有用过,因为上面有个进度条,可以看到大致的进度;file用来指定批处理所在的文件,在批处理的过程中,完成一批就会发送一个新的HTTP请求,新的请求过来以后,默认会加载所有的module文件,如果我们这里没有设置这个file文件的话,由于我们将它放到了uc_batch_price.admin.inc里面,Drupal就会找不到我们的批处理函数,这样就会报错,通过设置file,系统就可以加载uc_batch_price.admin.inc文件,继续执行里面的批处理函数了。

以前的时候,我都是把批处理的逻辑代码放到module文件里面的,也尝试过几次,放到admin.inc这样的文件里面,总是报错,后来在阅读别人的代码时,发现批处理没有放到module文件里面,我才学会用了通过设置这个file键,将批处理的逻辑放到inc文件里面。这是一个小的进步。我昨天,看别人写的一篇文章,如何在themetemplate.php文件里面实现mytheme_form_alter这个钩子函数,以前就不知道这个技巧,在Drupal6下面是不可以这样的,但是在Drupal7下面,却是可以的,技术在分享中进步。我通过阅读module_invoke_allmodule_implementsystem_listAPI函数,终于弄明白了为什么mytheme_form_alter也可以工作的原因,原来是这样的,在system_list里面,获取模块列表的SQL语句是这样写的:

$result = db_query("SELECT * FROM {system} WHERE type = 'theme' OR (type = 'module' AND status = 1) ORDER BY weight ASC, name ASC");

这里同时支持了thememodule,一下子就明白了为什么。我对批处理里面的file键的学习,就和这个差不多,在阅读别人的代码里面,看到了这个用法,留心了一下,下次实现同样的功能时,就派上了用场。

finished是用来指定批处理的完成函数的,这里指定为uc_batch_price_batch_finished。我们来看看批处理完成函数的模式:

function uc_batch_price_batch_finished($success, $results, $operations){

  if ($success) {

    // Here we do something meaningful with the results.

    $message = t('批量处理价格完成');

  }

  else {

    // An error occurred.

    // $operations contains the operations that remained unprocessed.

    $error_operation = reset($operations);

    $message = '在批量处理价格时出现一个错误'. $error_operation[0] .' 其参数为 :'. print_r($error_operation[0], TRUE);

  }

  drupal_set_message($message);

}

在这里面,逻辑处理非常简单,如果成功了,设置一个成功的消息,如果失败了设置一个失败的消息,消息的设置,使用的是drupal_set_message;这里唯一有点难以理解的是

$error_operation[0] .' 其参数为 :'. print_r($error_operation[0], TRUE)

我们只需要知道,这段代码是用来显示错误消息的即可,对于error_operation里面的结构,有兴趣的可以继续研究,工作中是用不到的。所以我们在编写批处理的过程中,对于这个批处理完成函数,只需要修改它的函数名字,消息的内容,包括成功时的消息、失败时的消息,就这三个地方。其它工作就是复制粘贴。

我们来看看批处理数组的最后一个参数operations

    'operations' => array(

      array('uc_batch_price_price_process',array($types)),

    ),

这是一个数组,数组里面又是一个数组;在里面的数组中,第一个uc_batch_price_price_process,就是批处理的处理函数,第二个array($types),就是传递给处理函数的参数;我们从operations这个名字可以看出,它是一个复数形式,就是可以支持多个处理函数,但是我用到的都是一个处理函数就够了的情况;另外需要注意的是,参数的传递,也是采用数组的形式,这意味着我们可以传递多个参数。注意,在Drupal7下面,带有参数的处理函数是这样写的:

function uc_batch_price_price_process($types, &$context)

这里的$types放到了&$context前面,我记得以前在Drupal6下面,参数的传递好像是这样的:

function uc_batch_price_price_process(&$context, $types)

如果在Drupal7下面也这样写的话,就会出问题,我开始就是这样搬过来的,结果接收不到参数,不知道为什么。所以这里特别强调一下,这个参数的顺序。

在表单提交函数的最后,使用batch_set($batch),这样就将我们定义好的批处理传递给了Drupal系统本身。batch_set($batch),这句话,就这么用的,不需要修改。

我们现在来看一下批处理的处理函数里面的逻辑,这里面的逻辑大致包含三大块,头部、中部、尾部。头部和尾部的代码在每个同类的函数中变化都不大,只有中间部分的代码,才真正是我们需要着重修改的。

我们来看头部代码:

  $size = 20;

  //drupal_set_message('123456');

  if(!isset($context['sandbox']['progress'])){

    $context['sandbox']['progress'] = 0;

    $context['sandbox']['last_nid'] = 0;

    $context['sandbox']['max'] = db_select('node', 'n')

      ->condition('n.type', $types, 'IN')

      ->countQuery()

      ->execute()

      ->fetchField();

  }

这里的size是用来定义一批次,能够处理多少条数据的,我一般使用20,根据实际也会有所调整。比如每条数据的计算工作量比较大时,我们可以把size调的小一点;单条数据的计算量比较小时,我们就可以把size相应的调的大一点。

接下来,是一个初始化设置,用来设置上下文里面的三个变量,progresslast_nidmax;其中progressmax是基本上所有的场合都用到的,progress表示已经处理了多少,max表示总共有多少需要处理,为了获取总共有多少条记录需要处理,我们这里使用了db_select查询语句;而last_nid是我们这里用到的,或者说是特有的,它表示最后处理的节点的nid。注意这些变量都是存放到$contextsandbox里面的,sandbox中文常常翻译为沙盒。

我们现在来看看底部的代码部分:

  if($context['sandbox']['progress'] ==$context['sandbox']['max']){

    $context['finished'] = 1;

  }else{

    $context['finished'] = $context['sandbox']['progress']/$context['sandbox']['max'];

  }

这段代码的含义是,如果进度progress等于最大记录时,将$context['finished']设置为1,否则将它设置为两者的除数。当$context['finished']等于1时,批处理系统就会结束处理,调用批处理的完成函数。

掐头去尾后,就剩下了中间的逻辑部分,在中间的这部分代码中,我们首先获取当前需要我们处理的这一批数据,然后对这一批数据进行迭代循环,每循环一次progress都会自动加1,我们同时将last_nid设置为当前的节点的nid。这里面唯一需要一点技巧的就是,如何获取当前批次的所有数据,我们这里使用了db_select

  $query = db_select('node', 'n')

    ->fields('n', array('nid'))

    ->condition('n.type', $types, 'IN')

->condition('n.nid', $context['sandbox']['last_nid'], '>')

->orderBy('n.nid', 'ASC')

    ->range(0, $size);

我们这里可以看到last_nid的用处了,由于我们是按照节点从小到大的顺序来处理的,通过使用last_nid,就可以将已经处理过了的排除在外了,读取过来的,恰好是我们当前需要处理的数据。

迭代循环里面的逻辑代码,我们这里的逻辑是加载当前节点,根据我们设置好的折扣,来修改对应角色的价格,修改过后保存节点。这部分是需要我们自己编写的。

通过这个例子,我们可以看到,批处理的相关代码,三段论,固定格式的,我们只需要改改里面的名字,然后再把中间部分的逻辑处理部分改为我们自己的即可,写过一次以后,下次再写,就跟走平地一样,和写一个只需要处理20条记录的程序代码,难度差不多,费不了多少事。

我们最后顺便看一下,这个模块的另一个钩子实现,hook_node_presave

function uc_batch_price_node_presave($node) {

  if (!empty($node->role_prices)) {

    $enabled = variable_get('uc_price_per_role_enabled', array());

    $enabled_roles = array_filter($enabled);

    $uc_price_per_role_default = variable_get('uc_price_per_role_default', array());

$list_price = $node->list_price;

foreach($enabled_roles as $rid => $enabled){

      if(empty($node->role_prices[$rid]) && !empty($uc_price_per_role_default[$rid])){

        $node->role_prices[$rid] = $list_price * $uc_price_per_role_default[$rid];

      }

    }

  }

}

    在保存节点之前,如果特定的会员价格为空的,我们根据默认折扣,为它赋值,这样用户就不需要分别计算不同会员的价格了。这里用的是hook_node_presave,类似的钩子还有

hook_node_insert,hook_node_update,hook_node_load,hook_node_view,hook_node_view_alter,hook_node_delete,hook_node_validate,hook_node_prepare。随着对应的entity钩子的流行,这些特定于节点的钩子函数,用途越来越窄了。


Drupal版本:

2批量删除模块batchdelete

作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com

我们再来看一个例子,批量删除模块batchdelete,这个模块主要用来批量删除节点的,还可以删除自定义区块、词汇表,不过后面两者的删除,没有用到批处理机制。

我们来看一下代码,菜单项定义:

function batchdelete_menu() {

  $items['admin/config/development/batchdelete'] = array(

    'title'            => 'Batch delete node',

    'page callback'    => 'drupal_get_form',

'page arguments'    =>  array('batchdelete_form'),

    'access arguments' => array('administer site configuration'),

    'weight'           => 1,

    'type'             => MENU_NORMAL_ITEM,

   

  );

  $items['admin/config/development/batchdelete/node'] = array(

    'title'            => 'Batch delete node',

    'page callback'    => 'drupal_get_form',

'page arguments'    =>  array('batchdelete_form'),

    'access arguments' => array('administer site configuration'),

    'weight'           => 1,

    'type'             => MENU_DEFAULT_LOCAL_TASK,

   

  );

  $items['admin/config/development/batchdelete/block'] = array(

    'title'            => 'Batch delete block',

    'page callback'    => 'drupal_get_form',

'page arguments'    =>  array('batchdelete_block_form'),

    'access arguments' => array('administer site configuration'),

    'weight'           => 2,

    'type'             => MENU_LOCAL_TASK,

   

  );

  $items['admin/config/development/batchdelete/taxonomy'] = array(

    'title'            => 'Batch delete taxonomy term',

    'page callback'    => 'drupal_get_form',

'page arguments'    =>  array('batchdelete_taxonomy_form'),

    'access arguments' => array('administer site configuration'),

    'weight'           => 3,

    'type'             => MENU_LOCAL_TASK,

   

  );

  return $items;

}

这里我们为节点、区块、分类,分别定义了一个菜单项,我们来看一下,节点的删除代码。

function batchdelete_form(){

  $options = array();

  $types = node_type_get_types();

  foreach ($types as $key => $values) {

    $options[$key] = $values->name;

  }


  $form['types'] = array(

    '#title' => t('Content types to be delete'),

    '#type' =>'checkboxes',

    '#description' =>t('All nodes of these selected types will be deleted'),

    '#options' => $options,

  );

 

  $form['submit'] = array(

    '#type' =>'submit',

    '#value' => t('Delete'),

  );

  return $form;

}

 

function batchdelete_form_submit($form,&$form_state){

  $types = array_filter($form_state['values']['types']);

  $batch =array(

    'operations' => array(

      array('batchdelete_node_process', array($types)),

    ),

    'finished' => 'batchdelete_node_finished',

    'title' => t('删除节点'),

    'init_message' => t('开始批量删除.'),

    //'progress_message' => t('Reindexed @current out of @total.'),

    'error_message' => t('批量删除遇到错误.'),

  );

  batch_set($batch);

}

function batchdelete_node_process($types, &$context){

  $size =100;

  //$types_str ='story,page';

  //$types_str = "'".implode("','", $types)."'";

  //drupal_set_message('types:'.print_r($types));

  //debug($types);

  if(!isset($context['sandbox']['progress'])){

    $context['sandbox']['progress'] = 0;

    $context['sandbox']['max'] = db_select('node', 'n')

      ->condition('n.type', $types, 'IN')

      ->countQuery()

      ->execute()

      ->fetchField();

    //drupal_set_message('max:'.$context['sandbox']['max']);

  }

  //$sql = "SELECT nid FROM {node} WHERE type in ($types_str)";

  //$result = db_query_range($sql,0,$size);

  $query =db_select('node', 'n')

    ->fields('n', array('nid'))

    ->condition('n.type', $types, 'IN')

    ->range(0, $size);

  $result = $query->execute();


  foreach ($result as $record) {

    //drupal_set_message('123max:'.$result->nid);  

    node_delete($result->nid);

    $context['sandbox']['progress']++;

    //$context['message'] = t('删除节点 %nid',array('%nid' => $node['nid']));

  }

  if($context['sandbox']['progress'] ==$context['sandbox']['max']){

    $context['finished'] = 1;

  }else{

    $context['finished'] = $context['sandbox']['progress']/$context['sandbox']['max'];

  }

}

 

function batchdelete_node_finished($success, $results, $operations){

  if ($success) {

    // Here we do something meaningful with the results.

    $message = t('节点删除完成');

  }

  else {

    // An error occurred.

    // $operations contains the operations that remained unprocessed.

    $error_operation = reset($operations);

    $message = '在删除节点时出现一个错误'. $error_operation[0] .' 其参数为 :'. print_r($error_operation[0], TRUE);

  }

  drupal_set_message($message);

}

这段代码,和我们前面第一个例子非常类似,其实我们第一个例子里面的代码,就是从这个模块复制过来的。而batchdelete模块,则是从Drupal6下面升级过来的,我们最初编写的是Drupal6下面的,现在升级到了Drupal7。升级的过程中,就遇到了向批处理的处理函数中传递参数的问题,batchdelete_node_process$types最初是放在后面的,结果后来发现了问题,调试了多次,我们可以看到代码里面还有很多的调试信息,最后才发现参数的顺序问题。

删除自定义区块的代码,还没有升级过来。我们看一下删除词汇表的:

function batchdelete_taxonomy_form(){

  $options=array();

  $vocabs = taxonomy_get_vocabularies();

  foreach($vocabs as $vocab){

    $options[$vocab->vid] = $vocab->name; 

  }

  $form['vids'] = array(

    '#title' => t('要删除的分类词汇表ID'),

    '#type' =>'checkboxes',

    '#description' =>t('请选择哪些分类词汇表要被删除。'),

    '#options' => $options,

  );

  $form['submit'] = array(

    '#type' =>'submit',

    '#value' => t('Delete'),

  );

  return $form;

}

function batchdelete_taxonomy_form_submit($form,&$form_state){

  $vids = array_filter($form_state['values']['vids']);

  foreach($vids as $vid){

    //taxonomy_del_vocabulary($vid);

    $transaction = db_transaction();

    try {

      // Only load terms without a parent, child terms will get deleted too.

      $result = db_query('SELECT t.tid FROM {taxonomy_term_data} t INNER JOIN {taxonomy_term_hierarchy} th ON th.tid = t.tid WHERE t.vid = :vid AND th.parent = 0', array(':vid' => $vid))->fetchCol();

      foreach ($result as $tid) {

        taxonomy_term_delete($tid);

      }

      return SAVED_DELETED;

    }

    catch (Exception $e) {

      $transaction->rollback();

      watchdog_exception('taxonomy', $e);

      throw $e;

    }

  }

}

我们这里面删除的是词汇表下面的所有分类术语,而没有删除词汇表本身。taxonomy_get_vocabularies,用来获取所有的词汇表;taxonomy_del_vocabulary可以用来删除词汇表,taxonomy_term_delete用来删除分类术语。我们这里在删除的过程中,使用了事务处理,它的代码结构大致是这样的:

    $transaction = db_transaction();

    try {

      //做一些操作.

    }

    catch (Exception $e) {

      $transaction->rollback();

      watchdog_exception('taxonomy', $e);

      throw $e;

    }

其实在编写好batchdelete的最初版本后,发现drupal.org上面有个类似的模块,Bulk delete,也是Drupal6下面的模块,我的模块比它晚写了三个月,所以编写的时候,没有注意到,已经存在这样一个模块了。不过,现在Bulk delete模块已经不再维护,为什么呢?因为VBO模块的存在。


Drupal版本:

3VBO

作者:老葛,北京亚艾元软件有限责任公司,http://www.yaiyuan.com

好像我们在前面并没有介绍过VBO,这是一个我在项目中,经常使用的模块,它的全称为Views Bulk Operations,翻译过来就是基于Views的批量操作。我们以批量删除某一类型的节点为例,看看VBO是如何实现这样的功能的。我们还在bookstore里面操作。

我们创建一个视图,初始配置如下:

图片1.png 

点击保存并继续按钮,在字段的添加对话框里面,找到“Bulk operations: 内容

图片2.png 

选中这个字段,点击应用按钮,该字段的配置选项很多,我们看与批处理相关的:

图片3.png 

这里我们采用默认配置就可以了,前面两个选项的意思是,采用下拉选择框的形式,还是逐个采用按钮的形式,来显示操作。Enable "Select all items on all pages",表示允许选中所有页面的全部条目;Force single,表示强制单选;Display processing result,表示是否显示处理结果的相关消息;Number of entities to load at once,就是一次加载多少个实体,也就是我们在批处理中,一次处理多少条记录。

再往下的配置,就是可选的操作:

图片4.png 

除了我们这里想要的删除操作以外,这里包括更多的动作可供选择,比如发布内容、使内容置顶、发送邮件、修改实体的值,等等,我们根据需要还可以编写自己的操作。VBO支持以下操作,系统自带的Action(动作),VBO定义的动作,RULES定义的动作。

我们这里选中,第一个删除条目Delete item (views_bulk_operations_delete_item)),其它采用默认配置即可。此时会显示出来三个新的复选框:

图片5.png 

Enqueue the operation instead of executing it directly,表示采用队列的形式,而不是直接执行;Skip confirmation step,表示跳过确认步骤;Override label,用来覆写标签。我们这里采用默认的即可。保存。

接着,我们将“Bulk operations: 内容 (内容)”调整到内容标题的上面:

图片6.png 

最后,添加一个内容类型过滤器,并将其暴露(exposed)出来:

图片7.png 

保存视图,现在访问页面,我们就实现了和batchdelete模块同样的功能:

图片8.png 

配置起来非常简单,所以,当我们需要批量操作Drupal系统里面的各种数据时,我们应该首先来看一看,VBO是否能够满足我们的需求,如果不能满足的话,再考虑Batch API的形式。其实VBO本身采用的也是Batch API的形式。我们有关批处理的,就介绍到这里。写完才发现,前面在第三集里面已经简单的介绍过VBO了。

 



Drupal版本: