In a Rails model I am trying to acheive a named_scope that filters on a start_date and end_date. This is easy. But I am going to have to do it on lots of different fields on many occasions.
Is this asking for trouble? If so why (SQL injection?) and is there another way to acheive this.
named_scope :between, lambda {|start_date, end_date, field|
{ :conditions => ["#{field} >= ? AND #{field} <= ?", start_date, end_date] }
}
EDIT: Solution Used
Using Eggdrop's line of thinking I went with:
##valid_fields = %w(fields in here)
named_scope :between, lambda{ |start_date, end_date, field_name|
field = (##valid_fields.include?(field_name)) ? (field_name) : raise (ActiveRecord::StatementInvalid)
{ :conditions => ["#{field} >= ? AND #{field} <= ?", start_date, end_date]}
}
Now I can reuse my named_scope for fields I wish to filter on date range without rewriting essentially the same scope over and over and whitelist the field names to avoid any mucking about with my column names and tricky SQL injection should the code ever get exposed to user input in the future.
Maybe you could write a method in your model to validate 'field':
If table x, then 'field' must be a particular existing date field in that table.
In other words you don't allow external input into 'field' directly - the external input has to map to known attributes and defined conditions specified in your validate method.
In general, though, this overall direction would not seem to be recommended.
Related
I'm combining the two .where like this:
#questions = (FirstQuestion.where(user_id: current_user) + SecondQuestion.where(user_id: current_user)).sort_by(&:created_at).reverse
Both .where searches are with one attribute... the user_id. But now I want to search with two attributes, the user_id and this created_at >= ?", Date.today + 60.days. So basically I want to find the object with a user_id: current_user and the objects that where created less then or equal to 60 days.
Any idea on how to implement this?
Please see my comment as well... because it is kind of a code smell when you have models names FirstQuestion, SecondQuestion. There's really no reason for having separate models. You could probably easily model the logic via an attribute question_depth or something (I don't know what you are trying to achieve exactly).
With regard to your question: ActiveRecord is quite a nice class, that allows for very customizable queries. In your case, you could easily write both conditions each in a separate where, or create a single where. That's totally up to you:
Question.where(user: current_user).where('created_at <= ?', 60.days.from_now)
Or in a single where
Question.where('user_id = ? AND created_at <= ?', current_user.id, 60.days.from_now)
Also, consider using scopes on your Question model for readability and reusability:
class Question < AppModel
scope :by_user, -> (user) { where(user: user) }
scope :min_age, -> (date) { where('created_at <= ?', date) }
end
And use it like:
Question.by_user(current_user).min_age(60.days.from_now)
In my model, I have this method which takes the last_name and first_name columns for an object and concatenates them into a string.
def name
last_name + " " + first_name
end
I want to define a scope that can sort my objects by that method. How would one go about doing that, using my method? I don't want to define some scope that first sorts by last_name, and then by first_name in the scope (if that's even possible). My understanding that you can only scope on actual columns in the rails framework? Is that incorrect?
Here's what I wrote, but obviously neither works, as there is no name field in my AdminUser table. Not sure why the second one doesn't work, but I'm guessing that the :name_field wouldn't work, as it's not actually in the model/database as a column.
scope :sorted, lambda { order("name ASC")}
scope :sorted, lambda { :name_field => name, order("name_field ASC")}
Unfortunately it is not possible to do this directly from Ruby/Rails to SQL. You could achieve what you want in two ways
You can load all the users into memory and sort them in Ruby
User.all.to_a.sort_by(&:name)
Or you can define an order in SQL as such
ORDER BY CONCAT(users.last_name, ' ', users.first_name) ASC;
In Rails, you'd have to do the following
scope :sorted, -> {
order("CONCAT(users.last_name, ' ', users.first_name) ASC")
}
Do note that this may not be portable between DBs.
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.
Facing problem for merging multiple having clause ... As in my model i had two scopes written
1.)
scope :vacant_list, lambda {|daterange|
where("vacancies.vacancy_date" => daterange , "vacancies.availability" => ["Y" ,"Q"]).joins(:vacancies).group("vacancies.property_id").having("count(vacancies.vacancy_date) >= #{daterange.count}") unless daterange.blank?
}
2.)
scope :amenity_type, lambda {|term|
where("amenities.name" => term).joins(:amenities).group("amenities.property_id").having("count(amenities.name) >= #{term.size}") unless term.blank?
}
I need to do something like this
#model = Model.vacant_list(daterange).amenity_type(term)
But i always get wrong number of arguments (2 for 1)
/home/vivek/.rvm/gems/ruby-1.9.2-p180/gems/arel-2.0.10/lib/arel/select_manager.rb:100:in `having'
/home/vivek/.rvm/gems/ruby-1.9.2-p180/gems/activerecord-3.0.6/lib/active_record/relation/query_methods.rb:180:in `build_arel'
/home/vivek/.rvm/gems/ruby-1.9.2-p180/gems/activerecord-3.0.6/lib/active_record/relation/query_methods.rb:149:in `arel'.
If i remove any one of scopes having clause then the above action works perfectly .
Is there any way to like-
#model = Model.vacant_list(daterange) and then remove the active record relation and then apply #model.amenity_type(term).
I tried lots of things but didnt find any solution for this.....
I think you're doing this wrong - it took me quite a while to dig out what the actual intent of the 'having' clauses above was. From what I can tell, it looks like the idea is that you pass in an array of dates or amenities and want to find properties that match all of them.
The underlying issue is that (AFAIK) code like this will NOT do the right thing:
# NOTE: WILL NOT WORK
scope :vacant_on, lambda { |date| where('vacancies.vacancy_date' => date, "vacancies.availability" => ["Y" ,"Q"]).joins(:vacancies) }
scope :vacant_list, lambda {|daterange|
daterange.inject(self) { |rel, date| rel.vacant_on(date) }
}
Unless this has changed in Arel (haven't poked it much) then this fails because you end up with exactly one join to the vacancies table, but multiple where clauses that specify incompatible values. The solution is to do multiple joins and alias them individually. Here's how I used to do it in Rails 2:
named_scope :vacant_on, lambda { |date|
n = self.connection.quote_table_name("vacancies_vacant_on_#{date}")
{ :joins => "INNER JOIN vacancies AS #{n} ON #{n}.property_id = properties.id",
:conditions => ["#{n}.vacancy_date = ? AND #{n}.availability IN (?)", date, ["Y","Q"]] }
}
Explicitly specifying an 'AS' here lets multiple versions of this scope coexist in one query, and you get the results you'd expect.
Here's a rough translation of this to modern Arel syntax:
scope :vacant_on, lambda { |date|
our_vacancies = Vacancy.arel_table.alias
joins(our_vacancies).on(our_vacancies[:property_id].eq(Property.arel_table[:id])).
where(our_vacancies[:vacancy_date].eq(date),
our_vacancies[:availability].in(["Y" ,"Q"]))
}
Haven't tried it, but this post:
http://www.ruby-forum.com/topic/828516
and the documentation seem to imply it would do the right thing...
I have a Model called Section which has many articles (Article). These articles are versioned (a column named version stores their version no.) and I want the freshest to be retrieved.
The SQL query which would retrieve all articles from section_id 2 is:
SELECT * FROM `articles`
WHERE `section_id`=2
AND `version` IN
(
SELECT MAX(version) FROM `articles`
WHERE `section_id`=2
)
I've been trying to make, with no luck, a named scope at the Article Model class which look this way:
named_scope :last_version,
:conditions => ["version IN
(SELECT MAX(version) FROM ?
WHERE section_id = ?)", table_name, section.id]
A named scope for fetching whichever version I need is working greatly as follows:
named_scope :version, lambda {|v| { :conditions => ["version = ?", v] }}
I wouldn't like to end using find_by_sql as I'm trying to keep all as high-level as I can. Any suggestion or insight will be welcome. Thanks in advance!
I would take a look at some plugins for versioning like acts as versioned or version fu or something else.
If you really want to get it working the way you have it now, I would add a boolean column that marks if it is the most current version. It would be easy to run through and add that for each column and get the data current. You could easily keep it up-to-date with saving callbacks.
Then you can add a named scope for latest on the Articles that checks the boolean
named_scope :latest, :conditions => ["latest = ?", true]
So for a section you can do:
Section.find(params[:id]).articles.latest
Update:
Since you can't add anything to the db schema, I looked back at your attempt at the named scope.
named_scope :last_version, lambda {|section| { :conditions => ["version IN
(SELECT MAX(version) FROM Articles
WHERE section_id = ?)", section.id] } }
You should be able to then do
section = Section.find(id)
section.articles.last_version(section)
It isn't the cleanest with that need to throw the section to the lambda, but I don't think there is another way since you don't have much available to you until later in the object loading, which is why I think your version was failing.