Начиная с версии фреймворка 1.1.13 RC доступен класс итератора CDataProviderIterator для провайдеров данных
CActiveDataProvider, CArrayDataProvider и CSqlDataProvider. Итераторы следует использовать тогда, когда
надо обработать большие объёмы данных, но при этом загрузить все данные сразу в память не представляется
возможным.
CDataProviderIterator производит процесс итерации по данным провайдера: с первой страницы и до последней.
Данный рецепт демонстрирует процесс использования итератора провайдеров данных на реальной задаче,
которая может возникнуть у пользователя фреймворка.
Представим себе ситуацию, когда мы разрабатываем крупное веб-приложение, в котором количество пользователей
может исчисляться сотнями тысяч. Любой крупный веб-сайт так или иначе связан со статистической обработкой
больших массивов данных.
Предположим, что нам была поставлена задача подсчитать средний рейтинг всех пользователей ресурса, имя
пользователя которых начинается на букву A, на букву B, С, D и так далее вплоть до Z. То есть результатом
решения задачи будет массив с ключами-буквами латинского алфавита, содержащий 26 вещественных чисел.
Было бы неправильно пытаться загрузить объекты сотен тысяч пользователей в память и затем осуществлять
вычисление агрегатного значения, основанного на пользовательских данных, т.к. во-первых это очень
неэкономно, а во-вторых оперативной памяти может просто не хватить. Итераторы решают данную задачу тем
образом, что они не загружают абсолютно все данные сразу в память, а делают это небольшими порциями
заданного размера.
Предположим, что модель пользователя сайта выглядит следующим образом:
/**
* @property integer $id
* @property string $username
* @property integer $rating
*/
class User extends CActiveRecord{
/**
* @param string $className
* @return User
*/
public static function model($className=__CLASS__){
return parent::model($className);
}
public function tableName(){
return '{{user}}';
}
}
DDL
(MySQL
) для данной модели выглядит так:
DROP TABLE IF EXISTS tbl_user;
CREATE TABLE tbl_user (
id INT(11) NOT NULL AUTO_INCREMENT,
username VARCHAR(100) NOT NULL,
rating INT(11) NOT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
Пересчитывать средний рейтинг пользователей сайта приемлемо постоянно через определённый период
времени посредством cron
. Результаты можно хранить как в таблице базы данных, так и в обычном
PHP-файле на диске.
Консольная команда, решающая описанную задачу выглядит следующим образом:
<?php
/**
* Консольная команда для работы с пользователями сайта.
*/
class UserCommand extends CConsoleCommand{
/**
* Количество пользователей, получаемых итератором одним блоком.
*/
const RATING_USERS_PER_ITERATION=5000;
/**
* Экшн для пересчёта среднего рейтинга пользователей. Результаты формируются
* для 26 групп пользователей, каждая группа соответствует пользователям, имена
* которых начинаются на одну латинскую букву.
*/
public function actionRating(){
Yii::import('application.models.User',true);
// раскомментируйте строчку ниже если вы используете версию 1.1.13-dev
// Yii::import('system.web.CDataProviderIterator',true);
$letters='abcdefghijklmnopqrstuvwxyz';
$results=array();
for($i=0; $i<strlen($letters); $i++){
$letter=$letters[$i];
echo "Working on '{$letter}' letter...\n";
// провайдер данных и итератор
$dataProvider=new CActiveDataProvider('User',array(
'criteria'=>array(
'select'=>'t.rating',
'order'=>'t.username',
'condition'=>'t.username LIKE :username',
'params'=>array(':username'=>$letter.'%'),
),
));
$iterator=new CDataProviderIterator($dataProvider,
self::RATING_USERS_PER_ITERATION);
// обходим данные для каждой буквы
$result=0;
foreach($iterator as $user)
$result+=$user->rating/($iterator->totalItemCount)*100000;
$results[$letter]=$result/100000;
// умножение на 100000 каждого значения, а потом деление на 100000 общего
// результата сделано для того, чтобы погрешность, вносимая особенностями
// хранения вещественных чисел в современных ПК была минимальна
echo "Letter '{$letter}' is done!\n";
}
// сохраняем результаты вычислений в файле
file_put_contents(Yii::getPathOfAlias('application.data').'/userRating.php',
"<?php\n\nreturn ".var_export($results,true).";\n",LOCK_EX);
}
}
Всё просто. Создаём провайдер данных для работы с AR-моделью пользователей. Затем создаём итератор
и начинаем обработку пользователей. Количество потребляемой памяти будет постоянным и при этом
мы можем осуществлять агрегации по большому объёму данных.
Консольная команда приведённая выше всего лишь наглядный пример и её можно улучшить следующим образом:
mutex
) для того, чтобы предотвратить повторный случайныйcron
. Для этого существует специальное расширениеАвтор
: resurtmОбсуждение и комментарии
: тема на форуме