Building dynamc queries in rails 3 - ruby-on-rails-3

I have been struggling far too long on this one. I have a fairly complex query that I have pieced together, but it seems to me to be way too verbose and ugly.
I have an advanced tickets search form that has 8 different search fields, plus created_at and updated_at search options in which users can choose the day, month, and/or year to filter their results.
I am building query conditions in my tickets_controller like this.
conditions = String.new
arguments = []
unless params[:id].blank?
conditions << ' AND ' unless conditions.length == 0
conditions << 'tickets.id = ?'
arguments << params[:id]
end
unless params[:organization].blank?
conditions << ' AND ' unless conditions.length == 0
conditions << 'organizations.name ILIKE ?'
arguments << "%#{params[:organization]}%"
end
unless params[:keywords].blank?
conditions << ' AND ' unless conditions.length == 0
conditions << 'subject ILIKE ?'
arguments << "%#{params[:keywords]}%"
end
unless params[:status_id].blank?
conditions << ' AND ' unless conditions.length == 0
conditions << 'status_id = ?'
arguments << params[:status_id]
end
unless params[:resolution_status_id].blank?
conditions << ' AND ' unless conditions.length == 0
conditions << 'resolution_status_id = ?'
arguments << params[:resolution_status_id]
end
unless params[:priority_id].blank?
conditions << ' AND ' unless conditions.length == 0
conditions << 'priority_id = ?'
arguments << params[:priority_id]
end
unless params[:assigned_to_id].blank?
conditions << ' AND ' unless conditions.length == 0
conditions << 'assigned_to_id = ?'
arguments << params[:assigned_to_id]
end
#tickets = Ticket.joins(:organization).where(conditions, arguments).paginate(:page => params[:page], :per_page => 20).order("updated_at DESC")
It gets confusing for me on fields that have special joins. For example, I have and email column on another table and have created a separate method in tickets.rb that looks like this.
def self.email(email)
joins('LEFT OUTER JOIN assignments ON assignments.person_id = tickets.owner_id').where('assignments.email_address ILIKE ?', email)
end
then call it with a terinary like this
params[:email].blank? ? nil : #tickets = #tickets.email("%#{params[:email]}%")
I also have a contact search field that searches both the first name, and last name of my people table. I created a seperate method that looks like this...
def self.contact(contact)
joins('LEFT OUTER JOIN people ON people.user_id = tickets.owner_id').where('people.last ILIKE ? OR people.first ILIKE ?', contact, contact)
end
Then call it in another terinary as I did with the params[:email].
This really doesn't seem like the best way to go about it to me, but I am new to programming and haven't been able to come up with anything more elegant. I have a number or these terniary operators that are building up my query, and while it is working just fine, I can't help but feel like I'm doing something wrong here.
I hope this is enough information to go on, if not I can clarify when needed.

It seems to me that you are doing manually all of ActiveRecord's job.
I would have a scope for each of your cases:
eg:
#class Ticket < ActiveRecord::Base
scope :with_subject lambda { |keyword| where('subject ILIKE ?', keyword) }
scope :with_status_id { |s_id| where(status_id: s_id) }
which will be used as follows:
#tickets = Ticket.scoped
#tickets = #tickets.with_subject(params[:keywords]) unless params[:keywords].blank?
#tickets = #tickets.with_status_id(params[:status_id]) if params[:status_id].present?
the joins can be also chained in AR syntax
#tickets = #tickets.joins('LEFT OUTER JOIN people ON people.user_id = tickets.owner_id')
The only problem is the use of OR clauses, which IMO are more readable in the SQL syntax
where('people.last ILIKE ? OR people.first ILIKE ?', contact, contact)
Hope it helps.
PS: You can find more on the subject on the relevant Rails guide

Related

Check for Dates Overlap in Ruby on Rails

