Spec-Zone.ru › Yii 2.0

Active Record

  1. Объявление классов Active Record
  2. Подключение к базам данных
  3. Запрос данных
  4. Доступ к данным
  5. Сохранение данных
  6. Удаление данных
  7. Жизненные циклы Active Record
  8. Работа с транзакциями
  9. Оптимистические блокировки
  10. Работа с реляционными данными
  11. Сохранение отношений
  12. Межбазовые отношения
  13. Настройка классов запросов
  14. Выбор дополнительных полей

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

Например, предположим Customer — это класс Active Record, который связан с таблицей customer, а name — столбец таблицы customer. Вы можете написать следующий код для вставки новой строки в таблицу customer:

$customer = new Customer();
$customer->name = 'Qiang';
$customer->save();

Вышеприведенный код эквивалентен следующему исходному SQL-запросу для MySQL, который менее интуитивен, более подвержен ошибкам и может даже иметь проблемы совместимости, если вы используете другой тип базы данных:

$db->createCommand('INSERT INTO `customer` (`name`) VALUES (:name)', [
    ':name' => 'Qiang',
])->execute();

Yii предоставляет поддержку Active Record для следующих реляционных баз данных:

  • MySQL 4.1 или более поздние версии: через yii\db\ActiveRecord
  • PostgreSQL 7.3 или более поздние версии: через yii\db\ActiveRecord
  • SQLite 2 и 3: через yii\db\ActiveRecord
  • Microsoft SQL Server 2008 или более поздние версии: через yii\db\ActiveRecord
  • Oracle: через yii\db\ActiveRecord
  • CUBRID 9.3 или более поздние версии: через yii\db\ActiveRecord (Обратите внимание, что из-за ошибки в расширении cubrid PDO, цитирование значений не будет работать, поэтому вам понадобится CUBRID 9.3 как для клиента, так и для сервера)
  • Sphinx: через yii\sphinx\ActiveRecord, требует расширение yii2-sphinx
  • ElasticSearch: через yii\elasticsearch\ActiveRecord, требует расширение yii2-elasticsearch

Кроме того, Yii также поддерживает использование Active Record с следующими базами данных NoSQL:

  • Redis 2.6.12 или более поздние версии: через yii\redis\ActiveRecord, требует расширение yii2-redis
  • MongoDB 1.3.0 или более поздние версии: через yii\mongodb\ActiveRecord, требует расширение yii2-mongodb

В этом учебнике мы в основном опишем использование Active Record для реляционных баз данных. Однако большая часть описанного здесь контента также применима к Active Record для баз данных NoSQL.

Объявление классов Active Record

Для начала объявите класс Active Record, расширив yii\db\ActiveRecord.

Установление имени таблицы

По умолчанию каждый класс Active Record связан со своей таблицей базы данных. Метод tableName() возвращает имя таблицы, преобразуя имя класса с помощью yii\helpers\Inflector::camel2id(). Вы можете переопределить этот метод, если таблица не имеет такого названия.

Также может применяться значение по умолчанию tablePrefix. Например, если tablePrefix равен tbl_, то Customer становится tbl_customer, а OrderItem становится tbl_order_item.

Если имя таблицы задано как {{%TableName}}, символ процента % будет заменен префиксом таблицы. Например, {{%post}} становится {{tbl_post}}. Скобки вокруг имени таблицы используются для цитирования в SQL-запросе.

В следующем примере мы объявляем класс Active Record с именем Customer для таблицы базы данных customer.

namespace app\models;

use yii\db\ActiveRecord;

class Customer extends ActiveRecord
{
    const STATUS_INACTIVE = 0;
    const STATUS_ACTIVE = 1;
    
    /**
     * @return string the name of the table associated with this ActiveRecord class.
     */
    public static function tableName()
    {
        return '{{customer}}';
    }
}

Экземпляры Active Record называются «моделями»

Экземпляры Active Record рассматриваются как модели. По этой причине мы обычно помещаем классы Active Record в пространство имен app\models (или другие пространства имен для хранения моделей).

Поскольку yii\db\ActiveRecord расширяет yii\base\Model, он наследует *все* функции моделей, такие как атрибуты, правила валидации, сериализация данных и т.д.

Подключение к базам данных

По умолчанию Active Record использует компонент db приложения в качестве соединения с базой данных для доступа и обработки данных базы данных. Как объяснено в объектах доступа к базам данных, вы можете настроить компонент db в конфигурации приложения, как показано ниже,

return [
    'components' => [
        'db' => [
            'class' => 'yii\db\Connection',
            'dsn' => 'mysql:host=localhost;dbname=testdb',
            'username' => 'demo',
            'password' => 'demo',
        ],
    ],
];

Если вы хотите использовать другое соединение с базой данных, отличное от компонента db, вы должны переопределить метод getDb():

class Customer extends ActiveRecord
{
    // ...

    public static function getDb()
    {
        // use the "db2" application component
        return \Yii::$app->db2;  
    }
}

Запрос данных

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

  1. Создайте новый объект запроса, вызвав метод yii\db\ActiveRecord::find();
  2. Сформируйте объект запроса, вызвав методы построения запросов;
  3. Вызовите метод запроса для получения данных в виде экземпляров Active Record.

Как видите, это очень похоже на процедуру с построителем запросов. Единственное отличие заключается в том, что вместо использования оператора new для создания объекта запроса, вы вызываете yii\db\ActiveRecord::find() для возврата нового объекта запроса, который является объектом класса yii\db\ActiveQuery.

Ниже приведены примеры использования Active Query для запроса данных:

// return a single customer whose ID is 123
// SELECT * FROM `customer` WHERE `id` = 123
$customer = Customer::find()
    ->where(['id' => 123])
    ->one();

// return all active customers and order them by their IDs
// SELECT * FROM `customer` WHERE `status` = 1 ORDER BY `id`
$customers = Customer::find()
    ->where(['status' => Customer::STATUS_ACTIVE])
    ->orderBy('id')
    ->all();

// return the number of active customers
// SELECT COUNT(*) FROM `customer` WHERE `status` = 1
$count = Customer::find()
    ->where(['status' => Customer::STATUS_ACTIVE])
    ->count();

// return all customers in an array indexed by customer IDs
// SELECT * FROM `customer`
$customers = Customer::find()
    ->indexBy('id')
    ->all();

В приведенном выше примере $customer — это объект Customer, а $customers — массив объектов Customer. Все они заполнены данными, полученными из таблицы customer.

Информация: Поскольку yii\db\ActiveQuery расширяет yii\db\Query, вы можете использовать *все* методы построения запросов и методы запросов, описанные в разделе Построитель запросов.

Поскольку запрос по значениям первичного ключа или набору значений столбцов является распространенной задачей, Yii предоставляет два сокращенных метода для этой цели:

  • yii\db\ActiveRecord::findOne(): возвращает один экземпляр Active Record, заполненный первой строкой результата запроса.
  • yii\db\ActiveRecord::findAll(): возвращает массив экземпляров Active Record, заполненный *всеми* результатами запроса.

Оба метода могут принимать один из следующих форматов параметров:

  • скалярное значение: значение обрабатывается как желаемое значение первичного ключа для поиска. Yii автоматически определит, какой столбец является первичным ключом, прочитав информацию из схемы базы данных.
  • массив скалярных значений: массив обрабатывается как желаемые значения первичного ключа для поиска.
  • ассоциативный массив: ключи — имена столбцов, а значения — соответствующие искомые значения столбцов. Подробнее см. Формат хэша.

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

