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

Question

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

Answers 1
Added at 2016-12-30 17:12
Tags
Question

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?

Answers to

Use Rails&#39; select() to add (not overwrite) selected attributes?

nr: #1 dodano: 2016-12-30 20:12

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 cause, it either uses when 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.

Source Show
◀ Wstecz