Skip to content

Commit

Permalink
Fix for yiisoft#7316: inverseOf should be ignored for "multiple" rela…
Browse files Browse the repository at this point in the history
…tion ("many" in 1-to-many or many-to-many).
  • Loading branch information
dmitry-kulikov committed Sep 29, 2017
1 parent 0a7dbe3 commit 6db7690
Show file tree
Hide file tree
Showing 2 changed files with 50 additions and 65 deletions.
67 changes: 29 additions & 38 deletions framework/db/ActiveRelationTrait.php
Expand Up @@ -114,11 +114,14 @@ public function via($relationName, callable $callable = null)

/**
* Sets the name of the relation that is the inverse of this relation.
* For example, an order has a customer, which means the inverse of the "customer" relation
* is the "orders", and the inverse of the "orders" relation is the "customer".
* If this property is set, the primary record(s) will be referenced through the specified relation.
* For example, a customer has orders, which means the inverse of the "orders" relation
* is the "customer". Note that "customer" relation has not inverse relation "orders",
* because one order model in general case cannot represent all models in "orders".
* If this property is set, the primary record will be referenced through the specified relation.
* For example, `$customer->orders[0]->customer` and `$customer` will be the same object,
* and accessing the customer of an order will not trigger a new DB query.
* But in `$order->customer->orders` accessing the orders of a customer will trigger a new DB query,
* because `$order` may not represent all orders.
*
* Use this method when declaring a relation in the [[ActiveRecord]] class:
*
Expand Down Expand Up @@ -175,14 +178,20 @@ private function addInverseRelations(&$result)
if (!isset($inverseRelation)) {
$inverseRelation = $relatedModel->getRelation($this->inverseOf);
}
$relatedModel->populateRelation($this->inverseOf, $inverseRelation->multiple ? [$this->primaryModel] : $this->primaryModel);
if ($inverseRelation->multiple) {
return;
}
$relatedModel->populateRelation($this->inverseOf, $this->primaryModel);
} else {
if (!isset($inverseRelation)) {
/* @var $modelClass ActiveRecordInterface */
$modelClass = $this->modelClass;
$inverseRelation = $modelClass::instance()->getRelation($this->inverseOf);
}
$result[$i][$this->inverseOf] = $inverseRelation->multiple ? [$this->primaryModel] : $this->primaryModel;
if ($inverseRelation->multiple) {
return;
}
$result[$i][$this->inverseOf] = $this->primaryModel;
}
}
}
Expand Down Expand Up @@ -309,43 +318,25 @@ private function populateInverseRelation(&$primaryModels, $models, $primaryName,
}

