0 follower

Реляционная Active Record

Мы уже рассмотрели использование Active Record (AR) для выбора данных из одной таблицы базы данных. В этом разделе мы расскажем, как использовать AR для соединения нескольких связанных таблиц и получения набора связанных данных.

Перед использованием реляционной AR рекомендуется установить ограничения внешних ключей для таблиц базы данных. Это позволит обеспечить непротиворечивость и целостность хранимых данных.

Для наглядности примеров в этом разделе мы будем использовать схему базы данных, представленную на следующей диаграмме сущность-связь (ER).

Диаграмма ER

Диаграмма ER

Информация: Поддержка ограничений внешних ключей различается в разных СУБД. SQLite 3.6.19 и более ранние версии не поддерживает ограничений, но вы, тем не менее, можете их объявить при создании таблиц. Движок MySQL MyISAM не поддерживает внешние ключи.

1. Установка связей между 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. Более подробно с ним можно ознакомиться в разделе Статистический запрос.

Установка связей производится внутри метода relations() класса CActiveRecord. Этот метод возвращает массив с конфигурацией связей. Каждый элемент массива представляет одну связь в следующем формате:

'VarName'=>array('RelationType', 'ClassName', 'ForeignKey', …дополнительные параметры)

где VarName — имя связи, RelationType указывает на один из четырёх типов связей, ClassName — имя AR-класса, связанного с данным классом, а ForeignKey обозначает один или несколько внешних ключей, используемых для связи. Кроме того, можно указать ряд дополнительных параметров, о которых будет рассказано позже.

В приведённом ниже коде показано, как установить связь между классами User и Post.

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'),
        );
    }
}

Информация: Внешний ключ может быть составным, то есть состоять из двух и более столбцов таблицы. В этом случае имена столбцов следует разделить запятыми и передать их либо в качестве строки, либо в виде массива 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.

2. Выполнение реляционного запроса

Самый простой способ выполнить реляционный запрос — использовать реляционное свойство AR-класса. Если обращение к этому свойству производится впервые, то будет выполнен реляционный запрос, который соединит связанные таблицы и оставит только данные, соответствующие первичному ключу текущего экземпляра AR. Результат запроса будет сохранён в свойстве как экземпляр (или массив экземпляров) связанного класса. Этот подход также известен как «отложенная загрузка» (lazy loading), при которой непосредственный запрос выполняется только в момент первого обращения к связанным объектам. Ниже приведён пример использования этого подхода:

// получаем запись с ID=10
$post=Post::model()->findByPk(10);
// Получаем автора записи. Здесь будет выполнен реляционный запрос.
$author=$post->author;

Информация: Если связанные данные не найдены, то соответствующее свойство примет значение 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() вместе с методом find или findAll. Например:

$posts=Post::model()->with('author')->findAll();

Приведённый код вернёт массив экземпляров Post. В отличие от отложенной загрузки, свойство author каждой записи будет заполнено связанным экземпляром User ещё до обращения к этому свойству. Таким образом, вместо выполнения отдельного запроса для каждой записи, жадная загрузка получит все записи вместе с их авторами в одном запросе!

В методе with() можно указать несколько связей, и жадная загрузка вернёт их за один раз. Например, следующий код вернёт записи вместе с их авторами и категориями:

$posts=Post::model()->with('author','categories')->findAll();

Кроме того, можно осуществлять вложенную жадную загрузку. Для этого вместо простого списка имён связей, мы передаем методу with() имена связей, упорядоченных иерархически, как в следующем примере:

$posts=Post::model()->with(
    'author.profile',
    'author.posts',
    'categories')->findAll();

Пример выше вернёт нам все записи вместе с их авторами и категориями, а также профиль каждого автора и все его записи.

Жадная загрузка может быть выполнена путём указания свойства CDbCriteria::with:

$criteria=new CDbCriteria;
$criteria->with=array(
    'author.profile',
    'author.posts',
    'categories',
);
$posts=Post::model()->findAll($criteria);

или

$posts=Post::model()->findAll(array(
    'with'=>array(
        'author.profile',
        'author.posts',
        'categories',
    )
));

3. Реляционный запрос без получения связанных моделей

Иногда требуется выполнить запрос с использованием связи, но при этом не требуются данные из связанной модели. Допустим, есть пользователи (User), которые публикуют множество записей (Post). Запись может быть опубликована, а может быть черновиком. Этот факт определяется значением поля published модели Post. Пусть нам необходимо получить всех пользователей, которые опубликовали хотя бы одну запись, при этом сами записи нам не интересны. Сделать это можно следующим образом:

