УрокОбработка большого количества данных с помощью Batch API

Продолжается тема обработки большого количества данных. Как делать это в фоновом режиме при помощи крона я уже писал. Теперь о том, как реализовать обработку данных в пользовательском интерфейсе.

С работой батча сталкивались все, кто работал с Друпалом. Во время установки самого Дру, во время установки новых модулей (импорт переводов), обновление базы данных с помощью update.php и так далее. Визуально батч представляет собой полоску с индикатором выполнения:

batch.png

Принцип работы следующий:

  • На батч подаётся массив с входными данными
  • Указывается функция, которая будет обрабатывать каждый элемент массива
  • Указывается количество элементов, обрабатываемых за один вызов обрабатывающей функции (колбэка)
  • Задаётся функция, которая будет вызвана после завершения обработки всего массива

С большего - это всё. Теперь ближе к нюансам реализации. Как и всегда, писать будем на примере некоторой задачи.

Задача

Добавить к заголовку каждого материала имя пользователя, который его написал (я осознаю абсурдность данной задачи, но для примера она подходит идеально).

Решение

Пишем небольшой модуль. Назовём его, к примеру, title_changer.

Для начала создаём функцию, которая вернёт нам массив с IDшниками всех материалов, которые есть на сайте.

function title_changer_load_nids() {
  // Выбираем из базы все nid
  $result = db_select('node')
    ->fields('node', array('nid'))
    ->orderBy('nid')
    ->execute();
 
  // Формируем массив с nid'ами
  $output = array();
  foreach ($result as $node) {
    $output[] = $node->nid;
  }
  return $output;
}

Далее создадим страницу, на которой будет форма с кнопкой "GO". При нажатии на кнопку будет запускаться процесс изменения заголовков с помощью батча:

В файле title_changer.module добавляем страницу, на которой будет располагаться форма:

/**
 * Implements hook_menu().
 */
function title_changer_menu() {
  $items = array();
 
  $items['title_changer'] = array(
    'title' => 'Example of Batch process',
    'page callback' => 'drupal_get_form',
    'page arguments' => array('title_changer_form'),
    'access callback' => TRUE,
    'file' => 'title_changer.forms.inc',
  );
 
  return $items;
}

Как создавать страницы я уже рассказывал, поэтому останавливаться на этом не буду.

В файле title_changer.forms.inc добавляем форму с кнопкой:

/**
 * Title changer form
 * Allows to start Batch operations
 */
function title_changer_form() {
  $form = array();
 
  $form['submit'] = array(
    '#type' => 'submit',
    '#value' => t('GO'),
  );
 
  return $form;
}
 
/**
 * Submit callback for title changer form
 */
function title_changer_form_submit($form, &$form_state) {
 
  // Получаем массив с ID материалов
  $data = title_changer_load_nids();
 
  // Создаём массив с данными для батча
  $batch = array(
    'title' => t('Node processing'),
    'operations' => array(
      array('title_changer_process_node', array($data)),
    ),
    'finished' => 'title_changer_finished_callback',
    'file' => drupal_get_path('module', 'title_changer') . '/title_changer.batch.inc',
  );
 
  // Создаём работу для батча
  batch_set($batch);
 
  // Стартуем батч
  batch_process();
}

По поводу создания массива для батча:

  • operations - список колбэков и данных, которые будут переданы в них. В данном случае у меня будет создана функция title_changer_process_node(), которая получит массив с ID материалов.
  • finished - функция, которая будет вызвана после окончания обработки данных
  • file - файл, в котором находятся колбэки батча. В данном случае - функции title_changer_process_node() и title_changer_finished_callback()

В результате выполнения этих действий на странице /title_changer появилась такая форма:

batch2.png

Осталось дописать функции, которые обрабатывают данные и завершают работу батча. Итак, файл title_changer.batch.inc:

/**
 * Process every item in batch
 */