I have a model that has two columns (started_at and ended_at). I want to add a custom validator that ensures that no other record exists with dates that overlap with the record I'm validating. So far I have:
# app/models/event.rb
class Event < ActiveRecord::Base
validates_with EventValidator
end
# app/validators/event_validator.rb
class EventValidator < ActiveModel::Validator
attr_reader :record
def validate(record)
#record = record
validate_dates
end
private
def validate_dates
started_at = record.started_at
ended_at = record.ended_at
arel_table = record.class.arel_table
# This is where I'm not quite sure what type of query I need to perform...
constraints = arel_table[:started_at].gteq(ended_at)
.and(arel_table[:ended_at].lteq(started_at))
if record.persisted?
constraints = constraints
.and(arel_table[:id].not_eq(record.id))
end
if record.class.where(constraints).exists?
record.error[:base] << "Overlaps with another event"
end
end
end
I don't know exactly what query I need to ensurethat there is no overlapping. Any help is greatly appreciated
I don't use Arel but I think the query should be:
constraints = arel_table[:started_at].lteq(ended_at)
.and(arel_table[:ended_at].gteq(started_at))
Two periods overlap when
period1.start < period2.end
period1.end > period2.start
Have a look at Validates Overlap gem
You can either use it instead your code or take condition code from it
# Return the condition string depend on exclude_edges option.
def condition_string(starts_at_attr, ends_at_attr)
except_option = Array(options[:exclude_edges]).map(&:to_s)
starts_at_sign = except_option.include?(starts_at_attr.to_s.split(".").last) ? "<" : "<="
ends_at_sign = except_option.include?(ends_at_attr.to_s.split(".").last) ? ">" : ">="
query = []
query << "(#{ends_at_attr} IS NULL OR #{ends_at_attr} #{ends_at_sign} :starts_at_value)"
query << "(#{starts_at_attr} IS NULL OR #{starts_at_attr} #{starts_at_sign} :ends_at_value)"
query.join(" AND ")
end
I would construct a query that looks something like this:
Event.exists?( 'started_at < ? AND ended_at > ?', ended_at, started_at )
If this returns true, an overlapping record exists.

Passing array to Rails where method

So I want to dynamically pass filter parameters to my where method so basically I have this
#colleges = College.where(#filter).order(#sort_by).paginate(:page => params[:page], :per_page => 20)
And the #where is just a string built with this method
def get_filter_parameters
if params[:action] == 'index'
table = 'colleges'
columns = College.column_names
else
table = 'housings'
columns = Housing.column_names
end
filters = params.except(:controller, :action, :id, :sort_by, :order, :page, :college_id)
filter_keys = columns & filters.keys
#filter = ""
first = true
if filter_keys
filter_keys.each do |f|
if first
#filter << "#{table}.#{f} = '#{filters[f]}'"
first = false
else
#filter << " AND #{table}.#{f} = '#{filters[f]}'"
end
end
else
#filter = "1=1"
end
The problem is I don't know how good it is to drop raw SQL into a where method like that. I know normally you can do stuff like :state => 'PA', but how do I do that dynamically?
UPDATE
Okay so I am now passing a hash and have this:
if params[:action] == 'index'
columns = College.column_names
else
columns = Housing.column_names
end
filters = params.except(:controller, :action, :id, :sort_by, :order, :page, :college_id)
filter_keys = columns & filters.keys
#filter = {}
if filter_keys
filter_keys.each do |f|
#filter[f] = filters[f]
end
end
Will that be enough to protect against injection?
in this code here:
College.where(:state => 'PA')
We are actually passing in a hash object. Meaning this is equivalent.
filter = { :state => 'PA' }
College.where(filter)
So you can build this hash object instead of a string:
table = "colleges"
field = "state"
value = "PA"
filter = {}
filter["#{table}.#{field}"] = value
filter["whatever"] = 'omg'
College.where(filter)
However, BE CAREFUL WITH THIS!
Depending on where this info is coming from, you be opening yourself up to SQL injection attacks by putting user provided strings into the fields names of your queries. When used properly, Rails will sanitize the values in your query. However, usually the column names are fixed by the application code and dont need to be sanitized. So you may be bypassing a layer of SQL injection protection by doing it this way.

ActiveRecord Arel OR condition

