УрокОбработка большого количества данных с помощью Batch API
Продолжается тема обработки большого количества данных. Как делать это в фоновом режиме при помощи крона я уже писал. Теперь о том, как реализовать обработку данных в пользовательском интерфейсе.
С работой батча сталкивались все, кто работал с Друпалом. Во время установки самого Дру, во время установки новых модулей (импорт переводов), обновление базы данных с помощью update.php и так далее. Визуально батч представляет собой полоску с индикатором выполнения:
Принцип работы следующий:
- На батч подаётся массив с входными данными
- Указывается функция, которая будет обрабатывать каждый элемент массива
- Указывается количество элементов, обрабатываемых за один вызов обрабатывающей функции (колбэка)
- Задаётся функция, которая будет вызвана после завершения обработки всего массива
С большего - это всё. Теперь ближе к нюансам реализации. Как и всегда, писать будем на примере некоторой задачи.
Задача
Добавить к заголовку каждого материала имя пользователя, который его написал (я осознаю абсурдность данной задачи, но для примера она подходит идеально).
Решение
Пишем небольшой модуль. Назовём его, к примеру, 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 появилась такая форма:
Осталось дописать функции, которые обрабатывают данные и завершают работу батча. Итак, файл 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. Пример работы модуля:
Практически все комментарии в коде. Единственное, что хочу добавить - это про переменную $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 версию), то стоит воспользоваться этой технологией.
- Spleshka
- 25.12.2011
- 21669
Комментарии
title_changer_process_node не самое лучшее название для функции - есть hook_process_node
Где ты такой хук увидел? Не могу найти. Ты уверен, что он существует?
http://drupal.org/node/223430
Это не хук, а функция темизации. В модуле она не цепляется.
а ты попробуй ;)
В том то и дело. Пробовал, на этом же модуле. Этот файл подключается только для выполнения батча, поэтому никакого ущерба не несёт.
Так все же title_changer_process_node или hook_process_node?
Спасибо за урок. Все ясно и доступно для понимания.
У меня вопрос:
В процессе между прогрег баром и "Now processing node ..." выводится "Completed 0 of 1.".
Что это параметр?
Это переменные
$context['sandbox']['progress']
и$context['sandbox']['max']
Недавно сталкнулся с такой петрушкой: необходимо запустить один бач, после его успешного выполнения, создать и запустить второй, затем аналогично третий. Сделал с помощью редиректов - т.е.:
batch_set($batch1);
batch_process('batch_page_1');
соответственно на колбеке для страницы "batch_page_1" описал создание второго бача:
batch_set($batch2);
batch_process('batch_page_2');
так же с третим после второго. Всё изумительно хорошо работает при прямом запуске, происходят редиректы, всё работает. Но при запуске по крону редиректы не работают и второй и третий бачи не запускаются. Кто может подсказать как решить вопрос?
$node = node_load($entity->nid); ???
Быть такого не может! $entity - это уже nid (из функции "title_changer_load_nids"), и из nid брать nid ??? Никто об этом и не пишет в комментах? Этот код не может быть рабочим, путаница в названиях переменных сбила автора с толку и он подумал, что работает с объектом, как мне видится.
Там или переменную $entity назвать $nid, или использовать ее помня, что там ID ноды:
$node = node_load($nid);
$node = node_load($entity);
А, вообще - большая благодарность за пост!
Как создать ноду в батче?
Алилуйя, пол вечера пытался вкурить как нормально обработать большой массив данных. Нашел Batch API и Queue API, не понял с лету в чем разница, а тут все по полочкам и с примерами стало на свои места, причем и по вопросу обработки данных и по вопросу а в чем между этими апишками разница. В общем +1 карме автора =)
Комментировать