Ruby/Rails - Chain unknown number of method calls

Viewed 119

I would like to dynamically create (potentially complex) Active Record queries from a 2D array passed into a method as an argument. In other words, I'd like to take this:

arr = [
    ['join', :comments],
    ['where', :author => 'Bob']
]

And create the equivalent of this:

Articles.join(:comments).where(:author => 'Bob')

One way to do this is:

Articles.send(*arr[0]).send(*arr[1])

But what if arr contains 3 nested arrays, or 4, or 5? A very unrefined way would be to do this:

case arr.length
    when 1
        Articles.send(*arr[0])
    when 2
        Articles.send(*arr[0]).send(*arr[1])
    when 3
        Articles.send(*arr[0]).send(*arr[1]).send(*arr[2])
    # etc.
end

But is there a cleaner, more succinct way (without having to hit the database multiple times)? Perhaps some way to construct a chain of method calls before executing them?

3 Answers

One convenient way would be to use a hash instead of a 2D array.

Something like this

query = {
  join: [:comments],
  where: {:author => 'Bob'}
}

This approach is not much complex and You don't need to worry if the key is not provided or is empty

Article.joins(query[:join]).where(query[:where])
#=> "SELECT  `articles`.* FROM `articles` INNER JOIN `comments` ON `comments`.`article_id` = `articles`.`id` WHERE `articles`.`author` = 'Bob'"

If the keys are empty or not present at all

query = {
  join: []
}

Article.joins(query[:join]).where(query[:where])
#=> "SELECT  `articles`.* FROM `articles`"

Or nested

query = {
  join: [:comments],
  where: {:author => 'Bob', comments: {author: 'Joe'}}
}
#=> "SELECT  `articles`.* FROM `articles` INNER JOIN `comments` ON `comments`.`article_id` = `articles`.`id` WHERE `articles`.`author` = 'Bob' AND `comments`.`author` = 'Joe'"

I created following query which will work on any model and associated chained query array.

def chain_queries_on(klass, arr)
  arr.inject(klass) do |relation, query|
    begin
      relation.send(query[0], *query[1..-1])
    rescue
      break;
    end
  end
end

I tested in local for following test,

arr = [['where', {id: [1,2]}], ['where', {first_name: 'Shobiz'}]]

chain_queries_on(Article, arr)

Query fired is like below to return proper output,

Article Load (0.9ms)  SELECT `article`.* FROM `article` WHERE `article`.`id` IN (1, 2) AND `article`.`first_name` = 'Shobiz'  ORDER BY created_at desc

Note-1: few noticeable cases

  1. for empty arr, it will return class we passed as first argument in method.

  2. It will return nil in case of error. Error can occur if we use pluck which will return array (output which is not chain-able) or if we do not pass class as first parameter etc.

More modification can be done for improvement in above & avoid edge cases.

Note-2: improvements

You can define this method as a class method for Object class also with one argument (i.e. array) and call directly on class like,

# renamed to make concise
Article.chain_queries(arr)
User.chain_queries(arr)

Inside method, use self instead of klass

arr.inject(Articles){|articles, args| articles.send(*args)}
Related