// returns a single customer whose ID is 123
// SELECT * FROM `customer` WHERE `id` = 123
$customer = Customer::findOne(123);

// returns customers whose ID is 100, 101, 123 or 124
// SELECT * FROM `customer` WHERE `id` IN (100, 101, 123, 124)
$customers = Customer::findAll([100, 101, 123, 124]);

// returns an active customer whose ID is 123
// SELECT * FROM `customer` WHERE `id` = 123 AND `status` = 1
$customer = Customer::findOne([
    'id' => 123,
    'status' => Customer::STATUS_ACTIVE,
]);

// returns all inactive customers
// SELECT * FROM `customer` WHERE `status` = 0
$customers = Customer::findAll([
    'status' => Customer::STATUS_INACTIVE,
]);

Примечание: Ни yii\db\ActiveRecord::findOne(), ни yii\db\ActiveQuery::one() не добавят LIMIT 1 в сгенерированный SQL-запрос. Если ваш запрос может вернуть много строк данных, вы должны явно вызвать limit(1) для повышения производительности, например Customer::find()->limit(1)->one().

Помимо использования методов построения запросов, вы также можете писать исходные SQL-запросы для запроса данных и заполнения результатов объектами Active Record. Вы можете сделать это, вызвав метод yii\db\ActiveRecord::findBySql():

// returns all inactive customers
$sql = 'SELECT * FROM customer WHERE status=:status';
$customers = Customer::findBySql($sql, [':status' => Customer::STATUS_INACTIVE])->all();

Не вызывайте дополнительных методов построения запросов после вызова findBySql(), так как они будут проигнорированы.

Доступ к данным

Как уже упоминалось, данные, возвращаемые из базы данных, заполняют экземпляры Active Record, и каждая строка результата запроса соответствует одному экземпляру Active Record. Вы можете получить значения столбцов, обратившись к атрибутам экземпляров Active Record, например,

// "id" and "email" are the names of columns in the "customer" table
$customer = Customer::findOne(123);
$id = $customer->id;
$email = $customer->email;

Примечание: Атрибуты Active Record имеют имена, соответствующие именам столбцов связанной таблицы в регистрозависимом формате. Yii автоматически определяет атрибут в Active Record для каждого столбца связанной таблицы. Вы не должны переобъявлять атрибуты.

Поскольку атрибуты Active Record имеют имена, соответствующие именам столбцов таблицы, вы можете обнаружить, что пишете PHP-код, например $customer->first_name, который использует подчеркивания для разделения слов в именах атрибутов, если ваши столбцы таблиц названы таким образом. Если вы беспокоитесь о согласованности стиля кода, вы должны переименовать столбцы таблиц соответственно (например, на camelCase).

Преобразование данных

Часто бывает, что вводимые и/или отображаемые данные имеют формат, отличный от формата, используемого для хранения данных в базе данных. Например, в базе данных вы храните даты рождения клиентов в виде отметки времени UNIX (хотя это не лучшая практика), а в большинстве случаев вы хотели бы обрабатывать даты рождения как строки в формате 'YYYY/MM/DD'. Для достижения этой цели вы можете определить методы преобразования данных в классе Active Record Customer Active Record, как показано ниже:

class Customer extends ActiveRecord
{
    // ...

    public function getBirthdayText()
    {
        return date('Y/m/d', $this->birthday);
    }
    
    public function setBirthdayText($value)
    {
        $this->birthday = strtotime($value);
    }
}

Теперь в вашем PHP-коде, вместо доступа к $customer->birthday, вы будете обращаться к $customer->birthdayText, что позволит вам вводить и отображать даты рождения клиентов в формате 'YYYY/MM/DD'.

Подсказка: Приведённый выше пример демонстрирует общий способ преобразования данных в различных форматах. Если вы работаете со значениями дат, вы можете использовать DateValidator и DatePicker, что проще в использовании и обладает большей мощностью.

Получение данных в массивах

Хотя получение данных в виде объектов Active Record удобно и гибко, это не всегда желательно, когда вам нужно получить большое количество данных из-за большого объёма занимаемой памяти. В этом случае вы можете получить данные, используя PHP-массивы, вызвав asArray() перед выполнением метода запроса:

// return all customers
// each customer is returned as an associative array
$customers = Customer::find()
    ->asArray()
    ->all();

Примечание: Хотя этот метод экономит память и улучшает производительность, он ближе к более низкому уровню абстракции базы данных, и вы потеряете большинство функций Active Record. Важным отличием является тип данных значений столбцов. Когда вы возвращаете данные в экземплярах Active Record, значения столбцов будут автоматически приводиться к типу в соответствии с фактическими типами столбцов; с другой стороны, когда вы возвращаете данные в массивах, значения столбцов будут строками (поскольку они являются результатом PDO без какой-либо обработки), независимо от их фактических типов столбцов.

Получение данных партиями

В Query Builder мы объяснили, что вы можете использовать пакетный запрос, чтобы минимизировать использование памяти при запросе большого количества данных из базы данных. Вы можете использовать ту же технику в Active Record. Например,

// fetch 10 customers at a time
foreach (Customer::find()->batch(10) as $customers) {
    // $customers is an array of 10 or fewer Customer objects
}

// fetch 10 customers at a time and iterate them one by one
foreach (Customer::find()->each(10) as $customer) {
    // $customer is a Customer object
}

// batch query with eager loading
foreach (Customer::find()->with('orders')->each() as $customer) {
    // $customer is a Customer object with the 'orders' relation populated
}

Сохранение данных

Используя Active Record, вы можете легко сохранить данные в базе данных, выполнив следующие шаги:

  1. Подготовьте экземпляр Active Record
  2. Присвойте новые значения атрибутам Active Record
  3. Вызовите yii\db\ActiveRecord::save() для сохранения данных в базе данных.

Например,

// insert a new row of data
$customer = new Customer();
$customer->name = 'James';
$customer->email = 'james@example.com';
$customer->save();

// update an existing row of data
$customer = Customer::findOne(123);
$customer->email = 'james@newexample.com';
$customer->save();

Метод save() может либо вставить, либо обновить строку данных, в зависимости от состояния экземпляра Active Record. Если экземпляр только что создан с помощью оператора new, вызов save() вызовет вставку новой строки; если экземпляр является результатом метода запроса, вызов save() обновит строку, связанную с экземпляром.

Вы можете различать два состояния экземпляра Active Record, проверяя значение его свойства isNewRecord. Это свойство также используется методом save() в нём:

public function save($runValidation = true, $attributeNames = null)
{
    if ($this->getIsNewRecord()) {
        return $this->insert($runValidation, $attributeNames);
    } else {
        return $this->update($runValidation, $attributeNames) !== false;
    }
}

Подсказка: Вы можете напрямую вызвать insert() или update() для вставки или обновления строки.

Валидация данных

Поскольку yii\db\ActiveRecord наследуется от yii\base\Model, он имеет те же возможности валидации данных. Вы можете определить правила валидации, переопределив метод rules(), и выполнить валидацию данных, вызвав метод validate().

