PHP. Про жадную загрузку в yii1 - insbor.ru
insbor.ru

insbor.ru

Привет, это я

Читаю, пишу, перечитываю и исправляю.


Что здесь происходит


Предыдущие записи


PHP. Про жадную загрузку в yii1

Опубликовано :   |  Кем :   |  Категория :  PHP

Добрый день! В данной записи расскажу про приём, который позволил радикально ускорить несколько жирных запросов и обойти недостаток старого фреймворка. Но сначала:

  • Во-первых. Да, я понимаю, на сколько устарел yii1 и на сколько он не модный. Но знали бы вы, сколько на нём работает внешне вполне благополучных серьёзных проектов. И будут работать ещё долго.

  • Во-вторых. Да, я знаю про ->with() в ActiveRecord. А вы знаете, как она работает?

Начнём издалека. Жадная загрузка - самое очевидное решение для моделей из MVC-паттерна, чтобы избежать однотипных запросов к БД в цикле. Допустим, у нас есть таблицы users, companies, cities и countries, иерархически связанные друг с другом. Т.е. каждый пользователь работает в компании, компания принадлежит городу, а тот - стране. Вам нужно получить и вывести в списке подробную информацию о N пользователях, а значит и о их компаниях, городах и странах. Если ничего не делать, то ActiveRecord (Doctrine, Eloquent) будет каждую связку дёргать отдельным запросом. Да, вы в курсе, получится 1+3N примитивных однотипных запросов `SELECT * FROM tablename WHERE id = :id`.

Мы используем выражение ->with('company.city.country'), конструктор запросов соберёт JOIN и одним запросом выберет всё нужное. Потом из этих данных построит модели User с уже существующими связями $user->company->city->country. В результате будет только одно обращение к БД и мы выигрываем время. В теории.

В действительности это будет полезно, если:

  • записей в таблицах связей будет немного и их JOIN не займёт много времени;
  • все записи будут разнообразными, т.е. большинство пользователей будут принадлежать разным компаниям, те - разным городам, а те - разным странам. В противном случае, понимаете, да? БД замножит повторяющиеся страны, города и компании на пользователей и отдаст это всё PHP. А тот надсадно кряхтя, будет раз за разом создавать из одинаковых данных одинаковые объекты моделей. Одинаковые, но дублирующиеся в памяти;
  • скорость доступа к БД достаточно высока, чтобы закрыть глаза на передачу избыточных данных.

Теперь представим, что у нас 10кк пользователей, 2кк компаний, 1кк городов и 200 несчастных стран. А показывать мы хотим персонал одной или нескольких компаний. Т.е. на условную тысячу пользователей будет приходится всего 10-50 компаний, несколько городов и пара стран. А если это вообще пользователи одной компании? Отели одного города? Товары одного производителя? Нехорошо.

На такой случай в ActiveRecord есть флаги CDbCriteria->together и CactiveRecord->together, которые определяет, будут ли таблицы джойниться при выборке, или же для всех задействованных связей будут собраны ключи и они будут получены одним запросом вида `SELECT * FROM tablename WHERE id IN(...)`, а затем расставлены по своим местам. Так по умолчанию работает Eloquent в Laravel, в чём можно убедиться в местном дебаг-баре.

Казалось бы, проблема решается одни булевым флагом, но вот незадача, посмотрите на скрин из класса ActiveFinder ниже:

За нас всё решили и для связей BELONGS_TO и HAS_ONE JOIN включается принудительно.

В-общем, мне не оставалось ничего, кроме как написать трейт с собственным методом экономного получения записей вместе с их связями.

 

Что в результате? Теперь мы можем писать как-то так:

User::model()->findAllWithRelated($criteria, 'company.city.country, city.country')

Да, функционал неполный - получаются связи только BELONGS_TO и только, если ключ не составной. Но зато есть три варианта действия на выбор, если очередная связь не найдена: выставить связи null, пропустить такую модель целиком или вызвать исключение.

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

Всем спасибо! Буду рад, если кому-то поможет.