Sometimes there's a need to crunch some custom data beyond what's stored in a model's table, for a report or somesuch. There are several ways to do this with differing benefits and trade-offs. Let's take an overview of available strategies. We'll be using a naive example where the custom data column is the length of a User record's email
field.
1. (naive) Ruby method
Just define model instance method! This is easy to implement, test and understand. Suffices for most cases, but can result in N+1 queries if you'll be accessing some association data, doing counting, summing etc.
class User
def email_length
email.size
end
end
2. Custom select
Pushes the processing into SQL for performance, becomes harder to maintain and use. Can cause headache if several such custom selects need to be used.
class User
scope :with_email_length, -> {
select_sql = <<~SQL
length(email) AS email_length
SQL
all.select("#{table_name}.*", select_sql)
}
end
User.relevant.with_email_length.first.email_length
3. Custom join
Goes even further into the SQL, but puts the custom data querying in a JOIN, rather the toplevel SELECT part, giving some flexibility as to the selects being used (even allows aliasing!). Can come with a performance hit because join will be evaluated for all records, before WHERE...
class User
scope :with_email_length, -> {
join_sql = <<~SQL
LEFT JOIN (
SELECT
id AS id
length(email) AS email_length
FROM #{table_name}
) AS email_length_data ON email_length_data.id = #{table_name}.id
SQL
all.joins(join_sql).select(
"#{table_name}.*",
"email_length_data.email_length AS email_length"
)
}
end
# Data access pattern is the same as in SELECT case,
# but will work better when chaining several such custom field scopes or doing more custom selecting
User.relevant.with_email_length.first.email_length
4. CTE
I haven't explored this much yet, but all.with(email_length_data: email_length_data)
where email_length_data
is some AR query object can also be a thing, especially if the gathered data need to be used by several WHERE?SELECT clauses in the main query, a way to DRY SQL.
Top comments (0)