How can you combine 2 different conditions using logical OR instead of AND?
NOTE: 2 conditions are generated as rails scopes and can't be easily changed into something like where("x or y") directly.
Simple example:
admins = User.where(:kind => :admin)
authors = User.where(:kind => :author)
It's easy to apply AND condition (which for this particular case is meaningless):
(admins.merge authors).to_sql
#=> select ... from ... where kind = 'admin' AND kind = 'author'
But how can you produce the following query having 2 different Arel relations already available?
#=> select ... from ... where kind = 'admin' OR kind = 'author'
It seems (according to Arel readme):
The OR operator is not yet supported
But I hope it doesn't apply here and expect to write something like:
(admins.or authors).to_sql
ActiveRecord queries are ActiveRecord::Relation objects (which maddeningly do not support or), not Arel objects (which do).
[ UPDATE: as of Rails 5, "or" is supported in ActiveRecord::Relation; see https://stackoverflow.com/a/33248299/190135 ]
But luckily, their where method accepts ARel query objects. So if User < ActiveRecord::Base...
users = User.arel_table
query = User.where(users[:kind].eq('admin').or(users[:kind].eq('author')))
query.to_sql now shows the reassuring:
SELECT "users".* FROM "users" WHERE (("users"."kind" = 'admin' OR "users"."kind" = 'author'))
For clarity, you could extract some temporary partial-query variables:
users = User.arel_table
admin = users[:kind].eq('admin')
author = users[:kind].eq('author')
query = User.where(admin.or(author))
And naturally, once you have the query you can use query.all to execute the actual database call.
I'm a little late to the party, but here's the best suggestion I could come up with:
admins = User.where(:kind => :admin)
authors = User.where(:kind => :author)
admins = admins.where_values.reduce(:and)
authors = authors.where_values.reduce(:and)
User.where(admins.or(authors)).to_sql
# => "SELECT \"users\".* FROM \"users\" WHERE ((\"users\".\"kind\" = 'admin' OR \"users\".\"kind\" = 'author'))"
As of Rails 5 we have ActiveRecord::Relation#or, allowing you to do this:
User.where(kind: :author).or(User.where(kind: :admin))
...which gets translated into the sql you'd expect:
>> puts User.where(kind: :author).or(User.where(kind: :admin)).to_sql
SELECT "users".* FROM "users" WHERE ("users"."kind" = 'author' OR "users"."kind" = 'admin')
From the actual arel page:
The OR operator works like this:
users.where(users[:name].eq('bob').or(users[:age].lt(25)))
I've hit the same problem looking for an activerecord alternative to mongoid's #any_of.
#jswanner answer is good, but will only work if the where parameters are a Hash :
> User.where( email: 'foo', first_name: 'bar' ).where_values.reduce( :and ).method( :or )
=> #<Method: Arel::Nodes::And(Arel::Nodes::Node)#or>
> User.where( "email = 'foo' and first_name = 'bar'" ).where_values.reduce( :and ).method( :or )
NameError: undefined method `or' for class `String'
To be able to use both strings and hashes, you can use this :
q1 = User.where( "email = 'foo'" )
q2 = User.where( email: 'bar' )
User.where( q1.arel.constraints.reduce( :and ).or( q2.arel.constraints.reduce( :and ) ) )
Indeed, that's ugly, and you don't want to use that on a daily basis. Here is some #any_of implementation I've made : https://gist.github.com/oelmekki/5396826
It let do that :
> q1 = User.where( email: 'foo1' ); true
=> true
> q2 = User.where( "email = 'bar1'" ); true
=> true
> User.any_of( q1, q2, { email: 'foo2' }, "email = 'bar2'" )
User Load (1.2ms) SELECT "users".* FROM "users" WHERE (((("users"."email" = 'foo1' OR (email = 'bar1')) OR "users"."email" = 'foo2') OR (email = 'bar2')))
Edit : since then, I've published a gem to help building OR queries.
Just make a scope for your OR condition:
scope :author_or_admin, where(['kind = ? OR kind = ?', 'Author', 'Admin'])
Using SmartTuple it's going to look something like this:
tup = SmartTuple.new(" OR ")
tup << {:kind => "admin"}
tup << {:kind => "author"}
User.where(tup.compile)
OR
User.where((SmartTuple.new(" OR ") + {:kind => "admin"} + {:kind => "author"}).compile)
You may think I'm biased, but I still consider traditional data structure operations being far more clear and convenient than method chaining in this particular case.
To extend jswanner answer (which is actually awesome solution and helped me) for googling people:
you can apply scope like this
scope :with_owner_ids_or_global, lambda{ |owner_class, *ids|
with_ids = where(owner_id: ids.flatten).where_values.reduce(:and)
with_glob = where(owner_id: nil).where_values.reduce(:and)
where(owner_type: owner_class.model_name).where(with_ids.or( with_glob ))
}
User.with_owner_ids_or_global(Developer, 1, 2)
# => ...WHERE `users`.`owner_type` = 'Developer' AND ((`users`.`owner_id` IN (1, 2) OR `users`.`owner_id` IS NULL))
What about this approach: http://guides.rubyonrails.org/active_record_querying.html#hash-conditions (and check 2.3.3)
admins_or_authors = User.where(:kind => [:admin, :author])
Unfortunately it is not supported natively, so we need to hack here.
And the hack looks like this, which is pretty inefficient SQL (hope DBAs are not looking at it :-) ):
admins = User.where(:kind => :admin)
authors = User.where(:kind => :author)
both = User.where("users.id in (#{admins.select(:id)}) OR users.id in (#{authors.select(:id)})")
both.to_sql # => where users.id in (select id from...) OR users.id in (select id from)
This generates subselets.
And a little better hack (from SQL perspective) looks like this:
admins_sql = admins.arel.where_sql.sub(/^WHERE/i,'')
authors_sql = authors.arel.where_sql.sub(/^WHERE/i,'')
both = User.where("(#{admins_sql}) OR (#{authors_sql})")
both.to_sql # => where <admins where conditions> OR <authors where conditions>
This generates proper OR condition, but obviously it only takes into account the WHERE part of the scopes.
I chose the 1st one until I'll see how it performs.
In any case, you must be pretty careful with it and watch the SQL generated.

