Codeigniter ActiveRecord and this->db->query conflict - codeigniter

I am experiencing a strange phenomenon while mixing $this->db->where() and $this->db->query().
if (!empty($args) && is_array($args)) {
foreach ($args as $key => $value) {
$this->db->where_in($field, $value);
}
}
$result = $this->db->query('SELECT * FROM ...')->result_array();
While I can understand it is probably better to not mix ActiveRecord and regular queries, I am experiencing that the WHERE clause I am building up with $this->db->where_in() is affecting subsequent queries (i.e., the clause executes on next query).
Should all my queries be rewritten to either not use ActiveRecord, or to use ONLY ActiveRecord? Or is there a way to prevent this "clash"? I thought about $ths->db->flush_cache() but I am not using database caching, and using it does not seem to have any effect.

I would suggest not calling db->where_in in a loop. Consider instead
if (!empty($args) && is_array($args))
{
foreach ($args as $key => $value)
{
$values[] = $value;
}
$this->db->where_in($field, $values);
}
In truth the loop in not needed if $args is an indexed (as opposed to associative) array. You could simply remove the foreach and use
$this->db->where_in($field, $args);
You probably shouldn't mix Query Builder (previously known as Active Record) and db->query(). The Query Builder (QB) methods essentially assemble a statement that you would provide to db->query() and then, pretty much literally calls query("The statement that QB built").
IMO, it is almost always easier to type the query statement and use it in $this->db->query("Select * from ...");. Query Builder is great but is often more work that it is worth.
In your example you should probably stick to Query Builder. That can still be very concise. If you're looking for all fields from a single table then db->get will do the job.
$result = $this->db->get('table_name')->result_array();
Typically QB clears all the various elements created that go into the statement when the statement is executed with a get, insert, or update call. You can start fresh at any point with this call.
$this->db->reset_query();

Related

When to use codeigniter where() function and when get_where()

I've been reading docs regarding these 2 functions, but I still can't quite get the difference between these two.
I get it that get_where selects data from DB, but when should we use where() function and not get_where()?
get_where()
There are tons of other ways to get data using CodeIgniter’s ActiveRecord implementation, but you also have full SQL queries if you need them:
Example:
$query = $this->db->get_where('people', array('id' => 449587));
Ultimately, get_where() is the naive case, and certainly the most commonly-used in my code anyway — I can’t think of an another framework in any other language that enables you to be this productive with data with a single line of code.
get_where([$table = ''[, $where = NULL[, $limit = NULL[, $offset = NULL]]]])
Parameters:
$table (mixed) – The table(s) to fetch data from; string or array
$where (string) – The WHERE clause
$limit (int) – The LIMIT clause
$offset (int) – The OFFSET clause
This function is working as get() but with also allows the WHERE to be added directly.
Identical to the $this->db->get(); except that it permits you to add
a where clause in the second parameter, instead of using the
db->where() function.
where()
This function enables you to set WHERE clauses in your query.
You can also add where clauses, sort conditions and so forth:
$this->db->select('first_name', 'last_name');
$this->db->from('people');
$this->db->where('id', 449587);
$this->db->order_by('last_name, first_name');
$query = $this->db->get();
It’s possible to chain all these conditions together on a single line, but I prefer putting them on separate lines for readability.
In simple word, get_where is a luxury to use but where() gives you more flexibility to use.
The get_where is a combined function -so to speak- of the both where() and get() functions,
according to the documentation :
$this->db->get_where()
Identical to the above function except that it permits you to add a
"where" clause in the second parameter, instead of using the
db->where() function
also by take a quick look at the source code of the get_where() method you will notice that
if ($where !== NULL)
{
$this->where($where);
}
where $where is the second parameter of get_where() method.
In simple terms, $this->db->get_where('table name', 'where clause') is an alias for $this->db->where('where clause')->get('table name');

Laravel - Collection with relations take a lot of time

