I am struggling with the best way to meta program a dynamic method, where I'll be limiting results based on conditions... so for example:
class Timeslip < ActiveRecord::Base
def self.by_car_trans(car, trans)
joins(:car)
.where("cars.trans IN (?) and cars.year IN (?) and cars.model ILIKE ?", trans, 1993..2002, car)
.order('et1320')
end
end
Let's say instead of passing in my arguments, i pass in an array of conditions with key being the fieldname, and value being the field value. so for example, I'd do something like this:
i'd pass in [["field", "value", "operator"],["field", "value", "operator"]]
def self.using_conditions(conditions)
joins(:car)
conditions.each do |key, value|
where("cars.#{key} #{operator} ?", value)
end
end
However, that doesn't work, and it's not very flexible... I was hoping to be able to detect if the value is an array, and use IN () rather than =, and maybe be able to use ILIKE for case insensitive conditions as well...
Any advice is appreciated. My main goal here is to have a "lists" model, where a user can build their conditions dynamically, and then save that list for future use. This list would filter the timeslips model based on the associated cars table... Maybe there is an easier way to go about this?
First of all, you might find an interest in the Squeel gem.
Other than that, use arel_table for IN or LIKE predicates :
joins( :car ).where( Car.arel_table[key].in values )
joins( :car ).where( Car.arel_table[key].matches value )
you can detect the type of value to select an adequate predicate (not nice OO, but still):
column = Car.arel_table[key]
predicate = value.respond_to?( :to_str ) ? :in : :matches # or any logic you want
joins( :car ).where( column.send predicate, value )
you can chain as many as those as you want:
conditions.each do |(key, value, predicate)|
scope = scope.where( Car.arel_table[key].send predicate, value )
end
return scope
So, you want dynamic queries that end-users can specify at run-time (and can be stored & retrieved for later use)?
I think you're on the right track. The only detail is how you model and store your criteria. I don't see why the following won't work:
def self.using_conditions(conditions)
joins(:car)
crit = conditions.each_with_object({}) {|(field, op, value), m|
m["#{field} #{op} ?"] = value
}
where crit.keys.join(' AND '), *crit.values
end
CAVEAT The above code as is is insecure and prone to SQL injection.
Also, there's no easy way to specify AND vs OR conditions. Finally, the simple "#{field} #{op} ?", value for the most part only works for numeric fields and binary operators.
But this illustrates that the approach can work, just with a lot of room for improvement.
Related
I know that
Object.where('key > ?', value)
works.
But if the query happens to have several tables involved, with multiple key columns, it might break as the query produced is:
SELECT "tablename".* FROM "tablename" WHERE "tablename"."user_id" = $1 AND (key > 0) [["user_id", 29]]
A solution would be
Object.where('tablename.key > ?', value)
But ain't there an arel way to write this instead? My app has (enforced) weird table names, I'd rather not write them there and that they get added dynamically by active record.
Thanks
I'd personally still try to stay with AR on that one, and do something with a range and a hash query:
Object.where(tablename: { key: value..Float::INFINITY}) # If value is a number
Object.where(tablename: { key: value..DateTime::Infinity.new}) # If value is a DateTime
It's a bit verbose, but you can use arel to do this. For example
Object.where(Object.arel_table[:key].gt(123))
will select objects where key > 123.
If I was doing this, I would probably define some helper methods, perhaps something along the lines of
class Foo < ActiveRecord::Base
def self.column(name)
Foo.arel_table[name]
end
#now you can do
def self.some_method
Foo.where(column(:key).gt(123))
end
end
If you're querying from one object (one table) you can drop the table name in the where clause.
Object.where('key > ?', value)
Unfortunately that's the best way there is to do it.
I have a model with an has_many association: Charts has_many ChartConditions
charts model has fields for:
name (title)
table_name (model)
The chart_conditions model has fields for
assoc_name (to .joins)
name (column)
value (value)
operator (operator
Basically my Chart tells us which model (using the table_name field) I want to run a dynamic query on. Then the chart_conditions for the Chart will tell us which fields in that model to sort on.
So In my models that will be queried, i need to dynamically build a where clause using multiple chart_conditions.
Below you can see that i do a joins first based on all the object's assoc_name fields
Example of what I came up with. This works, but not with a dynamic operator for the name/value and also throws a deprecation warning.
def self.dynamic_query(object)
s = joins(object.map{|o| o.assoc_name.to_sym})
#WORKS BUT GIVES DEPRECATED WARNING (RAILS4)
object.each do |cond|
s = s.where(cond.assoc_name.pluralize.to_sym => {cond.name.to_sym => cond.value})
end
end
How can i then add in my dynamic operator value to this query? Also why can I not say:
s = s.where(cond.assoc_name.pluralize : {cond.name : cond.value})
I have to use the => and .to_sym to get it to work. The above syntax errors: syntax error, unexpected ':' ...ere(cond.assoc_name.pluralize : {cond.name : cond.value}) ... ^
What if you store the query in a variable and append to that?
def self.dynamic_query(object)
q = joins(object.map{|o| o.assoc_name.to_sym})
object.each do |cond|
q = q.where(cond.assoc_name.pluralize : {cond.name : cond.value})
end
q # returns the full query
end
Another approach might be the merge(other) method. From the API Docs:
Merges in the conditions from other, if other is an ActiveRecord::Relation. Returns an array representing the intersection of the resulting records with other, if other is an array.
Post.where(published: true).joins(:comments).merge( Comment.where(spam: false) )
# Performs a single join query with both where conditions.
That could be useful to knot all the conditions together.
I have a Deal model with a lot of associations. One of the associations is Currency. The deals table and the currencies table both have a name column. Now, I have the following ActiveRecord query:
Deal.
joins(:currency).
where("privacy = ? or user_id = ?", false, doorkeeper_token.resource_owner_id).
select("deals.name as deal_name, deals.date as deal_creation_date, deals.amount as deal_amount, currencies.name as currency_name, currencies.symbol as currency_symbol")
This query doesn't work, its result is an array of Deal objects with no attributes. According to someone on IRC, the "as" parts are incorrect because the ORM doesn't know how to assign the columns to which attribute (or something like that) which is fair enough. I tried to add attr_accessor and attr_accessible clauses though but it didn't work.
How can I make the above query work please? What I expect the result to be is an Array of Deal objects with deal_name, deal_creation_date, etc. virtual attributes.
Most likely the query is working correctly, but the returned Deal objects appear not to have any attributes when you print them out because of the way that Deal implements the inspect method.
So if you assign the result of the query to a variable you will see that this appears empty:
v.each do |deal| ; puts deal.inspect ; end
While this shows the attributes you want:
v.each do |deal| ; puts deal.to_yaml ; end
More details are in this question.
I'm using Ruby on Rails. I have a couple of models which fit the normal order/order lines arrangement, i.e.
class Order
has_many :order_lines
end
class OrderLines
belongs_to :order
belongs_to :product
end
class Product
has_many :order_lines
end
(greatly simplified from my real model!)
It's fairly straightforward to work out the most popular individual products via order line, but what magical ruby-fu could I use to calculate the most popular combination(s) of products ordered.
Cheers,
Graeme
My suggestion is to create an array a of Product.id numbers for each order and then do the equivalent of
h = Hash.new(0)
# for each a
h[a.sort.hash] += 1
You will naturally need to consider the scale of your operation and how much you are willing to approximate the results.
External Solution
Create a "Combination" model and index the table by the hash, then each order could increment a counter field. Another field would record exactly which combination that hash value referred to.
In-memory Solution
Look at the last 100 orders and recompute the order popularity in memory when you need it. Hash#sort will give you a sorted list of popularity hashes. You could either make a composite object that remembered what order combination was being counted, or just scan the original data looking for the hash value.
Thanks for the tip digitalross. I followed the external solution idea and did the following. It varies slightly from the suggestion as it keeps a record of individual order_combos, rather than storing a counter so it's possible to query by date as well e.g. most popular top 10 orders in the last week.
I created a method in my order which converts the list of order items to a comma separated string.
def to_s
order_lines.sort.map { |ol| ol.id }.join(",")
end
I then added a filter so the combo is created every time an order is placed.
after_save :create_order_combo
def create_order_combo
oc = OrderCombo.create(:user => user, :combo => self.to_s)
end
And finally my OrderCombo class looks something like below. I've also included a cached version of the method.
class OrderCombo
belongs_to :user
scope :by_user, lambda{ |user| where(:user_id => user.id) }
def self.top_n_orders_by_user(user,count=10)
OrderCombo.by_user(user).count(:group => :combo).sort { |a,b| a[1] <=> b[1] }.reverse[0..count-1]
end
def self.cached_top_orders_by_user(user,count=10)
Rails.cache.fetch("order_combo_#{user.id.to_s}_#{count.to_s}", :expiry => 10.minutes) { OrderCombo.top_n_orders_by_user(user, count) }
end
end
It's not perfect as it doesn't take into account increased popularity when someone orders more of one item in an order.
I'm having trouble joining the values for querying multiple values to one column. Here's what I got so far:
def self.showcars(cars)
to_query = []
if !cars.empty?
to_query.push cars
end
return self.find_by_sql(["SELECT * FROM cars WHERE car IN ( ? )"])
end
That makes the query into:
SELECT * FROM cars WHERE car IN (--- \n- \"honda\"\n- \"toyota\"\n')
It seems find_by_sql sql_injection protection adds the extra characters. How do I get this to work?
Do you really need find_by_sql? Since you're performing a SELECT *, and assuming your method resides on the Car model, a better way would be:
class Car < ActiveRecord::Base
def self.showcars(*cars)
where('car in :cars', :cars => cars)
# or
where(:car => cars)
end
end
Note the * right after the parameter name... Use it and you won't need to write code to make a single parameter into an array.
If you really need find_by_sql, try to write it this way:
def self.showcars(*cars)
find_by_sql(['SELECT * FROM cars where car in (?)', cars])
end
Try joining the to_query array into a comma separated string with all values in single quotes, and then passing this string as a parameter "?".
Problem resolve.
def self.average_time(time_init, time_end)
query = <<-SQL
SELECT COUNT(*) FROM crawler_twitters AS twitter WHERE CAST(twitter.publish AS TIME) BETWEEN '#{time_init}' AND '#{time_end}'
GROUP BY user) AS total_tweets_time;
SQL
self.find_by_sql(sanitize_sql(query))
end