named scope vs. find_by_sql (specific example) - ruby-on-rails

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.)

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, ....

SQLite3::SQLException: ambiguous column name: id: SELECT DISTINCT id FROM "tickets"

I really don't think the title of this explains well of what I'm trying to do but I'm not even sure how to ask.
So I have ticket has_many tasks and task belongs_to account. I've this as a scope to return the ticket listing where an tickets task belongs to an account:
scope :for_tasks_account, lambda { |account| joins(:tasks => :account ).where("accounts.id = ?", account.id) }
but it's returning multiple of the same ticket because a ticket has multiple tasks that the account belongs to.
How can I get it to only return each ticket once rather for each task in that ticket that an account belongs to?
Thanks!
Update
I'd actually like to combine to scopes to list all that apply to the two lambdas:
scope :for_account, lambda { |account| joins(:group => :accounts ).where("accounts.id = ?", account.id) } || lambda { |account| joins(:tasks => :account ).where("accounts.id = ?", account.id) }
Is this possible? As well as the first issue.
Update 2
I've figured out how to get both of the queries to be combined but I'm still getting multiple of the same ticket in the returned query.
scope :for_group_with_account, lambda { |account| joins(:group => :accounts ).where("accounts.id = ?", account.id) }
scope :for_task_with_account, lambda { |account| joins(:tasks => :account ).where("accounts.id = ?", account.id) }
scope :for_account, lambda { |account| for_group_with_account(account) & for_task_with_account(account).select('DISTINCT id') }
I'm using DISTICNT but I still get
SQLite3::SQLException: ambiguous column name: id: SELECT DISTINCT id FROM "tickets" INNER JOIN "groups" ON "groups"."id" = "tickets"."group_id" INNER JOIN "assignments" ON "groups"."id" = "assignments"."group_id" INNER JOIN "accounts" ON "accounts"."id" = "assignments"."account_id" INNER JOIN "tasks" ON "tasks"."ticket_id" = "tickets"."id" INNER JOIN "accounts" "accounts_tasks" ON "accounts_tasks"."id" = "tasks"."account_id" WHERE ("tickets"."archived" IS NULL) AND (accounts.id = 20) LIMIT 20 OFFSET 0
Thanks again!
I think you should be able to use "distinct" in this scenario.
scope :for_tasks_account, lambda { |account| joins(:tasks => :account ).where("accounts.id = ?", account.id).select('distinct accounts.id') }

ActiveRecord OR clause in scoped query

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.

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

has_many association...class id not found

How can I write the code below so that it passes the user.id. Given what I have, it throws Class id not found error. (User has many fights. And Fight belongs to user. User can either be a challenger in the fight, or a challengee in the other.)
has_many :fight_wins, :class_name => 'Fight', :foreign_key => 'challenger_id or challengee_id',
:conditions => ["(challenger_id = ? and challenger_won = ?) or (challengee_id = ? and challenger_won = ?)", self.id, true, self.id, false]
You can use the finder_sql option for complex has_many conditions:
has_many :fight_wins, :class_name => 'Fight', :finder_sql =>
'#{sanitize_sql_array(
"SELECT f.*
FROM fights AS f
WHERE (f.challenger_id = ? AND f.challenger_won = ?) OR
(f.challengee_id = ? AND f.challenger_won = ?)
", id, true, id, false)}'

Resources