Квест → Как хакнуть форму
Прошли: 77
Когда вы просматриваете километры кода, у вас вполне может возникнуть вопрос: "Почему все сделано так, как сделано?" Лично я особенно замечаю вещи, которые могут и должны быть улучшены, когда дело касается тяжелых запросов в БД.
При работе с фреймворком запросы в БД в основном уже оптимизированы для разработчика, а сложная логика абстрагирована, что улучшает и оптимизирует получение и дальнейшее использования данных. Но бывает, что разработчикам надо накодить что-то без использования фреймворка. При этом основные возможности PHP часто используются не самым оптимальным образом.
$pdo = new \PDO(
$config['db']['dsn'],
$config['db']['username'],
$config['db']['password']
);
$sql = 'SELECT * FROM `gen_contact` ORDER BY `contact_modified` DESC';
$stmt = $pdo->prepare($sql);
$stmt->execute();
$data = $stmt->fetchAll(\PDO::FETCH_OBJ);
echo 'Getting the contacts that changed the last 3 months' . PHP_EOL;
foreach ($data as $row) {
$dt = new \DateTime('2015-04-01 00:00:00');
if ($dt->format('Y-m-d') . '00:00:00' < $row->contact_modified) {
echo sprintf(
'%s (%s)| modified %s',
$row->contact_name,
$row->contact_email,
$row->contact_modified
) . PHP_EOL;
}
}
Выше приведен пример кода, который наиболее часто используется для получения данных. На первый взгляд, этот код выглядит хорошо и чистенько, но, присмотревшись получше, вы увидите пару возможностей для улучшения.
$stmt->fetchAll(\PDO::FETCH_OBJ); у вас остается проблема, т.к. на выходе получится массив объектов. При большой выборке это пожрет море памяти.
Большинство современных фреймворков для получения данных применяет итераторы, потому что они быстры и пригодны для повторного использования. А еще они позволяют задействовать другие итераторы для фильтрации и изменения возвращаемых результатов. Но и без фреймворка вы можете использовать их, т.к. итераторы стали частью PHP еще с версии 5.0.0 Beta 2.
Итак, давайте представим, что вы продолжаете использовать PDO для получения данных. У нас есть для варианта:
PDOStatement::fetchAll() для получения всех данных за один проход.PDOSTatement::fetch() для получения одной строки за одну итерацию.Даже если первый вариант вам кажется очень заманчивым, я предпочитаю и советую использовать вариант за номером два. Он позволяет мне создать один итератор для извлечения данных не ограничиваясь условиями запроса (что делает его пригодным к повторному использованию для любых извлечений).
<?php
/**
* Class DbRowIterator
*
* File: Iterator/DbRowIterator.php
*/
class DbRowIterator implements Iterator
{
/** @var \PDOStatement $pdoStatement The PDO Statement to execute */
protected $pdoStatement;
/** @var int $key The cursor pointer */
protected $key;
/** @var bool|\stdClass The resultset for a single row */
protected $result;
/** @var bool $valid Flag indicating there's a valid resource or not */
protected $valid;
public function __construct(\PDOStatement $PDOStatement)
{
$this->pdoStatement = $PDOStatement;
}
/**
* @inheritDoc
*/
public function current()
{
return $this->result;
}
/**
* @inheritDoc
*/
public function next()
{
$this->key++;
$this->result = $this->pdoStatement->fetch(
\PDO::FETCH_OBJ,
\PDO::FETCH_ORI_ABS,
$this->key
);
if (false === $this->result) {
$this->valid = false;
return null;
}
}
/**
* @inheritDoc
*/
public function key()
{
return $this->key;
}
/**
* @inheritDoc
*/
public function valid()
{
return $this->valid;
}
/**
* @inheritDoc
*/
public function rewind()
{
$this->key = 0;
}
}
Этот итератор всего лишь имплементирует PHP Iterator interface, но для нашего примера этого более чем достаточно для достижения цели.
Как видите, мы реализуем логику извлечения данных в методе "next", который является оператором цикла. Это наш главный метод последовательного извлечения. Обратите внимания на второй и третий аргументы PDOSTatement::fetch(): с помощью второго мы можем контролировать курсор в нашем извлечении данных, третий — позиция курсора в извлечении. Установка курсора прокручиваемым делается вне итератора.
<?php
class LastPeriodIterator extends FilterIterator
{
protected $period;
public function __construct(\Iterator $iterator, $period = 'last week')
{
parent::__construct($iterator);
$this->period = $period;
}
public function accept()
{
if (!$this->getInnerIterator()->valid()) {
return false;
}
$row = $this->getInnerIterator()->current();
$dt = new \DateTime($this->period);
if ($dt->format('Y-m-d') . '00:00:00' < $row->contact_modified) {
return true;
}
return false;
}
}
Для фильтрации данных, расширим SPL FilterIterator. Это позволит нам мгновенно прикрутить фильтрацию к нашему DbRowIterator, делая его расширяемым и давая возможность повторного использования.
Теперь изменим наш изначальный код, добавив в него использование двух наших итераторов:
$pdo = new \PDO(
$config['db']['dsn'],
$config['db']['username'],
$config['db']['password']
);
$sql = 'SELECT * FROM `gen_contact` ORDER BY `contact_modified` DESC';
$stmt = $pdo->prepare($sql, [\PDO::ATTR_CURSOR => \PDO::CURSOR_SCROLL]);
$stmt->execute();
$data = new DbRowIterator($stmt);
echo 'Getting the contacts that changed the last 3 months' . PHP_EOL;
$lastPeriod = new LastPeriodIterator($data, '2015-04-01 00:00:00');
foreach ($lastPeriod as $row) {
echo sprintf(
'%s (%s)| modified %s',
$row->contact_name,
$row->contact_email,
$row->contact_modified
) . PHP_EOL;
}
Пожалуйста, обратите внимание на $pdo->prepare($sql, [\PDO::ATTR_CURSOR => \PDO::CURSOR_SCROLL]);. Теперь мы уверены, что курсор нашего извлечения данных прокручиваемый и мы можем идти от строки к строке.
Я знаю, что это все требует немного "лишней" работы и вы можете удивиться, зачем же вам нужно эта "дополнительная работа", ведь цикл foreach работает так же хорошо. Но давайте посмотрим на быстродействие этих двух вариантов:
Тестирование проводилось на машине с ОС Ubuntu 12.04 LTS (виртуальная машина), MySQL 5.5.43 и PHP 5.5.26. Другие версии PHP, MySQL или ОС могут дать другие результаты. 250000 записей были сгенерированы с помощью fzaninotto/Faker.
Используя простые итераторы в вашем PHP коде, вы можете ускорить получение и обработку данных, но, что важнее всего и это показали тесты, итераторы экономят кучу памяти.
Итераторы наиболее эффективны при работе с большими объемами данных. Для малых выборок (менее 5000 сущностей) итераторы могут работать медленнее, но при этом вы все же экономите память.