Reputation: 2143
I have a model item
that has a hasMany
relation to an image
model. In the item GridView
I have a column that shows how many images that item
has. I would like to be able to sort by this column, and possibly be able to filter with a checkbox to show only item
s with no image
s.
I tried adding a check to the search model to query with
the images
relation, and I thought I'd add a andFilterWhere
for where the image
count was less than the input value. However I'm not sure the best way to have that logic, is there a better Yii way to filter by that count?
Update
Following information from this question I've updated my item
model to have a method that returns the relationship count.
I've then update my search model like this:
class ItemSearch extends Item
{
public $image_count;
/**
* @inheritdoc
*/
public function rules()
{
return [
[['id', 'product_id', 'manufacturer_id', 'scale_id', 'owner_id', 'quantity'], 'integer'],
[['sku_number', 'description', 'details', 'condition'], 'safe'],
[['price', 'sale_price', 'image_count'], 'number'],
];
}
/**
* @inheritdoc
*/
public function scenarios()
{
// bypass scenarios() implementation in the parent class
return Model::scenarios();
}
/**
* Creates data provider instance with search query applied
*
* @param array $params
*
* @return ActiveDataProvider
*/
public function search($params)
{
$query = Item::find();
$subQuery = Image::find()->select('item_id, COUNT(id) as image_count')->groupBy('item_id');
$query->leftJoin(['imageCount' => $subQuery], 'imageCount.item_id = id');
// add conditions that should always apply here
$dataProvider = new ActiveDataProvider([
'query' => $query,
]);
$dataProvider->setSort([
'attributes' => [
'id',
'product_id',
'manufacturer_id',
'scale_id',
'owner_id',
'image_count' => [
'asc' => ['imageCount.image_count' => SORT_ASC],
'desc' => ['imageCount.image_count' => SORT_DESC],
'label' => 'Images'
]
]
]);
$this->load($params);
if (!$this->validate()) {
// uncomment the following line if you do not want to return any records when validation fails
// $query->where('0=1');
return $dataProvider;
}
// grid filtering conditions
if (!empty($this->id)) {
$query->andFilterWhere([
'id' => $this->id,
]);
}
if (!empty($this->product_id)) {
$query->andFilterWhere([
'product_id' => $this->product_id,
]);
}
if (!empty($this->manufacturer_id)) {
$query->andFilterWhere([
'manufacturer_id' => $this->manufacturer_id,
]);
}
if (!empty($this->scale_id)) {
$query->andFilterWhere([
'scale_id' => $this->scale_id,
]);
}
if (!empty($this->owner_id)) {
$query->andFilterWhere([
'owner_id' => $this->owner_id,
]);
}
if (!empty($this->image_count)) {
$query->andWhere(['is','imageCount.image_count',new \yii\db\Expression('null')]);
}
$query->andFilterWhere(['like', 'sku_number', $this->sku_number])
->andFilterWhere(['like', 'description', $this->description])
->andFilterWhere(['like', 'details', $this->details])
->andFilterWhere(['like', 'condition', $this->condition]);
return $dataProvider;
}
}
The issue now is that the above will produce a query something like this:
SELECT `item`.* FROM `item` LEFT JOIN (SELECT `item_id`, COUNT(id) as image_count FROM `image` GROUP BY `item_id`) `imageCount` ON imageCount.item_id = id WHERE `imageCount`.`image_count`=0
The issue being items
that have 0 image
s are no longer shown (and checking the box shows nothing) because the join will find nothing for that id in the image
table
Upvotes: 1
Views: 2188
Reputation: 2322
My understanding is that you need to count the amount of data in an associated table and sort and filter based on the statistics.
Then your ItemSearch::search()
method should be able to change to this:
public function search($params)
{
$query = Item::find()->select('*, (select count(*) from image where image.item_id = item.id) as image_count'); // If the table name of your Image table is image, if the table name of your Item table is item
// add conditions that should always apply here
$dataProvider = new ActiveDataProvider([
'query' => $query,
]);
$dataProvider->setSort([
'attributes' => [
'id',
'product_id',
'manufacturer_id',
'scale_id',
'owner_id',
'image_count'
]
]);
$this->load($params);
if (!$this->validate()) {
// uncomment the following line if you do not want to return any records when validation fails
// $query->where('0=1');
return $dataProvider;
}
// grid filtering conditions
if (!empty($this->id)) {
$query->andFilterWhere([
'id' => $this->id,
]);
}
if (!empty($this->product_id)) {
$query->andFilterWhere([
'product_id' => $this->product_id,
]);
}
if (!empty($this->manufacturer_id)) {
$query->andFilterWhere([
'manufacturer_id' => $this->manufacturer_id,
]);
}
if (!empty($this->scale_id)) {
$query->andFilterWhere([
'scale_id' => $this->scale_id,
]);
}
if (!empty($this->owner_id)) {
$query->andFilterWhere([
'owner_id' => $this->owner_id,
]);
}
if (!empty($this->image_count)) {
$query->andWhere([
'image_count' => $this->image_count,
]);
}
$query->andFilterWhere(['like', 'sku_number', $this->sku_number])
->andFilterWhere(['like', 'description', $this->description])
->andFilterWhere(['like', 'details', $this->details])
->andFilterWhere(['like', 'condition', $this->condition]);
return $dataProvider;
}
However, if your image_count often needs calculations and statistics, and your data volume is large, then it is recommended that you redefine this field into the item table.
UPDATE:
My fault, the aggregated fields cannot be filtered by the where condition, but you can use having:
$query->andFilterHaving([
'image_count' => $this->image_count,
]);
Upvotes: 1
Reputation: 23738
You should check for WHERE imageCount.image_count IS NULL
rather than WHERE imageCount.image_count=0
as those rows that don't have any related images would be shown as null
under the image_count
and change the condition from
$query->andFilterWhere(['imageCount.image_count' => 0]);
to
$query->andWhere(['is','imageCount.image_count',new \yii\db\Expression('null')]);`
Your query should be generated like
SELECT `item`.* FROM `item`
LEFT JOIN
(
SELECT `item_id`, COUNT(id) as image_count
FROM `image`
GROUP BY `item_id`
) `imageCount`
ON imageCount.item_id = id
WHERE `imageCount`.`image_count` is NULL
Update
As you were still facing issues so I thought I had some spare time to find this problem and I came up with the following
You were having a problem when filtering for the items having 0 counts for the images I don't have the exact schema that you have but I will state an example below with a similar scenario where I have Shoots
and their related tags in a junction table ShootTag
model
Shoots
Below is a sample for schema and data
+----+------------+--------+------------+
| id | name | active | shoot_type |
+----+------------+--------+------------+
| 1 | aslam omer | 1 | modeling |
| 2 | asif | 1 | modeling |
| 3 | saleem | 1 | modeling |
| 4 | sajid | 1 | modeling |
| 5 | tasleem | 1 | modeling |
| 6 | tehseen | 1 | modeling |
| 7 | amjad | 1 | modeling |
| 8 | shaban | 1 | modeling |
| 9 | irfan | 1 | modeling |
+----+------------+--------+------------+
ShootTags
Below is the sample for schema and data
+----------+--------+
| shoot_id | tag_id |
+----------+--------+
| 1 | 1 |
| 1 | 2 |
| 1 | 3 |
| 1 | 4 |
| 2 | 1 |
| 2 | 2 |
| 2 | 3 |
| 3 | 1 |
| 4 | 1 |
| 4 | 4 |
+----------+--------+
Now considering the above tables and data I have the Search Model with name ShootsSearch
which I am using to display all the shoots and I want to show the count of the tags against each of the shoot saved inside the ShootTag
model.
I am not adding the code for GridView as it is not relevant, my search model ShootsSearch
has the following search method which works correctly for any count against the shoots in the shoottags model.
What is different from your code is that i am Using the ShootsSearch
model for the first query rather than the Shoots
Model as you are using Item::find();
which should be ItemSearch::find();
instead as the alias you are using image_count
is declared in the search model,
Then the line new Expression('if(st.totalTags is NOT NULL,st.totalTags,0) as totalTags')
in the main query you need to display the null values as 0 so you can use the conditional select here.
Then you need to check for the if ($this->totalTags == '0') {
to apply $query->andWhere(['IS', 'totalTags', new Expression('null')]);
as the actual value would be null
for totalTags
where no tags are available against any Shoot
and in the else part you will use the query->andFilterWhere(['=', 'totalTags', $this->totalTags]);
This works correctly in all three scenarios that you want see the images below
Default view first time
Search for shoots with totalTags count is 4
Search for shoots with TotalTags count is 0
You should replace the following in your code
public $totalTags
with public $image_count
.ShootTags
with Image
.shoot_id
with item_id
.ShootsSearch
with ItemSearch
/self
.Here is the search model and the code is tested and working.
class ShootsSearch extends Shoots
{
/**
* @var mixed
*/
public $totalTags;
/**
* {@inheritdoc}
*/
public function rules()
{
return [
[['id', 'active', 'totalTags'], 'integer'],
[['name', 'shoot_type', 'description', 'totalTags'], 'safe']
];
}
/**
* {@inheritdoc}
*/
public function scenarios()
{
// bypass scenarios() implementation in the parent class
return Model::scenarios();
}
/**
* Creates data provider instance with search query applied
*
* @param array $params
*
* @return ActiveDataProvider
*/
public function search($params)
{
$subQuery = ShootTag::find()->select(
[
new Expression('shoot_id, COUNT(shoot_id) as totalTags')
]
)->groupBy('shoot_id');
$query = ShootsSearch::find()->alias('s')
->select(
[
's.*',
new Expression('if(st.totalTags is NOT NULL,st.totalTags,0) as totalTags')
]
)->leftJoin(['st' => $subQuery], 'st.shoot_id=s.id');
// add conditions that should always apply here
$dataProvider = new ActiveDataProvider(
[
'query' => $query
]
);
$this->load($params);
if (!$this->validate()) {
// uncomment the following line if you do not want to return any records when validation fails
// $query->where('0=1');
return $dataProvider;
}
// grid filtering conditions
$query->andFilterWhere([
'id' => $this->id,
'active' => $this->active
]);
$query->andFilterWhere(['like', 'name', $this->name])
->andFilterWhere(['like', 'shoot_type', $this->shoot_type])
->andFilterWhere(['like', 'description', $this->description]);
if ($this->totalTags == '0') {
$query->andWhere(['IS', 'totalTags', new Expression('null')]);
} else {
$query->andFilterWhere(['=', 'totalTags', $this->totalTags]);
}
return $dataProvider;
}
}
Upvotes: 3