Skip to content

Commit

Permalink
New methods: BaseActiveRecord::loadRelations() and BaseActiveRecord::…
Browse files Browse the repository at this point in the history
…loadRelationsFor().
  • Loading branch information
PowerGamer1 committed Jul 14, 2023
1 parent 4352b87 commit 7980720
Show file tree
Hide file tree
Showing 3 changed files with 100 additions and 0 deletions.
1 change: 1 addition & 0 deletions framework/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ Yii Framework 2 Change Log
2.0.49 under development
------------------------

- Enh #12743: Added new methods `BaseActiveRecord::loadRelations()` and `BaseActiveRecord::loadRelationsFor()` to eager load related models for existing primary model instances (PowerGamer1)
- Bug #19857: Fix AttributeTypecastBehavior::resetOldAttributes() causes "class has no attribute named" InvalidArgumentException (uaoleg)
- Bug #18859: Fix `yii\web\Controller::bindInjectedParams()` to not throw error when argument of `ReflectionUnionType` type is passed (bizley)
- Enh #19841: Allow jQuery 3.7 to be installed (wouter90)
Expand Down
53 changes: 53 additions & 0 deletions framework/db/BaseActiveRecord.php
Original file line number Diff line number Diff line change
Expand Up @@ -1781,4 +1781,57 @@ private function isValueDifferent($newValue, $oldValue)

return $newValue !== $oldValue;
}

/**
* Eager loads related models for the already loaded primary models.
*
* Helps to reduce the number of queries performed against database if some related models are only used
* when a specific condition is met. For example:
*
* ```php
* $customers = Customer::find()->where(['country_id' => 123])->all();
* if (Yii:app()->getUser()->getIdentity()->canAccessOrders()) {
* Customer::loadRelationsFor($customers, 'orders.items');
* }
* ```
*
* @param array|ActiveRecordInterface[] $models array of primary models. Each model should have the same type and can be:
* - an active record instance;
* - active record instance represented by array (i.e. active record was loaded using [[ActiveQuery::asArray()]]).
* @param string|array $relationNames the names of the relations of primary models to be loaded from database. See [[ActiveQueryInterface::with()]] on how to specify this argument.
* @param bool $asArray whether to load each related model as an array or an object (if the relation itself does not specify that).
* @since 2.0.49
*/
public static function loadRelationsFor(&$models, $relationNames, $asArray = false)
{
// ActiveQueryTrait::findWith() called below assumes $models array is non-empty.
if (empty($models)) {
return;
}

static::find()->asArray($asArray)->findWith((array)$relationNames, $models);
}

/**
* Eager loads related models for the already loaded primary model.
*
* Helps to reduce the number of queries performed against database if some related models are only used
* when a specific condition is met. For example:
*
* ```php
* $customer = Customer::find()->where(['id' => 123])->one();
* if (Yii:app()->getUser()->getIdentity()->canAccessOrders()) {
* $customer->loadRelations('orders.items');
* }
* ```
*
* @param string|array $relationNames the names of the relations of this model to be loaded from database. See [[ActiveQueryInterface::with()]] on how to specify this argument.
* @param bool $asArray whether to load each relation as an array or an object (if the relation itself does not specify that).
* @since 2.0.49
*/
public function loadRelations($relationNames, $asArray = false)
{
$models = [$this];
static::loadRelationsFor($models, $relationNames, $asArray);
}
}
46 changes: 46 additions & 0 deletions tests/framework/db/ActiveRecordTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2192,4 +2192,50 @@ public function testVirtualRelation()
$this->assertNotNull($order->virtualCustomer);
}

public function testLoadRelations()
{
// Test eager loading relations for multiple primary models using loadRelationsFor().
/** @var Customer[] $customers */
$customers = Customer::find()->all();
Customer::loadRelationsFor($customers, ['orders.items']);
foreach ($customers as $customer) {
$this->assertTrue($customer->isRelationPopulated('orders'));
foreach ($customer->orders as $order) {
$this->assertTrue($order->isRelationPopulated('items'));
}
}

// Test eager loading relations as arrays.
/** @var array $customers */
$customers = Customer::find()->asArray(true)->all();
Customer::loadRelationsFor($customers, ['orders.items' => function ($query) { $query->asArray(false); }], true);
foreach ($customers as $customer) {
$this->assertTrue(isset($customer['orders']));
$this->assertTrue(is_array($customer['orders']));
foreach ($customer['orders'] as $order) {
$this->assertTrue(is_array($order));
$this->assertTrue(isset($order['items']));
$this->assertTrue(is_array($order['items']));
foreach ($order['items'] as $item) {
$this->assertFalse(is_array($item));
}
}
}

// Test eager loading relations for a single primary model using loadRelations().
/** @var Customer $customer */
$customer = Customer::find()->where(['id' => 1])->one();
$customer->loadRelations('orders.items');
$this->assertTrue($customer->isRelationPopulated('orders'));
foreach ($customer->orders as $order) {
$this->assertTrue($order->isRelationPopulated('items'));
}

// Test eager loading previously loaded relation (relation value should be replaced with a new value loaded from database).
/** @var Customer $customer */
$customer = Customer::find()->where(['id' => 2])->with(['orders' => function ($query) { $query->orderBy(['id' => SORT_ASC]); }])->one();
$this->assertTrue($customer->orders[0]->id < $customer->orders[1]->id, 'Related models should be sorted by ID in ascending order.');
$customer->loadRelations(['orders' => function ($query) { $query->orderBy(['id' => SORT_DESC]); }]);
$this->assertTrue($customer->orders[0]->id > $customer->orders[1]->id, 'Related models should be sorted by ID in descending order.');
}
}

0 comments on commit 7980720

Please sign in to comment.