I have a convenience scope that includes
related models to speed up rendering of tables, etc:
class Post < ApplicationRecord
...
scope :includes_for_post_row, -> { includes(:reasons).includes(:feedbacks => [:user]) }
It works fine. Now, however, I'd like to select
an additional attribute. If I already knew what initial attributes I wanted, I could do this (in the console):
2.3.3 :005 > Post.select("`posts`.*, 42 AS column_forty_two").last.column_forty_two
Post Load (1.0ms) SELECT `posts`.*, 42 AS column_forty_two FROM `posts` ORDER BY `posts`.`id` DESC LIMIT 1
=> 42
This assumes that I know I want to select posts.*
, then I just tack on my column_forty_two
column and it all works.
I want to add column_forty_two
to my results, without affecting the initial select. For example, this should work:
p = Post.select("`posts`.*, 8 as column_eight").includes_for_post_row_with_forty_two
p.last.column_forty_two # => 42
p.last.column_eight # => 8
p.last.some_activerecord_property # => value
As should this:
p = Post.all.includes_for_post_row_with_forty_two.last
p.last.column_forty_two # => 42
p.last.some_activerecord_property # => value
How can I select
an additional column, without affecting or overwrite the existing columns selected by default by .all
or my own earlier select
?
If you go digging through the ActiveRecord source (an often necessary task with Rails), you'll see what's going on:
def build_select(arel)
if select_values.any?
arel.project(*arel_columns(select_values.uniq))
else
arel.project(@klass.arel_table[Arel.star])
end
end
select_values
is a list of everything you've handed to select
and is an empty array by default:
> Model.where(...).select_values
=> []
> Model.where(...).select('a').select_values
=> ["a"]
> Model.where(...).select('a').select('b').select_values
=> ["a", "b"]
and when ActiveRecord finally gets around to building the SELECT clause, it either uses what you've passed to select
(the if
branch in build_select
) or it uses table_name.*
(the else
branch in build_select
).
You should be able to use the same logic that build_select
uses to ensure that select_values
has something before you start adding more so that you sort of execute both the if
and else
branches of build_select
by pre-filling select_values
with the default table_name.*
. You could patch your own version of select
into the ActiveRecord::QueryMethods
module:
module ActiveRecord
module QueryMethods
def select_append(*fields)
if(!select_values.any?)
fields.unshift(arel_table[Arel.star])
end
select(*fields)
end
end
end
and then say things like:
> Post.select_append('6 as column_six').to_sql
=> "select `posts`.*, 6 as column_six from ..."
while leaving the "normal" select
behavior alone:
> Post.select('11 as column_eleven').to_sql
=> "select 11 as column_eleven from ..."
You don't have to monkey patch of course but it seems reasonable for this sort of thing.