When I first RoR programming, I was heavily dependent on Ruby language and Rails framework, so when searching for key on google I always added a keyword “ruby” or “rails”. At that time, mysql was really just a “data store” and not “a database management system”.
When encountering a potato problem about retrieving data, I will always search google with the keyword “rails” to see if there are any results related to the model, and if there is no workaround then: “Wow, this way. You can be indifferent! “.
It was a huge mistake of mine, because I completely ignored the cool features of SQL and followed the rather rigid things of the programming language. The following article I will present a tip I’ve just learned from a lead brother, I’m not sure if it is good or not, is it new to everyone, but until now, it helped me. A lot of hope and hope that it can help everyone in a certain case (in case the project rules allow writing this way).
So what’s the tip here?
Have you ever encountered a situation like this or similar?
You have 1 table but there are 2 date type fields to store 2 different date types (if a record has date_a, there is no date_b and vice versa) and the spec must sort both of those tables at the same time as ASC or DESC as in the example. after:
We have the User table has 2 fields date_a and date_b:
id | date_a | date_b |
---|---|---|
first | August 26, 2020 | nil |
2 | nil | June 4, 2020 |
3 | 04-04-2020 | nil |
4 | nil | September 23, 2020 |
And the spec forces us not to sort like this:
1 2 | scope <span class="token symbol">:sort_by_both_date</span> <span class="token punctuation">,</span> <span class="token operator">-</span> <span class="token operator">></span> <span class="token punctuation">{</span> order <span class="token punctuation">(</span> date_a <span class="token punctuation">:</span> <span class="token symbol">:asc</span> <span class="token punctuation">,</span> date_b <span class="token punctuation">:</span> <span class="token symbol">:asc</span> <span class="token punctuation">)</span> <span class="token punctuation">}</span> |
query:
1 2 | <span class="token keyword">SELECT</span> <span class="token punctuation">`</span> users <span class="token punctuation">`</span> <span class="token punctuation">.</span> <span class="token operator">*</span> <span class="token keyword">FROM</span> <span class="token punctuation">`</span> users <span class="token punctuation">`</span> <span class="token keyword">WHERE</span> <span class="token keyword">ORDER</span> <span class="token keyword">BY</span> <span class="token punctuation">`</span> users <span class="token punctuation">`</span> <span class="token punctuation">.</span> <span class="token punctuation">`</span> date_a <span class="token punctuation">`</span> <span class="token keyword">ASC</span> <span class="token punctuation">,</span> <span class="token punctuation">`</span> users <span class="token punctuation">`</span> <span class="token punctuation">.</span> <span class="token punctuation">`</span> date_a <span class="token punctuation">`</span> <span class="token keyword">ASC</span> |
result:
id | date_a | date_b |
---|---|---|
3 | 04-04-2020 | nil |
first | August 26, 2020 | nil |
2 | nil | June 4, 2020 |
4 | nil | September 23, 2020 |
as we see 04-06 less than 26-08 but below because its date_a
column is simply nil
. And the above query means to order date_a
first if two or more records with the value of date_a
are equal, then date_b:: asc
is really valid (it will sort the children with equal date_a
based on date_b
)
But for example the customer says “No, I don’t like it, I want it to sort both at the same time like this:”
id | date_a | date_b |
---|---|---|
3 | 04-04-2020 | nil |
2 | nil | June 4, 2020 |
first | August 26, 2020 | nil |
4 | nil | September 23, 2020 |
“That’s very difficult”, if you search google with the keyword “rails”, then it will make sense or if there is another way, it will be ambiguous. So now is the time to use SQL, but what else? I believe most people will think of SQL right away: D
So the problem is how to write to sort as above?
I used the conditional functions of SQL (IF ELSE, CASE WHEN), which at that time I thought “Eh SQL has IF ELSE?”:
In addition, I have added a flag column to use the IF control (if flag = true, use date_a, flag = false, then use date_b to sort). So now we have the following table:
id | date_a | date_b | flag |
---|---|---|---|
first | August 26, 2020 | nil | true |
2 | nil | June 4, 2020 | false |
3 | 04-04-2020 | nil | true |
4 | nil | September 23, 2020 | false |
and the first 1 scope as follows:
1 2 | scope <span class="token symbol">:sort_by_both_date</span> <span class="token punctuation">,</span> <span class="token operator">-</span> <span class="token operator">></span> <span class="token punctuation">{</span> order <span class="token punctuation">(</span> <span class="token string">"if(users.flag = true, users.date_a, users.date_b) asc"</span> <span class="token punctuation">)</span> <span class="token punctuation">}</span> |
The command above will produce satisfactory results for customers. But you should use Arel and change it like this.
1 2 | scope <span class="token symbol">:sort_by_both_date</span> <span class="token punctuation">,</span> <span class="token operator">-</span> <span class="token operator">></span> <span class="token punctuation">{</span> order <span class="token punctuation">(</span> <span class="token constant">Arel</span> <span class="token punctuation">.</span> sql <span class="token punctuation">(</span> <span class="token string">"if(users.flag = true, users.date_a, users.date_b) asc"</span> <span class="token punctuation">)</span> <span class="token punctuation">)</span> <span class="token punctuation">}</span> |
result:
id | date_a | date_b | flag |
---|---|---|---|
3 | 04-04-2020 | nil | true |
2 | nil | June 4, 2020 | false |
first | August 26, 2020 | nil | true |
4 | nil | September 23, 2020 | false |
We have the following query:
1 2 | <span class="token keyword">SELECT</span> <span class="token punctuation">`</span> users <span class="token punctuation">`</span> <span class="token punctuation">.</span> <span class="token operator">*</span> <span class="token keyword">FROM</span> <span class="token punctuation">`</span> users <span class="token punctuation">`</span> <span class="token keyword">WHERE</span> <span class="token keyword">ORDER</span> <span class="token keyword">BY</span> <span class="token keyword">IF</span> <span class="token punctuation">(</span> users <span class="token punctuation">.</span> flag <span class="token operator">=</span> <span class="token boolean">true</span> <span class="token punctuation">,</span> users <span class="token punctuation">.</span> date_a <span class="token punctuation">,</span> users <span class="token punctuation">.</span> date_b <span class="token punctuation">)</span> <span class="token keyword">ASC</span> |
And there is another great example of IF ELSE too:
We have the following 2 tables:
User:
id | name |
---|---|
first | Fitness |
2 | Beautiful |
Payment:
user_id | price | paid |
---|---|---|
first | 100 | true |
first | 50 | fasle |
2 | 30 | true |
2 | 70 | false |
And we have to display the table in the view as follows:
Name | Total money | Money paid |
---|---|---|
Fitness | 150 | 100 |
Beautiful | 100 | 30 |
Normally we will use the render collection on each row user, and each user we write a function to calculate the total amount, and a function to calculate the total amount paid to call outside the view.
And the result is sure to be 2n + 1 query (1 query total money, 1 query total paid by each user).
Some of you experienced “old projection” will immediately think of writing a scope when placed on the controller to resolve the n + 1 query column “Total amount” as follows:
1 2 3 4 5 | <span class="token comment"># user.rb</span> scope <span class="token symbol">:with_total_price</span> <span class="token punctuation">,</span> <span class="token punctuation">(</span> lambda <span class="token keyword">do</span> joins <span class="token punctuation">(</span> <span class="token symbol">:payments</span> <span class="token punctuation">)</span> <span class="token punctuation">.</span> select <span class="token punctuation">(</span> <span class="token string">"users.*, SUM(payments.price) as total_price"</span> <span class="token punctuation">)</span> <span class="token punctuation">.</span> group <span class="token punctuation">(</span> <span class="token string">"users.id"</span> <span class="token punctuation">)</span> <span class="token keyword">end</span> <span class="token punctuation">)</span> |
And call back on controller:
1 2 3 | <span class="token variable">@users</span> <span class="token operator">=</span> <span class="token constant">User</span> <span class="token punctuation">.</span> with_total_price <span class="token punctuation">.</span> <span class="token punctuation">.</span> <span class="token punctuation">.</span> <span class="token punctuation">.</span> |
and out of view we just need to call
1 2 3 4 5 | <span class="token operator"><</span> <span class="token operator">%</span> <span class="token variable">@users</span> <span class="token punctuation">.</span> <span class="token keyword">each</span> <span class="token keyword">do</span> <span class="token operator">|</span> user <span class="token operator">|</span> <span class="token string">%> //Ví dụ mọi người thích dùng each hơn dùng collection :D ... <%= user.total_price %></span> <span class="token punctuation">.</span> <span class="token punctuation">.</span> <span class="token punctuation">.</span> |
is solved the “total money” column is not queryed again.
What about “paid money”, potato spreads because it must have a paid = true field. Should you have to write another scope to add where paid = true, then join again with the User? That’s too crazy! Is that okay?
We will use IF ELSE in the scope just now:
1 2 3 4 5 | <span class="token comment"># user.rb</span> scope <span class="token symbol">:with_total_price</span> <span class="token punctuation">,</span> <span class="token punctuation">(</span> lambda <span class="token keyword">do</span> joins <span class="token punctuation">(</span> <span class="token symbol">:payments</span> <span class="token punctuation">)</span> <span class="token punctuation">.</span> select <span class="token punctuation">(</span> <span class="token string">"users.*, SUM(payments.price) as total_price, SUM(IF(payments.paid = true, payments.price, 0)) as total_paid_price"</span> <span class="token punctuation">)</span> <span class="token punctuation">.</span> group <span class="token punctuation">(</span> <span class="token string">"users.id"</span> <span class="token punctuation">)</span> <span class="token keyword">end</span> <span class="token punctuation">)</span> |
and out of call view
1 2 3 4 5 6 7 | <span class="token operator"><</span> <span class="token operator">%</span> <span class="token variable">@users</span> <span class="token punctuation">.</span> <span class="token keyword">each</span> <span class="token keyword">do</span> <span class="token operator">|</span> user <span class="token operator">|</span> <span class="token string">%> //Ví dụ mọi người thích dùng each hơn dùng collection :D ... <%= user.total_price %></span> <span class="token operator"><</span> <span class="token operator">%</span> <span class="token operator">=</span> user <span class="token punctuation">.</span> total_paid_price <span class="token operator">%</span> <span class="token operator">></span> <span class="token punctuation">.</span> <span class="token punctuation">.</span> <span class="token punctuation">.</span> <span class="token operator">/</span> <span class="token operator">/</span> total_price <span class="token punctuation">,</span> total_paid_price không phải là <span class="token number">1</span> trường hay <span class="token number">1</span> hàm của bảng user chỉ là chúng ta đã đặt tên sau <span class="token string">"as"</span> khi select là có thể gọi thoải mái nhé |
So that solves all n + 1 queries