how to check if email exists from object errors array Rails

I am creating users from emails.
If an email exists I want to skip creating that user.
Here is my model method:
def invite_users(emails, board)
invalid_emails =[]
emails.each do |email|
user = User.new(:email => email)
user.save ? Participant.make(user, board) : invalid_emails << email unless user.errors.each {|key, msg| msg == "has already been taken"}
end
invalid_emails if invalid_emails.any?
end
I want to check if the error generated from user.save is a duplicate email error. If so I don't want to put that email in the invalid_emails array.
How can I do this?
validates_uniqueness_of is useless for such purposes.
The canonical thing to do is to add a unique index on email to the users table, and use code like:
begin
-- do stuff that creates records --
rescue ActiveRecord::StatementInvalid => e
raise unless /Mysql::Error: Duplicate entry/.match(e)
end
if you want to emulate create or find, you do something like
result = nil
begin
result = User.create(:email => xxxx)
rescue ActiveRecord::StatementInvalid => e
if /Mysql::Error: Duplicate entry/.match(e)
user = User.find_by_email(xxx)
else
raise
end
end
just change
user = User.new(:email => email)
user.save ? Participant.make(user, board) : invalid_emails << email unless user.errors.each {|key, msg| msg == "has already been taken"}
to
user = User.new(:email => email)
if User.find_by_email( email ).blank?
Participant.make(user, board)
else
invalid_emails << email
end
Since errors is basically an array you could just change each to any to get a boolean value for your condition.

Rails: join with multiple conditions

I have a simple model like
class Interest < ActiveRecord::Base
has_and_belongs_to_many :user_profiles
end
class UserProfile < ActiveRecord::Base
has_and_belongs_to_many :interests
end
When I want to query all the users with a specific interests, that's fairly simple doing
UserProfile.joins(:interests).where('interests.id = ?', an_interest)
But how can I look for users who have multiple interests? Of course if I do
UserProfile.joins(:interests).where('interests.id = ?', an_interest).where('interests.id = ?', another_interest)
I get always an empty result, since after the join, no row can have simultaneously interest.id = an_interest and interest.id = another_interest.
Is there a way in ActiveRecord to express "I want the list of users who have 2 (specified) interests associated?
update (solution) that's the first working version I came up, kudos to Omar Qureshi
specified_interests.each_with_index do |i, idx|
main_join_clause = "interests_#{idx}.user_profile_id = user_profiles.id"
join_clause = sanitize_sql_array ["inner join interests_user_profiles interests_#{idx} on
(#{main_join_clause} and interests_#{idx}.interest_id = ?)", i]
relation = relation.joins(join_clause)
end
in (?) is no good - it's an OR like expression
what you will need to do is have multiple joins written out longhanded
profiles = UserProfile
interest_ids.each_with_index do |i, idx|
main_join_clause = "interests_#{idx}.user_profile_id = user_profiles.id"
join_clause = sanitize_sql_array ["inner join interests interests_#{idx} on
(#{main_join_clause} and interests_#{idx}.id = ?)", i]
profiles = profiles.join(join_clause)
end
profiles
You may need to change the main_join_clause to suit your needs.
This will get users that have at least one of the specified interests.
UserProfile.joins(:interests).where(:id => [an_interest_id, another_interest_id])
To get users that have both of the specified interests I'd probably do something like this:
def self.all_with_interests(interest_1, interest_2)
users_1 = UserProfile.where("interests.id = ?", interest_1.id)
users_2 = UserProfile.where("interests.id = ?", interest_2.id)
users_1 & users_2
end
Not amazingly efficient, but it should do what you need?
Try IN (?) and an array:
UserProfile.joins(:interests).where('interests.id IN (?)', [1,2,3,4,5])