ActiveRecord OR clause in scoped query - ruby-on-rails

statuses = %w(sick healthy hungry)
query = User.scoped(:joins => 'left outer join pets on pets.user_id = users.id', :conditions => { 'pets.id' => nil, 'users.job_status' => stati })
Given the code above, is it possible to add an OR clause to the :conditions to say something like
where (pets.id is NULL AND users.status IN ('sick', 'healthy', 'hungry')) OR (users.gender = 'male')

You can do the following using the MetaWhere gem
statuses = %w(sick healthy hungry)
query = User.include(:pets).where(
('pets.id' => nil, 'users.job_status' => statuses) |
('users.gender' => 'male')
)
The | symbol is used for OR condition.
PS : The include directive uses LEFT OUTER JOIN so there is no need to hand code the JOIN.

You could use an SQL condition instead of a Hash condition:
query = User.scoped(
:joins => 'left outer join pets on pets.user_id = users.id',
:conditions => [
'(pets.id is null AND users.status IN (?)) OR (users.gender = ?)',
statuses, 'male'
]
)
Or:
query = User.scoped(
:joins => 'left outer join pets on pets.user_id = users.id',
:conditions => [
'(pets.id is null AND users.status IN (:statuses)) OR (users.gender = :gender)',
{ :status => statuses, :gender => 'male' }
]
)
The downside is that you have to avoid trying pets.is = NULL by hand.

Related

IN clause in :conditions in rails

I am working in rails 2, I want to execute Query
PunchingInformation.all(
:select => "users.id, login, firstname, lastname,
sec_to_time(avg(time_to_sec(punching_informations.punch_in_time))) as 'avg_pit',
sec_to_time(avg(time_to_sec(punching_informations.punch_out_time))) as 'avg_pot'",
:joins => :user,
:group => "users.id",
:conditions => {
"punching_informations.date between '#{start_date}' and '#{end_date}'",
["punching_informations.user_id IN (?)", employees.map { |v| v.to_i } ]
}
)
But it always return error like
Mysql::Error: Unknown column 'punching_informations.date between '2012-09-01' and '2012-09-25'' in 'where clause': SELECT users.id,login, firstname,lastname, sec_to_time(avg(time_to_sec(punching_informations.punch_in_time))) as 'avg_pit',
sec_to_time(avg(time_to_sec(punching_informations.punch_out_time))) as 'avg_pot' FROM punching_informations INNER JOIN users ON users.id = punching_informations.user_id AND (users.type = 'User' OR users.type = 'AnonymousUser' ) WHERE (punching_informations.date between '2012-09-01' and '2012-09-25' IN ('punching_informations.user_id IN (?)','--- \n- 28\n- 90\n')) GROUP BY users.id
Need your help.
It is a bit unclear what you meant (you have array, but taken in curly braces {} like a hash), but it seems ruby treats first string ("punching_informations.date between '#{start_date}' and '#{end_date}'") as a column, and second array, as array of expected values, thus making the invalid IN condition.
Perhaps it would work if rewritten as
:conditions => {
[ "(punching_informations.date between '#{start_date}' AND '#{end_date}') AND punching_informations.user_id IN (?)", employees.map { |v| v.to_i } ]
}
or even better
:conditions => {
[ "(punching_informations.date between ? AND ?) AND punching_informations.user_id IN (?)", start_date, end_date, employees.map { |v| v.to_i } ]
}
add punching_informations.date and punching_informations.user_id in select
:select => "punching_informations.date, punching_informations.user_id, users.id, ....

Union of 2 named_scopes

I have the following 2 named_scopes:
named_scope :scope1, lambda { |pi_id|
{ :select => "DISTINCT REF_PRO.*",
:joins => "INNER JOIN LNK_PRO ON LNK_PRO.PR_PRO_FK = REF_PRO.RPR_ID
INNER JOIN EMI_SUBMISSION on EMI_SUBMISSION.SUB_ID = LNK_PRO.PR_SUBMISSION_FK
INNER JOIN EMI_PERSON on EMI_PERSON.PER_ID = EMI_SUBMISSION.SUB_PI_FK ",
:conditions=>["EMI_PERSON.PER_ID = ? ", pi_id],
:group => "REF_PRO.RPR_ID"
}
}
named_scope :scope2, lambda { |pi_id|
{ :select => "REF_PRO.*",
:joins => "INNER JOIN REF_USER ON REF_USER.USR_ID = REF_PRO.RPR_CREATED_BY
INNER JOIN LNK_USER_PI on LNK_USER_PI.USP_USER_FK = REF_USER.USR_ID ",
:conditions=>["LNK_USER_PI.USP_PI_ID = ? ", pi_id]
}
}
I need to join their results.
Is there a way of doing a union of the 2 resultsets? If not, how can I modify one named_scope so that it returns me the combined results of the above named scopes?
Thanks a lot for your help
You can do this:
Model.scope1+Model.scope2

Ruby: group an array of ActiveRecord objects in a hash

