Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
Y
yii2
Project
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
PSDI Army
yii2
Commits
c92a260a
Commit
c92a260a
authored
Feb 18, 2014
by
Qiang Xue
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Fixes #2415: Added support for inverse relations
parent
e1b55153
Show whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
288 additions
and
12 deletions
+288
-12
active-record.md
docs/guide/active-record.md
+90
-0
CHANGELOG.md
framework/CHANGELOG.md
+1
-0
ActiveRelationTrait.php
framework/db/ActiveRelationTrait.php
+131
-2
BaseActiveRecord.php
framework/db/BaseActiveRecord.php
+3
-10
Customer.php
tests/unit/data/ar/Customer.php
+5
-0
Order.php
tests/unit/data/ar/Order.php
+5
-0
ActiveRecordTest.php
tests/unit/framework/db/ActiveRecordTest.php
+53
-0
No files found.
docs/guide/active-record.md
View file @
c92a260a
...
@@ -413,6 +413,96 @@ $customers = Customer::find()->limit(100)->with([
...
@@ -413,6 +413,96 @@ $customers = Customer::find()->limit(100)->with([
```
```
Inverse Relations
-----------------
Relations can often be defined in pairs. For example,
`Customer`
may have a relation named
`orders`
while
`Order`
may have a relation
named
`customer`
:
```
php
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'
]);
}
}
```
If we perform the following query, we would find that the
`customer`
of an order is not the same customer object
that finds those orders, and accessing
`customer->orders`
will trigger one SQL execution while accessing
the
`customer`
of an order will trigger another SQL execution:
```
php
// SELECT * FROM tbl_customer WHERE id=1
$customer
=
Customer
::
find
(
1
);
// echoes "not equal"
// SELECT * FROM tbl_order WHERE customer_id=1
// SELECT * FROM tbl_customer WHERE id=1
if
(
$customer
->
orders
[
0
]
->
customer
===
$customer
)
{
echo
'equal'
;
}
else
{
echo
'not equal'
;
}
```
To avoid the redundant execution of the last SQL statement, we could declare the inverse relations for the
`customer`
and the
`orders`
relations by calling the
`inverseOf()`
method, like the following:
```
php
class
Customer
extends
ActiveRecord
{
....
public
function
getOrders
()
{
return
$this
->
hasMany
(
Order
::
className
,
[
'customer_id'
=>
'id'
])
->
inverseOf
(
'customer'
);
}
}
```
Now if we execute the same query as shown above, we would get:
```
php
// SELECT * FROM tbl_customer WHERE id=1
$customer
=
Customer
::
find
(
1
);
// echoes "equal"
// SELECT * FROM tbl_order WHERE customer_id=1
if
(
$customer
->
orders
[
0
]
->
customer
===
$customer
)
{
echo
'equal'
;
}
else
{
echo
'not equal'
;
}
```
In the above, we have shown how to use inverse relations in lazy loading. Inverse relations also apply in
eager loading:
```
php
// SELECT * FROM tbl_customer
// SELECT * FROM tbl_order WHERE customer_id IN (1, 2, ...)
$customers
=
Customer
::
find
()
->
with
(
'orders'
)
->
all
();
// echoes "equal"
if
(
$customers
[
0
]
->
orders
[
0
]
->
customer
===
$customers
[
0
])
{
echo
'equal'
;
}
else
{
echo
'not equal'
;
}
```
> Note: Inverse relation cannot be defined with a relation that involves pivoting tables.
> That is, if your relation iso defined with `via()` or `viaTable()`, you cannot call `inverseOf()` further.
Joining with Relations
Joining with Relations
----------------------
----------------------
...
...
framework/CHANGELOG.md
View file @
c92a260a
...
@@ -115,6 +115,7 @@ Yii Framework 2 Change Log
...
@@ -115,6 +115,7 @@ Yii Framework 2 Change Log
-
Enh #2387: Added support for fetching data from database in batches (nineinchnick, qiangxue)
-
Enh #2387: Added support for fetching data from database in batches (nineinchnick, qiangxue)
-
Enh #2417: Added possibility to set
`dataType`
for
`$.ajax`
call in yii.activeForm.js (Borales)
-
Enh #2417: Added possibility to set
`dataType`
for
`$.ajax`
call in yii.activeForm.js (Borales)
-
Enh #2436: Label of the attribute, which looks like
`relatedModel.attribute`
, will be received from the related model if it available (djagya)
-
Enh #2436: Label of the attribute, which looks like
`relatedModel.attribute`
, will be received from the related model if it available (djagya)
-
Enh #2415: Added support for inverse relations (qiangxue)
-
Enh: Added support for using arrays as option values for console commands (qiangxue)
-
Enh: Added support for using arrays as option values for console commands (qiangxue)
-
Enh: Added
`favicon.ico`
and
`robots.txt`
to default application templates (samdark)
-
Enh: Added
`favicon.ico`
and
`robots.txt`
to default application templates (samdark)
-
Enh: Added
`Widget::autoIdPrefix`
to support prefixing automatically generated widget IDs (qiangxue)
-
Enh: Added
`Widget::autoIdPrefix`
to support prefixing automatically generated widget IDs (qiangxue)
...
...
framework/db/ActiveRelationTrait.php
View file @
c92a260a
...
@@ -8,6 +8,7 @@
...
@@ -8,6 +8,7 @@
namespace
yii\db
;
namespace
yii\db
;
use
yii\base\InvalidConfigException
;
use
yii\base\InvalidConfigException
;
use
yii\base\InvalidParamException
;
/**
/**
* ActiveRelationTrait implements the common methods and properties for active record relation classes.
* ActiveRelationTrait implements the common methods and properties for active record relation classes.
...
@@ -40,6 +41,15 @@ trait ActiveRelationTrait
...
@@ -40,6 +41,15 @@ trait ActiveRelationTrait
* to set this property instead of directly setting it.
* to set this property instead of directly setting it.
*/
*/
public
$via
;
public
$via
;
/**
* @var string 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, `$customer->orders[0]->customer` and `$customer` will be the same object,
* and accessing the customer of an order will not trigger new DB query.
*/
public
$inverseOf
;
/**
/**
* Clones internal objects.
* Clones internal objects.
...
@@ -72,6 +82,67 @@ trait ActiveRelationTrait
...
@@ -72,6 +82,67 @@ trait ActiveRelationTrait
}
}
/**
/**
* 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, `$customer->orders[0]->customer` and `$customer` will be the same object,
* and accessing the customer of an order will not trigger new DB query.
* @param string $relationName the name of the relation that is the inverse of this relation.
* @return static the relation object itself.
*/
public
function
inverseOf
(
$relationName
)
{
$this
->
inverseOf
=
$relationName
;
return
$this
;
}
/**
* Finds the related records for the specified primary record.
* This method is invoked when a relation of an ActiveRecord is being accessed in a lazy fashion.
* @param string $name the relation name
* @param ActiveRecordInterface $model the primary model
* @return mixed the related record(s)
* @throws InvalidParamException if the relation is invalid
*/
public
function
findFor
(
$name
,
$model
)
{
if
(
method_exists
(
$model
,
'get'
.
$name
))
{
$method
=
new
\ReflectionMethod
(
$model
,
'get'
.
$name
);
$realName
=
lcfirst
(
substr
(
$method
->
getName
(),
3
));
if
(
$realName
!==
$name
)
{
throw
new
InvalidParamException
(
'Relation names are case sensitive. '
.
get_class
(
$model
)
.
" has a relation named
\"
$realName
\"
instead of
\"
$name
\"
."
);
}
}
$related
=
$this
->
multiple
?
$this
->
all
()
:
$this
->
one
();
if
(
$this
->
inverseOf
===
null
||
empty
(
$related
))
{
return
$related
;
}
$inverseRelation
=
(
new
$this
->
modelClass
)
->
getRelation
(
$this
->
inverseOf
);
if
(
$this
->
multiple
)
{
foreach
(
$related
as
$i
=>
$relatedModel
)
{
if
(
$relatedModel
instanceof
ActiveRecordInterface
)
{
$relatedModel
->
populateRelation
(
$this
->
inverseOf
,
$inverseRelation
->
multiple
?
[
$model
]
:
$model
);
}
else
{
$related
[
$i
][
$this
->
inverseOf
]
=
$inverseRelation
->
multiple
?
[
$model
]
:
$model
;
}
}
}
else
{
if
(
$related
instanceof
ActiveRecordInterface
)
{
$related
->
populateRelation
(
$this
->
inverseOf
,
$inverseRelation
->
multiple
?
[
$model
]
:
$model
);
}
else
{
$related
[
$this
->
inverseOf
]
=
$inverseRelation
->
multiple
?
[
$model
]
:
$model
;
}
}
return
$related
;
}
/**
* Finds the related records and populates them into the primary models.
* Finds the related records and populates them into the primary models.
* @param string $name the relation name
* @param string $name the relation name
* @param array $primaryModels primary models
* @param array $primaryModels primary models
...
@@ -109,6 +180,9 @@ trait ActiveRelationTrait
...
@@ -109,6 +180,9 @@ trait ActiveRelationTrait
}
else
{
}
else
{
$primaryModels
[
$i
][
$name
]
=
$model
;
$primaryModels
[
$i
][
$name
]
=
$model
;
}
}
if
(
$this
->
inverseOf
!==
null
)
{
$this
->
populateInverseRelation
(
$primaryModels
,
[
$model
],
$name
,
$this
->
inverseOf
);
}
}
}
return
[
$model
];
return
[
$model
];
}
else
{
}
else
{
...
@@ -129,18 +203,73 @@ trait ActiveRelationTrait
...
@@ -129,18 +203,73 @@ trait ActiveRelationTrait
$primaryModels
[
$i
][
$name
]
=
$value
;
$primaryModels
[
$i
][
$name
]
=
$value
;
}
}
}
}
if
(
$this
->
inverseOf
!==
null
)
{
$this
->
populateInverseRelation
(
$primaryModels
,
$models
,
$name
,
$this
->
inverseOf
);
}
return
$models
;
return
$models
;
}
}
}
}
private
function
populateInverseRelation
(
&
$primaryModels
,
$models
,
$primaryName
,
$name
)
{
if
(
empty
(
$models
)
||
empty
(
$primaryModels
))
{
return
;
}
$model
=
reset
(
$models
);
$relation
=
$model
instanceof
ActiveRecordInterface
?
$model
->
getRelation
(
$name
)
:
(
new
$this
->
modelClass
)
->
getRelation
(
$name
);
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
]
:
[];
}
}
}
}
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
;
}
}
}
}
}
/**
/**
* @param array $models
* @param array $models
* @param array $link
* @param array $link
* @param array $viaModels
* @param array $viaModels
* @param array $viaLink
* @param array $viaLink
* @param boolean $checkMultiple
* @return array
* @return array
*/
*/
private
function
buildBuckets
(
$models
,
$link
,
$viaModels
=
null
,
$viaLink
=
null
)
private
function
buildBuckets
(
$models
,
$link
,
$viaModels
=
null
,
$viaLink
=
null
,
$checkMultiple
=
true
)
{
{
if
(
$viaModels
!==
null
)
{
if
(
$viaModels
!==
null
)
{
$map
=
[];
$map
=
[];
...
@@ -180,7 +309,7 @@ trait ActiveRelationTrait
...
@@ -180,7 +309,7 @@ trait ActiveRelationTrait
}
}
}
}
if
(
!
$this
->
multiple
)
{
if
(
$checkMultiple
&&
!
$this
->
multiple
)
{
foreach
(
$buckets
as
$i
=>
$bucket
)
{
foreach
(
$buckets
as
$i
=>
$bucket
)
{
$buckets
[
$i
]
=
reset
(
$bucket
);
$buckets
[
$i
]
=
reset
(
$bucket
);
}
}
...
...
framework/db/BaseActiveRecord.php
View file @
c92a260a
...
@@ -231,19 +231,12 @@ abstract class BaseActiveRecord extends Model implements ActiveRecordInterface
...
@@ -231,19 +231,12 @@ abstract class BaseActiveRecord extends Model implements ActiveRecordInterface
}
}
$value
=
parent
::
__get
(
$name
);
$value
=
parent
::
__get
(
$name
);
if
(
$value
instanceof
ActiveRelationInterface
)
{
if
(
$value
instanceof
ActiveRelationInterface
)
{
if
(
method_exists
(
$this
,
'get'
.
$name
))
{
return
$this
->
_related
[
$name
]
=
$value
->
findFor
(
$name
,
$this
);
$method
=
new
\ReflectionMethod
(
$this
,
'get'
.
$name
);
}
else
{
$realName
=
lcfirst
(
substr
(
$method
->
getName
(),
3
));
if
(
$realName
!==
$name
)
{
throw
new
InvalidParamException
(
'Relation names are case sensitive. '
.
get_class
(
$this
)
.
" has a relation named
\"
$realName
\"
instead of
\"
$name
\"
."
);
}
}
$this
->
populateRelation
(
$name
,
$value
->
multiple
?
$value
->
all
()
:
$value
->
one
());
return
$this
->
_related
[
$name
];
}
return
$value
;
return
$value
;
}
}
}
}
}
/**
/**
* PHP setter magic method.
* PHP setter magic method.
...
...
tests/unit/data/ar/Customer.php
View file @
c92a260a
...
@@ -29,6 +29,11 @@ class Customer extends ActiveRecord
...
@@ -29,6 +29,11 @@ class Customer extends ActiveRecord
return
$this
->
hasMany
(
Order
::
className
(),
[
'customer_id'
=>
'id'
])
->
orderBy
(
'id'
);
return
$this
->
hasMany
(
Order
::
className
(),
[
'customer_id'
=>
'id'
])
->
orderBy
(
'id'
);
}
}
public
function
getOrders2
()
{
return
$this
->
hasMany
(
Order
::
className
(),
[
'customer_id'
=>
'id'
])
->
inverseOf
(
'customer2'
)
->
orderBy
(
'id'
);
}
public
function
afterSave
(
$insert
)
public
function
afterSave
(
$insert
)
{
{
ActiveRecordTest
::
$afterSaveInsert
=
$insert
;
ActiveRecordTest
::
$afterSaveInsert
=
$insert
;
...
...
tests/unit/data/ar/Order.php
View file @
c92a260a
...
@@ -22,6 +22,11 @@ class Order extends ActiveRecord
...
@@ -22,6 +22,11 @@ class Order extends ActiveRecord
return
$this
->
hasOne
(
Customer
::
className
(),
[
'id'
=>
'customer_id'
]);
return
$this
->
hasOne
(
Customer
::
className
(),
[
'id'
=>
'customer_id'
]);
}
}
public
function
getCustomer2
()
{
return
$this
->
hasOne
(
Customer
::
className
(),
[
'id'
=>
'customer_id'
])
->
inverseOf
(
'orders2'
);
}
public
function
getOrderItems
()
public
function
getOrderItems
()
{
{
return
$this
->
hasMany
(
OrderItem
::
className
(),
[
'order_id'
=>
'id'
]);
return
$this
->
hasMany
(
OrderItem
::
className
(),
[
'order_id'
=>
'id'
]);
...
...
tests/unit/framework/db/ActiveRecordTest.php
View file @
c92a260a
...
@@ -324,4 +324,57 @@ class ActiveRecordTest extends DatabaseTestCase
...
@@ -324,4 +324,57 @@ class ActiveRecordTest extends DatabaseTestCase
$this
->
assertEquals
(
0
,
count
(
$orders
[
1
]
->
books2
));
$this
->
assertEquals
(
0
,
count
(
$orders
[
1
]
->
books2
));
$this
->
assertEquals
(
1
,
count
(
$orders
[
2
]
->
books2
));
$this
->
assertEquals
(
1
,
count
(
$orders
[
2
]
->
books2
));
}
}
public
function
testInverseOf
()
{
// eager loading: find one and all
$customer
=
Customer
::
find
()
->
with
(
'orders2'
)
->
where
([
'id'
=>
1
])
->
one
();
$this
->
assertTrue
(
$customer
->
orders2
[
0
]
->
customer2
===
$customer
);
$customers
=
Customer
::
find
()
->
with
(
'orders2'
)
->
where
([
'id'
=>
[
1
,
3
]])
->
all
();
$this
->
assertTrue
(
$customers
[
0
]
->
orders2
[
0
]
->
customer2
===
$customers
[
0
]);
$this
->
assertTrue
(
empty
(
$customers
[
1
]
->
orders2
));
// lazy loading
$customer
=
Customer
::
find
(
2
);
$orders
=
$customer
->
orders2
;
$this
->
assertTrue
(
count
(
$orders
)
===
2
);
$this
->
assertTrue
(
$customer
->
orders2
[
0
]
->
customer2
===
$customer
);
$this
->
assertTrue
(
$customer
->
orders2
[
1
]
->
customer2
===
$customer
);
// ad-hoc lazy loading
$customer
=
Customer
::
find
(
2
);
$orders
=
$customer
->
getOrders2
()
->
all
();
$this
->
assertTrue
(
count
(
$orders
)
===
2
);
$this
->
assertTrue
(
$customer
->
orders2
[
0
]
->
customer2
===
$customer
);
$this
->
assertTrue
(
$customer
->
orders2
[
1
]
->
customer2
===
$customer
);
// the other way around
$customer
=
Customer
::
find
()
->
with
(
'orders2'
)
->
where
([
'id'
=>
1
])
->
asArray
()
->
one
();
$this
->
assertTrue
(
$customer
[
'orders2'
][
0
][
'customer2'
][
'id'
]
===
$customer
[
'id'
]);
$customers
=
Customer
::
find
()
->
with
(
'orders2'
)
->
where
([
'id'
=>
[
1
,
3
]])
->
asArray
()
->
all
();
$this
->
assertTrue
(
$customer
[
'orders2'
][
0
][
'customer2'
][
'id'
]
===
$customers
[
0
][
'id'
]);
$this
->
assertTrue
(
empty
(
$customers
[
1
][
'orders2'
]));
$orders
=
Order
::
find
()
->
with
(
'customer2'
)
->
where
([
'id'
=>
1
])
->
all
();
$this
->
assertTrue
(
$orders
[
0
]
->
customer2
->
orders2
===
[
$orders
[
0
]]);
$order
=
Order
::
find
()
->
with
(
'customer2'
)
->
where
([
'id'
=>
1
])
->
one
();
$this
->
assertTrue
(
$order
->
customer2
->
orders2
===
[
$order
]);
$orders
=
Order
::
find
()
->
with
(
'customer2'
)
->
where
([
'id'
=>
1
])
->
asArray
()
->
all
();
$this
->
assertTrue
(
$orders
[
0
][
'customer2'
][
'orders2'
][
0
][
'id'
]
===
$orders
[
0
][
'id'
]);
$order
=
Order
::
find
()
->
with
(
'customer2'
)
->
where
([
'id'
=>
1
])
->
asArray
()
->
one
();
$this
->
assertTrue
(
$order
[
'customer2'
][
'orders2'
][
0
][
'id'
]
===
$orders
[
0
][
'id'
]);
$orders
=
Order
::
find
()
->
with
(
'customer2'
)
->
where
([
'id'
=>
[
1
,
3
]])
->
all
();
$this
->
assertTrue
(
$orders
[
0
]
->
customer2
->
orders2
===
[
$orders
[
0
]]);
$this
->
assertTrue
(
$orders
[
1
]
->
customer2
->
orders2
===
[
$orders
[
1
]]);
$orders
=
Order
::
find
()
->
with
(
'customer2'
)
->
where
([
'id'
=>
[
2
,
3
]])
->
orderBy
(
'id'
)
->
all
();
$this
->
assertTrue
(
$orders
[
0
]
->
customer2
->
orders2
===
$orders
);
$this
->
assertTrue
(
$orders
[
1
]
->
customer2
->
orders2
===
$orders
);
$orders
=
Order
::
find
()
->
with
(
'customer2'
)
->
where
([
'id'
=>
[
2
,
3
]])
->
orderBy
(
'id'
)
->
asArray
()
->
all
();
$this
->
assertTrue
(
$orders
[
0
][
'customer2'
][
'orders2'
][
0
][
'id'
]
===
$orders
[
0
][
'id'
]);
$this
->
assertTrue
(
$orders
[
0
][
'customer2'
][
'orders2'
][
1
][
'id'
]
===
$orders
[
1
][
'id'
]);
$this
->
assertTrue
(
$orders
[
1
][
'customer2'
][
'orders2'
][
0
][
'id'
]
===
$orders
[
0
][
'id'
]);
$this
->
assertTrue
(
$orders
[
1
][
'customer2'
][
'orders2'
][
1
][
'id'
]
===
$orders
[
1
][
'id'
]);
}
}
}
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment