removing redundant queries from laravel / eloquent - laravel

In a laravel code base I'm working with there's this line of code:
if ($limit === '-1') {
$items = $query->get();
if ($items->count() < 1) {
return Paginator::make([], 0, Config::get('api.result_limit'));
}
return $query->paginate($items->count());
}
$query is a \Illuminate\Database\Eloquent\Relations\HasManyThrough object.
Anyway, the problem is that a SELECT table_name.* is being performed twice. $query->get() does it and then $query->paginate does a SELECT COUNT(*) (which itself is redundant since we already have the count from the first query) and a SELECT table_name.*, table_name.*. ie. it's just excessively redundant and I'm trying to remove that redundancy.
Here's what I tried.
return $items right after $query->get(). Unfortunately, doing so yielded the following error:
Call to undefined method Illuminate\Database\Eloquent\Collection::getCollection()
$query->paginate() returns a \MyApp\Services\Pagination\Paginator object which itself extends (\Illuminate\Pagination\Paginator) whereas $query->get() returns a \Illuminate\Database\Eloquent\Collection object.
If I could somehow convert \Illuminate\Database\Eloquent\Collection to \Illuminate\Pagination\Paginator I think that'd do the trick but if that is possible idk how.
Doing return $query->paginate(99999999) right after the if ($limit === '-1'). That works but I'd rather not specify a hard limit. I don't specify a limit when doing a SELECT with SQL I'm writing and I don't think I should have to specify one here either. I tried return $query->paginate(-1) but that gave an error.
Any ideas?

Related

Laravel 5.6 Custom Query Build showing empty result

No errors, just an empty result. I am trying to work out why this query within a model is showing an empty collection.
Mysql Workbench query:
select
u.`name`, u.email, ual.admin, a.account_name
from
users as u
join users_account_link as ual on u.id = ual.user_id and u.account_id_in_use = ual.account_id
join accounts a on ual.account_id = a.id
where
u.sub = 'ABCDE';
Spits one row containing the desired result set.
Recreating this in Laravel query builder:
$settings = DB::table('users as u')
->join('users_account_link as ual', function ($join) {
$join->on('u.id', '=', 'ual.user_id')
->where('u.account_id_in_use', '=', 'ual.account_id');
})
->join('accounts as a', 'ual.account_id', '=', 'a.id')
->select('u.name as user_name', 'u.email as user_email', 'ual.admin as admin_check', 'a.account_name')
->where('u.sub',auth()->user()->sub)
->get();
dd($settings);
Provides an empty collection. I have done many custom queries which work well however the problem I have narrowed down the result set is the additional condition for join users_account_link as ual on u.id = ual.user_id and u.account_id_in_use = ual.account_id and have tried to move this condition to a where clause which still provides an empty result.
'u.account_id_in_use', '=', 'ual.account_id' are both integers however replacing u.account_id_in_use with a hardcoded integer e.g. 2 would return a result. Therefore, Laravel seems to have an issue with this field and replaced the where-> with a whereRaw now returns the desired result.
For anyone having a similar issue, try replacing fields with hardcode values to isolate the issue and look into using raw when possible to overcome the issue.
Hope this helps anyone in need.

How can I build multiple queries at once with CodeIgniter QueryBuilder?

I have a complex SELECT with several time consuming calculated fields. I only want the first 100 rows, however, I also need the total rows found. This means executing the query once with LIMIT and once without.
I would like to be able to build two SELECT's simultaneously with CodeIgniter's QueryBuilder and eliminate the time consuming calculated fields on the second query.
I would like to know if there is a good way to do this with QB or should I just build the SELECT without QB.
UPDATE: Apparently I have confused people. I need to do something like this:
To get the records to display:
SELECT id, [ several really time intensive calculated fields ]
FROM table
WHERE [some complicated criteria]
LIMIT 100;
To get the total records I would like to re-use the built query or build a 2nd one simultaneously but without the limit and with only the id field:
SELECT id
FROM table
WHERE [some complicated criteria];
Another option; I have learned that if I put the time intensive calculated fields in a sub SELECT, Postgres will not execute it until the row is actually retrieved.
For now I am manually building two queries but I'd like to do it the correct QueryBuilder way.
There is no way to run multiple query in one statement in codeigniter. But you may do as following
$query1 = $this->db->query('SELECT * FROM table_name LIMIT 10');
$query2 = $this->db->query('SELECT * FROM table_name');
But what I do is make a method and call based on parameter like
public function get_rows($table, $num_rows = FALSE){
$this->db->select('*');
$this->db->from($table);
if($num_rows === FALSE){
$this->db->limit(10);
}
$query = $this->db->get();
if($num_rows){
return $query->num_rows();
}
return $query->result_array();
}
Now you may call it as your requirements
//call for only count rows (without limit)
$this->model_name->get_rows('table_name', TRUE);
//call with limit for result rows
$this->model_name->get_rows('table_name');
with count you can do this with query builder I think
look at this code, you may want something like that
if($limit != null){
$this->db->limit($limit, $offset);
}
if($order_by != null){
$this->db->order_by($order_by, $sort);
}
$query = $this->db->get('courses');
$courses['rows'] = $query->result();
$courses['num_rows'] = $this->db->count_all('courses');
return $courses;

