В этой статье я решил предоставить как можно больше примеров, т.к. здесь они гораздно полезнее рассуждений.
Однако, если что-то останется непонятным, то полную документацию по работе с 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]);
}
}