Ruby/Rails - Chain unknown number of method calls - ruby-on-rails

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?

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
for empty arr, it will return class we passed as first argument in method.
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

how to match all multiple conditions with rails where method

Using active record, I want to perform a lookup that returns a collection of items that have ALL matching id's.
Given that the below example matches on ANY id in the array, I am trying to figure out the syntax so that it will match when ALL of the id's match. (given that in this example there is a many to many relationship).
The array length of the id's is also variable which prohibits chaining .where()
x.where(id: [1,2])
Note: this question got removed before and there are a lot of answers for performing a sql "where in" but this question is about performing a sql "where and"
You can use exec_query and execute your own bound query:
values = [1, 2]
where_condition = values.map.with_index(1) { |_, index| "id = $#{index}" }.join(" AND ")
sql = "SELECT * FROM table WHERE #{ where_condition }"
binds = values.map { |i| ActiveRecord::Relation::QueryAttribute.new(nil, i, ActiveRecord::Type::Integer.new) }
ActiveRecord::Base.connection.exec_query(sql, nil, binds)
I completely agree with #muistooshort's comment
where(id: [1,2]) doesn't make sense unless you're joining to an association table and in that case,..."where in" combined with HAVING [solves your problem].
But for the sake of answering the question and the assumption that id was just and example.
While #SebastianPalma's answer will work it will return an ActiveRecord::Result whereas most of the time the desire is an ActiveRecord::Relation.
We can achieve this by using Arel to build the where clause like so:
(I modified the example to use description rather than id so that it makes more logical sense)
table = MyObject.arel_table
values = ['Jamesla','Example']
where_clause = values.map {|v| table[:description].matches("%{v}%")}.reduce(&:and)
# OR
where_clause = table[:description].matches_all(values.map {|v| "%#{v}%"})
MyObject.where(where_clause)
This will result in the following SQL query:
SELECT
my_objects.*
FROM
my_objects
WHERE
my_objects.description LIKE '%Jamesla%'
AND my_objects.description LIKE '%Example%'

Selecting MIN and MAX for a collection, based on an ActiveRecord scope, in one SQL query

