Consider the following model relationships:
class User < ActiveRecord::Base
class Subscription < ActiveRecord::Base
class Payment < ActiveRecord::Base
Extremely straight forward right. However, consider a situation where you want to give a user a list of all of their payments. A terrible solution might be to do
payments = user.subscriptions.map(&:payments).flatten. Which is just a golfed version of what some would consider an "ok" solution, but when you think about it, is still bad (in HAML)
- if @user.subscriptions.count > 0
%h2 User Payments
- @user.subscriptions.each do |subscription|
- subscription.payments.each do |payment|
The problem with this solution, is that you are triggering a query for the count, and then the classic n+1 query issue for the subscription/payment relationship. Gems like bullet will help you find places in your code where your query issues can be improved, but why should a developer need a gem to show them something they should already know? Which is, make sure you know what SQL you are running at all times.
The solution to the above n+1 problem in ActiveRecord, as bullet will tell you, is to make sure you use
includes(subscriptions: :payments)when you are setting the
@uservariable from a finder. But this is not always practical. Perhaps you have extracted the finder code out into a
before_filterin your controller, especially if all actions require a
Not to mention that
User.includes(subscriptions: :payments).find(params[:id])reads like complete crap. My main concern here is that ActiveRecord is hiding all this SQL generation away from developers, to the point where there are probably a huge amount of people calling themselves "developers" now that don't know a lick of SQL.
Using the above relationships, a common requirement in systems is to allow user objects to still be deleted, but not remove their payments as they are required for permanent record. So imagine a payment view with the following HAML:
- if @payment.subscription && @payment.subscription.user
%h2 User details
= render 'shared/_user', user: @payment.subscription.user
Here the developer probably doesn't know they are again triggering 2 more queries. And that's only 2 because of ActiveRecord's built in caching, it could be up to 5 in other ORMs. This non stop relationship chaining is what is causing developers to ignore what's happening at the database level.
For those playing along at home, the best way to get all of a user's payments in rails 4 is
Payment.joins(:subscription).where('subscriptions.user_id = ?', user.id).references(:subscription)but putting all that in your controller is not recommended, and putting it into a model method makes it harder to customise the
includes()that you may need to make your SQL more efficient.
Are you saving any code by writing that instead of
select p.* from payments p join subscriptions s on (s.id = p.subscription_id) where s.user_id = ?? At least if you do it this way, you know exactly what is going on at the database layer.