We are developing an API with LUMEN.
Today we had a confused problem with getting the collection of our "TimeLog"-model.
We just wanted to get all time logs with additional informationen from the board model and task model.
In one row of time log we had a board_id and a task_id. It is a 1:1 relation on both.
This was our first code for getting the whole data. This took a lot of time and sometimes we got a timeout:
BillingController.php
public function byYear() {
$timeLog = TimeLog::get();
$resp = array();
foreach($timeLog->toArray() as $key => $value) {
if(($timeLog[$key]->board_id && $timeLog[$key]->task_id) > 0 ) {
array_push($resp, array(
'board_title' => isset($timeLog[$key]->board->title) ? $timeLog[$key]->board->title : null,
'task_title' => isset($timeLog[$key]->task->title) ? $timeLog[$key]->task->title : null,
'id' => $timeLog[$key]->id
));
}
}
return response()->json($resp);
}
The TimeLog.php where the relation has been made.
public function board()
{
return $this->belongsTo('App\Board', 'board_id', 'id');
}
public function task()
{
return $this->belongsTo('App\Task', 'task_id', 'id');
}
Our new way is like this:
BillingController.php
public function byYear() {
$timeLog = TimeLog::
join('oc_boards', 'oc_boards.id', '=', 'oc_time_logs.board_id')
->join('oc_tasks', 'oc_tasks.id', '=', 'oc_time_logs.task_id')
->join('oc_users', 'oc_users.id', '=', 'oc_time_logs.user_id')
->select('oc_boards.title AS board_title', 'oc_tasks.title AS task_title','oc_time_logs.id','oc_time_logs.time_used_sec','oc_users.id AS user_id')
->getQuery()
->get();
return response()->json($timeLog);
}
We deleted the relation in TimeLog.php, cause we don't need it anymore. Now we have a load time about 1 sec, which is fine!
There are about 20k entries in the time log table.
My questions are:
Why is the first method out of range (what causes the timeout?)
What does getQuery(); exactly do?
If you need more information just ask me.
--First Question--
One of the issues you might be facing is having all those huge amount of data in memory, i.e:
$timeLog = TimeLog::get();
This is already enormous. Then when you are trying to convert the collection to array:
There is a loop through the collection.
Using the $timeLog->toArray() while initializing the loop based on my understanding is not efficient (I might not be entirely correct about this though)
Thousands of queries are made to retrieve the related models
So what I would propose are five methods (one which saves you from hundreds of query), and the last which is efficient in returning the result as customized:
Since you have many data, then chunk the result ref: Laravel chunk so you have this instead:
$timeLog = TimeLog::chunk(1000, function($logs){
foreach ($logs as $log) {
// Do the stuff here
}
});
Other way is using cursor (runs only one query where the conditions match) the internal operation of cursor as understood is using Generators.
foreach (TimeLog::where([['board_id','>',0],['task_id', '>', 0]])->cursor() as $timelog) {
//do the other stuffs here
}
This looks like the first but instead you have already narrowed your query down to what you need:
TimeLog::where([['board_id','>',0],['task_id', '>', 0]])->get()
Eager Loading would already present the relationship you need on the fly but might lead to more data in memory too. So possibly the chunk method would make things more easier to manage (even though you eagerload related models)
TimeLog::with(['board','task'], function ($query) {
$query->where([['board_id','>',0],['task_id', '>', 0]]);
}])->get();
You can simply use Transformer
With transformer, you can load related model, in elegant, clean and more controlled methods even if the size is huge, and one greater benefit is you can transform the result without having to worry about how to loop round it
You can simply refer to this answer in order to perform a simple use of it. However incase you don't need to transform your response then you can take other options.
Although this might not entirely solve the problem, but because the main issues you face is based on memory management, so the above methods should be useful.
--Second question--
Based on Laravel API here You could see that:
It simply returns the underlying query builder instance. To my observation, it is not needed based on your example.
UPDATE
For question 1, since it seems you want to simply return the result as response, truthfully, its more efficient to paginate this result. Laravel offers pagination The easiest of which is SimplePaginate which is good. The only thing is that it makes some few more queries on the database, but keeps a check on the last index; I guess it uses cursor as well but not sure. I guess finally this might be more ideal, having:
return TimeLog::paginate(1000);
I have faced a similar problem. The main issue here is that Elloquent is really slow doing massive task cause it fetch all the results at the same time so the short answer would be to fetch it row by row using PDO fetch.
Short example:
$db = DB::connection()->getPdo();
$query_sql = TimeLog::join('oc_boards', 'oc_boards.id', '=', 'oc_time_logs.board_id')
->join('oc_tasks', 'oc_tasks.id', '=', 'oc_time_logs.task_id')
->join('oc_users', 'oc_users.id', '=', 'oc_time_logs.user_id')
->select('oc_boards.title AS board_title', 'oc_tasks.title AS task_title','oc_time_logs.id','oc_time_logs.time_used_sec','oc_users.id AS user_id')
->toSql();
$query = $db->prepare($query->sql);
$query->execute();
$logs = array();
while ($log = $query->fetch()) {
$log_filled = new TimeLog();
//fill your model and push it into an array to parse it to json in future
array_push($logs,$log_filled);
}
return response()->json($logs);