При вызове save(), по умолчанию он автоматически вызовет validate(). Только при успешной валидации он фактически сохранит данные; в противном случае он просто вернёт false, и вы можете проверить свойство errors для получения сообщений об ошибках валидации.

Подсказка: Если вы уверены, что ваши данные не требуют валидации (например, данные поступают из надёжных источников), вы можете вызвать save(false), чтобы пропустить валидацию.

Массовое присваивание

Как и обычные модели, экземпляры Active Record также обладают возможностью массового присваивания. Используя эту функцию, вы можете присвоить значения нескольким атрибутам экземпляра Active Record в одном операторе PHP, как показано ниже. Однако имейте в виду, что массовому присваиванию доступны только безопасные атрибуты.

$values = [
    'name' => 'James',
    'email' => 'james@example.com',
];

$customer = new Customer();

$customer->attributes = $values;
$customer->save();

Обновление счётчиков

Частая задача – инкрементировать или декрементировать столбец в таблице базы данных. Такие столбцы называются «столбцами счётчиков». Вы можете использовать updateCounters() для обновления одного или нескольких столбцов счётчиков. Например,

$post = Post::findOne(100);

// UPDATE `post` SET `view_count` = `view_count` + 1 WHERE `id` = 100
$post->updateCounters(['view_count' => 1]);

Примечание: Если вы используете yii\db\ActiveRecord::save() для обновления столбца счётчика, вы можете получить неточный результат, поскольку несколько запросов могут одновременно читать и записывать одно и то же значение счётчика.

Изменённые атрибуты

При вызове save() для сохранения экземпляра Active Record, сохраняются только изменённые атрибуты. Атрибут считается изменённым, если его значение было изменено после загрузки из базы данных или последнего сохранения в базу данных. Обратите внимание, что валидация данных будет выполняться независимо от того, есть ли у экземпляра Active Record изменённые атрибуты или нет.

Active Record автоматически поддерживает список изменённых атрибутов. Для этого он сохраняет более старую версию значений атрибутов и сравнивает их с последней. Вы можете вызвать yii\db\ActiveRecord::getDirtyAttributes() для получения атрибутов, которые в данный момент изменены. Вы также можете вызвать yii\db\ActiveRecord::markAttributeDirty() для явного обозначения атрибута как изменённого.

Если вам нужны значения атрибутов до их последнего изменения, вы можете вызвать getOldAttributes() или getOldAttribute().

Примечание: Сравнение старых и новых значений будет выполнено с использованием оператора ===, поэтому значение будет считаться изменённым даже в том случае, если оно имеет то же значение, но разный тип. Это часто происходит, когда модель получает пользовательский ввод из HTML-форм, где каждое значение представлено как строка. Чтобы гарантировать правильный тип, например, для целочисленных значений, вы можете применить фильтр валидации: ['attributeName', 'filter', 'filter' => 'intval']. Это работает со всеми функциями приведения типов PHP, такими как intval(), floatval(), boolval и т.д...

Значения атрибутов по умолчанию

Некоторые столбцы вашей таблицы могут иметь значения по умолчанию, определённые в базе данных. Иногда вы можете захотеть предварительно заполнить свою веб-форму для экземпляра Active Record этими значениями по умолчанию. Чтобы избежать повторного ввода тех же значений по умолчанию, вы можете вызвать loadDefaultValues() для заполнения значений по умолчанию, определённых в БД, соответствующими атрибутами Active Record:

$customer = new Customer();
$customer->loadDefaultValues();
// $customer->xyz will be assigned the default value declared when defining the "xyz" column

Приведение типов атрибутов

Будучи заполненными результатами запросов, yii\db\ActiveRecord выполняет автоматическое приведение типов для своих значений атрибутов, используя информацию из схемы таблицы базы данных. Это позволяет данным, полученным из столбца таблицы, объявленного как целое число, быть загруженными в экземпляр ActiveRecord с целочисленным значением PHP, булевым значением с булевым и т. д. Однако механизм приведения типов имеет ряд ограничений:

  • Значения с плавающей точкой не преобразуются и будут представлены как строки, в противном случае они могут потерять точность.
  • Преобразование целочисленных значений зависит от объёма целочисленных значений используемой вами операционной системы. В частности: значения столбца, объявленного как «целое без знака» или «большое целое», будут преобразованы в целые числа PHP только в 64-битной операционной системе, а в 32-битных - они будут представлены как строки.

Обратите внимание, что приведение типов атрибутов выполняется только при заполнении экземпляра ActiveRecord результатами запроса. Нет автоматического преобразования значений, загружаемых из HTTP-запроса или заданных напрямую через доступ к свойствам. Схема таблицы также будет использоваться при подготовке SQL-запросов для сохранения данных Active Record, гарантируя, что значения будут связаны с запросом с правильным типом. Однако значения атрибутов экземпляра Active Record не будут преобразовываться во время процесса сохранения.

Подсказка: Вы можете использовать yii\behaviors\AttributeTypecastBehavior для облегчения приведения типов значений атрибутов в Active Record при валидации или сохранении.

Обновление нескольких строк

Описанные выше методы работают с отдельными экземплярами Active Record, что приводит к вставке или обновлению отдельных строк таблицы. Для одновременного обновления нескольких строк необходимо вызвать updateAll(), который является статическим методом.

// UPDATE `customer` SET `status` = 1 WHERE `email` LIKE `%@example.com%`
Customer::updateAll(['status' => Customer::STATUS_ACTIVE], ['like', 'email', '@example.com']);

Аналогично, вы можете вызвать updateAllCounters() для одновременного обновления столбцов счётчиков нескольких строк.

// UPDATE `customer` SET `age` = `age` + 1
Customer::updateAllCounters(['age' => 1]);

Удаление данных

Для удаления одной строки данных сначала получите экземпляр Active Record, соответствующий этой строке, а затем вызовите метод yii\db\ActiveRecord::delete().

$customer = Customer::findOne(123);
$customer->delete();

Вы можете вызвать yii\db\ActiveRecord::deleteAll() для удаления нескольких или всех строк данных. Например,

Customer::deleteAll(['status' => Customer::STATUS_INACTIVE]);

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

Жизненный цикл Active Record

Важно понимать жизненный цикл Active Record, когда он используется для различных целей. В течение каждого жизненного цикла будет вызвана определённая последовательность методов, и вы можете переопределить эти методы, чтобы иметь возможность настроить жизненный цикл. Вы также можете реагировать на определённые события Active Record, срабатывающие во время жизненного цикла, чтобы встроить свой пользовательский код. Эти события особенно полезны при разработке поведений Active Record behaviors, которые должны настраивать жизненные циклы Active Record.

В дальнейшем мы обобщим различные жизненные циклы Active Record и методы/события, участвующие в них.

Жизненный цикл нового экземпляра

При создании нового экземпляра Active Record через оператор new, произойдет следующий жизненный цикл:

  1. Конструктор класса.
  2. init(): вызывает событие EVENT_INIT.

Жизненный цикл запроса данных

При запросе данных через один из методов запроса, каждый заполненный Active Record будет проходить следующий жизненный цикл:

  1. Конструктор класса.
  2. init(): вызывает событие EVENT_INIT.
  3. afterFind(): вызывает событие EVENT_AFTER_FIND.

Жизненный цикл сохранения данных

