0
votes

UPDATE:

How do I add a virtual attribute to a model and preserve the active record relation.

I tried the below, but .each returns an array, not active record. What other method can I use?


My set_listable_for method is converting an activerecord relation into an array. I want to preserve the ActiveRecord Relation.

At runtime, I added an attr_access to an active record model.

def add_listable_attribute_to(*relation)
  relation.each do |rel|
    rel[1].first.class.class_eval do
      attr_accessor :listable
    end
  end
end

Then I used this method to set the value of the attribute to the same value for all records....

   def set_listable_for(relation, object)
      relation.each do |record|
        record.listable = object
      end
    end

However, my ActiveRecord relation gets converted to an Array afterwords.

How to I preserve the Active Record relation, as I don't want an array. Since I continue to use it here and continue to scope and query on it...

def union_scope(*relation)
  add_listable_attribute_to(*relation)

  listable = relation.first[0]
  combined = set_listable_for(relation.first[1], listable)
  relation.drop(1).each do |relation_set|
    listable = relation[0]
    set_listable_for(relation_set[1], listable)
    combined = combined.or(relation_set[1])
  end
  combined
end

Thank you

2
This is exactly how an ActiveRecord::Relation should behave. It represents a scope or query that can be run against the database (no records included yet). And as soon as you need one or more of the actual records, it loads the records and returns them in an array. This happens if you call a method like first, count, or each on the Relation. Therefore your question is unclear to me because you request to disable its core functionality.spickermann
@spickermann, I am looking for an alternative solution, which would replace my code so I can return an AR Relation, rather than array.user2012677

2 Answers

1
votes

The .each call executes the query and iterates over the result. It wouldn't be a problem if this happens in the controller after scoping and pagination, but if it gets called before scoping and pagination, the whole model dataset will be loaded which isn't good.

To avoid that, you'll need to set up the listable as late as possible after data is retrieved from the database. I can see three approaches to handle this:

  1. Use a decorator to wrap instances of your relation after it is loaded into the controller or view. This is simpler to understand but pulls the functionality out of the model layer.

  2. Set listable in an after_initialize callback. This keeps the functionality in the model layer, but adds a lot of complexity.

  3. Ensure that you only call set_listable_for in the controller after scoping. Variant of #1.

1
votes

By adding an 'AS' Statement in Select, I was able to return an ActiveRecord model. Only issue is that when I call .count, I need to use .count(:all), or .count(:id), to prevent errors.

def union_scope(*relation)
  listable = relation.first[0]
  scope = relation.first[1]
  combined = scope.select("#{scope.table_name}.*, \'#{listable.class.name}\' as listable")
  relation.drop(1).each do |relation_set|
    listable = relation_set[0]
    scope = relation_set[1].select("#{scope.table_name}.*, \'#{listable.class.name}\' as listable")
    combined = combined.or(scope)
  end
  combined
end