ruby-on-railsactiverecordrails-activerecord

Use Rails' select() to add (not overwrite) selected attributes?


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?


Solution

  • 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.