При вызове save() для вставки или обновления экземпляра Active Record, произойдет следующий жизненный цикл:

  1. событие EVENT_BEFORE_VALIDATE. Если метод возвращает false или yii\base\ModelEvent::$isValid равно false, остальные шаги будут пропущены.

  2. Выполняется валидация данных. Если валидация данных завершается неудачно, шаги после шага 3 будут пропущены.
  3. событие EVENT_AFTER_VALIDATE.

  4. событие EVENT_BEFORE_INSERT или EVENT_BEFORE_UPDATE. Если метод возвращает false или yii\base\ModelEvent::$isValid равно false, остальные шаги будут пропущены.

  5. Выполняется фактическая вставка или обновление данных.
  6. событие EVENT_AFTER_INSERT или EVENT_AFTER_UPDATE.

Жизненный цикл удаления данных

При вызове delete() для удаления экземпляра Active Record, произойдет следующий жизненный цикл:

  1. событие EVENT_BEFORE_DELETE. Если метод возвращает false или yii\base\ModelEvent::$isValid равно false, остальные шаги будут пропущены.

  2. Выполняется фактическое удаление данных.
  3. событие EVENT_AFTER_DELETE.

Примечание: Вызов любого из следующих методов НЕ инициирует вышеуказанные жизненные циклы, потому что они работают непосредственно с базой данных, а не с записями:

  • yii\db\ActiveRecord::updateAll()
  • yii\db\ActiveRecord::deleteAll()
  • yii\db\ActiveRecord::updateCounters()
  • yii\db\ActiveRecord::updateAllCounters()

Жизненный цикл обновления данных

При вызове refresh() для обновления экземпляра Active Record, событие EVENT_AFTER_REFRESH срабатывает, если обновление прошло успешно и метод возвращает true.

Работа с транзакциями

Существует два способа использования транзакций при работе с Active Record.

Первый способ заключается в явном заключении вызовов методов Active Record в блоке транзакций, как показано ниже,

$customer = Customer::findOne(123);

Customer::getDb()->transaction(function($db) use ($customer) {
    $customer->id = 200;
    $customer->save();
    // ...other DB operations...
});

// or alternatively

$transaction = Customer::getDb()->beginTransaction();
try {
    $customer->id = 200;
    $customer->save();
    // ...other DB operations...
    $transaction->commit();
} catch(\Exception $e) {
    $transaction->rollBack();
    throw $e;
} catch(\Throwable $e) {
    $transaction->rollBack();
    throw $e;
}

Примечание: в приведенном выше коде у нас есть два блока catch для совместимости с PHP 5.x и PHP 7.x. \Exception реализует интерфейс \Throwable начиная с PHP 7.0, поэтому вы можете пропустить часть с \Exception если ваш приложение использует только PHP 7.0 и выше.

Второй способ заключается в перечислении операций базы данных, требующих поддержки транзакций, в методе yii\db\ActiveRecord::transactions(). Например,

class Customer extends ActiveRecord
{
    public function transactions()
    {
        return [
            'admin' => self::OP_INSERT,
            'api' => self::OP_INSERT | self::OP_UPDATE | self::OP_DELETE,
            // the above is equivalent to the following:
            // 'api' => self::OP_ALL,
        ];
    }
}

Метод yii\db\ActiveRecord::transactions() должен возвращать массив, ключами которого являются имена сценариев, а значениями — соответствующие операции, которые должны быть заключены в транзакции. Вы должны использовать следующие константы для ссылки на разные операции базы данных:

  • OP_INSERT: операция вставки, выполняемая insert();
  • OP_UPDATE: операция обновления, выполняемая update();
  • OP_DELETE: операция удаления, выполняемая delete().

Используйте операторы | для конкатенации вышеперечисленных констант, чтобы указать несколько операций. Вы также можете использовать сокращенную константу OP_ALL для ссылки на все три вышеуказанные операции.

Транзакции, созданные этим методом, будут начаты до вызова beforeSave() и будут подтверждены после того, как будет выполнен afterSave().

Оптимистические блокировки

Оптимистическая блокировка — это способ предотвращения конфликтов, которые могут возникнуть, когда одна строка данных обновляется несколькими пользователями. Например, оба пользователя A и B редактируют одну и ту же статью вики одновременно. После того, как пользователь A сохранил свои правки, пользователь B нажимает кнопку «Сохранить», пытаясь сохранить свои правки тоже. Поскольку пользователь B фактически работал со устаревшей версией статьи, желательно иметь способ предотвратить сохранение статьи и показать ему некоторое сообщение об ошибке.

Оптимистическая блокировка решает эту проблему, используя столбец для записи номера версии каждой строки. Когда строка сохраняется с устаревшей версией, будет брошено исключение yii\db\StaleObjectException, которое предотвращает сохранение строки. Оптимистическая блокировка поддерживается только при обновлении или удалении существующей строки данных с помощью yii\db\ActiveRecord::update() или yii\db\ActiveRecord::delete() соответственно.

Для использования оптимистической блокировки,

  1. Создайте столбец в таблице базы данных, связанной с классом Active Record, для хранения номера версии каждой строки. Столбец должен быть целого типа большого размера (в MySQL это будет BIGINT DEFAULT 0).
  2. Переопределите метод yii\db\ActiveRecord::optimisticLock(), чтобы он возвращал имя этого столбца.
  3. В веб-форме, которая принимает пользовательские вводы, добавьте скрытое поле для хранения текущего номера версии строки, которая обновляется. Убедитесь, что у вашего атрибута версии есть правила валидации ввода и валидация проходит успешно.
  4. В действии контроллера, который обновляет строку с помощью Active Record, попробуйте перехватить исключение yii\db\StaleObjectException. Реализуйте необходимую бизнес-логику (например, объединение изменений, отображение устаревших данных) для разрешения конфликта.

Например, предположим, что столбец версии имеет имя version. Вы можете реализовать оптимистическую блокировку с кодом, подобным следующему.

// ------ view code -------

use yii\helpers\Html;

// ...other input fields
echo Html::activeHiddenInput($model, 'version');


// ------ controller code -------

use yii\db\StaleObjectException;

public function actionUpdate($id)
{
    $model = $this->findModel($id);

    try {
        if ($model->load(Yii::$app->request->post()) && $model->save()) {
            return $this->redirect(['view', 'id' => $model->id]);
        } else {
            return $this->render('update', [
                'model' => $model,
            ]);
        }
    } catch (StaleObjectException $e) {
        // logic to resolve the conflict
    }
}

Работа с реляционными данными

Помимо работы с отдельными таблицами базы данных, Active Record также способен объединять связанные данные, делая их легкодоступными через основные данные. Например, данные клиента связаны с данными заказа, потому что один клиент может разместить один или несколько заказов. При соответствующей декларации этой связи вы сможете получить доступ к информации о заказах клиента с помощью выражения $customer->orders, которое возвращает информацию о заказах клиента в виде массива экземпляров Active Record Order.

Объявление отношений

Чтобы работать с реляционными данными с помощью Active Record, сначала необходимо объявить отношения в классах Active Record. Задача проста, как объявление метода отношения для каждого интересующего отношения, как показано ниже,

class Customer extends ActiveRecord
{
    // ...

    public function getOrders()
    {
        return $this->hasMany(Order::className(), ['customer_id' => 'id']);
    }
}

class Order extends ActiveRecord
{
    // ...