function title_changer_process_node($nodes, &$context) {
 
  // Количество материалов, которые будут обработаны одной пачкой за раз
  $limit = 1;
 
  // Задаём начальные значения для батча
  if (empty($context['sandbox']['progress'])) {
    // Текущее количество обработанных материалов
    $context['sandbox']['progress'] = 0;
    // Общее количество материалов, которые надо обработать
    $context['sandbox']['max'] = count($nodes);
  }
 
  // Сохраняем массив с материалами
  // Далее этот массив будет меняться
  if(empty($context['sandbox']['items'])) {
    $context['sandbox']['items'] = $nodes;
  }
 
  $counter = 0;
  if(!empty($context['sandbox']['items'])) {
 
    // Убираем из массива с данными уже обработанные материалы
    if ($context['sandbox']['progress'] != 0) {
      array_splice($context['sandbox']['items'], 0, $limit);
    }
 
    foreach ($context['sandbox']['items'] as $entity) {
      if ($counter != $limit) {
 
        // Загружаем материал
        $node = node_load($entity->nid);
        // Загружаем автора материала
        $user = user_load($node->uid);
        // Добавляем к заголовку материала автора, который его создал
        $node->title .= t(' Created by !user', array('!user' => $user->name));
        // Сохраняем материал
        node_save($node);
 
        // Увеличиваем счётчики
        $counter++;
        $context['sandbox']['progress']++;
 
        $context['message'] = t('Now processing node %progress of %count', array('%progress' => $context['sandbox']['progress'], '%count' => $context['sandbox']['max']));
        $context['results']['processed'] = $context['sandbox']['progress'];
      }
    }
  }
 
  // Проверка, не пора ли закончить обработку данных.
  // Как только количество обработанных будет равно общему количеству материалов - обработка завершится
  if ($context['sandbox']['progress'] != $context['sandbox']['max']) {
      $context['finished'] = $context['sandbox']['progress'] / $context['sandbox']['max'];
  }
}
 
/**
 * Finish callback for Batch
 */
function title_changer_finished_callback($success, $results, $operations) {
  if ($success) {
    $message = format_plural($results['processed'], 'One node processed.', '@count nodes processed.');
  }
  else {
    $message = t('Finished with an error.');
  }
  drupal_set_message($message);
}

Вот и всё. Теперь обработка данных будет красивая и правильная :) Кто хочет - может скачать исходники модуля title_changer. Пример работы модуля:

batch2_0.png

Практически все комментарии в коде. Единственное, что хочу добавить - это про переменную $context, которая появляется в колбэке. Она всегда добавляется к списку аргументов, которые были переданы при инициализации работы батча:

    'operations' => array(
      array('title_changer_process_node', array($data)),
    ),

Перемененная $context должна содержать элемент $context['finished']. Как только значение его будет больше либо равно единице, выполнение батча завершится.

Так же желательно наличие $context['results']. Здесь хранятся данные, которые будут переданы в завершающий колбэк (в данном случае - title_changer_finished_callback()) в качестве переменной $results. Благодаря этой переменной можно построить дальнейшую логику работы. Например, вывести сообщение с количеством обработанных материалов.

В $context['message'] передаётся сообщение о текущем состоянии выполнения операции.

В $context['sandbox'] хранятся пользовательские данные. То есть программист может сам ложить туда любые значения, которые ему необходимы для выполнения операций.

Основные преимущества батча:

  • Выполнение скрипта не прервётся из-за превышения лимита на выполнение скрипта (max_execution_time)
  • Если случится что-то непредвиденное (например, выключится компьютер, или сервер ляжет), то всегда можно продолжить с того места, где выполнение прервалось.

Недостатки:

  • Страницу с батчем нельзя закрыть, пока он не завершит все свои операции, иначе работа будет прервана.

Заключение