$users=User::model()->with(array(
    'posts'=>array(
        // записи нам не нужны
        'select'=>false,
        // но нужно выбрать только пользователей с опубликованными записями
        'joinType'=>'INNER JOIN',
        'condition'=>'posts.published=1',
    ),
))->findAll();

4. Параметры реляционного запроса

Выше мы упоминали о том, что в реляционном запросе можно указать дополнительные параметры. Эти параметры — пары имя-значение — используются для тонкой настройки реляционного запроса. Список параметров представлен ниже.

  • 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, добавив несколько вышеприведенных параметров:

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, мы получим записи автора, отсортированные в обратном порядке по времени их создания. Для каждой записи будут загружены её категории.

5. Устранение конфликта имён столбцов

При совпадении имён столбцов в двух и более соединяемых таблицах, приходится разрешать конфликт имён. Это делается при помощи добавления псевдонима таблицы к имени столбца.

В реляционном запросе псевдоним главной таблицы всегда равен t, а псевдоним связанной таблицы по умолчанию равен имени связи. В приведённом ниже коде псевдонимы таблиц для моделей Post и Comment будут соответственно t и comments:

$posts=Post::model()->with('comments')->findAll();

Допустим, что и в Post, и в Comment есть столбец create_time, в котором хранится время создания записи или комментария, и нам необходимо получить записи вместе с комментариями к ним, отсортированные сначала по времени создания записи, а затем по времени написания комментария. Для этого нам понадобится устранить конфликт столбцов create_time следующим образом:

$posts=Post::model()->with('comments')->findAll(array(
    'order'=>'t.create_time, comments.create_time'
));

Подсказка: Псевдоним таблицы связи по умолчанию равен названию самой связи. Имейте ввиду, что при использовании одной связи внутри другой будет использовано название последней из них. При этом название родительской связи не будет использовано в качестве префикса. Например, псевдонимом связи 'author.group' является 'group', а не 'author.group'.

$posts=Post::model()->with('author', 'author.group')->findAll(array(
  'order'=>'group.name, author.name, t.title'
));

Вы можете избежать конфликта псевдонимов таблиц задав свойство связи alias.

$comments=Comment::model()->with(
  'author',
  'post',
  'post.author'=>array('alias'=>'p_author'))->findAll(array(
  'order'=>'author.name, p_author.name, post.title'
));

6. Динамические параметры реляционного запроса

Мы можем использовать динамические параметры как для метода with(), так и для параметра with. Динамические параметры переопределяют существующие параметры в соответствии с описанием метода relations(). К примеру, если для модели User, приведённой выше, мы хотим воспользоваться жадной загрузкой для получения записей автора в порядке возрастания (параметр order в определении связи задает убывающий порядок), можно сделать это следующим образом:

User::model()->with(array(
    'posts'=>array('order'=>'posts.create_time ASC'),
    'profile',
))->findAll();

Динамические параметры в реляционных запросах можно использовать вместе с отложенной загрузкой. Для этого необходимо вызвать метод с тем же именем, что и имя связи, и передать параметры в качестве аргументов. К примеру, следующий код вернёт публикации пользователя, у которых status равен 1:

$user=User::model()->findByPk(1);
$posts=$user->posts(array('condition'=>'status=1'));

7. Производительность реляционного запроса

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

Рассмотрим пример, в котором нам необходимо найти последние добавленные записи вместе с их комментариями. Предположив, что у каждой записи 10 комментариев, при использовании одного большого SQL-запроса мы получим множество лишних данных, так как каждая запись будет повторно выбираться с каждым её комментарием. Теперь попробуем по-другому: сначала выберем последние записи, а затем комментарии к ним. В этом случае нам необходимо выполнить два SQL-запроса. Плюс в том, что полученные данные не будут содержать дублирующую информацию.

Какой же подход более эффективен? Однозначного ответа на этот вопрос нет. Выполнение одного большого SQL-запроса может оказаться более эффективным, так как СУБД не приходится лишний раз разбирать и выполнять дополнительные запросы. С другой стороны, используя один SQL-запрос, мы получаем лишние данные, а значит нам требуется больше времени на их передачу и обработку. По умолчанию Yii использует жадную загрузку, то есть генерирует один SQL-запрос за исключением случая, когда к главной модели применяется LIMIT. Если выставить опцию together в описании связи в true, то мы получим единственный SQL-запрос даже если используется LIMIT. Если использовать false, то выборка из некоторых таблиц будет производиться отдельными запросами. Например, для того чтобы использовать отдельные SQL-запросы для выборки последних записей и комментариев к ним, связь comments модели Post следует описать следующим образом:

public function relations()
{
    return array(
        'comments' => array(self::HAS_MANY, 'Comment', 'post_id', 'together'=>false),
    );
}