    public function getCustomer()
    {
        return $this->hasOne(Customer::className(), ['id' => 'customer_id']);
    }
}

В приведенном выше коде мы объявили отношение orders для класса Customer, и отношение customer для класса Order.

Каждый метод отношения должен иметь имя getXyz. Мы называем xyz (первая буква в нижнем регистре) именем отношения. Обратите внимание, что имена отношений чувствительны к регистру.

При объявлении отношения вы должны указать следующую информацию:

  • множественность отношения: указывается путем вызова либо hasMany(), либо hasOne(). В приведенном выше примере вы легко можете прочитать в объявлениях отношений, что клиент имеет много заказов, а заказ имеет только одного клиента.
  • имя связанного класса Active Record: указывается в качестве первого параметра для hasMany() или hasOne(). Рекомендуется вызывать Xyz::className() для получения строки имени класса, чтобы получить поддержку автодополнения IDE, а также обнаружение ошибок на этапе компиляции.
  • связь между двумя типами данных: указывает столбец(ы), через который связаны два типа данных. Значения массива — это столбцы первичных данных (представленные классом Active Record, для которого вы объявляете отношения), а ключи массива — столбцы связанных данных.

    Легкое правило для запоминания этого — как вы видите в примере выше, вы записываете столбец, который принадлежит связанному Active Record непосредственно рядом с ним. Вы видите там, что customer_id является свойством Order, а id является свойством Customer.

Доступ к реляционным данным

После объявления отношений вы можете получить доступ к реляционным данным через имена отношений. Это так же, как доступ к свойству объекта, определенному методом отношения. По этой причине мы называем его свойством отношения. Например,

// SELECT * FROM `customer` WHERE `id` = 123
$customer = Customer::findOne(123);

// SELECT * FROM `order` WHERE `customer_id` = 123
// $orders is an array of Order objects
$orders = $customer->orders;

Информация: Когда вы объявляете отношение с именем xyz через метод-получатель getXyz(), вы сможете получить доступ к xyz как к свойству объекта. Обратите внимание, что имя чувствительно к регистру.

Если отношение объявлено с помощью hasMany(), обращение к этому свойству отношения вернёт массив связанных экземпляров Active Record; если отношение объявлено с помощью hasOne(), обращение к свойству отношения вернёт связанный экземпляр Active Record или null , если соответствующих данных нет.

При первом обращении к свойству отношения выполняется SQL-запрос, как показано в примере выше. Если к этому же свойству обратиться повторно, будет возвращён предыдущий результат без повторного выполнения SQL-запроса. Чтобы принудительно перевыполнить SQL-запрос, сначала необходимо удалить свойство отношения: unset($customer->orders).

Примечание: Хотя эта концепция похожа на функцию свойства объекта, есть важное отличие. Для обычных свойств объекта значение свойства имеет тот же тип, что и определяющий метод-геттер. Однако метод отношения возвращает экземпляр yii\db\ActiveQuery, а обращение к свойству отношения возвращает либо экземпляр yii\db\ActiveRecord, либо массив таких экземпляров.

$customer->orders; // is an array of `Order` objects
$customer->getOrders(); // returns an ActiveQuery instance

Это полезно для создания настраиваемых запросов, которые описаны в следующей секции.

Динамический запрос отношения

Поскольку метод отношения возвращает экземпляр yii\db\ActiveQuery, вы можете дополнительно построить этот запрос, используя методы построения запросов, прежде чем выполнять запрос к базе данных. Например,

$customer = Customer::findOne(123);

// SELECT * FROM `order` WHERE `customer_id` = 123 AND `subtotal` > 200 ORDER BY `id`
$orders = $customer->getOrders()
    ->where(['>', 'subtotal', 200])
    ->orderBy('id')
    ->all();

В отличие от обращения к свойству отношения, каждый раз, когда вы выполняете динамический запрос отношения через метод отношения, будет выполняться SQL-запрос, даже если тот же динамический запрос отношения выполнялся ранее.

Иногда вам может потребоваться параметризовать объявление отношения, чтобы легче выполнять динамические запросы к отношениям. Например, вы можете объявить bigOrders отношение следующим образом,

class Customer extends ActiveRecord
{
    public function getBigOrders($threshold = 100)
    {
        return $this->hasMany(Order::className(), ['customer_id' => 'id'])
            ->where('subtotal > :threshold', [':threshold' => $threshold])
            ->orderBy('id');
    }
}

Тогда вы сможете выполнить следующие запросы к отношениям:

// SELECT * FROM `order` WHERE `customer_id` = 123 AND `subtotal` > 200 ORDER BY `id`
$orders = $customer->getBigOrders(200)->all();

// SELECT * FROM `order` WHERE `customer_id` = 123 AND `subtotal` > 100 ORDER BY `id`
$orders = $customer->bigOrders;

Отношения через таблицу связи

В моделировании баз данных, когда множественность между двумя связанными таблицами — «многие ко многим», обычно вводится таблица связи. Например, таблица order и таблица item могут быть связаны через таблицу связи с именем order_item. Один заказ будет соответствовать множеству пунктов заказа, а один пункт товара также будет соответствовать множеству пунктов заказа.

При объявлении таких отношений вы должны вызвать либо via(), либо viaTable(), чтобы указать таблицу связи. Различие между via() и viaTable() заключается в том, что первый указывает таблицу связи в терминах именования существующего отношения, а второй напрямую использует таблицу связи. Например,

class Order extends ActiveRecord
{
    public function getItems()
    {
        return $this->hasMany(Item::className(), ['id' => 'item_id'])
            ->viaTable('order_item', ['order_id' => 'id']);
    }
}

или альтернативно,

class Order extends ActiveRecord
{
    public function getOrderItems()
    {
        return $this->hasMany(OrderItem::className(), ['order_id' => 'id']);
    }

    public function getItems()
    {
        return $this->hasMany(Item::className(), ['id' => 'item_id'])
            ->via('orderItems');
    }
}

Использование отношений, объявленных с помощью таблицы связи, такое же, как и обычных отношений. Например,

// SELECT * FROM `order` WHERE `id` = 100
$order = Order::findOne(100);

// SELECT * FROM `order_item` WHERE `order_id` = 100
// SELECT * FROM `item` WHERE `item_id` IN (...)
// returns an array of Item objects
$items = $order->items;

Ленивая и жадная загрузка

В Доступ к данным отношений мы объяснили, что вы можете получить доступ к свойству отношения экземпляра Active Record, как к обычному свойству объекта. SQL-запрос выполняется только при первом обращении к свойству отношения. Мы называем такой метод доступа к данным отношений ленивой загрузкой. Например,

// SELECT * FROM `customer` WHERE `id` = 123
$customer = Customer::findOne(123);

// SELECT * FROM `order` WHERE `customer_id` = 123
$orders = $customer->orders;

// no SQL executed
$orders2 = $customer->orders;

Ленивая загрузка очень удобна в использовании. Однако она может столкнуться с проблемой производительности, когда вам нужно получить доступ к одному и тому же свойству отношения для нескольких экземпляров Active Record. Рассмотрим следующий пример кода. Сколько SQL-запросов будет выполнено?

Customer

Как видно из комментария кода выше, выполняется 101 SQL-запрос! Это потому, что каждый раз, когда вы обращаетесь к свойству отношения orders разных объектов Customer в цикле for, выполняется SQL-запрос.