Выполнение обработки данных с помощью Batch API рекомендуется делать в том случае, если операция запускается непосредственно пользователем, обычно после нажатия кнопки на форме, причём не известно, сколько времени займёт выполняемая операция. Если речь идёт о простейших обработках данных, которые в течение нескольких секунд завершаться - то задумываться о батче не стоит. Однако если предусматриваются некие массовые операции, которые могут занять неопределённый промежуток времени (например, обновление базы данных при миграции Друпала с 6 на 7 версию), то стоит воспользоваться этой технологией.

Комментарии

Аватар пользователя xandeadx
xandeadx написал:

title_changer_process_node не самое лучшее название для функции - есть hook_process_node

25.12.2011 18:51
Аватар пользователя Spleshka
Spleshka написал:

Где ты такой хук увидел? Не могу найти. Ты уверен, что он существует?

25.12.2011 19:01
Аватар пользователя xandeadx
xandeadx написал:

http://drupal.org/node/223430

25.12.2011 20:39
Аватар пользователя Spleshka
Spleshka написал:

Это не хук, а функция темизации. В модуле она не цепляется.

25.12.2011 21:04
Аватар пользователя xandeadx
xandeadx написал:

а ты попробуй ;)

26.12.2011 00:12
Аватар пользователя Spleshka
Spleshka написал:

В том то и дело. Пробовал, на этом же модуле. Этот файл подключается только для выполнения батча, поэтому никакого ущерба не несёт.

26.12.2011 00:41
Аватар пользователя Рефффери
Рефффери написал:

Так все же title_changer_process_node или hook_process_node?

12.07.2012 17:36
Аватар пользователя Максим
Максим написал:

Спасибо за урок. Все ясно и доступно для понимания.

28.08.2012 19:19
Аватар пользователя slivorezka
slivorezka написал:

У меня вопрос:
В процессе между прогрег баром и "Now processing node ..." выводится "Completed 0 of 1.".
Что это параметр?

26.12.2012 19:05
Аватар пользователя Spleshka
Spleshka написал:

Это переменные $context['sandbox']['progress'] и $context['sandbox']['max']

26.12.2012 22:53
Аватар пользователя Кирилл1
Кирилл1 написал:

Недавно сталкнулся с такой петрушкой: необходимо запустить один бач, после его успешного выполнения, создать и запустить второй, затем аналогично третий. Сделал с помощью редиректов - т.е.:
batch_set($batch1);
batch_process('batch_page_1');

соответственно на колбеке для страницы "batch_page_1" описал создание второго бача:
batch_set($batch2);
batch_process('batch_page_2');

так же с третим после второго. Всё изумительно хорошо работает при прямом запуске, происходят редиректы, всё работает. Но при запуске по крону редиректы не работают и второй и третий бачи не запускаются. Кто может подсказать как решить вопрос?

30.05.2014 11:01
Аватар пользователя KissFM
KissFM написал:

$node = node_load($entity->nid); ???
Быть такого не может! $entity - это уже nid (из функции "title_changer_load_nids"), и из nid брать nid ??? Никто об этом и не пишет в комментах? Этот код не может быть рабочим, путаница в названиях переменных сбила автора с толку и он подумал, что работает с объектом, как мне видится.
Там или переменную $entity назвать $nid, или использовать ее помня, что там ID ноды:
$node = node_load($nid);
$node = node_load($entity);
А, вообще - большая благодарность за пост!

12.12.2014 01:50
Аватар пользователя Assasin
Assasin написал:

Как создать ноду в батче?

13.01.2015 11:11
Аватар пользователя Ярослав
Ярослав написал:

Алилуйя, пол вечера пытался вкурить как нормально обработать большой массив данных. Нашел Batch API и Queue API, не понял с лету в чем разница, а тут все по полочкам и с примерами стало на свои места, причем и по вопросу обработки данных и по вопросу а в чем между этими апишками разница. В общем +1 карме автора =)

09.05.2016 00:11

Комментировать