diff --git a/framework/CHANGELOG.md b/framework/CHANGELOG.md index 894b1ad2a5d..dff044e9640 100644 --- a/framework/CHANGELOG.md +++ b/framework/CHANGELOG.md @@ -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) diff --git a/framework/db/BaseActiveRecord.php b/framework/db/BaseActiveRecord.php index 88fb2f11d7c..3a67ac4100c 100644 --- a/framework/db/BaseActiveRecord.php +++ b/framework/db/BaseActiveRecord.php @@ -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); + } } diff --git a/tests/framework/db/ActiveRecordTest.php b/tests/framework/db/ActiveRecordTest.php index f1370f47282..9ddd9a0ad87 100644 --- a/tests/framework/db/ActiveRecordTest.php +++ b/tests/framework/db/ActiveRecordTest.php @@ -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.'); + } }