Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 561
Undo Avatar asked Dec 30 '16 16:12

Undo


1 Answers

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.

like image 195
mu is too short Avatar answered Oct 05 '22 17:10

mu is too short