Для решения этой проблемы производительности вы можете использовать подход, называемый жадной загрузкой, как показано ниже,

// SELECT * FROM `customer` LIMIT 100;
// SELECT * FROM `orders` WHERE `customer_id` IN (...)
$customers = Customer::find()
    ->with('orders')
    ->limit(100)
    ->all();

foreach ($customers as $customer) {
    // no SQL executed
    $orders = $customer->orders;
}

Вызвав yii\db\ActiveQuery::with(), вы указываете Active Record на получение заказов для первых 100 клиентов в одном SQL-запросе. В результате вы сокращаете количество выполняемых SQL-запросов с 101 до 2!

Вы можете загружать одно или несколько отношений жадно. Вы даже можете загружать вложенные отношения. Вложенное отношение — это отношение, объявленное внутри связанного класса Active Record. Например, Customer связано с Order через отношение orders, а Order связано с Item через отношение items. При запросе Customer вы можете жадно загрузить items с помощью обозначения вложенного отношения orders.items.

Следующий код демонстрирует различные способы использования with(). Предположим, что класс Customer имеет два отношения orders и country, а класс Order имеет одно отношение items.

// eager loading both "orders" and "country"
$customers = Customer::find()->with('orders', 'country')->all();
// equivalent to the array syntax below
$customers = Customer::find()->with(['orders', 'country'])->all();
// no SQL executed 
$orders= $customers[0]->orders;
// no SQL executed 
$country = $customers[0]->country;

// eager loading "orders" and the nested relation "orders.items"
$customers = Customer::find()->with('orders.items')->all();
// access the items of the first order of the first customer
// no SQL executed
$items = $customers[0]->orders[0]->items;

Вы можете жадно загрузить глубоко вложенные отношения, такие как a.b.c.d. Все родительские отношения будут загружаться жадно. То есть, когда вы вызываете with() с a.b.c.d, вы будете жадно загружать a, a.b, a.b.c и a.b.c.d.

Информация: В общем случае, при жадной загрузке N отношений, среди которых M отношения определены с помощью таблицы связи, будет выполнено общее количество N+M+1 SQL-запросов. Обратите внимание, что вложенное отношение a.b.c.d считается 4 отношениями.

При жадной загрузке отношения вы можете настроить соответствующий запрос отношения с помощью анонимной функции. Например,

// find customers and bring back together their country and active orders
// SELECT * FROM `customer`
// SELECT * FROM `country` WHERE `id` IN (...)
// SELECT * FROM `order` WHERE `customer_id` IN (...) AND `status` = 1
$customers = Customer::find()->with([
    'country',
    'orders' => function ($query) {
        $query->andWhere(['status' => Order::STATUS_ACTIVE]);
    },
])->all();

При настройке запроса отношения для отношения вы должны указать имя отношения в качестве ключа массива и использовать анонимную функцию в качестве соответствующего значения массива. Анонимная функция получит параметр $query, который представляет собой объект yii\db\ActiveQuery, используемый для выполнения запроса отношения для отношения. В примере кода выше мы изменяем запрос отношения, добавив дополнительное условие о статусе заказа.

Примечание: Если вы вызываете select() при жадной загрузке отношений, вы должны убедиться, что столбцы, на которые ссылаются объявления отношений, выбираются. В противном случае связанные модели могут не загружаться должным образом. Например,

$orders = Order::find()->select(['id', 'amount'])->with('customer')->all();
// $orders[0]->customer is always `null`. To fix the problem, you should do the following:
$orders = Order::find()->select(['id', 'amount', 'customer_id'])->with('customer')->all();

Соединение с отношениями

Примечание: Содержимое, описанное в этом подразделе, применимо только к реляционным базам данных, таким как MySQL, PostgreSQL и т. д.

Запросы к отношениям, которые мы описали до сих пор, ссылаются только на столбцы основной таблицы при запросе первичных данных. На самом деле нам часто необходимо ссылаться на столбцы в связанных таблицах. Например, мы можем захотеть вернуть клиентов, у которых есть хотя бы один активный заказ. Для решения этой проблемы мы можем построить запрос с объединением следующим образом:

// SELECT `customer`.* FROM `customer`
// LEFT JOIN `order` ON `order`.`customer_id` = `customer`.`id`
// WHERE `order`.`status` = 1
// 
// SELECT * FROM `order` WHERE `customer_id` IN (...)
$customers = Customer::find()
    ->select('customer.*')
    ->leftJoin('order', '`order`.`customer_id` = `customer`.`id`')
    ->where(['order.status' => Order::STATUS_ACTIVE])
    ->with('orders')
    ->all();

Примечание: Важно различать имена столбцов при построении запросов к отношениям, включающих операторы JOIN SQL. Общей практикой является добавление префикса к именам столбцов, соответствующий именам таблиц.

Однако лучший подход заключается в использовании существующих объявлений отношений, вызвав yii\db\ActiveQuery::joinWith():

$customers = Customer::find()
    ->joinWith('orders')
    ->where(['order.status' => Order::STATUS_ACTIVE])
    ->all();

Оба подхода выполняют один и тот же набор SQL-запросов. Однако второй подход намного чище и компактнее.

По умолчанию joinWith() будет использовать LEFT JOIN для объединения основной таблицы со связанной таблицей. Вы можете указать другой тип объединения (например, RIGHT JOIN) через его третий параметр $joinType. Если желаемый тип объединения — INNER JOIN, вы можете просто вызвать innerJoinWith() вместо этого.

Вызов joinWith() будет жадно загружать связанные данные по умолчанию. Если вы не хотите загружать связанные данные, вы можете указать его второй параметр $eagerLoading как false.

Как и with(), вы можете объединять одно или несколько отношений; вы можете настраивать запросы к отношениям динамически; вы можете объединять вложенные отношения; и вы можете комбинировать использование with() и joinWith(). Например,

$customers = Customer::find()->joinWith([
    'orders' => function ($query) {
        $query->andWhere(['>', 'subtotal', 100]);
    },
])->with('country')
    ->all();

Иногда при объединении двух таблиц вам может потребоваться указать дополнительные условия в части ON запроса JOIN. Это можно сделать, вызвав метод yii\db\ActiveQuery::onCondition() следующим образом:

// SELECT `customer`.* FROM `customer`
// LEFT JOIN `order` ON `order`.`customer_id` = `customer`.`id` AND `order`.`status` = 1 
// 
// SELECT * FROM `order` WHERE `customer_id` IN (...)
$customers = Customer::find()->joinWith([
    'orders' => function ($query) {
        $query->onCondition(['order.status' => Order::STATUS_ACTIVE]);
    },
])->all();

Этот запрос возвращает всех клиентов и для каждого клиента возвращает все активные заказы. Обратите внимание, что это отличается от нашего предыдущего примера, который возвращал только клиентов, у которых есть хотя бы один активный заказ.

Информация: Когда yii\db\ActiveQuery задан с условием через onCondition(), условие будет помещено в часть ON , если запрос включает объединение. Если запрос не включает объединение, условие on будет автоматически добавлено к части WHERE запроса. Таким образом, он может содержать только условия, включающие столбцы связанной таблицы.

Псевдонимы таблиц отношений