I would like to run the following query:
/* Inside a heredoc */
SELECT
MIN("books"."page_count") AS min,
MAX("books"."page_count") AS max
FROM "books"
WHERE "books"."author_id" IN (#{authors.pluck(:id).map { |id| "'#{id}'" }.join(",")})
AND "books"."publisher_id" IN (#{publishers.pluck(:id).map { |id| "'#{id}'" }.join(",")});
But instead of having to manually write the WHERE clauses at the end with that ugly interpolation, I'd like to use scopes I've defined on Book, something like:
query = Book.by_author(authors).by_publisher(publishers).select('MIN("books"."page_count") as min, MAX("books"."page_count") as max').first
attrs = [query["min"], query["max"]]
I understand that this doesn't work because Book.my_scope... expects to return a collection of books, while I'm looking for numeric values.
I am aware I could do e.g.:
query = Book.by_author(authors).by_publisher(publishers)
attrs = [query.minimum(:page_count), query.maximum(:page_count)]
But this results in two SQL queries, which seems quite unnecessary.
Is there a Rails-y way I can do this while keeping the flexibility of chainable scopes, without heredoc interpolation, and in one SQL query?
You could define it as a class method:
class Book
class << self
def minmax_page_count
count = select('MIN(page_count) OVER (), MAX(page_count) OVER ()').take
[count.min, count.max]
end
end
end
Book.by_author(authors).by_publisher(publishers).minmax_page_count # => [1, 2]

Rails - How can I avoid using the database while performing a detect method?

When performing detect on a int array, it works:
#number = [1,2,3,4,5,6,7].detect{|n| n == 4}
Variable #number becomes 4.
But when I do something like this:
#categories = Category.all
#current_category = #categories.detect{|cat| cat.id == params[:category]}
The program outputs
Category Load (0.2ms) SELECT "categories".* FROM "categories"
Which means it's using the database to find it.
However, the element I'm trying to find is already in the collection #categories, I just want to find it to assign it to a variable.
Of course another solution would be to implement a linear search algorithm, but I just want to keep the code as clean as possible.
How can I avoid using the database for this search?
EDIT: I just realized that this could be lazy fetching. Because before detect, I never use #categories, so it does the query when I do detect. Could this be true?
Rails is actually performing a SELECT COUNT(*) query when you call #categories.all, essentially performing a lazy-fetch.
Your #categories object still needs to query the database for the data.
See the documentation here: http://apidock.com/rails/ActiveRecord/Scoping/Named/ClassMethods/all
posts = Post.all
posts.size # Fires "select count(*) from posts" and returns the count
posts.each {|p| puts p.name } # Fires "select * from posts" and loads post objects
fruits = Fruit.all
fruits = fruits.where(color: 'red') if options[:red_only]
fruits = fruits.limit(10) if limited?
In your case, you should use active record and SQL requesting.
#current_category = #categories.find_by(id: params[:category])
Using array methods on Active Record relations tend to fetch all the data then apply the algorithm in-memory, while SQL filtering is faster.
In you case I love to define the operator [] on my model:
#in category.rb
def self.[](x)
self.find_by(id: x)
end
# anywhere after:
if c = Category[params[:id]]
puts "Category found and it's #{c.name} !"
else
puts "Not found :("
end

Rails 4 string interpolation in raw SQL request

What would be the best way of rewriting this query without interpolation?
def case_joins(type)
subquery = <<-SQL.squish
SELECT id FROM cases c2
WHERE c2.title_id = titles.id AND c2.value = 0 AND c2.type = '#{type}'
ORDER BY c2.created_at DESC LIMIT 1
SQL
"LEFT OUTER JOIN cases ON cases.title_id = titles.id AND cases.value = 0 AND cases.type = '#{type}' AND cases.id = (#{subquery})"
end
I'm assuming that you want to avoid interpolation of variables, which is dangerous since its open to SQL injection. I would simply join onto the cases selected from the subquery instead of putting the subquery into the WHERE conditions. This does involve interpolation, but only of AR-generated SQL. I would also implement it as a scope to leverage AR scope chaining:
class Title < ActiveRecord::Base
def self.case_joins(type)
case_query = Case.from("cases c").where(c: {title_id: title_id, value: 0, type: type}).order('c.created_at DESC').limit(1)
joins("LEFT OUTER JOIN (#{case_query.to_sql}) cases ON cases.title_id = titles.id")
end
end
This way, you can chain the scope to others like so:
Title.where(attribute1: value1).case_joins("typeA")
(Note that removed the superfluous WHERE conditions in the outer SELECT.)
It's difficult to infer what the rest of your code looks like, but I presume titles is being used in a query further up your call stack.
If you were to use ActiveRecord instead of native SQL, you could do something like this:
def case_joins(scope, title_id, type)
ids = Case.where(title_id: title_id, value: 0, type: type)
.order('created_at desc').limit(1).pluck(:id)
scope.joins('left outer join cases on cases.title_id = titles.id')
.where(value: 0, type: type, id: ids)
end
scope here is the current AR query you are modifying.
This is off the top of my head, so I'm not sure if the AR syntax above is correct, but it does avoid the need to interpolate SQL and also uses scoping.
To be honest, though, it's not all that much more readable than native SQL, and so YMMV. It does at least mean that (apart from the join) you're not encoding SQL in your code.
Here is modification of #eirikir's answer, that works the same way as method in question.
def case_joins(type)
case_query = Case.from("cases c").where('c.title_id = titles.id AND c.value = 0 AND c.type = ?', type).order('c.created_at DESC').select(:id).limit(1)
"LEFT OUTER JOIN cases ON cases.title_id = titles.id AND cases.id = (#{case_query.to_sql})"
end

How to get a single column's values into an array

Right now I'm doing something like this to select a single column of data:
points = Post.find_by_sql("select point from posts")
Then passing them to a method, I'd like my method to remain agnostic, and now have to call hash.point from within my method. How can I quickly convert this into an array and pass the data set to my method, or is there a better way?
In Rails 3.2 there is a pluck method for this
Just like this:
Person.pluck(:id) # SELECT people.id FROM people
Person.pluck(:role).uniq # unique roles from array of people
Person.distinct.pluck(:role) # SELECT DISTINCT role FROM people SQL
Person.where(:confirmed => true).limit(5).pluck(:id)
Difference between uniq and distinct
You should use the pluck method as #alony suggested. If you are stuck before Rails 3.2 you can use the ActiveRecord select method together with Array#map:
Post.select(:point).map(&:point)
#=> ["foo", "bar", "baz"]
before Ruby 1.9 you'd have to do .map{|x| x.title} though, because Symbol#to_proc (aliased by the unary & operator) is not defined in earlier versions of Ruby.
If you see the definition of select_values , then it using 'map(&:field_name)'
def select_values(arel, name = nil)
result = select_rows(to_sql(arel), name)
result.map { |v| v[0] }
end
The common and general Rails way to collect all the fields values in array is like :
points = Post.all(:select => 'point').map(&:point)
points = Post.all.collect {|p| p.point}

Resources