Квест → Как хакнуть форму
Прошли: 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 сущностей) итераторы могут работать медленнее, но при этом вы все же экономите память.