Merging multiple objects which uses same id

I'm trying to merge multiple objects (like Receipts, Reports, etc) with Collection->merge().
This is the code I used:
$receipts = Receipt::all();
$reports = Report::all();
$collection = $receipts->merge($reports);
This is the result:
The above screenshot shows two elements, but the third element is missing because it has the same id (id: "1") as the first one. What I'm trying to achieve is to display all three of them as a collection.
EDIT:
I need the result to be objects (collection) because I also use the code on my view, where I check the class to determine what to display. Also, I use this function to sort the objects in the collection.
$collection->sort(function($a, $b)
{
$a = $a->created_at;
$b = $b->created_at;
if ($a === $b) {
return 0;
}
return ($a > $b) ? 1 : -1;
});
I know that this is an old question, but I will still provide the answer just in case someone comes here from the search like I did.
If you try to merge two different eloquent collections into one and some objects happen to have the same id, one will overwrite the other. I dunno why it does that and if that's a bug or a feature - more research needed. To fix this just use push() method instead or rethink your approach to the problem to avoid that.
Example of a problem:
$cars = Car::all();
$bikes = Bike::all();
$vehicles = $cars->merge($bikes);
// if there is a car and a bike with the same id, one will overwrite the other
A possible solution:
$collection = collect();
$cars = Car::all();
$bikes = Bike::all();
foreach ($cars as $car)
$collection->push($car);
foreach ($bikes as $bike)
$collection->push($bike);
Source: https://medium.com/#tadaspaplauskas/quick-tip-laravel-eloquent-collections-merge-gotcha-moment-e2a56fc95889
I know i'm bumping a 4 years old thread but i came across this and none of the answers were what i was looking for; so, like #Tadas, i'll leave my answer for people who will come across this. After Looking at the laravel 5.5 documentation thoroughly i found that concat was the go-to method.
So, in the OP's case the correct solution would be:
$receipts = Receipt::all();
$reports = Report::all();
$collection = $receipts->concat($reports);
This way every element in the Report collection will be appended to every element in the Receipts collection, event if some fields are identical.
Eventually you could shuffle it to get a more visual appealing result for e.g. a view:
$collection->shuffle();
Another way to go about it is to convert one of your collections to a base collection with toBase() method. You can find it in Illuminate\Support\Collection
Method definition:
/**
* Get a base Support collection instance from this collection.
*
* #return \Illuminate\Support\Collection
*/
public function toBase()
{
return new self($this);
}
Usage:
$receipts = Receipt::all();
$reports = Report::all();
$collection = $receipts->toBase()->merge($reports);
You could put all collections in an array and use this. Depends on what you want to do with the collection.
$list = array();
$list = array_merge($list, Receipt::all()->toArray());
$list = array_merge($list, Report::all()->toArray());

Magento 1.7: Unable to incorporate joinField in product collection