if ($relation->multiple) {
$buckets = $this->buildBuckets($primaryModels, $relation->link, null, null, false);
if ($model instanceof ActiveRecordInterface) {
foreach ($models as $model) {
$key = $this->getModelKey($model, $relation->link);
$model->populateRelation($name, isset($buckets[$key]) ? $buckets[$key] : []);
}
} else {
foreach ($primaryModels as $i => $primaryModel) {
if ($this->multiple) {
foreach ($primaryModel as $j => $m) {
$key = $this->getModelKey($m, $relation->link);
$primaryModels[$i][$j][$name] = isset($buckets[$key]) ? $buckets[$key] : [];
}
} elseif (!empty($primaryModel[$primaryName])) {
$key = $this->getModelKey($primaryModel[$primaryName], $relation->link);
$primaryModels[$i][$primaryName][$name] = isset($buckets[$key]) ? $buckets[$key] : [];
return;
}

if ($this->multiple) {
foreach ($primaryModels as $i => $primaryModel) {
foreach ($primaryModel[$primaryName] as $j => $m) {
if ($m instanceof ActiveRecordInterface) {
$m->populateRelation($name, $primaryModel);
} else {
$primaryModels[$i][$primaryName][$j][$name] = $primaryModel;
}
}
}
} else {
if ($this->multiple) {
foreach ($primaryModels as $i => $primaryModel) {
foreach ($primaryModel[$primaryName] as $j => $m) {
if ($m instanceof ActiveRecordInterface) {
$m->populateRelation($name, $primaryModel);
} else {
$primaryModels[$i][$primaryName][$j][$name] = $primaryModel;
}
}
}
} else {
foreach ($primaryModels as $i => $primaryModel) {
if ($primaryModels[$i][$primaryName] instanceof ActiveRecordInterface) {
$primaryModels[$i][$primaryName]->populateRelation($name, $primaryModel);
} elseif (!empty($primaryModels[$i][$primaryName])) {
$primaryModels[$i][$primaryName][$name] = $primaryModel;
}
foreach ($primaryModels as $i => $primaryModel) {
if ($primaryModels[$i][$primaryName] instanceof ActiveRecordInterface) {
$primaryModels[$i][$primaryName]->populateRelation($name, $primaryModel);
} elseif (!empty($primaryModels[$i][$primaryName])) {
$primaryModels[$i][$primaryName][$name] = $primaryModel;
}
}
}
Expand Down
48 changes: 21 additions & 27 deletions tests/framework/db/ActiveRecordTest.php
Expand Up @@ -985,12 +985,20 @@ public function testAlias()

public function testInverseOf()
{
// inverseOf should work for non-multiple relation

// eager loading: find one and all
$customer = Customer::find()->with('orders2')->where(['id' => 1])->one();
$this->assertSame($customer->orders2[0]->customer2, $customer);
$customers = Customer::find()->with('orders2')->where(['id' => [1, 3]])->all();
$this->assertSame($customers[0]->orders2[0]->customer2, $customers[0]);
$this->assertEmpty($customers[1]->orders2);
// eager loading as array: find one and all
$customer = Customer::find()->with('orders2')->where(['id' => 1])->asArray()->one();
$this->assertSame($customer['orders2'][0]['customer2']['id'], $customer['id']);
$customers = Customer::find()->with('orders2')->where(['id' => [1, 3]])->asArray()->all();
$this->assertSame($customer['orders2'][0]['customer2']['id'], $customers[0]['id']);
$this->assertEmpty($customers[1]['orders2']);
// lazy loading
$customer = Customer::findOne(2);
$orders = $customer->orders2;
Expand All @@ -1006,36 +1014,22 @@ public function testInverseOf()
$this->assertSame($orders[0]->customer2, $customer);
$this->assertSame($orders[1]->customer2, $customer);

// the other way around
$customer = Customer::find()->with('orders2')->where(['id' => 1])->asArray()->one();
$this->assertSame($customer['orders2'][0]['customer2']['id'], $customer['id']);
$customers = Customer::find()->with('orders2')->where(['id' => [1, 3]])->asArray()->all();
$this->assertSame($customer['orders2'][0]['customer2']['id'], $customers[0]['id']);
$this->assertEmpty($customers[1]['orders2']);
// inverseOf should be ignored for multiple relation

$orders = Order::find()->with('customer2')->where(['id' => 1])->all();
$this->assertSame($orders[0]->customer2->orders2, [$orders[0]]);
// eager loading: find one and all
$order = Order::find()->with('customer2')->where(['id' => 1])->one();
$this->assertSame($order->customer2->orders2, [$order]);

$orders = Order::find()->with('customer2')->where(['id' => 1])->asArray()->all();
$this->assertSame($orders[0]['customer2']['orders2'][0]['id'], $orders[0]['id']);
$this->assertFalse($order->customer2->isRelationPopulated('orders2'));
$orders = Order::find()->with('customer2')->where(['id' => 1])->all();
$this->assertFalse($orders[0]->customer2->isRelationPopulated('orders2'));
// eager loading as array: find one and all
$order = Order::find()->with('customer2')->where(['id' => 1])->asArray()->one();
$this->assertSame($order['customer2']['orders2'][0]['id'], $orders[0]['id']);

$orders = Order::find()->with('customer2')->where(['id' => [1, 3]])->all();
$this->assertSame($orders[0]->customer2->orders2, [$orders[0]]);
$this->assertSame($orders[1]->customer2->orders2, [$orders[1]]);

$orders = Order::find()->with('customer2')->where(['id' => [2, 3]])->orderBy('id')->all();
$this->assertSame($orders[0]->customer2->orders2, $orders);
$this->assertSame($orders[1]->customer2->orders2, $orders);

$orders = Order::find()->with('customer2')->where(['id' => [2, 3]])->orderBy('id')->asArray()->all();
$this->assertSame($orders[0]['customer2']['orders2'][0]['id'], $orders[0]['id']);
$this->assertSame($orders[0]['customer2']['orders2'][1]['id'], $orders[1]['id']);
$this->assertSame($orders[1]['customer2']['orders2'][0]['id'], $orders[0]['id']);
$this->assertSame($orders[1]['customer2']['orders2'][1]['id'], $orders[1]['id']);
$this->assertFalse(array_key_exists('orders2', $order['customer2']));
$orders = Order::find()->with('customer2')->where(['id' => 1])->asArray()->all();
$this->assertFalse(array_key_exists('orders2', $orders[0]['customer2']));
// lazy loading
$this->assertFalse(Order::findOne(1)->customer2->isRelationPopulated('orders2'));
// ad-hoc lazy loading
$this->assertFalse(Order::findOne(1)->getCustomer2()->one()->isRelationPopulated('orders2'));
}

public function testInverseOfDynamic()
Expand Down

0 comments on commit 6db7690

Please sign in to comment.