When I execute a PDO statement, internally a result set is stored, and I can use ->fetch() to get a row from the result.
If I wanted to convert the entire result to an array, I could do ->fetchAll().
With Laravel, in the Query Builder docs, I only see a way to get an array result from executing the query.
// example query, ~30,000 rows
$scores = DB::table("highscores")
->select("player_id", "score")
->orderBy("score", "desc")
->get();
var_dump($scores);
// array of 30,000 items...
// unbelievable ...
Is there any way to get a result set from Query Builder like PDO would return? Or am I forced to wait for Query Builder to build an entire array before it returns a value ?
Perhaps some sort of ->lazyGet(), or ->getCursor() ?
If this is the case, I can't help but see Query Builder is an extremely short-sighted tool. Imagine a query that selects 30,000 rows. With PDO I can step through row by row, one ->fetch() at a time, and handle the data with very little additional memory consumption.
Laravel Query Builder on the other hand? "Memory management, huh? It's fine, just load 30,000 rows into one big array!"
PS yes, I know I can use ->skip() and ->take() to offset and limit the result set. In most cases, this would work fine, as presenting a user with 30,000 rows is not even usable. If I want to generate large reports, I can see PHP running out of memory easily.
After #deczo pointed out an undocumented function ->chunk(), I dug around in the source code a bit. What I found is that ->chunk() is a convenience wrapper around multiplying my query into several queries queries but automatically populating the ->step($m)->take($n) parameters. If I wanted to build my own iterator, using ->chunk with my data set, I'd end up with 30,000 queries on my DB instead of 1.
This doesn't really help, too, because ->chunk() takes a callback which forces me to couple my looping logic at the time I'm building the query. Even if the function was defined somewhere else, the query is going to happen in the controller, which should have little interest in the intricacies of my View or Presenter.
Digging a little further, I found that all Query Builder queries inevitably pass through \Illuminate\Database\Connection#run.
// https://github.com/laravel/framework/blob/3d1b38557afe0d09326d0b5a9ff6b5705bc67d29/src/Illuminate/Database/Connection.php#L262-L284
/**
* Run a select statement against the database.
*
* #param string $query
* #param array $bindings
* #return array
*/
public function select($query, $bindings = array())
{
return $this->run($query, $bindings, function($me, $query, $bindings)
{
if ($me->pretending()) return array();
// For select statements, we'll simply execute the query and return an array
// of the database result set. Each element in the array will be a single
// row from the database table, and will either be an array or objects.
$statement = $me->getReadPdo()->prepare($query);
$statement->execute($me->prepareBindings($bindings));
return $statement->fetchAll($me->getFetchMode());
});
}
See that nasty $statement->fetchAll near the bottom ?
That means arrays for everyone, always and forever; your wishes and dreams abstracted away into an unusable tool Laravel Query Builder.
I can't express the valley of my depression right now.
One thing I will say though is that the Laravel source code was at least organized and formatted nicely. Now let's get some good code in there!
Use chunk:
DB::table('highscores')
->select(...)
->orderBy(...)
->chunk($rowsNumber, function ($portion) {
foreach ($portion as $row) { // do whatever you like }
});
Obviously returned result will be just the same as calling get, so:
$portion; // array of stdObjects
// and for Eloquent models:
Model::chunk(100, function ($portion) {
$portion; // Collection of Models
});
Here is a way to use the laravel query builder for making the query, but to then use the underlying pdo fetch to loop over the record set which I believe will solve your problem - running one query and looping the record set so you don't run out of memory on 30k records.
This approach will use all the config stuff you setup in laravel so you don't have to config pdo separately.
You could also abstract out a method to make this easy to use that takes in the query builder object, and returns the record set (executed pdo statement), which you would then while loop over as below.
$qb = DB::table("highscores")
->select("player_id", "score")
->orderBy("score", "desc");
$connection = $qb->getConnection();
$pdo = $connection->getPdo();
$query = $qb->toSql();
$bindings = $qb->getBindings();
$statement = $pdo->prepare($query);
$statement->execute($bindings);
while ($row = $statement->fetch($connection->getFetchMode()))
{
// do stuff with $row
}
Related
I frequently encounter the programming pattern which takes a Eloquent Query object and adds a filter condition to it. (You may think of a filter as a scope, if you like.) This mostly happens in the context of authorization, whether an authenticated user may or may not see certain subsets of a result.
The pattern looks like this:
use Illuminate\Database\Eloquent\Builder;
public function applyFilter(Builder $query): Builder {
$model = $query->getModel();
if (!($model instanceof MyModel)) {
throw new \InvalidArgumentException('the given query does not query for <MyModel>');
}
$user = Auth::user();
return $query->where(function (Builder $subquery) use ($user) {
// Add some filter conditions to the query
// Typically those depend on the currently authenticated user,
// privileges of the user and and some properties of the model.
});
}
Sometimes it happens to be already clear that the query must not return any result, just from inspecting the $user object or some other property like the state of the application, even without querying for the actual models. In those cases, the filter function above adds a condition to the query which always evaluates to false like ->where('1', '=', '2') or similar. Although, this does the trick and works perfectly, it feels wrong. Eloquent still queries the DB back-end and receives the (expected) empty result.
Primary question:
Is there a way, how one could tell Eloquent that a query is known to return the empty result and that no actual query must be made, but that ->get (and friends) simply return an empty collection? I am thinking of a method like $query->toNoOp() or similar.
Further question:
Moreover, also consider the case where the construction above is used as part of a more complex query and happens to end up in a sub-query. For example consider the following case:
$relatedModels = MyRelatedModel::query()
->where(...)
->whereHas('my_model', $q => applyFilter($q))
->get();
At the moment, this construction results into the rather complicated SQL query
SELECT *
FROM my_related_models
WHERE
...
AND
EXISTS (
SELECT *
FROM my_models
WHERE
my_models.id = my_related_models.model_id
AND
1 = 2
);
Although, this again returns the expected empty result, it would be much better if the fact that the inner query is a "no-op" could be propagated to the outer query. In other words, if a sub-query is used within an EXIST clause (constructed by ->whereHas and friends) and this sub-query is known to be a "no-op" query, then the outer query should also become a "no-op" query.
If you know the query is invalid, you should not even run the query / execute it. Just make your API return empty array.
public function index() {
if (! $this->shouldRunQuery(auth()->user())) {
return response()->json([]);
}
return response()->json(Model::query()->get());
}
In your sub question, you can execute queries depending on conditions with when(), this is thou handled by Laravel and not by SQL.
Model::query()
->when($this->shouldRunQuery(auth()->user()), function ($query) {
// your complex query
})
->get();
In general, you seem very fascinating about solving the problem in SQL, instead of handling conditions and states even before the SQL is executed. I have seen a lot, but never seen anyone execute this approach.
I have quite an issue I can not seem to solve. I am trying to get a row count from a select statement.
I should start by saying I have tried most all methods resulting from google searches on this issue.
I am using the result set so I would prefer not to make a second query.
The query uses a prepared select statement which seems to be a main issue if I understand it correctly.
I decided to try a simple approach using PHP's native count() function. Which lead me here because I finally reached the end of the rope on this.
On to the details...within a class of mine, I make the query like this.
// Create database connection
$database = DatabaseFactory::getFactory()->getConnection();
// Set and execute database query
$sql = "SELECT * FROM `service_orders` WHERE `agency_id` = :agency_id $filter ORDER BY $sort $order $per_page";
$query = $database->prepare($sql);
$query->execute($query_array);
// If results
if ($query->rowCount() > 0) {
$results = $query->fetchAll();
self::$order_count = "Count: " . count($results);
return $results;
}
// Default, return false
return false;
Findings
If I perform count($results) like I did above, I get the total rows in the database (Let's say 50).
If I print_r($results), it shows the array with the proper number of entries (Let's say 10) that of course differs from the total rows in the database.
How can these two differ? It's as if the count($results) is misreading the result array.
More Findings
Within my actual php page, I call the class to retrieve the data like this.
$results = OrderModel::getServiceOrders();
echo count($results);
Strangely enough, if I then perform count($results) it gives me the correct reading of the result array (which in my example here would be 10).
I am perplexed by this as the count function is being performed on the exact same array. The only difference is one is called on the array within the class, and the other is called on the array returned from the class.
Does anyone have any ideas on how to solve this or why there is the discrepancy when using count() in this instance?
Thank you all in advance!
James
Additional Info
This is another mind numbing scenario. If I return the count along with the actual results, I can access it on the page with the correct value (10 rows). Yet, if I set it into a session variable, and access it that way, the count is the whole data set (50 rows). How is it even possible these two values are not the same?!
$results = $query->fetchAll();
Session::set("order_count", $total[0]); // Yields 50 (incorrect)
return [
"results"=> $results,
"count"=> $total[0], // Yields 10 (correct)
];
I have a query that returns roughly 6,000 results. Although this query executes in under a second in MySQL, once it is run through Zend Framework 2, it experiences a significant slowdown.
For this reason, I tried to do it a more "raw" way with PDO:
class ThingTable implements ServiceLocatorAwareInterface
{
// ...
public function goFast()
{
$db_config = $this->getServiceLocator()->get('Config')['db'];
$pdo = new PDO($db_config['dsn'], $db_config['username'], $db_config['password']);
$statement = $pdo->prepare('SELECT objectNumber, thingID, thingmaker, hidden, title FROM Things ', array(PDO::MYSQL_ATTR_COMPRESS, PDO::CURSOR_FWDONLY));
$statement->execute();
return $statement->fetchAll(PDO::FETCH_ASSOC);
}
}
This doesn't seem to have much of a speedup, though.
I think the problem might be that Zend is still trying to create a new Thing object for each record, even though it is only a partial list of columns. I'd really be okay not populating any objects. I really just need a few columns with those attributes to iterate over.
As suggested by user MonkeyZeus, the following was used for bench-marking:
$start = microtime(true);
$result = $statement->fetchAll(PDO::FETCH_ASSOC);
echo (microtime(true) - $start).' seconds';
And in response:
In a VM, that returns 0.0050520896911621. This is in line with what it
is when I just run the command straight in MySQL. I believe the
overhead is in Zend, but not sure how to quite go about that. Again if
I had to guess, I'd say it is because Zend is adding overhead while
trying to be nice with the results, but I'm not quite sure how to
proceed after that.
[I'm] not so worried about the query. It is a single select statement.
goFast() gets called by the Zend indexAction() --similar to other
actions used across the project--this one is just way slower at
returning the page. One problem I found was that Zend's $this->url()
was slowing things down a bit. So I removed those, but the performance
still isn't great.
How can I speed this up?
When you say , that query runs under a second in MySQL , what do you mean ? did you try to run this query and print ALL 6000 rows ? or you just queried this and command line printed first/last few of them ?
The problem might be that , you are fetching them all , going through cursor , you are copying all the data ( 6000 rows ) from MySQL to PHP and then returning it , are you sure you want to do this ?
Maybe you could return a statement/cursor to the Query and then iterate through rows when you really need it ?
Your problem is not the SQL itself , but fetching them into PHP array all at once.
You can test it by logging the time it needs to actually execute SQL and fetching it into PHP array.
Do not use fetchall , return the statement itself and in the function/code where you have to run "foreach" this array , use statement to fetch each row one by one.
I'm quite annoyed with CI's behavior lately, may be because i'm little ignorant towards small stuff, now to the question :D. i'm making an application where i'm trying to get one single row using active records, now as a good programmer(i think) i try this
when i select such as login
if($query->num_rows() == 1)
{
// do something
}
// now when i make an update, that also a single row i tend to use this command
if($query->affected_rows() == 1)
{
// perform some action
}
// now i use this code when i've to get all entries of one single user
if($query->num_rows() > 0)
{
// show them
}
now many time i get error like "Call to member function num_rows() on non object"
can anyone please explain what to use, when to use and how to use, so that code igniter never gets made a me
1.num_rows()
num_rows() are result helper functions, they are used against a query resultobject, note that we don't use it like $this->db->num_rows(), instead we use it like:
$query = $this-query('query');
OR
$query = $this->db->get('tableName');
if($query->num_rows()>0){ ....}
Now lets see affected_rowS().
2. affected_rows()
affected_rows is a query helper function, they are used like $this->db->affected_rows(). Note that $this->db->affected_rows() yields valid result only after a valid db write operation. i.e insert, update and delete. The database class has a small hack that allows it to return the correct number of affected rows after a delete operation because MySQL "DELETE FROM TABLE" returns 0 affected rows.
3.$query->result()
This function returns the query result as an array of objects, or an empty array on failure.
i.e if you have more than one result you have to loop the result object like :
foreach ($query->result() as $row){....}
4.$query->row()
This function returns a single result row as an object. If your query has more than one row, it returns only the first row.
I hope my explanation is readable enough.. Although you could find all of these in CI docs with relevant examples.
I'm a bit new to OOP, but i've been playing with it for about a month now. Usually, i create a class called Mysql which has a __construct function that connects to a database directly. And after that i have lots of different functions that gets or inserts data into different tables.
On the bus home today, i began thinking and i came up with a brilliant idea that would make it less cluttered. My idea is to use one single function that selects data (and one for inserting), and depending on how the query that's passed in looks, it will select different data from different tables. Nice, right?
But i'm kind of stuck here. I'm not sure at all how to achieve this. I've got a small clue how it could work, but i don't know how i would bind the results, or fetch them into an array. The query will be created in another method, and then be passed into the select/insert function within the Mysql class.
I drew a "sketch" on how i think it may work. Here it is:
Of course, the function below will be placed in the Mysql class, and will already have connection to a database.
// This is an example query that could be passed in.
$query = "SELECT * FROM table WHERE id=150";
function select_data($query) {
if ( $smtp = $this->conn->prepare($query) ) {
$smtp->execute();
$smtp->bind_results(What happens here?);
if ( $smtp->fetch() ) {
foreach ( fetched_row? as $key => $value ) {
$return[] = $key => $value;
}
return $return;
}
else return NULL;
}
else return $this->conn->error;
}
Thanks a lot to anyone who can show me how this can be achieved.
You have more options to use in PHP and they has their own specifics.
I can recommend some ORM
like Doctrine because of ease of use, stability, community and most importantly efectivity.
You can use it as easy as:
$q = Doctrine_Query::create()
->select('u.username, p.phone')
->from('User u')
->leftJoin('u.Phonenumbers p');
$users = $q->fetchArray();
or:
// Delete phonenumbers for user id = 5
$deleted = Doctrine_Query::create()
->delete()
->from('Phonenumber')
->andWhere('user_id = 5')
->execute();
// Make all usernames lowercase
Doctrine_Query::create()
->update('User u')
->set('u.username', 'LOWER(u.username)')
->execute();
// 'like' condition
$q = Doctrine_Query::create()
->from('User u')
->where('u.username LIKE ?', '%jwage%');
$users = $q->fetchArray();
I think you are running into problems when you need related data. In other words, when an object of yours has a property which is another object that data should also be gathered and dynamically filled. I once came pretty far but when stuff like INNER, LEFT and RIGHT joins come accross you'll think twice about going further ;)
About bind_results:http://php.net/manual/en/mysqli-stmt.bind-result.php
(Maybe slightly off topic; SMTP? That's a mailprotocol, are you sure you don't mean MySQLi's STMT?)
For reference, PDO already does a lot of what you seem to want to do. It even has a fetchAll method that returns an array of rows, much like your function does. You don't need to bind anything in order to use it, unless you have parameters in your query string (and of course, values to bind to those parameters).
Check out the PDO documentation, and see if that doesn't fit your needs. Particularly PDOStatement->fetchAll().