Для жадной загрузки мы можем задать этот параметр динамически:

$posts = Post::model()->with(array('comments'=>array('together'=>false)))->findAll();

8. Статистический запрос

Помимо реляционных запросов, описанных выше, Yii также поддерживает так называемые статистические запросы (или запросы агрегирования). Этот тип запросов используется для получения агрегированных данных, относящихся к связанным объектам (количество комментариев к каждой записи, средний рейтинг для каждого наименования продукции и т.д.). Статистические запросы могут быть использованы только для связей типа HAS_MANY (например, у записи есть много комментариев) или MANY_MANY (например, запись принадлежит многим категориям, а категориия может относиться ко множеству записей).

Выполнение статистического запроса аналогично выполнению реляционного запроса. Первым делом необходимо объявить статистический запрос в методе relations() класса CActiveRecord.

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

$posts=Post::model()->with('commentCount', 'categoryCount')->findAll();

Выражение выше выполняет три SQL-запроса для получения всех записей вместе с количеством комментариев к ним и числом категорий. В случае отложенной загрузки нам бы понадобилось выполнить 2*N+1 SQL-запросов для N записей.

По умолчанию статистический запрос считает количество с использованием выражения COUNT. Его можно уточнить путём указания дополнительных параметров в момент объявления в методе relations(). Доступные параметры перечислены ниже:

  • select: статистическое выражение, по умолчанию равно COUNT(*), что соответствует количеству связанных объектов;

  • defaultValue: значение, которое присваивается в случае, если результат статистического запроса пуст. Например, если запись не имеет ни одного комментария, то свойству commentCount будет присвоено это значение. По умолчанию значение данного параметра равно 0;

  • condition: соответствует SQL оператору WHERE, по умолчанию значение параметра пустое;

  • params: параметры для связывания в генерируемом SQL-выражении. Параметры передаются как массив пар имя-значение;

  • order: соответствует SQL оператору ORDER BY, по умолчанию значение параметра пустое;

  • group: соответствует SQL оператору GROUP BY, по умолчанию значение параметра пустое;

  • having: соответствует SQL оператору HAVING, по умолчанию значение параметра пустое.

9. Реляционные запросы с именованными группами условий

В реляционном запросе именованные группы условий могут быть использованы двумя способами. Их можно применить к основной модели и к связанным моделям.

Следующий код иллюстрирует случай их применения к основной модели:

$posts=Post::model()->published()->recently()->with('comments')->findAll();

Данный код очень похож на нереляционный запрос. Единственное отличие состоит в том, что присутствует вызов with() после вызовов групп условий. Данный запрос вернёт недавно опубликованные записи вместе с комментариями к ним.

В следующем примере показано, как применить группы условий к связанным моделям:

$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. Имя связи и группы условий разделяются двоеточием.

Вам может понадобится использовать вместо «жадной» выборки «отложенную» для связи с группой условий. Синтаксис для этого такой:

// имя связи comments повторяется два раза
$approvedComments = $post->comments('comments:approved');

Именованные группы могут быть использованы при описании связей модели в методе CActiveRecord::relations() в параметре with. В следующем примере при обращении к $user->posts вместе с публикациями будут получены все одобренные комментарии.

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 можно следующим образом:

Примечание: до версии 1.1.7 именованные группы условий, применяемые к реляционным моделям, должны быть описаны в CActiveRecord::scopes. Поэтому они не могут быть параметризованы.

$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;
    }
 
    ......
}

10. Реляционные запросы с through

При использовании through определение связи должно выглядеть следующим образом:

'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 ER

Пример использования HAS_MANY с through — получение пользователей, состоящих в определённой группе, если они записаны в группу через роли.

Более сложным примером является получение всех комментариев для всех пользователей определённой группы. В этом случае необходимо использовать несколько связей с through в одной модели:

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'),
       );
   }
}

Примеры

// получаем все группы с соответствующими им пользователями
$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 ER

Пример использования HAS_ONE с through — получение адреса пользователя в случае, если пользователь связан с адресом через профиль. Все задействованные сущности (пользователь, профиль и адрес) имеют соответствующие им модели:

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'),
       );
   }
}

Примеры

// получаем адрес пользователя с ID, равным 1
$user=User::model()->findByPk(1);
$address=$user->address;

through с собой

through можно использовать для модели, связанной с собой через мост. В нашем случае это пользователь, обучающий других пользователей:

through self ER

through self ER

Связи для данного случая определяются следующим образом:

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'),
       );
   }
}

Примеры

// получаем всех студентов учителя с ID, равным 1
$teacher=User::model()->findByPk(1);
$students=$teacher->students;

Found a typo or you think this page needs improvement?
Edit it on github !