108
votes

I have an array of objects, let's call it an Indicator. I want to run Indicator class methods (those of the def self.subjects variety, scopes, etc) on this array. The only way I know to run class methods on a group of objects is to have them be an ActiveRecord::Relation. So I end up resorting to adding a to_indicators method to Array.

def to_indicators
  # TODO: Make this less terrible.
  Indicator.where id: self.pluck(:id)
end

At times I chain quite a few of these scopes to filter down the results, within the class methods. So, even though I call a method on an ActiveRecord::Relation, I don't know how to access that object. I can only get to the contents of it through all. But all is an Array. So then I have to convert that array to a ActiveRecord::Relation. For example, this is part of one of the methods:

all.to_indicators.applicable_for_bank(id).each do |indicator|
  total += indicator.residual_risk_for(id)
  indicator_count += 1 if indicator.completed_by?(id)
end

I guess this condenses down to two questions.

  1. How can I convert an Array of objects to an ActiveRecord::Relation? Preferably without doing a where each time.
  2. When running a def self.subjects type method on an ActiveRecord::Relation, how do I access that ActiveRecord::Relation object itself?

Thanks. If I need to clarify anything, let me know.

5
If your only reason for trying to convert that array back to a relation is because you got it via .all, just use .scoped like Andrew Marshall's answer indicates (Although in rails 4 it'll work with .all). If you find yourself needing to turn an array into a relation you've gone wrong somewhere...nzifnab

5 Answers

47
votes

How can I convert an Array of objects to an ActiveRecord::Relation? Preferably without doing a where each time.

You cannot convert an Array to an ActiveRecord::Relation since a Relation is just a builder for a SQL query and its methods do not operate on actual data.

However, if what you want is a relation then:

  • for ActiveRecord 3.x, don’t call all and instead call scoped, which will give back a Relation which represents the same records that all would give you in an Array.

  • for ActiveRecord 4.x, simply call all, which returns a Relation.

When running a def self.subjects type method on an ActiveRecord::Relation, how do I access that ActiveRecord::Relation object itself?

When the method is called on a Relation object, self is the relation (as opposed to the model class it’s defined in).

172
votes

You can convert an array of objects arr to an ActiveRecord::Relation like this (assuming you know which class the objects are, which you probably do)

MyModel.where(id: arr.map(&:id))

You have to use where though, it's a useful tool which you shouldn't be reluctant to use. And now you have a one-liner converting an array to a relation.

map(&:id) will turn your array of objects to an array containing only their id's. And passing an array to a where clause will generate a SQL statement with IN that looks something like:

SELECT .... WHERE `my_models`.id IN (2, 3, 4, 6, ....

Keep in mind that the ordering of the array will be lost - But since your objective is only to run a class method on the collection of these objects, I assume it won't be a problem.

5
votes

Well, in my case, I need to converting an array of objects to ActiveRecord::Relation as well as sorting them with a specific column(id for instance). Since I'm using MySQL, the field function could be helpful.

MyModel.where('id in (?)',ids).order("field(id,#{ids.join(",")})") 

The SQL looks like:

SELECT ... FROM ... WHERE (id in (11,5,6,7,8,9,10))  
ORDER BY field(id,11,5,6,7,8,9,10)

MySQL field function

1
votes

ActiveRecord::Relation binds database query which retrieves data from database.

Suppose to make sense, We have array with objects of same class, then with which query we suppose to bind them?

When I run,

users = User.where(id: [1,3,4,5])
  User Load (0.6ms)  SELECT `users`.* FROM `users` WHERE `users`.`id` IN (1, 3, 4, 5)  ORDER BY created_at desc

Here in above, usersreturn Relation object but binds database query behind it and you can view it,

users.to_sql
 => "SELECT `users`.* FROM `users` WHERE `users`.`id` IN (1, 3, 4, 5)  ORDER BY created_at desc"

So it is not possible to return ActiveRecord::Relation from array of objects which is independent of sql query.

1
votes

First of all, this is NOT a silver bullet. Out of my experience, I found that converting to relation is sometimes easier than alternatives. I try to use this approach very sparingly and only in cases where the alternative would be more complex.

That being said here is my solution, I've extended Array class

# lib/core_ext/array.rb

class Array

  def to_activerecord_relation
    return ApplicationRecord.none if self.empty?

    clazzes = self.collect(&:class).uniq
    raise 'Array cannot be converted to ActiveRecord::Relation since it does not have same elements' if clazzes.size > 1

    clazz = clazzes.first
    raise 'Element class is not ApplicationRecord and as such cannot be converted' unless clazz.ancestors.include? ApplicationRecord

    clazz.where(id: self.collect(&:id))
  end
end

A usage example would be array.to_activerecord_relation.update_all(status: 'finished'). Now where do I use it?

Sometimes you need to filter out ActiveRecord::Relation for example take out not completed elements. In those cases best is to use scope elements.not_finished and you would still keep ActiveRecord::Relation.

But sometimes that condition is more complex. Take out all elements that are not finished, and that has been produced in the last 4 weeks and have been inspected. To avoid creating new scopes you can filter to an array and then convert back. Keep in mind that you still do a query to DB, quick since it searches by id but still a query.