128

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.

Nathan
  • 1,864
  • 3
  • 18
  • 22
  • 3
    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 Jun 26 '13 at 23:47

6 Answers6

194

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.

How it works:

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 IN statement 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.

Marco Prins
  • 7,189
  • 11
  • 41
  • 76
  • 3
    Well done, exactly what I needed. You can use this for any attribute on the model. works perfectly for me. – nfriend21 Jul 30 '14 at 19:07
  • 8
    Why build literal SQL yourself when you can do `where(id: arr.map(&:id))`? And strictly speaking, this doesn’t convert the array to a relation, but instead gets new instances of the objects (once the relation is realized) with those IDs which may have different attribute values than the already in-memory instances in the array. – Andrew Marshall Jul 31 '14 at 11:54
  • 12
    This loses the ordering, though. – Velizar Hristov Mar 17 '15 at 01:36
  • 2
    @VelizarHristov That's because it's now a relation, which can only be ordered by a column, and not any way you want. Relations are faster to handle large data sets, and there will be some trade-offs. – Marco Prins Mar 17 '15 at 09:45
  • 1
    Exactly what I needed. But can anyone shed some light on the efficiency of this approach? Doesn't seem to be the most efficient thing to reiterate through the entire array. Nonetheless works for me. – Tristan Tao Dec 23 '15 at 17:52
  • 12
    Very inefficient! You've turned a collection of objects you already have in memory into ones you are going to do a database query to access. I would look at refactoring those class methods you are wanting to iterate over the array. – james2m Jan 25 '16 at 15:35
  • 3
    @james2m can you think of a more efficient way to convert any array of objects to an active record relation? As far as i know there's no in-built method or way to do this in rails. This is the most practical approach in Rails. – Paa Yaw May 09 '18 at 20:22
  • @MarcoPrins THANK YOU. This is exactly what I was looking for. – Mark Allen Nov 14 '18 at 20:17
49

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

Andrew Marshall
  • 95,083
  • 20
  • 220
  • 214
  • 1
    See @Marco Prins below about the solution. – Justin Jan 13 '16 at 03:38
  • what about .push deprecated method in rails 5 – Jaswinder Feb 14 '17 at 12:18
  • @GstjiSaini I’m unsure of the exact method you’re referring to, please provide doc or source link. Though, if it’s deprecated, not a very viable solution as it’ll likely go away soon. – Andrew Marshall Feb 14 '17 at 14:09
  • And that's why you can do `class User; def self.somewhere; where(location: "somewhere"); end; end`, then `User.limit(5).somewhere` – Dorian Apr 03 '17 at 23:29
  • This is the only answer that is truly enlightening the OP. The question reveals flawed knowledge of how ActiveRecord works. @Justin is just reinforcing a lack of understanding about why it’s bad to keep passing arrays of objects around just to map over them and build another unnecessary query. – james2m May 12 '18 at 01:48
10

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

Xingcheng Xia
  • 109
  • 1
  • 5
  • For a version of this that works with PostgreSQL, look no further than this thread: https://stackoverflow.com/questions/1309624/simulating-mysqls-order-by-field-in-postgresql – armchairdj Dec 13 '17 at 00:15
  • ActiveRecord::StatementInvalid (PG::TooManyArguments: ERROR: cannot pass more than 100 arguments to a function) This workaround can not stand over 100 arguments to a function, thus, it works with relations of total length up to 100, ids like 112 and 12 won't be separated from each other. – Anton Semenichenko Sep 22 '20 at 22:09
  • So `MyModel.where(id: ids)` ? – coisnepe Oct 20 '20 at 09:38
4

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.

Haris Krajina
  • 14,824
  • 12
  • 64
  • 81
1

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.

ray
  • 5,454
  • 1
  • 18
  • 40
0

In PostgreSQL, to maintain the order (based on this answer), given arr of objects:

ids = arr.pluck(:id)
cases = ids.map.with_index { |id, index| "WHEN id='#{id}' THEN #{index + 1}" }.join(" ")
Model.where(id: ids).order(Arel.sql("CASE #{cases} ELSE #{ids.size + 1} END"))

Example produced SQL:

SELECT "records".*
FROM "records"
WHERE "records"."id"
IN (
  '84b61039-193b-4263-9de6-5185e2cf4e06',
  '5955c8d0-a809-4e13-b15a-ac3a1b17569e',
  '37c44652-3195-4839-8130-409bb4c3a564'
  )
ORDER BY
  CASE
  WHEN id='84b61039-193b-4263-9de6-5185e2cf4e06' THEN 1
  WHEN id='5955c8d0-a809-4e13-b15a-ac3a1b17569e' THEN 2
  WHEN id='37c44652-3195-4839-8130-409bb4c3a564' THEN 3
  ELSE 4
END;

Note: it won't work with .distinct; in this scenario use ids = arr.pluck(:id).uniq!

wscourge
  • 10,657
  • 14
  • 59
  • 80