Как отмечалось ранее, при использовании JOIN в запросе нам необходимо различать имена столбцов. Поэтому часто для таблицы определяется псевдоним. Установка псевдонима для запроса отношения была бы возможна путем настройки запроса отношения следующим образом:

$query->joinWith([
    'orders' => function ($q) {
        $q->from(['o' => Order::tableName()]);
    },
])

Однако это выглядит очень сложно и предполагает либо жёсткое кодирование имени таблицы связанных объектов, либо вызов Order::tableName(). С версии 2.0.7 Yii предоставляет ярлык для этого. Теперь вы можете определить и использовать псевдоним для таблицы отношения следующим образом:

// join the orders relation and sort the result by orders.id
$query->joinWith(['orders o'])->orderBy('o.id');

Вышеупомянутый синтаксис работает для простых отношений. Если вам нужен псевдоним для промежуточной таблицы при объединении по вложенным отношениям, например, $query->joinWith(['orders.product']), вам нужно вложить вызовы joinWith, как показано в следующем примере:

$query->joinWith(['orders o' => function($q) {
        $q->joinWith('product p');
    }])
    ->where('o.amount > 100');

Обратные отношения

Объявления отношений часто являются взаимными между двумя классами Active Record. Например, Customer относится к Order через отношение orders, а Order относится обратно к Customer через отношение customer.

class Customer extends ActiveRecord
{
    public function getOrders()
    {
        return $this->hasMany(Order::className(), ['customer_id' => 'id']);
    }
}

class Order extends ActiveRecord
{
    public function getCustomer()
    {
        return $this->hasOne(Customer::className(), ['id' => 'customer_id']);
    }
}

Теперь рассмотрим следующий фрагмент кода:

// SELECT * FROM `customer` WHERE `id` = 123
$customer = Customer::findOne(123);

// SELECT * FROM `order` WHERE `customer_id` = 123
$order = $customer->orders[0];

// SELECT * FROM `customer` WHERE `id` = 123
$customer2 = $order->customer;

// displays "not the same"
echo $customer2 === $customer ? 'same' : 'not the same';

Мы бы подумали, что $customer и $customer2 одинаковы, но это не так! На самом деле они содержат одинаковые данные о клиенте, но это разные объекты. При обращении к $order->customer, выполняется дополнительное SQL-запрос для заполнения нового объекта $customer2.

Чтобы избежать избыточного выполнения последнего SQL-запроса в приведенном выше примере, мы должны сказать Yii, что customer является обратным отношением для orders путем вызова метода inverseOf(), как показано ниже:

class Customer extends ActiveRecord
{
    public function getOrders()
    {
        return $this->hasMany(Order::className(), ['customer_id' => 'id'])->inverseOf('customer');
    }
}

С этим изменённым объявлением отношения, у нас будет:

// SELECT * FROM `customer` WHERE `id` = 123
$customer = Customer::findOne(123);

// SELECT * FROM `order` WHERE `customer_id` = 123
$order = $customer->orders[0];

// No SQL will be executed
$customer2 = $order->customer;

// displays "same"
echo $customer2 === $customer ? 'same' : 'not the same';

Примечание: Обратные отношения не могут быть определены для отношений, включающих таблицу соединений. То есть, если отношение определено с помощью via() или viaTable(), вы не должны вызывать inverseOf() далее.

Сохранение отношений

При работе с реляционными данными часто необходимо установить отношения между различными данными или разрушить существующие отношения. Это требует установки соответствующих значений для столбцов, определяющих отношения. Используя Active Record, вы можете в конечном итоге написать код, подобный следующему:

$customer = Customer::findOne(123);
$order = new Order();
$order->subtotal = 100;
// ...

// setting the attribute that defines the "customer" relation in Order
$order->customer_id = $customer->id;
$order->save();

Active Record предоставляет метод link(), который позволяет вам выполнить эту задачу более элегантно:

$customer = Customer::findOne(123);
$order = new Order();
$order->subtotal = 100;
// ...

$order->link('customer', $customer);

Метод link() требует указать имя отношения и целевой экземпляр Active Record, с которым должно быть установлено отношение. Метод изменит значения атрибутов, связывающих два экземпляра Active Record, и сохранит их в базе данных. В приведенном выше примере он установит атрибут customer_id экземпляра Order в значение атрибута id экземпляра Customer и затем сохранит его в базе данных.

Примечание: Вы не можете связать два только что созданных экземпляра Active Record.

Преимущество использования link() становится еще более очевидным, когда отношение определено через таблицу соединения. Например, вы можете использовать следующий код для связывания экземпляра Order с экземпляром Item:

$order->link('items', $item);

Вышеприведенный код автоматически вставит строку в таблицу соединения order_item для связи заказа с элементом.

Информация: Метод link() НЕ будет выполнять никакую валидацию данных при сохранении затронутого экземпляра Active Record. Вам необходимо валидировать любые входные данные перед вызовом этого метода.

Обратной операцией к link() является unlink(), которая разрывает существующее отношение между двумя экземплярами Active Record. Например,

$customer = Customer::find()->with('orders')->where(['id' => 123])->one();
$customer->unlink('orders', $customer->orders[0]);

По умолчанию метод unlink() установит значение(я) внешнего ключа, определяющие существующее отношение, в null. Однако вы можете выбрать удаление строки таблицы, содержащей значение внешнего ключа, передав параметр $delete как true в метод.

Если в отношении участвует таблица соединения, вызов unlink() приведет к очистке внешних ключей в таблице соединения или к удалению соответствующей строки в таблице соединения, если $delete равно true.

Межбазовые отношения

Active Record позволяет объявлять отношения между классами Active Record, которые работают с различными базами данных. Базы данных могут быть разных типов (например, MySQL и PostgreSQL или MS SQL и MongoDB), и они могут работать на разных серверах. Вы можете использовать тот же синтаксис для выполнения реляционных запросов. Например,

// Customer is associated with the "customer" table in a relational database (e.g. MySQL)
class Customer extends \yii\db\ActiveRecord
{
    public static function tableName()
    {
        return 'customer';
    }

    public function getComments()
    {
        // a customer has many comments
        return $this->hasMany(Comment::className(), ['customer_id' => 'id']);
    }
}

// Comment is associated with the "comment" collection in a MongoDB database
class Comment extends \yii\mongodb\ActiveRecord
{
    public static function collectionName()
    {
        return 'comment';
    }

    public function getCustomer()
    {
        // a comment has one customer
        return $this->hasOne(Customer::className(), ['id' => 'customer_id']);
    }
}

$customers = Customer::find()->with('comments')->all();

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

Примечание: Использование joinWith() ограничено базами данных, которые позволяют выполнять запросы JOIN между базами данных. По этой причине вы не можете использовать этот метод в приведенном выше примере, поскольку MongoDB не поддерживает JOIN.

Настройка классов запросов

По умолчанию все запросы Active Record поддерживаются yii\db\ActiveQuery. Чтобы использовать настраиваемый класс запросов в классе Active Record, вы должны переопределить метод yii\db\ActiveRecord::find() и вернуть экземпляр своего настраиваемого класса запроса. Например,

// file Comment.php
namespace app\models;

use yii\db\ActiveRecord;

class Comment extends ActiveRecord
{
    public static function find()
    {
        return new CommentQuery(get_called_class());
    }
}