I'm having a problem where I can't get add joinField() to a product collection.. I have no clue why it doesn't work because it should be really simple or throw some errors at least. Needless to say, it is driving me nuts. I'm interested in looking at products and the total dollar amount in sales from them. This is what I have from a book called "Magento PHP Developer's Guide" and the Magento Wiki.
public function getProducts($categoryId) {
$productCollection = Mage::getModel('catalog/category')->load($categoryId)
->getProductCollection()
->joinField('o', 'sales_flat_order_item', array('o.row_total', 'o.product_id'), 'main_table.entity_id = o.product_id');
}
// die; when uncommented, this function WILL NOT die here
return $productCollection;
}
I'm getting the ->joinField() method right out of the book, but it doesn't grab any product. Strangely, the function doesn't even return anything because when the die line is uncommented, the function does not terminate there. Instead, the front-end will simply just skip this function without throwing any errors (that I can see at this time) and just doesn't display any blocks using this function. What am I missing here?
It works when I remove joinField() like below.
$productCollection = Mage::getModel('catalog/category')->load($categoryIds)
->getProductCollection();
UPDATE:
Further testing show that the following works. Note that if I use main_table instead of e, it does not work. If I look at the query generated from this, main_table is not replaced by the main table; instead, query contains the literal string "main_table".
$productCollection = Mage::getModel('catalog/category')->load($categoryIds)
->getSelect()
->join(array('o' => 'sales_flat_order_item'),
'e.entity_id = o.product_id',
'o.row_total'
);
While this doesn't.
$productCollection = Mage::getModel('catalog/category')->load($categoryIds)
->joinTable(
array('o' => 'sales_flat_order_item'),
'e.entity_id = o.product_id',
'o.row_total'
);
Maybe I don't see some simple mistake.. but I just don't see what's wrong.
public function getProducts($categoryId) {
$productCollection = Mage::getModel('catalog/category')->load($categoryId)
->getProductCollection()
->joinTable( // JoinTable makes more sense.
array('o' => 'sales/order_item'),
'main_table.entity_id = o.product_id'
array('row_total'),
)
;
return $productCollection;
}
It will probably return all fields in the catalog_product_entity table PLUS the row_total from the sales_order_item table. You might be able to use addAttributeToSelect('o.product_id') right before the join, just to clear the unwanted fields.

Codeigniter Pagination: Run the Query Twice?

I'm using codeigniter and the pagination class. This is such a basic question, but I need to make sure I'm not missing something. In order to get the config items necessary to paginate results getting them from a MySQL database it's basically necessary to run the query twice is that right?
In other words, you have to run the query to determine the total number of records before you can paginate. So I'm doing it like:
Do this query to get number of results
$this->db->where('something', $something);
$query = $this->db->get('the_table_name');
$num_rows = $query->num_rows();
Then I'll have to do it again to get the results with the limit and offset. Something like:
$this->db->where('something', $something);
$this->db->limit($limit, $offset);
$query = $this->db->get('the_table_name');
if($query->num_rows()){
foreach($query->result_array() as $row){
## get the results here
}
}
I just wonder if I'm actually doing this right in that the query always needs to be run twice? The queries I'm using are much more complex than what is shown above.
Unfortunately, in order to paginate you must know how many elements you are breaking up into pages.
You could always cache the result for the total number of elements if it is too computationally expensive.
Yeah, you have to run two queries, but $this->db->count_all('table_name'); is one & line much cleaner.
Pagination requires reading a record set twice:
Once to read the whole set so that it can count the total number records
Then to read a window of records to display
Here's an example I used for a project. The 'banner' table has a list of banners, which I want to show on a paginated screen:
Using a public class property to store the total records (public $total_records)
Using a private function to build the query (that is common for both activities). The parameter ($isCount) we pass to this function reduces the amount of data the query generate, because for the row count we only need one field but when we read the data window we need all required fields.
The get_list() function first calls the database to find the total and stores it in $total_records and then reads a data window to return to the caller.
Remember we cannot access $total_records without first calling the get_list() method !
class Banner_model extends CI_Model {
public $total_records; //holds total records for get_list()
public function get_list($count = 10, $start = 0) {
$this->build_query();
$query = $this->db->get();
$result = $query->result();
$this->total_records = count($result); //store the count
$this->build_query();
$this->db->limit($count, $start);
$query = $this->db->get();
$result = $query->result();
return $result;
}
private function build_query($isCount = FALSE) {
$this->db->select('*, b.id as banner_id, b.status as banner_status');
if ($isCount) {
$this->db->select('b.id');
}
$this->db->from('banner b');
$this->db->join('company c', 'c.id = b.company_id');
$this->db->order_by("b.id", "desc"); //latest ones first
}
And now from the controller we call:
$data['banner_list'] = $this->banner_model->get_list();
$config['total_rows'] = $this->banner_model->total_records;
Things get complicated when you start using JOINs, like in my example where you want to show banners from a particular company! You may read my blog post on this issue further:
http://www.azmeer.info/pagination-hitting-the-database-twise/

Resources