visit
I ran the tests, made a minimal change, and ran the tests again. It is like saying hello to a codebase I have never seen before. Two-hundred something tests in twenty seconds. Ok, we can be friends. Let’s focus on my task and run testTotalSpent()
only. Nine seconds. What the? Tried again. (Do you have also this habit of not believing your eyes and trying the same thing again, fingers crossed?) Nine seconds. Annoying. But I set out to solve a different problem, I realized.
My first Eloquent query felt like “look ma, no hands.” It had some syntactic errors, though. I left out a few parens accidentally. And found a few peculiarities of the database. The customers.customer_id
refers to … no, not to itself (what would that mean anyway?) but to users.id
. Some references are even reversed which is fine as long it is only a one-to-one relation. One day, they will come back and shoot you in the foot. Some fields are remnants of an optimistic design decision back in the old days, like products.price
. They are not used anymore. Probably.
No one had the courage to remove them and rely on the new prices
table only. The code does some gymnastics to handle these cases. I seriously considered refactoring the whole pricing and product model. A long-time friend talked me out of it. One of the reasons I call him a friend.
My junior colleague already had a simple solution. He commented out a line in the test case, the line responsible for all the cleanup magic that says use RefreshTable
. He just had to pay attention and never commit this change. If he accidentally did, he could fix it in the next commit. Not very elegant. I can do better.
Four commits later, it worked on my machine. I dug elbow-deep into phpunit configuration. Finally, the tables stood naked in front of me with all their data exposed. It was easy to see now what I had got wrong. The OrderItem
table contained so many fields that they did not fit all on my screen. I scrolled to the right to see what to be had, then I simply ignored what I did not understand. The fields are displayed not alphabetically but in the order of creation. Some important fields were added later, like price
whose value gets copied from the Prices
table at the time the order is placed.
I typed a neat Eloquent query, six lines altogether, some join
s, some where
s, and a groupBy
. Let me reproduce it here to keep it for eternity
OrderItem::query
->join('orders', 'orders.customer_id', '=', 'customer.id')
->join('order_items', 'order_items.order_id', '=', 'orders.id')
->where('order_items.created_at', '>=', $startDate)
->where('customer.id', $customer->id)
->sum('order_item.price')
->groupBy('order_item.type');
I captured the essence of the data structure. This is the moment when the symphonic orchestra starts to play in a movie. The main character slowly lifts his hand. And hits Enter on the keyboard. Cut to his face. You see anticipation. The first signs of victory. Then something goes awry. His face melts into disbelief. Cut to the screen. The cursor blinks. Data is printed. totalSpent: [Buy => 3]
. Obviously wrong. No “Rent”, only “Buy”? And a month worth of spending cannot be three.
In SQL we trust, in Eloquent we do not. The query builder has a toSql()
method. It is too smart (or too dumb, depending on how you look at it) and returns a question mark for the parameters. This is not a reason to despair, getBindings()
will give you the parameters you want. A few lines of PHP wizardry from Stackoverflow merge these two into a SQL statement.
$query = str_replace(array('?'), array('\'%s\''), $builder->toSql());
$query = vsprintf($query, $builder->getBindings());
This method is not flawless, though. I tried to execute the generated query. Mysql complained about the last line, about the group-by clause. Nobrainer. I deleted the line. After all, I wanted to see the records selected. Grouping them is secondary at this point. The bastard did not run out of complaints. About the select clause this time. Understandable, without a group-by, it makes no sense to aggregate the prices. Delete that line too. Replace it with a simple select *
, it never hurts. It does not execute, either, since no table is specified. Ok, select order_items.*
. Now shut up and show me what you have.
number_of_commits_ahead_of_main: 8
number_of_commits_behind_main: 32
days_passed_fixing_the_damn_thing: 19
pressure_that_i_should_show_something: 1.8 bar
I decided to start afresh. A new branch, a new life. Instead of the select()
method of my first attempt, I used
->selectRaw('order_items.type, sum(order_items.price * order_items.quantity)'
OrderItem::select()
->groupBy('order_item.type')
->sum('order_item.price')
Error Code: 1055. Expression #1 of SELECT list is not in GROUP BY clause and contains nonaggregated column 'order_items.id' which is not functionally dependent on columns in GROUP BY clause; this is incompatible with sql_mode=only_full_group_by
There is this sql_mode
parameter that seems to be set to only_full_group_by
by default. But look at this snippet from my config/database.php
'connections' => [
'mysql' => [
'strict' => false,
]
]
It instructs Mysql to behave a lot more relaxed. That includes skipping checking the group-by clause. Eloquent was not at fault here. As I said earlier: read the error message.