Order Activerecord by array values sequence - ruby-on-rails

I have a ActiveRecord::Relation #formulas with many rows, i want to order and group them by scope with this sequence %w[dre dre_cc cash_flow attachment_table]
#formulas = current_user.company.formulas
scopes = %w[dre dre_cc cash_flow attachment_table]
ordered_formulas = ...

In Ruby on Rails 7.0 in_order_of was introduced that can be used like this (assuming that scope is the name of to column):
scopes = %w[dre dre_cc cash_flow attachment_table]
#formulas = current_user.company.formulas
ordered_formulas = #formulas.in_order_of(:scope, scopes)
When you are still on an older version of Rails then you can get the same result by building a complex order statement by yourself:
scopes = %w[dre dre_cc cash_flow attachment_table]
#formulas = current_user.company.formulas
order_clause = "CASE scope "
scopes.each_with_index do |scope, index|
order_clause << sanitize_sql_array(["WHEN ? THEN ? ", scope, index])
end
order_clause << sanitize_sql_array(["ELSE ? END", scopes.length])
ordered_formulas = #formulas.order(order_clause)
When you need this behavior more often in your app then it might make sense to create a helper method or scope for it.

Related

rails - get the SQL of a model save

I need to be able to output the SQL UPDATES that would be generated by Rails, without actually running them or Saving the records. I will be outputting the SQL updates to a file instead.
Is there a way to do this in Rails, without using string interpolation?
Is it possible to do something like the following:
p = Post.where (something)
p.some_value = some_new_value
p.to_sql??? # how to generate the update statement
rather than:
"UPDATE TABLE SET field_1 = #{new_field} WHERE ID = " etc etc
Took from #R.F. Nelson and wrap it to a method. You could just calling to_update_sql with your model as the argument to get the SQL.
def to_update_sql(model)
return '' if model.changes.empty?
table = Arel::Table.new(model.class.table_name)
update_manager = Arel::UpdateManager.new(model.class)
update_manager.set(model.changes.map{|name, (_, val)| [table[name], val] })
.where(table[:id].eq(model.id))
.table(table)
return update_manager.to_sql
end
post = Post.first
post.some_value = xxxx
to_update_sql(post)
# => UPDATE `posts` SET `some_value` = xxx WHERE `posts`.`id` = 1
Taken from this post:
You can achieve this goal with AREL:
# Arel::InsertManager
table = Arel::Table.new(:users)
insert_manager = Arel::InsertManager.new
insert_manager.into(table)
insert_manager.insert([ [table[:first_name], 'Eddie'] ])
sql_transaction = insert_manager.to_sql
File.open('file_name.txt', 'w') do |file|
file.write(sql)
end
# Arel::UpdateManager
table = Arel::Table.new(:users)
update_manager = Arel::UpdateManager.new
update_manager.set([[table[:first_name], "Vedder"]]).where(table[:id].eq(1)).table(table)
sql_transaction = update_manager.to_sql
File.open('file_name.txt', 'w') do |file|
file.write(sql)
end
Here you can find all Arel managers, like delete_manager.rb, select_manager.rb and the others.
Good read: http://jpospisil.com/2014/06/16/the-definitive-guide-to-arel-the-sql-manager-for-ruby.html

Dynamically create query - Rails 5