I'd like to group an array of ActiveRecord objects into a hash with an interface that's simple to query after an SQL statement that looks like the following:
SELECT name,value from foo where name IN ('bar', 'other_bar') LIMIT 2;
After that query, I want a hash where I can go:
foo[:bar] # output: value1
foo[:other_bar] # output: value2
What's the best way to collect the objects with ActiveRecord and group them so I can use the interface above?
In Rails 2
foos = Foo.all :select => "name, value",
:conditions => ["name in (?)", %w(bar other_bar)],
:limit => 2
In Rails 3
foos = Foo.where("name in (?)", %w(bar other_bar)).select("name, value").limit(2)
Then
foo = Hash[foos.map { |f| [f.name.to_sym, f.value] }]
or
foo = foos.inject({}) { |h, f| h[f.name.to_sym] = f.value; h }
or in Ruby 1.9
foo = foos.each_with_object({}) { |f, hash| hash[f.name.to_sym] = f.value }
If I understood you correctly:
foo = Hash[Foo.find(:all, :limit => 2, :select => "name, value", :conditions => ["name in ('bar', 'other_bar')"]).map { |s| [s.name.intern, s.value] }]
Hash[result.map { |r| [r[:name].to_sym, r[:value]] } ]
models.inject({}) {|h,m| h[ m.name.to_sym ] = m.value; h }

same code in different actions of the controller in rails -- how to make DRY?

I have a controller with two different actions, but both need this same code, which is a little long, how can I allow them access to this same behavior but keep it DRY?
#list = Contact.find :all,
:select => "companies.name AS co_name,
companies.id AS comp_id,
COUNT(contact_emails.id) AS email_count,
COUNT(contact_calls.id) AS call_count,
COUNT(contact_letters.id) AS letter_count,
COUNT(contact_postalcards.id) AS postalcard_count",
:conditions => ['contact_emails.date_sent < ? and contact_emails.date_sent > ?',
report_end_date, report_start_date],
:joins => [
"LEFT JOIN companies ON companies.id = contacts.company_id",
"LEFT JOIN contact_emails ON contact_emails.contact_id = contacts.id",
"LEFT JOIN contact_letters ON contact_letters.contact_id = contacts.id",
"LEFT JOIN contact_postalcards ON contact_postalcards.contact_id = contacts.id",
"LEFT JOIN contact_calls ON contact_calls.contact_id = contacts.id"
],
#:group => "companies.id"
:group => "companies.name"
puts #list[0].attributes.inspect
You should move this code to model:
# Contatct model
def self.get_list(report_start_date, report_end_date)
self.find :all,
:select => "companies.name AS co_name,
companies.id AS comp_id,
COUNT(contact_emails.id) AS email_count,
COUNT(contact_calls.id) AS call_count,
COUNT(contact_letters.id) AS letter_count,
COUNT(contact_postalcards.id) AS postalcard_count",
:conditions => ['contact_emails.date_sent < ? and contact_emails.date_sent > ?',
report_end_date, report_start_date],
:joins => [
"LEFT JOIN companies ON companies.id = contacts.company_id",
"LEFT JOIN contact_emails ON contact_emails.contact_id = contacts.id",
"LEFT JOIN contact_letters ON contact_letters.contact_id = contacts.id",
"LEFT JOIN contact_postalcards ON contact_postalcards.contact_id = contacts.id",
"LEFT JOIN contact_calls ON contact_calls.contact_id = contacts.id"
],
#:group => "companies.id"
:group => "companies.name"
end
Then you can use it in controllers:
#list = Contact.get_list(report_start_date, report_end_date)
Probably you can also split it to smaller parts and use scopes and defined associations instead of writing all of it on your own.
I would add a function for generating the count and join sql:
class Contact < ActiveRecord::Base
def self.get_list(report_start_date, report_end_date)
all(:select => "companies.name AS co_name,
companies.id AS comp_id,
#{table_count_col(
:contact_emails,
:contact_calls,
:contact_letters,
:contact_postalcards
)}",
:conditions => ['contact_emails.date_sent < ? AND
contact_emails.date_sent > ?',
report_end_date, report_start_date],
:joins => join_table(
:companies,
:contact_emails,
:contact_letters,
:contact_postalcards,
:contact_calls
),
)
end
end
Where table_count_col and table_join are static methods inside Contact class:
def self.table_count_col(*args)
args.collect do |table|
count_col = "#{table.to_s.gsub(/^contact_/, '').singularize}_count"
"COUNT(#{table}.id) AS #{count_col}"
end.join(",")
end
def self.table_join(*args)
args.collect do |table|
"LEFT JOIN #{table} ON #{table}.id = contacts.company_id"
end.join(",")
end

named scope vs. find_by_sql (specific example)

Just out of curiosity, does anyone know a better way of building the following collection using named scopes (as opposed to find_by_sql)?
#available = Workflow.find_by_sql(["
SELECT workflows.id FROM workflows
WHERE workflows.project_id = ? AND workflows.status < 5 AND
( workflows.created_by = ? OR workflows.id IN
(
SELECT workflow_id FROM workflow_histories
INNER JOIN workflow_recipients on workflow_histories.id = workflow_recipients.workflow_history_id
WHERE workflow_recipients.recipient_id = ? AND workflow_recipients.recipient_type = ?
)
)", project.id, #current_user.id, #current_user.id , 'USER'])
I haven't tested this, but I think it would work:
named_scope :available, lambda { |user_id, project_id|
{ :select => :id,
:conditions => [ "project_id = :project_id AND status < 5 AND
(created_by = :user_id OR id IN (
SELECT workflow_id FROM workflow_histories
INNER JOIN workflow_recipients ON workflow_histories.id = workflow_recipients.workflow_history_id
WHERE workflow_recipients.recipient_id = :user_id AND workflow_recipients.recipient_type = :recipient_type
)",
{ :user_id => user_id,
:project_id => project_id,
:recipient_type => "USER"
}
]
}
}
(A previous version of my answer breaks the sub-select out into its own query, which I think is unnecessary.)

Resources