ActiveQuery count() and all() return different results - php

I have a query:
$query = Products::find();
$query->joinWith('vendor', true, 'LEFT JOIN');
$query->joinWith('values', true,'LEFT JOIN');
$query->where(['<>', 'stock', 7]);
$query->andWhere(['category_id' => $model->id]);
if (!empty($activeVendors))
$query->andWhere(['lan_vendors.id' => array_flip($activeVendors)]);
if (!empty($activeValues)){
$query->andWhere(['lan_products_values.value_id' => $activeValues]);
}
$totalProducts = $query->count();
$products = $query->all();
In result:
$totalProducts = 12;
count($products) = 3;
I can not solve this problem. Reading the documentation did not help. Is there something wrong with the database itself?

your left join statements generate duplicate rows.
after a the query runs yii removes duplicate data and creates a usable array of uniqe Product models
the duplicate rows are not avoidable in your case since you enforce eager loading with left join
$query->joinWith('vendor', true, 'LEFT JOIN');
$query->joinWith('values', true,'LEFT JOIN');
you can try to run something like this to adjust the relations to your conditions, and follow the generated queries
in the debug log,
$query->with([
'vendor' => function (\yii\db\ActiveQuery $query) use ($activeVendors) {
$query->andFilterWhere(['lan_vendors.id' => array_flip($activeVendors)]);
},
'values' => function (\yii\db\ActiveQuery $query) use ($activeValues) {
$query->andWhere(['lan_products_values.value_id' => $activeValues]);
},
])
also follow the generated queries in the debug log, it's a usefull way of figuring out what happens in the two cases

Because you are joining additional tables here most probably you have got dupicated results - you can check it by running this query manually outside of Yii.
Query count() is showing you all the rows fetched from database (with duplicates).
all() on the other hand takes the fetched rows and while populating the Yii 2 models it removes duplicates so you have got unique ones.

Related

How to sort a collection by its relation in laravel