If I manually write a query, it will be like
User.where("name LIKE(?) OR desc LIKE(?)",'abc','abc')
.where("name LIKE(?) OR desc LIKE(?)",'123','123')
However, I need to dynamically generate that query.
I am getting data like
def generate_query(the_query)
query,keywords = the_query
# Here
# query = "name LIKE(?) OR desc LIKE(?)"
# keywords = [['abc','abc'],['123','123']]
keywords.each do |keyword|
users = User.where(query,*keyword) <-- not sure how to dynamically add more 'where' conditions.
end
end
I am using Rails 5. Hope it is clear. Any help appreciated :)
Something like this:
q = User.where(a)
.where(b)
.where(c)
is equivalent to:
q = User
q = q.where(a)
q = q.where(b)
q = q.where(c)
So you could write:
users = User
keywords.each do |keyword|
users = users.where(query, *keyword)
end
But any time you see that sort of feedback pattern (i.e. apply an operation to the operation's result or f(f( ... f(x)))) you should start thinking about Enumerable#inject (AKA Enumerable#reduce):
users = keywords.inject(User) { |users, k| users.where(query, *k) }
That said, your query has two placeholders but keywords is just a flat array so you won't have enough values in:
users.where(query, *k)
to replace the placeholders. I think you'd be better off using a named placeholder here:
query = 'name like :k or desc like :k'
keywords = %w[abc 123]
users = keywords.inject(User) { |users, k| users.where(query, k: k) }
You'd probably also want to include some pattern matching for your LIKE so:
query = "name like '%' || :k || '%' or desc like '%' || :k || '%'"
users = keywords.inject(User) { |users, k| users.where(query, k: k)
where || is the standard SQL string concatenation operator (which AFAIK not all databases understand) and % in a LIKE pattern matches any sequence of characters. Or you could add the pattern matching in Ruby and avoid having to worry about the different ways that databases handle string concatenation:
query = 'name like :k or desc like :k'
users = keywords.inject(User) { |users, k| users.where(query, k: "%#{k}%")
Furthermore, this:
User.where("name LIKE(?) OR desc LIKE(?)",'abc','abc')
.where("name LIKE(?) OR desc LIKE(?)",'123','123')
produces a WHERE clause like:
where (name like 'abc' or desc like 'abc')
and (name like '123' or desc like '123')
so you're matching all the keywords, not any of them. This may or may not be your intent.

Trouble adding "and" and "or" clauses in query through rails query interface

I am trying to run following query through Rails query interface but unable to translate my logic. The query is
Select f.* from feeds f
Left join feed_items fi on fi.id = f.feedable_id
where
f.feedable_type in ('Homework', 'Datesheet')
and
(
(fi.assignable_type = 'Level' and assignable_id IN (1)) or
(fi.assignable_type = 'Student' and assignable_id IN (1)) or
(fi.assignable_type = 'Section' and assignable_id IN (1))
)
Scenario:
I receive following params hash in my action containing filters which will be added dynamically in my query
{"page"=>"1", "limit"=>"2", "type_filter"=>["Homework", "Datesheet"], "assignable_filter"=>{"Student"=>"[2]", "Section"=>"[1]", "Level"=>"[1]"}}
So far, what I have done is joining the tables and added where clause for type filter but not sure how to dynamically add assignable_filters. Here is my rails code, options are params in following code
def get_feeds(options)
base = Feed.includes(:feed_item)
base = add_type_filters base, options
base = add_assignable_filters base, options
format_response base, options
end
def add_type_filters(base, options)
type_filter = options[:type_filter]
if !type_filter.nil? and type_filter.length > 0
base = base.where('feedable_type IN (?)', options[:type_filter])
end
base
end
def add_assignable_filters(base, options)
assignable_filter = options[:assignable_filter]
if !assignable_filter.nil?
assignable_filter.each do |key, value|
# code for adding filters combined with or conditions
end
# wrap the or conditions and join them with an and in main where clause
end
base
end
P.S I am using rails 5
There was no straight forward way of building the query dynamically. I had to construct the where string to solve the problem. My current solution is
def get_feeds(options)
params_hash = {}
type_filters = add_type_filters options, params_hash
assignable_filters = add_assignable_filters options, params_hash
where = type_filters
where = where ? "#{where} and (#{assignable_filters})" : assignable_filters
base = Feed.eager_load(:feed_item).where(where, params_hash)
format_response base, options
end
def add_type_filters(options, params_hash)
type_filter = options[:type_filter]
type_filter_sql = nil
if !type_filter.nil? and type_filter.length > 0
type_filter_sql = 'feeds.feedable_type in (:type_filter)'
params_hash[:type_filter] = type_filter
end
type_filter_sql
end
def add_assignable_filters(options, params_hash)
assignable_filter_sql = []
assignable_filter = options[:assignable_filter]
if !assignable_filter.nil?
assignable_filter.each do |key, value|
assignable_filter_sql.push("(feed_items.assignable_type = '#{key}' and feed_items.assignable_id IN (:#{key}))")
params_hash[key.to_sym] = JSON.parse(value)
end
end
assignable_filter_sql.join(' or ')
end

How to build a query with arbitrary placeholder conditions in ActiveRecord?

Assume I have an arbitrary number of Group records and I wanna query User record which has_many :groups, the catch is that users are queries by two bound fields from the groups table.
At the SQL level, I should end up with something like this:
SELECT * FROM users where (categories.id = 1 OR users.status = 0) OR(categories.id = 2 OR users.status = 1) ... -- to infinity
This is an example of what I came up with:
# Doesn't look like a good solution. Just for illustration.
or_query = groups.map do |g|
"(categories.id = #{g.category.id} AND users.status = #{g.user_status.id} )"
end.join('OR')
User.joins(:categories).where(or_query) # Works
What I think I should be doing is something along the lines of this:
# Better?
or_query = groups.map do |g|
"(categories.id = ? AND users.status = ? )".bind(g.category.id, g.user_status.id) #Fake method BTW
end.join('OR')
User.joins(:categories).where(or_query) # Works
How can I achieve this?
There has to be a better way, right?
I'm using Rails 4.2. So the shiny #or operator isn't supported for me.
I would collect the condition parameters separately into an array and pass that array (splatted, i.e. as an arguments list) to the where condition:
or_query_params = []
or_query = groups.map do |g|
or_query_params += [g.category_id, g.user_status.id]
"(categories.id = ? AND users.status = ?)"
end.join(' OR ')
User.joins(:categories).where(or_query, *or_query_params)
Alternatively, you might use ActiveRecord sanitization:
or_query = groups.map do |g|
"(categories.id = #{ActiveRecord::Base.sanitize(g.category_id)} AND users.status = #{ActiveRecord::Base.sanitize(g.user_status.id)})"
end.join(' OR ')
User.joins(:categories).where(or_query)

search models tagged_with OR title like (with acts_as_taggable_on)

I'm doing a search on a model using a scope. This is being accessed by a search form with the search parameter q. Currently I have the code below which works fine for searches on tags associated with the model. But I would also like to search the title field. If I add to this scope then I will get all results where there is a tag and title matching the search term.
However, I need to return results that match the company_id and category_id, and either/or matching title or tag. I'm stuck with how to add an OR clause to this scope.
def self.get_all_products(company, category = nil, subcategory = nil, q = nil)
scope = scoped{}
scope = scope.where "company_id = ?", company
scope = scope.where "category_id = ?", category unless category.blank?
scope = scope.tagged_with(q) unless q.blank?
scope
end
I'm using Rails 3.
Ever consider Arel? You can do something like this
t = TableName.arel_table
scope = scope.where(t[:title].eq("BLAH BLAH").or(t[:tag].eq("blah blah")))
or else you can do
scope = scope.where("title = ? OR tag = ", title_value, tag_value)
I could be wrong, but I don't think scopes can help you to construct an or condition. You'll have to hand-write the code to buid your where clause instead. Maybe something like this...
clause = "company_id=?"
qparams = [company]
unless category.blank?
clause += " or category_id=?"
qparams <= category
end
scope.where clause, *qparams

Resources