Теперь всякий раз, когда вы выполняете запрос (например, find(), findOne()) или определяете отношение (например, hasOne()) с Comment, вы будете вызывать экземпляр CommentQuery вместо ActiveQuery.

Теперь вам нужно определить класс CommentQuery, который можно настраивать множеством творческих способов для улучшения вашего опыта построения запросов. Например,

// file CommentQuery.php
namespace app\models;

use yii\db\ActiveQuery;

class CommentQuery extends ActiveQuery
{
    // conditions appended by default (can be skipped)
    public function init()
    {
        $this->andOnCondition(['deleted' => false]);
        parent::init();
    }

    // ... add customized query methods here ...

    public function active($state = true)
    {
        return $this->andOnCondition(['active' => $state]);
    }
}

Примечание: Вместо вызова onCondition(), обычно следует вызывать andOnCondition() или orOnCondition(), чтобы добавить дополнительные условия при определении новых методов построения запросов, чтобы любые существующие условия не перезаписывались.

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

$comments = Comment::find()->active()->all();
$inactiveComments = Comment::find()->active(false)->all();

Подсказка: В больших проектах рекомендуется использовать настраиваемые классы запросов для хранения большей части кода, связанного с запросами, чтобы классы Active Record можно было содержать чистыми.

Вы также можете использовать новые методы построения запросов при определении отношений для Comment или при выполнении реляционного запроса:

class Customer extends \yii\db\ActiveRecord
{
    public function getActiveComments()
    {
        return $this->hasMany(Comment::className(), ['customer_id' => 'id'])->active();
    }
}

$customers = Customer::find()->joinWith('activeComments')->all();

// or alternatively
class Customer extends \yii\db\ActiveRecord
{
    public function getComments()
    {
        return $this->hasMany(Comment::className(), ['customer_id' => 'id']);
    }
}

$customers = Customer::find()->joinWith([
    'comments' => function($q) {
        $q->active();
    }
])->all();

Информация: В Yii 1.1 существует понятие scope. Scope больше не поддерживается напрямую в Yii 2.0, и вы должны использовать настраиваемые классы запросов и методы запросов для достижения той же цели.

Выбор дополнительных полей

Когда экземпляр Active Record заполняется результатами запроса, его атрибуты заполняются соответствующими значениями столбцов из полученного набора данных.

Вы можете извлечь дополнительные столбцы или значения из запроса и сохранить их внутри Active Record. Например, предположим, что у нас есть таблица под названием room, которая содержит информацию о доступных номерах в отеле. Каждый номер хранит информацию о его геометрических размерах, используя поля length, width, height. Представьте, что нам нужно получить список всех доступных номеров с их объёмом в порядке убывания. Так вы не можете рассчитать объём с помощью PHP, потому что нам нужно отсортировать записи по его значению, но вы также хотите, чтобы volume отображалось в списке. Для достижения цели вам нужно объявить дополнительное поле в вашем классе Active Record Room, которое будет хранить значение volume:

class Room extends \yii\db\ActiveRecord
{
    public $volume;

    // ...
}

Затем вам нужно составить запрос, который вычисляет объем номера и выполняет сортировку:

$rooms = Room::find()
    ->select([
        '{{room}}.*', // select all columns
        '([[length]] * [[width]] * [[height]]) AS volume', // calculate a volume
    ])
    ->orderBy('volume DESC') // apply sort
    ->all();

foreach ($rooms as $room) {
    echo $room->volume; // contains value calculated by SQL
}

Возможность выбора дополнительных полей может быть очень полезна для агрегирующих запросов. Предположим, вам нужно отобразить список клиентов с количеством заказов, которые они сделали. Прежде всего, вам нужно объявить класс Customer с отношением orders и дополнительным полем для хранения счётчика:

class Customer extends \yii\db\ActiveRecord
{
    public $ordersCount;

    // ...

    public function getOrders()
    {
        return $this->hasMany(Order::className(), ['customer_id' => 'id']);
    }
}

Затем вы можете составить запрос, который объединит заказы и вычислит их количество:

$customers = Customer::find()
    ->select([
        '{{customer}}.*', // select all customer fields
        'COUNT({{order}}.id) AS ordersCount' // calculate orders count
    ])
    ->joinWith('orders') // ensure table junction
    ->groupBy('{{customer}}.id') // group the result to ensure aggregation function works
    ->all();

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

$room = new Room();
$room->length = 100;
$room->width = 50;
$room->height = 2;

$room->volume; // this value will be `null`, since it was not declared yet

Используя магические методы __get() и __set(), мы можем эмулировать поведение свойства:

class Room extends \yii\db\ActiveRecord
{
    private $_volume;
    
    public function setVolume($volume)
    {
        $this->_volume = (float) $volume;
    }
    
    public function getVolume()
    {
        if (empty($this->length) || empty($this->width) || empty($this->height)) {
            return null;
        }
        
        if ($this->_volume === null) {
            $this->setVolume(
                $this->length * $this->width * $this->height
            );
        }
        
        return $this->_volume;
    }

    // ...
}

Когда запрос select не предоставляет объём, модель сможет вычислить его автоматически, используя атрибуты модели.

Вы также можете вычислить агрегированные поля, используя определённые отношения:

class Customer extends \yii\db\ActiveRecord
{
    private $_ordersCount;

    public function setOrdersCount($count)
    {
        $this->_ordersCount = (int) $count;
    }

    public function getOrdersCount()
    {
        if ($this->isNewRecord) {
            return null; // this avoid calling a query searching for null primary keys
        }

        if ($this->_ordersCount === null) {
            $this->setOrdersCount($this->getOrders()->count()); // calculate aggregation on demand from relation
        }

        return $this->_ordersCount;
    }

    // ...

    public function getOrders()
    {
        return $this->hasMany(Order::className(), ['customer_id' => 'id']);
    }
}

С этим кодом, в случае, если «ordersCount» присутствует в операторе «select», Customer::ordersCount будет заполнен результатами запроса, в противном случае он будет вычислен по требованию, используя отношение Customer::orders.

Этот подход также можно использовать для создания сокращений для некоторых реляционных данных, особенно для агрегации. Например:

class Customer extends \yii\db\ActiveRecord
{
    /**
     * Defines read-only virtual property for aggregation data.
     */
    public function getOrdersCount()
    {
        if ($this->isNewRecord) {
            return null; // this avoid calling a query searching for null primary keys
        }
        
        return empty($this->ordersAggregation) ? 0 : $this->ordersAggregation[0]['counted'];
    }

    /**
     * Declares normal 'orders' relation.
     */
    public function getOrders()
    {
        return $this->hasMany(Order::className(), ['customer_id' => 'id']);
    }

    /**
     * Declares new relation based on 'orders', which provides aggregation.
     */
    public function getOrdersAggregation()
    {
        return $this->getOrders()
            ->select(['customer_id', 'counted' => 'count(*)'])
            ->groupBy('customer_id')
            ->asArray(true);
    }

    // ...
}

foreach (Customer::find()->with('ordersAggregation')->all() as $customer) {
    echo $customer->ordersCount; // outputs aggregation data from relation without extra query due to eager loading
}

$customer = Customer::findOne($pk);
$customer->ordersCount; // output aggregation data from lazy loaded relation

© 2008–2017 by Yii Software LLC
Licensed under the three clause BSD license.
http://www.yiiframework.com/doc-2.0/guide-db-active-record.html

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API