I have a complicated filter for my hotels and in the end i have a collection that I want to sort the parent relations by its nested relationship so here I have as below :
public function resultFilter($from_date, $to_date, $bed_count, $city_id, $stars, $type_id, $hits, $price, $accommodation_name, $is_recommended, $start_price, $end_price, $has_discount, $facility_id)
{
// $data = QueryBuilder::for(Accommodation::class)
// ->allowedFilters(['city_id','grade_stars','accommodation_type_id'])
// ->allowedIncludes('gallery')
// ->when($bed_count, function ($q, $bed_count) {
// $q->with([
// 'accommodationRoomsLimited' => function ($q) use ($bed_count) {
// $q->where('bed_count', $bed_count);
// }
// ]);
// })
// ->paginate(10);
// ->get();
// ->orderBy('hits','DESC')->paginate(10);
$data = Accommodation::with(['city','accommodationFacilities', 'gallery', 'accommodationRoomsLimited.discount', 'accommodationRoomsLimited', 'accommodationRoomsLimited.roomPricingHistorySearch' => function ($query) use ($from_date, $to_date) {
$query->whereDate('from_date', '<=', $from_date);
$query->whereDate('to_date', '>=', $to_date);
}])->when($bed_count, function ($q, $bed_count) {
$q->whereHas('accommodationRoomsLimited', function($query) use ($bed_count) {
$query->where('bed_count', $bed_count);
});
})->when($accommodation_name, function ($query, $accommodation_name) {
$query->where('name', 'like', $accommodation_name);
})->when($is_recommended, function ($query,$is_recommended){
$query->where('is_recommended', $is_recommended);
})->when($start_price, function ($query, $start_price) {
$query->with([
'accommodationRoomsLimited.roomPricingHistorySearch' => function ($q) use ($start_price) {
$q->where('sales_price', '<', $start_price);
}
]);
})->when($has_discount, function ($query, $has_discount) {
$query->with([
'accommodationRoomsLimited' => function ($q) use ($has_discount) {
$q->has('discount');
}
]);
})
->whereIn('city_id', $city_id)
->whereIn('grade_stars', $stars)
->orWhere('accommodation_type_id', $type_id);
if ($hits) { // or == 'blabla'
$data = $data->orderBy('hits','DESC');
} elseif ($price) { // == A-Z or Z-A for order asc,desc
$f = $data->get();
foreach ($f as $datas) {
foreach ($datas->accommodationRoomsLimited as $g) {
dd($data);
$data = $data->accommodationRoomsLimited()->orderBy($g->roomPricingHistorySearch->sales_price);
}
}
}
$data = $data->paginate(10);
return $data;
}
So if you read code I added the sales_price that I want to sort my $data by it if the $price exists in the request. So in a short term question, I want to sort $data by sales_price in this query above.
NOTE
: this filters may get more complicated so any other best practice or better way for that like spatie Query builder or local scopes would be appreciated although i tried both and yet they have their own limitation
I've faced that problem before. And it seems I need to explain a little about eager loading first.
You can't order by eager loading, you can order it after you fetch the data. Because
eager load will split join query for better performance. For example you querying accomodation and has relation with city. The accomodation table has 1000 records and the city table has 10.000 records. let's say the maximum id for eager loading is 250, the unique city_id from accomodation table is 780. There will be 5 query generated.
$data = Accomodation::with('city')->get();
select * from accomodation
select * from city where id in [unique_id_1 - unique_id_250]
select * from city where id in [unique_id_251 - unique_id_500]
select * from city where id in [unique_id_501 - unique_id_750]
select * from city where id in [unique_id_751 - unique_id_780]
Then laravel will do the job to create the relation by city query results. By this method you will fix N+1 problem from join query, thus it's should be faster.
Then imagine you want to order accomodation bycity.name with with method in query builder. let's take the 3rd query for example.
$data = Accomodation::with([
'city' => function($q) { return $q->orderBy('name'); },
])->get();
the query will be:
select * from city where id in [unique_id_251 - unique_id_500] order by name
The city results will be ordered, but laravel will read it the same way. It'll create accomodation first, then relate it with city queries. So the order from city won't affected accomodation order.
Then how to order it? I found out couple ways to achieve that.
Join query. this is the easiest way, but will make query slower. if your data isn't really big and the join query won't hurt your performance. Maybe 0.003 seconds better performance isn't really worth your 8 hours.
sortBy in collection function. You can sort it with a method from collection.
for example if you want to order the accomodation based on country.name from city relation, this script will help you.
$data = Accomodation::with('city.country')->get();
$data->sortBy(function($item) {
return $item->city->country->name;
});
Flatten the collection. This method will try to flatten the collection so the results will be like join query then sorting it. You can use map method from collection. I do believe all the filters and searchable strings is should be included in data.
$data->map(function($item) {
return [
'city_name' => $city->name,
...
all_searchable_data,
all_shareable_data,
...
];
})->sortBy('key1);
Change eager loading direction if possible. You can order it with changing base models. For example you use city instead accomodation to order it by city.name
$data = City::with('accomodation')->orderBy('name')->get();
And last, If your data rarely changes (example every 2 hours), You might thinking to use cache. You only need to invalidate the cache every 2 hours and create the new one. From my experiences, cache always faster than querying database if the data is big. You just need to know the interval or event to invalidate the cache.
Anything you choose is up to you. But please remember this, when you processing bulk data with the collection from laravel, It could be slower than querying from the database. Maybe it's because PHP performance.
For me the best way is using eager loading then ->map() it then cache it. Why do I need to map it first before cache it? The reason is, by selecting some attribute will reduce the cache size. Then you'll be gain more performance by. And I can say it will produce more readable and beatiful code.
Bonus
this is how I doing this.
$data = Cache::remember("accomodation", 10, function() {
$data = Accommodation::with([
'city',
...
])
->get();
return $data->map(function($item) {
return [
'city_name' => $item->city->name,
...
all_additional_data,
all_searchable_data,
all_shareable_data,
...
];
});
}
return $data->search(function($item) use ($searchKey, $searchAnnotiation, $searchValue) {
switch ($searchAnnotiation) {
case '>':
return $item[$searchKey] > $searchValue;
break;
case '<':
return $item[$searchKey] < $searchValue;
break;
}
})->sortBy($sortKey)->paginate();
The cache will save the processed data. thus the execution time needed is fetch data from cache, filter it, and sorting it. then transform it into paginate. you can set any additional cache in those flow for faster results.
$data->paginate() by create macro paginate for Collection.

Join query retrieving data from only one table cakephp 3

I am new to cakephp. I am usign query builder to fetch details from two tables using join in cakephp query builder.But the query i am writing is fetching details only from one table. Need help to get data from other table as well.
This is my code to fetch data by joining two tables:
public function edit($id = null) {
$events_table = TableRegistry::get('Events');
$events = $events_table->find('all')
->hydrate(false)
->join([
'CourseType'=> [
'table' => 'coursetype',
'type' => 'LEFT',
'conditions' => 'Events.type = CourseType.coursetype_id',
]
])->where(['Events.id' => $id]);
$event = $events->toArray();
$this->set('event', $event);
}
As a result i am getting only details from events table. But i need data from coursetype also.Any help is appreciated. Thank you.
Manually adding joins won't cause any additional data to be selected, you'd have to do that on your own by specifying the fields to be selected via the select() method.
$events = $events_table
->find('all')
->hydrate(false)
->select($events_table)
// either add the fields one by one, or pass a table object
// as above to select all fields of the table
->select(['CourseType.id', 'CourseType.xyz', /* ... */])
// ...
I'd suggest to use use containments instead, ie setup the required association if not already present, and then simply use contain():
$events = $events_table
->find('all')
->hydrate(false)
->contain('CourseType')
->where(['Events.id' => $id]);
See also
Cookbook > Database Access & ORM > Query Builder > Selecting Data
Cookbook > Database Access & ORM > Query Builder > Loading Associations
your solution here: CakePHP find method with JOIN

How to use query builder with sum() column and groupBy

How would I use query builder in Laravel to generate the following SQL statement:
SELECT costType, sum(amountCost) AS amountCost
FROM `itemcosts`
WHERE itemid=2
GROUP BY costType
I have tried several things, but I can't get the sum() column to work with a rename.
My latest code:
$query = \DB::table('itemcosts');
$query->select(array('itemcosts.costType'));
$query->sum('itemcosts.amountCost');
$query->where('itemcosts.itemid', $id);
$query->groupBy('itemcosts.costType');
return $query->get();
Using groupBy and aggregate function (sum / count etc) doesn't make sense.
Query Builder's aggregates return single result, always.
That said, you want raw select for this:
return \DB::table('itemcosts')
->selectRaw('costType, sum(amountCost) as sum')
->where('itemid', $id)
->groupBy('costType')
->lists('sum', 'costType');
Using lists instead of get is more appropriate here, it will return array like this:
[
'costType1' => 'sumForCostType1',
'costType2' => 'sumForCostType2',
...
]
With get you would have:
[
stdObject => {
$costType => 'type1',
$sum => 'value1'
},
...
]

MANY_MANY Yii query limiting to list of related records

I have a Yii app that contains Products and Interests with a MANY_MANY relationship. These are mapped, obviously, with the following relation:
'interests'=>array(self::MANY_MANY, 'Interest', 'interest_product_assignment(product_id,interest_id)'),
I wish to query Products using CDbCriteria like so:
$products = Product::model()->with('interests')->findAll($criteria);
This query is working fine. I need to extend it to limit it to only certain interests that I have the ids of stored in an array. I believe this should be possible with something like:
$products = Product::model()->with(
'interests',
array('condition' => {not_sure_what_to_put_here})
)->findAll($criteria);
I'm not sure how to finish the above query and have been looking for a while. It's not that I can't find anything on this, but I can't understand anything I've dug up.
Can anyone spot how to complete this query?
EDIT
What I've tried upon Telvin's suggestion:
$products = Product::model()->with(
'interests',
array('condition' => "interests_interests.interest_id IN ($selectedInterestsString)")
)->findAll($criteria);
Not adding an 'IN' statement to query.
array of provided ids:
$array_ids = array('1','24','350','4609', ....)
$array_ids_str = '"' . implode('","', array_values($array_ids)) . '"';
$products = Product::model()->with(array(
'interests'=> array('condition' => "interest_id_column IN ($array_ids_str)"
)))->findAll($criteria);

Summing over multiple fields in Laravel

I'm wondering if it is possible to take the sum of multiple fields in one query using the fluent query builder.
I currently have two tables: events and attendees. Attendees belong to events and have two fields: total_raised and total_hours. What I want to do is select all events and the total amount raised/total number of hours spent on that event. Now, if I were just using SQL I would do something to the effect of:
SELECT Event.id, sum(Attendees.total_raised), sum(Attendees.total_hours)
FROM Events JOIN Attendees ON Events.id = Attendees.event_id
GROUP BY Event.id
However, I can't seem to find a way to take multiple sums at once using the fluent query builder. Is there any way to do what I'm trying to do using fluent, or should I just make it a raw SQL query?
You can use sum() i.e.:
$q = DB::table('events')
->join('attendees', 'events.id', '=', 'attendees.event_id')
->sum('total_raised')
->sum('total_hours');
If that doesn't work you can try:
...
->get(
array(
'events.id',
DB::raw('SUM(attendees.total_raised)'),
DB::raw('SUM(attendees.total_hours)')
)
);
Building on simones answer. You could do this by essentially running two queries.
$query = DB::table('events')->join('attendees', 'events.id', '=', 'attendees.event_id');
$raised = $query->sum( 'total_raised' );
$hours = $query->sum( 'total_hours' );
It depends on the situation. If it were on the admin/CMS side of things I'd be lean towards this solution. If it is on the front end it should be done in a single query which will be faster. Depending on the content it may or may not be a significant difference.
$result = DB::table('events')->join('attendees', 'events.id', '=', 'attendees.event_id')
->get( array(
DB::raw( 'SUM(attendees.total_raised) AS raised' ),
DB::raw( 'SUM(attendees.total_hours) AS hours' ),
));
I am writing this answer to help those who are in search to sum multiple fields in a single table.
If you want to sum multiple fields inside a single table so there would be no need to "join" you can simply do it likewise, assuming the table like this.
In your controller do this:
$billInfo= Bills::where('reports_id',2)->get( array(
DB::raw('SUM(Price) as total_price'),
DB::raw('SUM(balance) as total_balance'),
DB::raw('SUM(paid) as total_paid'),
));
this will result the below data:
[{"total_price":17500,"total_balance":17500,"total_paid":null}]
I am doing the same thing in my project, Here is the solution which I found. I am using Laravel 5.2 Eloquent here is the Eloquent statement.
This statement which I use in my project, Please made change according to your need.
$result = self::select("*", DB::raw('SUM(auction_amount) as total_auction_amount') , DB::raw('SUM(commission_amount) as total_commission_amount'),
DB::raw('SUM(deposit_amount) as total_deposit_amount'))
->groupBy('cp_user_id')
->get()
->toArray();
Same way you can use for your query like
$result = self::select("*", DB::raw('SUM(auction_amount) as total_auction_amount') , DB::raw('SUM(Attendees.total_raised) as total_raised'),
DB::raw('SUM(Attendees.total_hours) as total_hours'))
->with('Attendees')
->groupBy('id')
->get()
->toArray();

Categories