$this->_lazy и повторные запросы к БД при $object->toArray() и ->get();

Наткнулся сегодня на странное поведение xPDO при попытке извлечения записи объекта из базы данных не со всеми колонками. Вот запрос для примеров

$q = $modx->newQuery('modResource');
$q->select(array(
    'modResource.id',  
)); 
$q->where(array(
    'id' => 2    
));
if($o = $modx->getObject('modResource', $q)){
    print_r($o->toArray());    
}

И вот что интересно: здесь мы формируем запрос так, что из базы данных мы извлекаем только одну колонку — id. То есть наш конечный объект должен содержать только значение id, а все остальные поля должны были содержать дефолтовые значения из мета-описания объекта. Но при этом, когда мы выводим данные объекта через $o->toArray(), то видим все значения из базы данных. В чем подвох? Может запрос полный со всеми колонками, а не только с id? Посмотрим.

print $q->toSQL();
SELECT modResource.id FROM `modx_site_content` AS `modResource` WHERE `modResource`.`id` = 2

Нет, в запросе только одна колонка id…

А прикол весь в том, что xPDO четко следит за тем, все ли «колонки» объекта заполнены (получены из базы данных). Но это довольно сложный и запутанный механизм.

Итак, у xPDO-объектов есть свойство $this->_lazy, которое содержит массив всех колонок, которые не были получены из базы данных. Изначально этот массив пустой, и наполняется только в методе xPDOObject::_loadInstance, и только тогда, когда не все колонки объекта запрошены из базы данных. Давайте внимательней взглянем на эту строку

$instance->_lazy= $actualClass !== $className ? array_keys($xpdo->getFieldMeta($actualClass)) : array_keys($instance->_fieldMeta);

То есть в этот массив попадают или все колонки актуального класса (этот механизм я расписывал здесь), или массив колонок из мета-описания этого объекта, не содержащихся в массиве запрошенных колонок. В нашем случае в этот массив попали все колонки, кроме id.

А что происходит дальше и на все влияет этот массив ->lazy? Для этого стоит посмотреть методы xPDOObject::get(), xPDOObject::toArray() и xPDOObject::toArray().

В методе xPDOObject::get() есть строки:

$lazy = array_intersect($k, $this->_lazy);
if ($lazy) {
    $this->_loadFieldData($lazy);
}

То есть если это поле — lazy (дословно «ленивое»), то он пытается подгрузить значение через метод $this->_loadFieldData.

А что там?

protected function _loadFieldData($fields) {
    if (!is_array($fields)) $fields= array($fields);
    else $fields= array_values($fields);
    $criteria= $this->xpdo->newQuery($this->_class, $this->getPrimaryKey());
    $criteria->select($fields);
    if ($rows= xPDOObject :: _loadRows($this->xpdo, $this->_class, $criteria)) {
        $row= $rows->fetch(PDO::FETCH_ASSOC);
        $rows->closeCursor();
        $this->fromArray($row, '', false, true);
        $this->_lazy= array_diff($this->_lazy, $fields);
    }
}

А там, как видно, формируется новый запрос к БД.

Примерно тоже самое видно и в методе xPDOObject::toArray()

if (!$excludeLazy && $this->isLazy()) {
    $this->_loadFieldData($this->_lazy);
}

То есть, если вы решили сократить запрос к БД, указав меньшее количество колонок в запросе (в xPDOCriteria), то на первых порах вы получите небольшую экономию, но в дальнейшем можно автоматом наплодить новые запросы к БД при попытке получить значения объекта.

Но в чем еще казусы? В методе xPDOObject::fromArray происходит перезапись ->_lazy (полученные из массива колонки отмечаются как «не ленивые»)

if ($this->isLazy($key)) {
    $this->_lazy = array_diff($this->_lazy, array($key));
}

А вот в методе xPDOObject::set (который по сути делает тоже самое, что и :fromArray(), только для одного поля) — нифига. К чему это приводит?

Вот такая конструкция:

$q = $modx->newQuery('modResource');
$q->select(array(
    'modResource.id',  
)); 
$q->where(array(
    'id' => 2    
));
if($o = $modx->getObject('modResource', $q)){
    $o->set('pagetitle', 'new  pagetitle');
    print "<br />Pagetitle 1: ". $o->pagetitle;
    print "<br />Pagetitle 2: ". $o->get('pagetitle');   
}

То есть мы получили объект практически без данных. Затем установили ему pagetitle 'new pagetitle'.

Вот если после этого мы получаем pagetitle как свойство объекта, то мы получим установленное значение.

А вот если попытаемся получить через метод ->get('pagetitle');, то так как поле pagetitle _lazy, то он получит данные из БД и перетрет это значение.

Так же это значение будет затерто, если мы выполним ->toArray().

Не буду писать все, что хотел бы, но общая картина нарисована…