====== Жадная загрузка или Eager fetching ======
Начиная с релиза 2008.1 (доступно также в SVN-версии), пакет ACTIVE_RECORD поддерживает так называемую "жадную загрузку" или eager-fetching.
Сразу же сделаем небольшое отступление: Eager fetching в ACTIVE_RECORD реализован совершенно иным образом, чем, например, Doctrine Query Language. Eager fetching в ACTIVE_RECORD - намного более простой механизм, он не ставил перед собой цель сделать некий аналог SQL-языка с учитыванием отношений между классами модели.
===== Введение в eager fetching =====
Жадная загрузка - это процесс выполнения операций по загрузке **связанных** объектов ActiveRecord из базы данных с использованием минимального количества запросов. Рассмотрим eager fetching для начала на простом примере, а затем расскажем о всех возможностях eager fetching, реализованных в пакете lmbActiveRecord.
Допустим, у нас есть след. доменная модель: Программы содержат Курсы, Курсы содержат Лекции:
class Program extends lmbActiveRecord
{
protected $_has_many = array('courses' => array('field' => 'program_id',
'class' => 'Course'));
}
class Course extends lmbActiveRecord
{
protected $_has_many = array('lectures' => array('field' => 'course_id',
'class' => 'Lecture'));
protected $_many_belongs_to = array('program' => array('field' => 'program_id',
'class' => 'Program',
'can_be_null' => true));
}
class Lecture extends lmbActiveRecord
{
protected $_many_belongs_to = array('course' => array('field' => 'course_id',
'class' => 'Course'),
'alt_course' => array('field' => 'alt_course_id',
'class' => 'Course',
'can_be_null' => true));
}
Допустим у нас есть 2 курса по 5 лекций. Тогда вывод полного списка всех лекций с указанием курса, к которому каждая лекция принадлежит будет выгдять следующим образом:
$lectures = lmbActiveRecord :: find('Lecture');
foreach($lectures as $lecture)
echo 'Lecture '$lecture->getTitle() . ' of ' . $lecture->getCourse()->getTitle() . " course.\n";
Проблема с этим кодом в том, что для подобного вывода потребуется сделать 6 запросов к базе данных: 1 для лекций и еще 5 для курсов (или как минимум 3, если бы мы кешировали загруженные курсы)
Eager fetching дает возможность загрузить все эти данные при помощи 1 запроса:
$lectures = lmbActiveRecord :: find('Lecture', array('join' => 'course'));
foreach($lectures as $lecture)
echo 'Lecture '$lecture->getTitle() . ' of ' . $lecture->getCourse()->getTitle() . " course.\n";
Обратите внимание на использование параметра **join**, а качестве значения использовалось название отношения, то есть course. Использование join-а автоматически приведет к тому, что запрос к таблице lecture будет дополнен LEFT JOIN course. Отметим сразу, что join можно использовать для отношений, когда с базовым объектом связан только 1 дополнительный объект, то есть для отношений has_one, many_belongs_to и belongs_to. В других случаях используется параметр **attach** (см. ниже).
Теперь рассмотрим обратную ситуацию, нужно вывести курсы с указанием списка лекций по каждому курсу:
$courses = lmbActiveRecord :: find('Course');
foreach($courses as $course)
{
echo 'Course ' . $course->getTitle() . ": \n";
foreach($course->getLectures() as $lecture)
echo ' Lecture ' . $lecture->getTitle() . ".\n";
}
Здесь проблема аналогична, для каждого курса мы будем выполнять запрос на список его лекций, а это n+1 запрос, где n-кол-во курсов.
Eager fetching позволяет уменьшить количество запросов в данному случае до 2-х: первый - это курсы, второй - все лекции к этим курсам. (Именно 2 запроса, в отличие от Doctrine, где в подобной ситуации будет выполнен 1 запрос). Для этого воспользуемся параметром **attach** для метода lmbActiveRecord :: find():
$courses = lmbActiveRecord :: find('Course', array('attach' => 'lectures'));
foreach($courses as $course)
{
echo 'Course ' . $course->getTitle() . ": \n";
foreach($course->getLectures() as $lecture)
echo ' Lecture ' . $lecture->getTitle() . ".\n";
}
Начиная с релиза 2008.1 (или SVN-версия) lmbActiveRecord :: find() реализован таким образом, что все запросы от делегирует классу lmbARQuery, который является наследником от [[limb3:ru:packages:dbal:intro_to_query_and_criteria|lmbSelectRawQuery]]. Наши примеры можно переписать след. образом:
$lectures = lmbARQuery :: create('Lecture')->eagerJoin('course')->fetch();
[...]
$courses = lmbARQuery :: create('Course')->eagerAttach('lectures')->fetch();
[...]
Подробнее об использовании lmbARQuery для eager будет рассказо ниже.
===== Join-операции =====
**Join-операции** загружают связанные объекты при помощи **одного запроса, то есть расширяют базовый запрос** соответствующими полями связаных объектов и при получение данных формируют объекты-active_record из этих дополнительных данных.
Join-операции поддерживаются для следующих типов отношений:
* has_one
* belongs_to
* many_belongs_to
Также допускается использование **вложенных join-ов**. Поясним на примере. Пусть нам нужно вывести список всех лекций с указанием курса и программы этого курса. Одним запросом эти данные можно получить при помощи следующей операции:
$lectures = lmbActiveRecord :: find('Lecture', array('join' => array('course' => array('join' => 'program'))));
/* или, что тоже самое */
$lectures = lmbARQuery :: create('Lecture')->eagerJoin('course', array('join' => 'program'))->fetch();
Вот какой sql-запрос будет осуществлен в результате вышеуказанных операций:
SELECT lecture.id as id, lecture.title as title, lecture.course_id as course_id,
course.id as course__id, course.title as course__title, course.program_id as course__program_id,
course__program.id as course__program__id, course__program.title as course__program__title
FROM lecture
LEFT JOIN course as course ON lecture.course_id = course.id
LEFT JOIN program as course__program ON course.program_id = program__course.id
При получении данных поля с префиксом %%course__program__%% будут переданы в объект класса Program, с префиксом %%course__%% - в объект класса Course, остальные - в Lecture.
Поля префиксуются в соответствие с названиями отношений. Поля из таблицы program префиксуются 2 раза, это сделано чтобы не было конфликтов, например, если бы Lecture сама имел отношение с названием "program".
Понимание того, как формируется конечный sql-запрос при join-операции иногда требуется, когда необходимо наложить какое-либо условие на данные связанных таблиц и чтобы в этом случае не было ошибок. Например, нам нужно вывести все лекции, которые относятся к программе с идентификатором 5:
$lectures = lmbActiveRecord :: find('Lecture', array('join' => array('course' => array('join' => 'program')),
'criteria' => 'course_program.id = 5'));
/* или, что тоже самое */
$lectures = lmbARQuery :: create('Lecture')->eagerJoin('course', array('join' => 'program'))->where('course__program.id = 5')->fetch();
**Внимание** Eager fetching расставляет связи между загруженными объектами только в одну сторону. То есть лекции получают свои курсы при соответствующей join-операции, но курсы не получают при этом своих лекций!
===== Attach-операции =====
**Attach-операции** загружают связанные объекты при помощи **дополнительно запроса** на каждое отношение.
Attach-операции поддерживаются для всех типов отношений, кроме composed_of. Обратите внимание, что attach можно использовать даже для тех отношений, где применяются join-операции, так как в некоторых случаях для этих отношений предпочтительно сделать все-таки дополнительный запрос к базе данных, чем выполнять запрос с LEFT JOIN.
Допускается использование вложенных attach-операций. Поясним это на примере. Допустим для нашей цепочки Program -> Course -> Lecture необходимо вывести список всех программ с курсами и указать все лекции для каждого курса. При помощи attach-операций, мы сможем загрузить все требуемые данные при помощи 3-х запросов:
$programs = lmbActiveRecord :: find('Program', array('attach' => array('courses' => array('attach' => 'lectures'))));
/* или, что тоже самое */
$lectures = lmbARQuery :: create('Program')->eagerAttach('courses', array('attach' => 'lectures'))->fetch();
В результате будут выполнены следующие 3 запроса к базе данных:
SELECT program.id as id, program.title as title FROM program;
SELECT course.id as id, course.title as title, course.program_id as program_id
FROM course WHERE program_id IN (...) ORDER BY program_id ASC;
SELECT lecture.id as id, lecture.title as title, lecture.course_id as course_id
FROM lecture WHERE course_id IN (...) ORDER BY course_id ASC;
При помощи дополнительных параметров к методам lmbARQuery :: eagerAttach() или к параметру "attach" мы можем задать, например, критерии выборки дополнительных объектов или их сортировку:
$programs = lmbActiveRecord :: find('Program', array('attach' => array('courses' => array('criteria' => 'cost < 100'))));
/* или, что тоже самое */
$lectures = lmbARQuery :: create('Program')->eagerAttach('courses', array('criteria' => 'cost < 100'))->fetch();
===== Комбинирование Join и Attach-операции =====
Join- и Attach-операции можно легко комбинировать. Рассмотрим некоторые примеры запросов, которые можно сделать для нашей доменной модели:
//
$query->eagerJoin('course', array('join' => 'program'))->fetch();
//
$query->eagerAttach('courses', array('attach' => 'lectures'))->fetch();
//
$query = lmbARQuery :: create('Course', array(), $this->conn);
$query->where(lmbSQLCriteria :: in('id', array($course_id1, $course_id2)));
$rs = $query->eagerAttach('lectures', array('join' => 'alt_course'))->fetch();
//
$query = lmbARQuery :: create('Course');
$query->where(lmbSQLCriteria :: in('id', array($course_id1, $course)id2)));
$query->eagerAttach('lectures', array('join' => array('alt_course' => array('attach' => 'lectures'))))->fetch();
//
$query = lmbARQuery :: create('Lecture');
$query->where(lmbSQLCriteria :: equal('course_id', $course_id));
$query->eagerJoin('alt_course', array('attach' => 'lectures'))->fetch();