В этой статье я решил предоставить как можно больше примеров, т.к. здесь они гораздно полезнее рассуждений.
Однако, если что-то останется непонятным, то полную документацию по работе с ActiveRecord в Yii2 всегда можно найти по ссылке:
https://github.com/yiisoft/yii2/blob/master/docs/guide-ru/db-active-record.md
Это ссылка на документацию, которая постоянно обновляется и дополняется.
Извлечение данных
// возвращает покупателя с идентификатором 123 // SELECT * FROM `customer` WHERE `id` = 123 $customer = Customer::find() ->where(['id' => 123]) ->one(); // возвращает всех активных покупателей, сортируя их по идентификаторам // SELECT * FROM `customer` WHERE `status` = 1 ORDER BY `id` $customers = Customer::find() ->where(['status' => Customer::STATUS_ACTIVE]) ->orderBy('id') ->all(); // возвращает количество активных покупателей // SELECT COUNT(*) FROM `customer` WHERE `status` = 1 $count = Customer::find() ->where(['status' => Customer::STATUS_ACTIVE]) ->count(); // возвращает всех покупателей массивом, индексированным их идентификаторами // SELECT * FROM `customer` $customers = Customer::find() ->indexBy('id') ->all();
Шорткаты для методов, которые сразу возвращают данные:
// возвращает покупателя с идентификатором 123 // SELECT * FROM `customer` WHERE `id` = 123 $customer = Customer::findOne(123); // возвращает покупателей с идентификаторами 100, 101, 123 и 124 // SELECT * FROM `customer` WHERE `id` IN (100, 101, 123, 124) $customers = Customer::findAll([100, 101, 123, 124]); // возвращает активного покупателя с идентификатором 123 // SELECT * FROM `customer` WHERE `id` = 123 AND `status` = 1 $customer = Customer::findOne([ 'id' => 123, 'status' => Customer::STATUS_ACTIVE, ]); // возвращает всех неактивных покупателей // SELECT * FROM `customer` WHERE `status` = 0 $customers = Customer::findAll([ 'status' => Customer::STATUS_INACTIVE, ]);
Note: Ни метод [[yii\db\ActiveRecord::findOne()]], ни [[yii\db\ActiveQuery::one()]] не добавляет условие LIMIT 1 к генерируемым SQL-запросам. Если ваш запрос может вернуть много строк данных, вы должны вызвать метод limit(1) явно в целях улучшения производительности, например: Customer::find()->limit(1)->one().
Пример на голом SQL
// возвращает всех неактивных покупателей $sql = 'SELECT * FROM customer WHERE status=:status'; $customers = Customer::findBySql($sql, [ ':status' => Customer::STATUS_INACTIVE ])->all();
Можно получить данные в виде массива:
// возвращает всех покупателей // каждый покупатель будет представлен в виде ассоциативного массива $customers = Customer::find() ->asArray() ->all();
Пример пакетной выборки для снижения потребления ресурсов:
// получить 10 покупателей одновременно foreach (Customer::find()->batch(10) as $customers) { // $customers - это массив, в котором находится 10 // или меньше объектов класса Customer } // получить одновременно десять покупателей и перебрать их одного за другим foreach (Customer::find()->each(10) as $customer) { // $customer - это объект класса Customer } // пакетная выборка с жадной загрузкой foreach (Customer::find()->with('orders')->each() as $customer) { // $customer - это объекта класса Customer }
Обновление счетчика. Метод save() здесь не особо пригоден из-за параллельных запросов.
$post = Post::findOne(100); // UPDATE `post` SET `view_count` = `view_count` + 1 WHERE `id` = 100 $post->updateCounters(['view_count' => 1]);
ActiveRecord не получает автоматически данные по умолчанию из таблиц. Но их можно загрузить:
$customer = new Customer(); $customer->loadDefaultValues(); // $customer->xyz получит значение по умолчанию, // которое было указано при объявлении столбца "xyz"
Обновление данных
Обновление нескольких строк в таблице:
// UPDATE `customer` SET `status` = 1 WHERE `email` LIKE `%@example.com%` Customer::updateAll( ['status' => Customer::STATUS_ACTIVE], ['like', 'email', '@example.com'] );
Обновление счетчиков:
// UPDATE `customer` SET `age` = `age` + 1 Customer::updateAllCounters(['age' => 1]);
Удаление данных
$customer = Customer::findOne(123); $customer->delete();
Удаление сразу многих записей. Осторожно: немного напортачишь с условиями и удалятся все данные.
Customer::deleteAll(['status' => Customer::STATUS_INACTIVE]);
Работа с транзакциями
$customer = Customer::findOne(123); Customer::getDb()->transaction(function($db) use ($customer) { $customer->id = 200; $customer->save(); // ...другие операции с базой данных... }); // или по-другому $transaction = Customer::getDb()->beginTransaction(); try { $customer->id = 200; $customer->save(); // ...другие операции с базой данных... $transaction->commit(); } catch(\Exception $e) { $transaction->rollBack(); throw $e; } catch(\Throwable $e) { $transaction->rollBack(); throw $e; }
Так можно указать в ActiveRecord, какие именно операции требуют транзакционного выполнения:
class Customer extends ActiveRecord { public function transactions() { return [ 'admin' => self::OP_INSERT, 'api' => self::OP_INSERT | self::OP_UPDATE | self::OP_DELETE, // вышеприведённая строка эквивалентна следующей: // 'api' => self::OP_ALL, ]; } }
Связи
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 // $orders - это массив объектов Order $orders = $customer->orders;
Можно получить и объект запроса:
$customer->orders; // массив объектов `Order` $customer->getOrders(); // объект 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();
Связи также могут быть и динамическими:
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;
Объявление связи многие ко многим.
С указанием названия связующей таблицы:
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'); } }
Отложенная и жадная загрузка (with)
Отложенная:
// SELECT * FROM `customer` LIMIT 100 $customers = Customer::find()->limit(100)->all(); foreach ($customers as $customer) { // SELECT * FROM `order` WHERE `customer_id` = ... $orders = $customer->orders; }
Тут запрос на извлечение товаров будет выполнен 100 раз.
Жадная:
// SELECT * FROM `customer` LIMIT 100; // SELECT * FROM `orders` WHERE `customer_id` IN (...) $customers = Customer::find() ->with('orders') ->limit(100) ->all(); foreach ($customers as $customer) { // SQL-запрос не выполняется $orders = $customer->orders; }
Тут сначала извлекутся клиенты, а следующим запросом список заказов. Итого 2 запроса.
Разные способы выполнение жадной загрузки:
// жадная загрузка "orders" и "country" одновременно $customers = Customer::find()->with('orders', 'country')->all(); // аналог с использованием синтаксиса массива $customers = Customer::find()->with(['orders', 'country'])->all(); // SQL-запрос не выполняется $orders= $customers[0]->orders; // SQL-запрос не выполняется $country = $customers[0]->country; // жадная загрузка связи "orders" и вложенной связи "orders.items" $customers = Customer::find()->with('orders.items')->all(); // доступ к деталям первого заказа первого покупателя // SQL-запрос не выполняется $items = $customers[0]->orders[0]->items;
При жадной загрузке можно настроить запрос при помощи анонимной функции:
// найти покупателей и получить их вместе с их странами и активными заказами // 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();
Если используется select то для жадной загрузки нужно перечислять поля участвующие в связи:
$orders = Order::find() ->select(['id', 'amount']) ->with('customer') ->all(); // $orders[0]->customer всегда равно null. // Для исправления проблемы вы должны сделать следующее: $orders = Order::find() ->select(['id', 'amount', 'customer_id']) ->with('customer') ->all();
Использование JOIN со связями (with, joinWith)
Иногда нужно ссылаться ссылаться на столбцы связанных таблиц.
Так можно получить клиентов, имеющих активные заказы:
// 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();
Но если у нас есть связь orders, то нет необходимости указывать ее вручную, а можно сделать так:
$customers = Customer::find() ->joinWith('orders') ->where(['order.status' => Order::STATUS_ACTIVE]) ->all();
В обоих случаях получается один и тот же sql запрос.
Во втором параметре joinWith можно передать тип join. По умолчанию используется LEFT.
Если не нужно жадно загружать данные, вторым параметром в joinWith нужно передать false.
joinWith можно смешивать с with. Например:
$customers = Customer::find()->joinWith([ 'orders' => function ($query) { $query->andWhere(['>', 'subtotal', 100]); }, ])->with('country')->all();
Для join запроса, можно указать условие, которое будет добавлено в on:
// 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();
Вышеприведённый запрос вернёт всех покупателей и для каждого покупателя вернёт все активные заказы. Заметьте, что это поведение отличается от нашего предыдущего примера, в котором возвращались только покупатели, у которых был как минимум один активный заказ.
При использовании JOIN нужно явно решать конфликты имен. Например:
$query->joinWith([ 'orders' => function ($q) { $q->from(['o' => Order::tableName()]); }, ])
Более простой способ задать псевдоним
$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');
Есть способ задать обратную связь:
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]; // SELECT * FROM `customer` WHERE `id` = 123 $customer2 = $order->customer; // выведет "not the same" echo $customer2 === $customer ? 'same' : 'not the same';
Сохранение связанных данных.
Вместо:
$order->customer_id = $customer->id; $order->save();
Можно писать:
$order->link('customer', $customer);
Преимущество более очевидно, когда используется связующая таблица. Этот метод автоматически создаст запись в связующей таблице.
У ->link есть противоположная операция unlink:
$customer = Customer::find()->with('orders')->where(['id' => 123])->one(); $customer->unlink('orders', $customer->orders[0]);
По умолчанию метод [[yii\db\ActiveRecord::unlink()|unlink()]] задаст вторичному ключу (или ключам), который определяет существующую связь, значение null. Однако вы можете запросить удаление строки таблицы, которая содержит значение вторичного ключа, передав значение true в параметре $delete для этого метода.
Класс Query можно переопределять, для более тонкой настройки.
namespace app\models; use yii\db\ActiveRecord; use yii\db\ActiveQuery; class Comment extends ActiveRecord { public static function find() { return new CommentQuery(get_called_class()); } } class CommentQuery extends ActiveQuery { // ... }
Например в своем классе запросов, можно объявить вспомогательные методы:
class CommentQuery extends ActiveQuery { public function active($state = true) { return $this->andWhere(['active' => $state]); } }