Files
yii/docs/guide/ru/database.arr.txt
2011-01-16 14:55:27 +00:00

516 lines
36 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
Реляционная Active Record
=========================
Мы уже рассмотрели использование Active Record (AR) для выбора данных из одной таблицы базы данных.
В этом разделе мы расскажем, как использовать AR для объединения нескольких связанных таблиц и получить
объединенный набор данных.
Для использования реляционной AR рекомендуется, чтобы все связи отношения
первичный-внешний ключ были четко определены для объединяемых таблиц.
Это помогает поддерживать связность и целостность данных.
Для наглядности примеров в данном разделе мы будем использовать схему базы данных,
представленную на этой диаграмме сущность-отношение (ER).
![Диаграмма ER](er.png)
> Info|Информация: Поддержка ограничений по внешнему ключу различна в разных СУБД.
> SQLite < 3.6.19 не поддерживает ограничений, но вы, тем не менее, можете их объявить
> при создании таблиц.
Объявление отношения
----------------------
Перед тем, как использовать 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`.
Объявляя отношение в AR, мы переопределяем метод [relations()|CActiveRecord::relations] класса [CActiveRecord]. Этот метод возвращает
массив с конфигурацией отношений. Каждый элемент массива представляет одно отношение в следующем формате:
~~~
[php]
'VarName'=>array('RelationType', 'ClassName', 'ForeignKey', …дополнительные параметры)
~~~
где `VarName` — имя отношения, `RelationType` указывает на один из четырех типов отношения,
`ClassName` — имя AR-класса, связанного с данным 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|Информация: Если внешний ключ составной, мы должны объединить имена полей внешнего ключа и отделить
друг от друга запятыми. Для типа отношения `MANY_MANY` имя ассоциативной таблицы также должно быть
указано во внешнем ключе. Например, отношение `categories` в модели `Post` обозначено внешним ключом `tbl_post_category(post_id, category_id)`.
При объявлении отношения в AR-классе для каждого отношения в класс неявно добавляется свойство. После выполнения реляционного запроса
соответствующее свойство будет заполнено связанным(-и) экземпляром(-ами) 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 одновременно с
основным экземпляром AR. Реализуется этот подход путем использования в 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();
~~~
Пример выше вернет нам все записи с их авторами и категориями, а также профиль каждого автора и все его записи.
Начиная с версии 1.1.0, жадная загрузка может быть выполнена путём указания
свойства [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',
)
);
~~~
Параметры реляционного запроса
------------------------------
Выше мы упоминали о том, что в реляционном запросе можно указать дополнительные параметры.
Эти параметры — пары имя-значение — используются для тонкой настройки реляционного запроса.
Список параметров представлен ниже.
- `select`: список выбираемых полей для связанного AR-класса. По умолчанию значение параметра равно '*',
т.е. выбираются все поля таблицы. Для используемых столбцов должны быть разрешены конфликты имён.
- `condition`: соответствует оператору `WHERE`, по умолчанию значение параметра пустое.
Для используемых столбцов должны быть разрешены конфликты имён.
- `params`: параметры для связывания в генерируемом SQL-выражении. Параметры передаются как массив пар имя-значение. Параметр
доступен, начиная с версии 1.0.3;
- `on`: соответствует оператору `ON`. Условие, указываемое в этом параметре,
будет добавлено к условию объединения с использованием оператора `AND`. Для используемых столбцов должны быть разрешены конфликты имён.
Данный параметр неприменим для отношений типа `MANY_MANY`. Параметр доступен, начиная с версии 1.0.2;
- `order`: соответствует оператору `ORDER BY`, по умолчанию значение параметра пустое.
Для используемых столбцов должны быть разрешены конфликты имён.
- `with`: список дочерних связанных объектов, которые должны быть загружены с самим объектом.
Неправильное использование данной возможности может привести к бесконечному циклу.
- `joinType`: тип объединения для отношения. По умолчанию значение параметра равно `LEFT
OUTER JOIN`;
- `alias`: псевдоним таблицы, ассоциированной с отношением. Этот параметр доступен с версии 1.0.1. По умолчанию значение параметра
равняется null, что означает, что псевдоним соответствует имени отношения.
- `together`: параметр, устанавливающий необходимость принудительного объединения таблицы, ассоциированной с этим отношением,
с другими таблицами. Этот параметр имеет смысл только для отношений типов `HAS_MANY` и `MANY_MANY`. Если параметр не установлен или
равен false, тогда каждое отношение `HAS_MANY` или `MANY_MANY` будет использовать отдельный SQL запрос для связанных данных,
что может улучшить скорость выполнения запроса т.к. уменьшается количество выбираемых данных.
Если данный параметр равен `true`, зависимая таблица при запросе будет всегда
объединяться с основной, то есть будет сделан один запрос даже в том случае, если
к основной таблице применяется постраничная разбивка. Если данный параметр не
задан, зависимая таблица будет объединена с основной только в случае, когда не
к основной таблице не применяется постраничная разбивка. Более подробное описание
можно найти в разделе «производительность реляционного запроса».
Параметр доступен, начиная с версии 1.0.3;
- `join`: дополнительный оператор `JOIN`. По умолчанию пуст. Этот параметр
доступен с версии 1.1.3.
- `group`: соответствует оператору `GROUP BY`, по умолчанию значение параметра пустое.
Для используемых столбцов должны быть разрешены конфликты имён.
- `having`: соответствует оператору `HAVING`, по умолчанию значение параметра пустое.
Для используемых столбцов должны быть разрешены конфликты имён. Параметр доступен, начиная с версии 1.0.1.
- `index`: имя столбца, значения которого должны быть использованы в
качестве ключей массива, хранящего связанные объекты. Без установки этого
параметра, массив связанных объектов использует целочисленный индекс,
начинающийся с нуля. Параметр может быть установлен только для отношений
`HAS_MANY` и `MANY_MANY`. Параметр доступен с версии 1.0.7.
Кроме того, для отложенной загрузки некоторых типов отношений доступен ряд дополнительных параметров:
- `limit`: параметр для ограничения количества строк в выборке. Параметр не применим для отношений `BELONGS_TO`;
- `offset`: параметр для указания начальной строки выборки. Параметр не применим для отношений `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'
));
~~~
> Note|Примечание: разрешение конфликта имён изменилось по сравнению с версией 1.1.0.
> До этого по умолчанию Yii генерировал псевдоним таблицы для каждой связанной таблицы,
> а для его использования необходимо было использовать префикс `??.` в нужных местах запроса.
> Также в версиях 1.0.x псевдоним главной таблицы соответствовал имени таблицы.
Динамические параметры реляционного запроса
-------------------------------------------
Начиная с версии 1.0.2, мы можем использовать динамические параметры как для параметра
[with()|CActiveRecord::with], так и для параметра `with`. Динамические параметры переопределяют существующие
параметры в соответствии с описанием метода [relations()|CActiveRecord::relations]. К примеру, если для модели `User`, приведенной выше,
мы хотим воспользоваться жадной загрузкой для получения записей автора в порядке возрастания (параметр `order` в определении отношения
задает убывающий порядок), можно сделать это следующим образом:
~~~
[php]
User::model()->with(array(
'posts'=>array('order'=>'posts.create_time ASC'),
'profile',
))->findAll();
~~~
Начиная с версии 1.0.5 динамические параметры в реляционных запросах можно использовать вместе с
отложенной загрузкой. Для этого необходимо вызвать метод с тем же именем, что и
имя связи, и передать параметры как его аргумент. К примеру, следующий код
вернёт публикации пользователя, у которых `status` равен&nbsp;1:
~~~
[php]
$user=User::model()->findByPk(1);
$posts=$user->posts(array('condition'=>'status=1'));
~~~
Производительность реляционного запроса
---------------------------------------
Как было описано выше, жадная загрузка используется, главным образом, когда
требуется получить множество связанных объектов. В этом случае соединением
всех таблиц генерируется большой сложный SQL-запрос. Такой запрос во многих случаях
является предпочтительным т.к. упрощает фильтрацию по значению столбца связанной таблицы.
Тем не менее, в некоторых случаях такие запросы не являются эффективными.
Рассмотрим пример, в котором нам надо найти последние записи вместе с их комментариями.
Учитывая, что у каждой записи 10 комментариев, при использовании одного большого SQL-запроса
мы получим множество лишних данных так как каждая запись будет повторно выбираться с каждым
её комментарием. Теперь попробуем по-другому: сначала выберем последние записи, а затем комментарии к ним.
В данном случае нам необходимо выполнить два SQL запроса. Плюс в том, что в полученных данных не будет
ничего лишнего.
Так какой подход более эффективен? Абсолютно верного ответа на этот вопрос нет. Выполнение одного большого SQL запроса
может быть более эффективным так как СУБД не приходится лишний раз разбирать и выполнять дополнительные запросы.
С другой стороны, используя один SQL запрос, мы получаем больше лишних данных и соответственно нам требуется больше
времени на их передачу и обработку.
По этой причине в Yii имеется параметр запроса `together`, позволяющий выбрать между двумя описанным подходами.
По умолчанию Yii использует первый подход, то есть генерирует один SQL запрос для жадной загрузки.
Если выставить параметр `together` в 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();
~~~
> Note|Примечание: В версии 1.0.x по умолчанию генерировалось и выполнялось
> `N+1` SQL запросов при наличии `N` связей типа `HAS_MANY` или `MANY_MANY`.
> Каждая связь `HAS_MANY` или `MANY_MANY` выполнялась в своём запросе. Вызовом
> метода `together()` после `with()` можно было сгенерировать и выполнить единый
> SQL запрос:
>
> ~~~
> [php]
> $posts=Post::model()->with(
> 'author.profile',
> 'author.posts',
> 'categories')->together()->findAll();
> ~~~
>
Статистический запрос
-----------------
> Note|Примечание: статистические запросы доступны, начиная с версии 1.0.4.
Помимо реляционных запросов, описанных выше, 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`: соответствует оператору `WHERE`, по умолчанию значение параметра пустое;
- `params`: параметры для связывания в генерируемом SQL-выражении. Параметры передаются
как массив пар имя-значение;
- `order`: соответствует оператору `ORDER BY`, по умолчанию значение параметра пустое;
- `group`: соответствует оператору `GROUP BY`, по умолчанию значение параметра пустое;
- `having`: соответствует оператору `HAVING`, по умолчанию значение параметра пустое.
Реляционные запросы с именованными группами условий
---------------------------------------------------
> Note|Примечание: Группы условий поддерживаются, начиная с версии 1.0.5.
В реляционном запросе [именованные группы условий](/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();
~~~
Этот запрос вернёт все записи вместе с одобренными комментариями. Здесь `comments`
относится к имени отношения. `recently` и `approved` — именованные группы, описанные
в модели `Comment`. Имя отношения и группы параметров разделяются двоеточием.
Именованные группы могут быть использованы при описании отношений модели в
методе [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'),
);
}
}
~~~
> Note|Примечание: Именованные группы параметров, применяемые к реляционным моделям,
должны описываться в методе [CActiveRecord::scopes], поэтому они не могут быть
параметризованы.
<div class="revision">$Id: database.arr.txt 2782 2010-12-28 16:18:06Z qiang.xue $</div>