Is it possible to eager load arbitrary queries in Eloquent?

I'm working in Laravel 4, and I have a Child model with multiple EducationProfiles:
class Child extends EloquentVersioned
{
public function educationProfiles()
{
return $this->hasMany('EducationProfile');
}
}
If I wanted to get all the EducationProfiles for each kid under age 10 it would be easy:
Child::where('date_of_birth','>','2004-03-27')->with('educationProfiles')->all();
But say (as I do) that I would like to use with() to grab a calculated value for the Education Profiles of each of those kids, something like:
SELECT `education_profiles`.`child_id`, GROUP_CONCAT(`education_profiles`.`district`) as `district_list`
In theory with() only works with relationships, so do I have any options for associating the district_list fields to my Child models?
EDIT: Actually, I was wondering whether with('educationProfiles') generates SQL equivalent to:
EducationProfile::whereIn('id',array(1,2,3,4))
or whether it's actually equivalent to
DB::table('education_profiles')->whereIn('id',array(1,2,3,4))
The reason I ask is that in the former I'm getting models, if it's the latter I'm getting unmodeled data, and thus I can probably mess it up as much as I want. I assume with() generates an additional set models, though. Anybody care to correct or confirm?
Ok, I think I've cracked this nut. No, it is NOT possible to eager load arbitrary queries. However, the tools have been provided by the Fluent query builder to make it relatively easy to replicate eager loading manually.
First, we leverage the original query:
$query = Child::where('date_of_birth','>','2004-03-27')->with('educationProfiles');
$children = $query->get();
$eagerIds = $query->lists('id');
Next, use the $eagerIds to filterDB::table('education_profile') in the same way that with('educationProfiles') would filter EducationProfile::...
$query2 = DB::table('education_profile')->whereIn('child_id',$eagerIds)->select('child_id', 'GROUP_CONCAT(`education_profiles`.`district`) as `district_list`')->groupBy('child_id');
$educationProfiles = $query2->lists('district_list','child_id');
Now we can iterate through $children and just look up the $educationProfiles[$children->id] values for each entry.
Ok, yes, it's an obvious construction, but I haven't seen it laid out explicitly anywhere before as a means of eager loading arbitrary calculations.
You can add a where clause to your hasMany() call like this:
public function educationProfilesUnderTen() {
$ten_years_ago = (new DateTime('10 years ago'))->format('Y-m-d');
return $this->hasMany('EducationProfile')->where('date_of_birth', '>', $ten_years_ago)
}

Eloquent Collection: Counting and Detect Empty

This may be a trivial question but I am wondering if Laravel recommends a certain way to check whether an Eloquent collection returned from $result = Model::where(...)->get() is empty, as well as counting the number of elements.
We are currently using !$result to detect empty result, is that sufficient? As for count($result), does it actually cover all cases, including empty result?
When using ->get() you cannot simply use any of the below:
if (empty($result)) { }
if (!$result) { }
if ($result) { }
Because if you dd($result); you'll notice an instance of Illuminate\Support\Collection is always returned, even when there are no results. Essentially what you're checking is $a = new stdClass; if ($a) { ... } which will always return true.
To determine if there are any results you can do any of the following:
if ($result->first()) { }
if (!$result->isEmpty()) { }
if ($result->count()) { }
if (count($result)) { }
You could also use ->first() instead of ->get() on the query builder which will return an instance of the first found model, or null otherwise. This is useful if you need or are expecting only one result from the database.
$result = Model::where(...)->first();
if ($result) { ... }
Notes / References
->first() http://laravel.com/api/4.2/Illuminate/Database/Eloquent/Collection.html#method_first
isEmpty() http://laravel.com/api/4.2/Illuminate/Database/Eloquent/Collection.html#method_isEmpty
->count() http://laravel.com/api/4.2/Illuminate/Database/Eloquent/Collection.html#method_count
count($result) works because the Collection implements Countable and an internal count() method: http://laravel.com/api/4.2/Illuminate/Database/Eloquent/Collection.html#method_count
Bonus Information
The Collection and the Query Builder differences can be a bit confusing to newcomers of Laravel because the method names are often the same between the two. For that reason it can be confusing to know what one you’re working on. The Query Builder essentially builds a query until you call a method where it will execute the query and hit the database (e.g. when you call certain methods like ->all() ->first() ->lists() and others). Those methods also exist on the Collection object, which can get returned from the Query Builder if there are multiple results. If you're not sure what class you're actually working with, try doing var_dump(User::all()) and experimenting to see what classes it's actually returning (with help of get_class(...)). I highly recommend you check out the source code for the Collection class, it's pretty simple. Then check out the Query Builder and see the similarities in function names and find out when it actually hits the database.
Laravel 5.2 Collection Class
Laravel 5.2 Query Builder
I think you are looking for:
$result->isEmpty()
This is different from empty($result), which will not be true because the result will be an empty collection. Your suggestion of count($result) is also a good solution. I cannot find any reference in the docs
I agree the above approved answer. But usually I use $results->isNotEmpty() method as given below.
if($results->isNotEmpty())
{
//do something
}
It's more verbose than if(!results->isEmpty()) because sometimes we forget to add '!' in front which may result in unwanted error.
Note that this method exists from version 5.3 onwards.
There are several methods given in Laravel for checking results count/check empty/not empty:
$result->isNotEmpty(); // True if result is not empty.
$result->isEmpty(); // True if result is empty.
$result->count(); // Return count of records in result.
I think better to used
$result->isEmpty();
The isEmpty method returns true if the collection is empty; otherwise,
false is returned.
According to Laravel Documentation states you can use this way:
$result->isEmpty();
The isEmpty method returns true if the collection is empty; otherwise, false is returned.
I think you try something like
#if(!$result->isEmpty())
// $result is not empty
#else
// $result is empty
#endif
or also use
if (!$result) { }
if ($result) { }
You can do
$result = Model::where(...)->count();
to count the results.
You can also use
if ($result->isEmpty()){}
to check whether or not the result is empty.
so Laravel actually returns a collection when just using Model::all();
you don't want a collection you want an array so you can type set it.
(array)Model::all(); then you can use array_filter to return the results
$models = (array)Model::all()
$models = array_filter($models);
if(empty($models))
{
do something
}
this will also allow you to do things like count().
You can use: $counter = count($datas);
The in_array() checks if a value exists in an array.
public function isAbsolutelyEmpty($value)
{
return in_array($value, ["", "0", null, 0, 0.0], true);
}
You want to check these two cases of count().
#1
If the result contains only a single row (one record) from the database by using ->first().
if(count($result)) {
// record is exist true...
}
#2
If result contain set of multiple row (multiple records) by using ->get() or ->all().
if($result->count()) {
//record is exist true...
}

How to average multiple columns using Eloquent?

I'm looking to get the average value across multiple columns on a related model, something like this:
$this->reviews()->avg('communication', 'friendliness')
Where communication and friendliness are an array of column names. However it appears the aggregate functions only support single column names, so I'm doing this:
$attributes = array('communication', 'friendliness');
$score = array();
foreach ($attributes as $attribute)
{
$score[] = $this->reviews()->avg($attribute);
}
return round(array_sum($score) / sizeof($attributes), 1);
Which results in multiple queries. Any suggestions for a best practice here?
Thanks
To avoid multiple queries you can use a raw database expression within Eloquent as shown below:
$averages = $this->reviews()
->select(DB::raw('avg(communication) c, avg(friendliness) f'))
->first();
echo $averages->c;
echo $averages->f;
Since the aggregate function name avg is recognized by all supported database by Laravel, this will not be a big deal.

Resources