Реляционная Active Record ========================= Мы уже рассмотрели использование Active Record (AR) для выбора данных из одной таблицы базы данных. В этом разделе мы расскажем, как использовать AR для соединения нескольких связанных таблиц и получения набора связанных данных. Перед использованием реляционной AR рекомендуется установить ограничения внешних ключей для таблиц базы данных. Это позволит обеспечить непротиворечивость и целостность хранимых данных. Для наглядности примеров в этом разделе мы будем использовать схему базы данных, представленную на следующей диаграмме сущность-связь (ER). ![Диаграмма ER](er.png) > Info|Информация: Поддержка ограничений внешних ключей различается в разных СУБД. > SQLite 3.6.19 и более ранние версии не поддерживает ограничений, но вы, тем не менее, можете их объявить > при создании таблиц. Движок MySQL MyISAM не поддерживает внешние ключи. Установка связей между AR-классами ---------------- Перед тем как использовать AR для выполнения реляционных запросов, нам необходимо установить связи между AR-классами. Связь между двумя AR-классами напрямую зависит от связей между соответствующими таблицами базы данных. С точки зрения БД, связь между таблицами A и В может быть трёх типов: один-ко-многим (например, `tbl_user` и `tbl_post`), один-к-одному (например, `tbl_user` и `tbl_profile`) и многие-ко-многим (например, `tbl_category` и `tbl_post`). В AR существует четыре типа связей: - `BELONGS_TO`: если связь между А и В один-ко-многим, значит В принадлежит А (например, `Post` принадлежит `User`); - `HAS_MANY`: если связь между таблицами А и В один-ко-многим, значит у А есть много В (например, у `User` есть много `Post`); - `HAS_ONE`: это частный случай `HAS_MANY`, где А может иметь максимум одно В (например, у `User` есть только один `Profile`); - `MANY_MANY`: эта связь соответствует типу связи многие-ко-многим в БД. Поскольку многие СУБД не поддерживают непосредственно этот тип связи, требуется ассоциативная таблица для преобразования связи многие-ко-многим в связи один-ко-многим. В нашей схеме базы данных этой цели служит таблица `tbl_post_category`. В терминологии AR связь `MANY_MANY` можно описать как комбинацию `BELONGS_TO` и `HAS_MANY`. Например, `Post` принадлежит многим `Category`, а у `Category` есть много `Post`. Существует пятый, специальный тип связи, который предназначен для статистических запросов над связанными записями (запросы агрегирования) — называется он `STAT`. Более подробно с ним можно ознакомиться в разделе [Статистический запрос](/doc/guide/database.arr#statistical-query). Установка связей производится внутри метода [relations()|CActiveRecord::relations] класса [CActiveRecord]. Этот метод возвращает массив с конфигурацией связей. Каждый элемент массива представляет одну связь в следующем формате: ~~~ [php] 'VarName'=>array('RelationType', 'ClassName', 'ForeignKey', …дополнительные параметры) ~~~ где `VarName` — имя связи, `RelationType` указывает на один из четырёх типов связей, `ClassName` — имя AR-класса, связанного с данным классом, а `ForeignKey` обозначает один или несколько внешних ключей, используемых для связи. Кроме того, можно указать ряд дополнительных параметров, о которых будет рассказано позже. В приведённом ниже коде показано, как установить связь между классами `User` и `Post`. ~~~ [php] class Post extends CActiveRecord { … public function relations() { return array( 'author'=>array(self::BELONGS_TO, 'User', 'author_id'), 'categories'=>array(self::MANY_MANY, 'Category', 'tbl_post_category(post_id, category_id)'), ); } } class User extends CActiveRecord { … public function relations() { return array( 'posts'=>array(self::HAS_MANY, 'Post', 'author_id'), 'profile'=>array(self::HAS_ONE, 'Profile', 'owner_id'), ); } } ~~~ > Info|Информация: Внешний ключ может быть составным, то есть состоять из двух и более столбцов таблицы. В этом случае имена столбцов следует разделить запятыми и передать их либо в качестве строки, либо в виде массива `array('key1','key2')`. Задать свою связь первичного ключа с внешним можно в виде массива `array('fk'=>'pk')`. Для составных ключей это будет `array('fk_c1'=>'pk_c1','fk_c2'=>'pk_c2')`. Для типа связи `MANY_MANY` имя ассоциативной таблицы также должно быть указано во внешнем ключе. Например, связи `categories` модели `Post` соответствует внешний ключ `tbl_post_category(post_id, category_id)`. Для каждой добавленной связи неявно создаётся свойство класса. После выполнения реляционного запроса соответствующее свойство будет содержать связанный экземпляр класса AR (или массив экземпляров для связей типа один-ко-многим и многие-ко-многим). Например, если `$author` является экземпляром AR-класса `User`, то можно использовать `$author->posts` для доступа к связанным экземплярам `Post`. Выполнение реляционного запроса ------------------------------- Самый простой способ выполнить реляционный запрос — использовать реляционное свойство AR-класса. Если обращение к этому свойству производится впервые, то будет выполнен реляционный запрос, который соединит связанные таблицы и оставит только данные, соответствующие первичному ключу текущего экземпляра AR. Результат запроса будет сохранён в свойстве как экземпляр (или массив экземпляров) связанного класса. Этот подход также известен как «отложенная загрузка» (lazy loading), при которой непосредственный запрос выполняется только в момент первого обращения к связанным объектам. Ниже приведён пример использования этого подхода: ~~~ [php] // получаем запись с ID=10 $post=Post::model()->findByPk(10); // Получаем автора записи. Здесь будет выполнен реляционный запрос. $author=$post->author; ~~~ > Info|Информация: Если связанные данные не найдены, то соответствующее свойство примет значение null для связей `BELONGS_TO` и `HAS_ONE` или будет являться пустым массивом для `HAS_MANY` и `MANY_MANY`. Стоит отметить, что связи `HAS_MANY` и `MANY_MANY` возвращают массивы объектов, и обращаться к их свойствам необходимо в цикле, иначе можно получить ошибку «Trying to get property of non-object». Способ отложенной загрузки удобен, но не всегда эффективен. Например, если нам потребуется получить информацию об авторах `N` записей, то использование отложенной загрузки потребует выполнения `N` дополнительных запросов к базе данных. В данной ситуации разумно использовать метод «жадной загрузки» (eager loading). Этот метод заключается в загрузке всех связанных данных вместе с основным экземпляром AR. Реализуется этот подход путем использования метода [with()|CActiveRecord::with] вместе с методом [find|CActiveRecord::find] или [findAll|CActiveRecord::findAll]. Например: ~~~ [php] $posts=Post::model()->with('author')->findAll(); ~~~ Приведённый код вернёт массив экземпляров `Post`. В отличие от отложенной загрузки, свойство `author` каждой записи будет заполнено связанным экземпляром `User` ещё до обращения к этому свойству. Таким образом, вместо выполнения отдельного запроса для каждой записи, жадная загрузка получит все записи вместе с их авторами в одном запросе! В методе [with()|CActiveRecord::with] можно указать несколько связей, и жадная загрузка вернёт их за один раз. Например, следующий код вернёт записи вместе с их авторами и категориями: ~~~ [php] $posts=Post::model()->with('author','categories')->findAll(); ~~~ Кроме того, можно осуществлять вложенную жадную загрузку. Для этого вместо простого списка имён связей, мы передаем методу [with()|CActiveRecord::with] имена связей, упорядоченных иерархически, как в следующем примере: ~~~ [php] $posts=Post::model()->with( 'author.profile', 'author.posts', 'categories')->findAll(); ~~~ Пример выше вернёт нам все записи вместе с их авторами и категориями, а также профиль каждого автора и все его записи. Жадная загрузка может быть выполнена путём указания свойства [CDbCriteria::with]: ~~~ [php] $criteria=new CDbCriteria; $criteria->with=array( 'author.profile', 'author.posts', 'categories', ); $posts=Post::model()->findAll($criteria); ~~~ или ~~~ [php] $posts=Post::model()->findAll(array( 'with'=>array( 'author.profile', 'author.posts', 'categories', ) )); ~~~ Реляционный запрос без получения связанных моделей -------------------------------------------------- Иногда требуется выполнить запрос с использованием связи, но при этом не требуются данные из связанной модели. Допустим, есть пользователи (`User`), которые публикуют множество записей (`Post`). Запись может быть опубликована, а может быть черновиком. Этот факт определяется значением поля `published` модели `Post`. Пусть нам необходимо получить всех пользователей, которые опубликовали хотя бы одну запись, при этом сами записи нам не интересны. Сделать это можно следующим образом: ~~~ [php] $users=User::model()->with(array( 'posts'=>array( // записи нам не нужны 'select'=>false, // но нужно выбрать только пользователей с опубликованными записями 'joinType'=>'INNER JOIN', 'condition'=>'posts.published=1', ), ))->findAll(); ~~~ Параметры реляционного запроса ------------------------------ Выше мы упоминали о том, что в реляционном запросе можно указать дополнительные параметры. Эти параметры — пары имя-значение — используются для тонкой настройки реляционного запроса. Список параметров представлен ниже. - `select`: список выбираемых полей для связанного AR-класса. По умолчанию значение параметра равно '*', что соответствует всем полям таблицы. Для используемых столбцов должны быть разрешены конфликты имён. - `condition`: соответствует SQL оператору `WHERE`, по умолчанию значение параметра пустое. Для используемых столбцов должны быть разрешены конфликты имён. - `params`: параметры для связывания в генерируемом SQL-выражении. Параметры передаются как массив пар имя-значение. - `on`: соответствует SQL оператору `ON`. Условие, указанное в этом параметре, будет добавлено к основному условию соединения при помощи SQL оператора `AND`. Для используемых столбцов должны быть разрешены конфликты имён. Данный параметр неприменим для связей типа `MANY_MANY`. - `order`: соответствует SQL оператору `ORDER BY`, по умолчанию значение параметра пустое. Для используемых столбцов должны быть разрешены конфликты имён. - `with`: список дочерних связанных объектов, которые должны быть загружены с самим объектом. Неправильное использование данной возможности может привести к бесконечному циклу. - `joinType`: тип соединения таблиц. По умолчанию значение параметра равно `LEFT OUTER JOIN`; - `alias`: псевдоним таблицы, ассоциированной со связью. По умолчанию значение параметра равняется null, что означает, что псевдоним соответствует имени связи. - `together`: параметр, устанавливающий необходимость принудительного соединения таблицы, ассоциированной с этой связью, с другими таблицами. Этот параметр имеет смысл только для связей типов `HAS_MANY` и `MANY_MANY`. Если параметр не установлен или равен false, тогда каждая связь `HAS_MANY` или `MANY_MANY` будет использовать отдельный SQL-запрос для связанных данных, что может улучшить скорость выполнения запроса, т.к. уменьшается количество выбираемых данных. Если параметр равен `true`, то зависимая таблица при выполнении запроса всегда будет соединяться с основной, то есть будет выполнен один SQL-запрос даже в том случае, если к основной таблице применяется постраничная разбивка. Если данный параметр не задан, зависимая таблица будет соединена с основной только в случае, когда к основной таблице не применяется постраничная разбивка. Более подробное описание можно найти в разделе «производительность реляционного запроса». - `join`: дополнительный оператор `JOIN`. По умолчанию пуст. Этот параметр доступен с версии 1.1.3. - `group`: соответствует SQL оператору `GROUP BY`, по умолчанию значение параметра пустое. Для используемых столбцов должны быть разрешены конфликты имён. - `having`: соответствует SQL оператору `HAVING`, по умолчанию значение параметра пустое. Для используемых столбцов должны быть разрешены конфликты имён. - `index`: имя столбца таблицы, значения которого должны быть использованы в качестве ключей массива, хранящего связанные объекты. Без установки этого параметра массив связанных объектов использует целочисленный индекс, начинающийся с нуля. Параметр может быть установлен только для связей типа `HAS_MANY` и `MANY_MANY`. - `scopes`: группы условий, которые необходимо применить. В случае одной группы может задаваться в виде строки `'scopes'=>'scopeName'`. Если же групп несколько, то их необходимо перечислить в массиве `'scopes'=>array('scopeName1','scopeName2')`. Этот параметр доступен с версии 1.1.9. Кроме того, для отложенной загрузки некоторых типов связей доступен ряд дополнительных параметров: - `limit`: параметр для ограничения количества строк в выборке. Параметр неприменим для связей `BELONGS_TO`; - `offset`: параметр для указания начальной строки выборки. Параметр неприменим для связей `BELONGS_TO`. - `through`: имя связи модели, которое при получении данных будет использоваться как мост. Параметр может быть установлен только для связей `HAS_ONE` и `HAS_MANY`. Этот параметр доступен с версии 1.1.7, в которой можно применять его к `HAS_ONE` и `HAS_MANY`. Начиная с версии 1.1.14 он может использоваться с `BELONGS_TO`. Ниже мы изменим определение связи `posts` в модели `User`, добавив несколько вышеприведенных параметров: ~~~ [php] class User extends CActiveRecord { public function relations() { return array( 'posts'=>array(self::HAS_MANY, 'Post', 'author_id', 'order'=>'posts.create_time DESC', 'with'=>'categories'), 'profile'=>array(self::HAS_ONE, 'Profile', 'owner_id'), ); } } ~~~ Теперь при обращении к `$author->posts`, мы получим записи автора, отсортированные в обратном порядке по времени их создания. Для каждой записи будут загружены её категории. Устранение конфликта имён столбцов ---------------------------------- При совпадении имён столбцов в двух и более соединяемых таблицах, приходится разрешать конфликт имён. Это делается при помощи добавления псевдонима таблицы к имени столбца. В реляционном запросе псевдоним главной таблицы всегда равен `t`, а псевдоним связанной таблицы по умолчанию равен имени связи. В приведённом ниже коде псевдонимы таблиц для моделей `Post` и `Comment` будут соответственно `t` и `comments`: ~~~ [php] $posts=Post::model()->with('comments')->findAll(); ~~~ Допустим, что и в `Post`, и в `Comment` есть столбец `create_time`, в котором хранится время создания записи или комментария, и нам необходимо получить записи вместе с комментариями к ним, отсортированные сначала по времени создания записи, а затем по времени написания комментария. Для этого нам понадобится устранить конфликт столбцов `create_time` следующим образом: ~~~ [php] $posts=Post::model()->with('comments')->findAll(array( 'order'=>'t.create_time, comments.create_time' )); ~~~ > Tip|Подсказка: Псевдоним таблицы связи по умолчанию равен названию самой связи. Имейте ввиду, > что при использовании одной связи внутри другой будет использовано название последней из них. > При этом название родительской связи не будет использовано в качестве префикса. Например, псевдонимом > связи 'author.group' является 'group', а не 'author.group'. > > ~~~ > [php] > $posts=Post::model()->with('author', 'author.group')->findAll(array( > 'order'=>'group.name, author.name, t.title' > )); > ~~~ > > Вы можете избежать конфликта псевдонимов таблиц задав свойство связи [alias|CActiveRelation::alias]. > > ~~~ > [php] > $comments=Comment::model()->with( > 'author', > 'post', > 'post.author'=>array('alias'=>'p_author'))->findAll(array( > 'order'=>'author.name, p_author.name, post.title' > )); > ~~~ Динамические параметры реляционного запроса ------------------------------------------- Мы можем использовать динамические параметры как для метода [with()|CActiveRecord::with], так и для параметра `with`. Динамические параметры переопределяют существующие параметры в соответствии с описанием метода [relations()|CActiveRecord::relations]. К примеру, если для модели `User`, приведённой выше, мы хотим воспользоваться жадной загрузкой для получения записей автора в порядке возрастания (параметр `order` в определении связи задает убывающий порядок), можно сделать это следующим образом: ~~~ [php] User::model()->with(array( 'posts'=>array('order'=>'posts.create_time ASC'), 'profile', ))->findAll(); ~~~ Динамические параметры в реляционных запросах можно использовать вместе с отложенной загрузкой. Для этого необходимо вызвать метод с тем же именем, что и имя связи, и передать параметры в качестве аргументов. К примеру, следующий код вернёт публикации пользователя, у которых `status` равен 1: ~~~ [php] $user=User::model()->findByPk(1); $posts=$user->posts(array('condition'=>'status=1')); ~~~ Производительность реляционного запроса --------------------------------------- Как было сказано выше, жадная загрузка используется, главным образом, когда требуется получить множество связанных объектов. В этом случае при соединении всех таблиц генерируется сложный SQL-запрос. Такой запрос во многих случаях является предпочтительным, т.к. упрощает фильтрацию по значению столбца связанной таблицы. Тем не менее, в некоторых случаях такие запросы не являются эффективными. Рассмотрим пример, в котором нам необходимо найти последние добавленные записи вместе с их комментариями. Предположив, что у каждой записи 10 комментариев, при использовании одного большого SQL-запроса мы получим множество лишних данных, так как каждая запись будет повторно выбираться с каждым её комментарием. Теперь попробуем по-другому: сначала выберем последние записи, а затем комментарии к ним. В этом случае нам необходимо выполнить два SQL-запроса. Плюс в том, что полученные данные не будут содержать дублирующую информацию. Какой же подход более эффективен? Однозначного ответа на этот вопрос нет. Выполнение одного большого SQL-запроса может оказаться более эффективным, так как СУБД не приходится лишний раз разбирать и выполнять дополнительные запросы. С другой стороны, используя один SQL-запрос, мы получаем лишние данные, а значит нам требуется больше времени на их передачу и обработку. По умолчанию Yii использует жадную загрузку, то есть генерирует один SQL-запрос за исключением случая, когда к главной модели применяется `LIMIT`. Если выставить опцию `together` в описании связи в `true`, то мы получим единственный SQL-запрос даже если используется `LIMIT`. Если использовать `false`, то выборка из некоторых таблиц будет производиться отдельными запросами. Например, для того чтобы использовать отдельные SQL-запросы для выборки последних записей и комментариев к ним, связь `comments` модели `Post` следует описать следующим образом: ~~~ [php] public function relations() { return array( 'comments' => array(self::HAS_MANY, 'Comment', 'post_id', 'together'=>false), ); } ~~~ Для жадной загрузки мы можем задать этот параметр динамически: ~~~ [php] $posts = Post::model()->with(array('comments'=>array('together'=>false)))->findAll(); ~~~ Статистический запрос --------------------- Помимо реляционных запросов, описанных выше, Yii также поддерживает так называемые статистические запросы (или запросы агрегирования). Этот тип запросов используется для получения агрегированных данных, относящихся к связанным объектам (количество комментариев к каждой записи, средний рейтинг для каждого наименования продукции и т.д.). Статистические запросы могут быть использованы только для связей типа `HAS_MANY` (например, у записи есть много комментариев) или `MANY_MANY` (например, запись принадлежит многим категориям, а категориия может относиться ко множеству записей). Выполнение статистического запроса аналогично выполнению реляционного запроса. Первым делом необходимо объявить статистический запрос в методе [relations()|CActiveRecord::relations] класса [CActiveRecord]. ~~~ [php] class Post extends CActiveRecord { public function relations() { return array( 'commentCount'=>array(self::STAT, 'Comment', 'post_id'), 'categoryCount'=>array(self::STAT, 'Category', 'post_category(post_id, category_id)'), ); } } ~~~ Выше мы объявили два статистических запроса: `commentCount` подсчитывает количество комментариев к записи, а `categoryCount` считает количество категорий, к которым относится запись. Обратите внимание, что связь между `Post` и `Comment` — типа `HAS_MANY`, а связь между `Post` и `Category` — типа `MANY_MANY` (с использованием преобразующей таблицы `post_category`). Как можно видеть, способ объявления похож на объявление связей, описанных выше. Единственное различие состоит в том, что в данном случае тип связи равен `STAT`. За счёт объявленных связей мы можем получить количество комментариев к записи, используя выражение `$post->commentCount`. В момент первого обращения к данному свойству для получения соответствующего результата неявным образом выполняется SQL-запрос. Как мы уже говорили, это называется подходом *отложенной загрузки*. Можно также использовать *жадный* вариант загрузки, если необходимо получить количество комментариев к нескольким записям: ~~~ [php] $posts=Post::model()->with('commentCount', 'categoryCount')->findAll(); ~~~ Выражение выше выполняет три SQL-запроса для получения всех записей вместе с количеством комментариев к ним и числом категорий. В случае отложенной загрузки нам бы понадобилось выполнить `2*N+1` SQL-запросов для `N` записей. По умолчанию статистический запрос считает количество с использованием выражения `COUNT`. Его можно уточнить путём указания дополнительных параметров в момент объявления в методе [relations()|CActiveRecord::relations]. Доступные параметры перечислены ниже: - `select`: статистическое выражение, по умолчанию равно `COUNT(*)`, что соответствует количеству связанных объектов; - `defaultValue`: значение, которое присваивается в случае, если результат статистического запроса пуст. Например, если запись не имеет ни одного комментария, то свойству `commentCount` будет присвоено это значение. По умолчанию значение данного параметра равно 0; - `condition`: соответствует SQL оператору `WHERE`, по умолчанию значение параметра пустое; - `params`: параметры для связывания в генерируемом SQL-выражении. Параметры передаются как массив пар имя-значение; - `order`: соответствует SQL оператору `ORDER BY`, по умолчанию значение параметра пустое; - `group`: соответствует SQL оператору `GROUP BY`, по умолчанию значение параметра пустое; - `having`: соответствует SQL оператору `HAVING`, по умолчанию значение параметра пустое. Реляционные запросы с именованными группами условий --------------------------------------------------- В реляционном запросе [именованные группы условий](/doc/guide/database.ar#named-scopes) могут быть использованы двумя способами. Их можно применить к основной модели и к связанным моделям. Следующий код иллюстрирует случай их применения к основной модели: ~~~ [php] $posts=Post::model()->published()->recently()->with('comments')->findAll(); ~~~ Данный код очень похож на нереляционный запрос. Единственное отличие состоит в том, что присутствует вызов `with()` после вызовов групп условий. Данный запрос вернёт недавно опубликованные записи вместе с комментариями к ним. В следующем примере показано, как применить группы условий к связанным моделям: ~~~ [php] $posts=Post::model()->with('comments:recently:approved')->findAll(); // или, начиная с версии 1.1.7 $posts=Post::model()->with(array( 'comments'=>array( 'scopes'=>array('recently','approved') ), ))->findAll(); // или, начиная с версии 1.1.7 $posts=Post::model()->findAll(array( 'with'=>array( 'comments'=>array( 'scopes'=>array('recently','approved') ), ), )); ~~~ Этот запрос вернёт все записи вместе с одобренными комментариями. Здесь `comments` соответствует имени связи. `recently` и `approved` — именованные группы, описанные в модели `Comment`. Имя связи и группы условий разделяются двоеточием. Вам может понадобится использовать вместо «жадной» выборки «отложенную» для связи с группой условий. Синтаксис для этого такой: ~~~ [php] // имя связи comments повторяется два раза $approvedComments = $post->comments('comments:approved'); ~~~ Именованные группы могут быть использованы при описании связей модели в методе [CActiveRecord::relations()] в параметре `with`. В следующем примере при обращении к `$user->posts` вместе с публикациями будут получены все *одобренные* комментарии. ~~~ [php] class User extends CActiveRecord { public function relations() { return array( 'posts'=>array(self::HAS_MANY, 'Post', 'author_id', 'with'=>'comments:approved'), ); } } // или, начиная с версии 1.1.7 class User extends CActiveRecord { public function relations() { return array( 'posts'=>array(self::HAS_MANY, 'Post', 'author_id', 'with'=>array( 'comments'=>array( 'scopes'=>'approved' ), ), ), ); } } ~~~ В версии 1.1.7 появилась возможность передавать параметры именованным группам условий связи. К примеру, если в `Post` есть именованная группа условий `rated`, принимающая минимальный рейтинг записи, использовать её в `User` можно следующим образом: > Note|Примечание: до версии 1.1.7 именованные группы условий, применяемые к реляционным моделям, > должны быть описаны в CActiveRecord::scopes. Поэтому они не могут быть параметризованы. ~~~ [php] $users=User::model()->findAll(array( 'with'=>array( 'posts'=>array( 'scopes'=>array( 'rated'=>5, ), ), ), )); class Post extends CActiveRecord { ...... public function rated($rating) { $this->getDbCriteria()->mergeWith(array( 'condition'=>'rating=:rating', 'params'=>array(':rating'=>$rating), )); return $this; } ...... } ~~~ Реляционные запросы с through ----------------------------- При использовании `through` определение связи должно выглядеть следующим образом: ~~~ [php] 'comments'=>array(self::HAS_MANY,'Comment',array('key1'=>'key2'),'through'=>'posts'), ~~~ В коде выше, а именно в `array('key1'=>'key2')`: - `key1` — ключ, определённый в связи, на которую указывает `through` (в нашем случае `posts`). - `key2` — ключ, определённый в модели, на которую указывает связь (в нашем случае `Comment`). `through` может использоваться с `HAS_ONE`, `BELONGS_TO` и `HAS_MANY`. ### `HAS_MANY` through ![HAS_MANY through ER](has_many_through.png) Пример использования `HAS_MANY` с `through` — получение пользователей, состоящих в определённой группе, если они записаны в группу через роли. Более сложным примером является получение всех комментариев для всех пользователей определённой группы. В этом случае необходимо использовать несколько связей с `through` в одной модели: ~~~ [php] class Group extends CActiveRecord { ... public function relations() { return array( 'roles'=>array(self::HAS_MANY,'Role','group_id'), 'users'=>array(self::HAS_MANY,'User',array('user_id'=>'id'),'through'=>'roles'), 'comments'=>array(self::HAS_MANY,'Comment',array('id'=>'user_id'),'through'=>'users'), ); } } ~~~ #### Примеры ~~~ [php] // получаем все группы с соответствующими им пользователями $groups=Group::model()->with('users')->findAll(); // получаем все группы с соответствующими им пользователями и ролями $groups=Group::model()->with('roles','users')->findAll(); // получаем всех пользователей и роли для группы с ID, равным 1 $group=Group::model()->findByPk(1); $users=$group->users; $roles=$group->roles; // получаем все комментарии для группы с ID, равным 1 $group=Group::model()->findByPk(1); $comments=$group->comments; ~~~ ### `HAS_ONE` through ![HAS_ONE through ER](has_one_through.png) Пример использования `HAS_ONE` с `through` — получение адреса пользователя в случае, если пользователь связан с адресом через профиль. Все задействованные сущности (пользователь, профиль и адрес) имеют соответствующие им модели: ~~~ [php] class User extends CActiveRecord { ... public function relations() { return array( 'profile'=>array(self::HAS_ONE,'Profile','user_id'), 'address'=>array(self::HAS_ONE,'Address',array('id'=>'profile_id'),'through'=>'profile'), ); } } ~~~ #### Примеры ~~~ [php] // получаем адрес пользователя с ID, равным 1 $user=User::model()->findByPk(1); $address=$user->address; ~~~ ### through с собой `through` можно использовать для модели, связанной с собой через мост. В нашем случае это пользователь, обучающий других пользователей: ![through self ER](through_self.png) Связи для данного случая определяются следующим образом: ~~~ [php] class User extends CActiveRecord { ... public function relations() { return array( 'mentorships'=>array(self::HAS_MANY,'Mentorship','teacher_id','joinType'=>'INNER JOIN'), 'students'=>array(self::HAS_MANY,'User',array('student_id'=>'id'),'through'=>'mentorships','joinType'=>'INNER JOIN'), ); } } ~~~ #### Примеры ~~~ [php] // получаем всех студентов учителя с ID, равным 1 $teacher=User::model()->findByPk(1); $students=$teacher->students; ~~~