Rails scalar query - ruby-on-rails

I need to display a UI element (e.g. a star or checkmark) for employees that are 'favorites' of the current user (another employee).
The Employee model has the following relationship defined to support this:
has_and_belongs_to_many :favorites, :class_name => "Employee", :join_table => "favorites",
:association_foreign_key => "favorite_id", :foreign_key => "employee_id"
The favorites has two fields: employee_id, favorite_id.
If I were to write SQL, the following query would give me the results that I want:
SELECT id, account,
IF(
(
SELECT favorite_id
FROM favorites
WHERE favorite_id=p.id
AND employee_id = ?
) IS NULL, FALSE, TRUE) isFavorite
FROM employees
Where the '?' would be replaced by the session[:user_id].
How do I represent the isFavorite scalar query in Rails?
Another approach would use a query like this:
SELECT id, account, IF(favorite_id IS NULL, FALSE, TRUE) isFavorite
FROM employees e
LEFT OUTER JOIN favorites f ON e.id=f.favorite_id
AND employee_id = ?
Again, the '?' is replaced by the session[:user_id] value.
I've had some success writing this in Rails:
ee=Employee.find(:all, :joins=>"LEFT OUTER JOIN favorites ON employees.id=favorites.favorite_id AND favorites.employee_id=1", :select=>"employees.*,favorites.favorite_id")
Unfortunately, when I try to make this query 'dynamic' by replacing the '1' with a '?', I get errors.
ee=Employee.find(:all, :joins=>["LEFT OUTER JOIN favorites ON employees.id=favorites.favorite_id AND favorites.employee_id=?",1], :select=>"employees.*,favorites.favorite_id")
Obviously, I have the syntax wrong, but can :joins expressions be 'dynamic'? Is this a case for a Lambda expression?
I do hope to add other filters to this query and use it with will_paginate and acts_as_taggable_on, if that makes a difference.
edit
errors from trying to make :joins dynamic:
ActiveRecord::ConfigurationError: Association named 'LEFT OUTER JOIN favorites ON employees.id=favorites.favorite_id AND favorites.employee_id=?' was not found; perhaps you misspelled it?
from /Users/craibuc/.gem/ruby/1.8/gems/activerecord-2.3.5/lib/active_record/associations.rb:1906:in `build'
from /Users/craibuc/.gem/ruby/1.8/gems/activerecord-2.3.5/lib/active_record/associations.rb:1911:in `build'
from /Users/craibuc/.gem/ruby/1.8/gems/activerecord-2.3.5/lib/active_record/associations.rb:1910:in `each'
from /Users/craibuc/.gem/ruby/1.8/gems/activerecord-2.3.5/lib/active_record/associations.rb:1910:in `build'
from /Users/craibuc/.gem/ruby/1.8/gems/activerecord-2.3.5/lib/active_record/associations.rb:1830:in `initialize'
from /Users/craibuc/.gem/ruby/1.8/gems/activerecord-2.3.5/lib/active_record/base.rb:1789:in `new'
from /Users/craibuc/.gem/ruby/1.8/gems/activerecord-2.3.5/lib/active_record/base.rb:1789:in `add_joins!'
from /Users/craibuc/.gem/ruby/1.8/gems/activerecord-2.3.5/lib/active_record/base.rb:1686:in `construct_finder_sql'
from /Users/craibuc/.gem/ruby/1.8/gems/activerecord-2.3.5/lib/active_record/base.rb:1548:in `find_every'
from /Users/craibuc/.gem/ruby/1.8/gems/activerecord-2.3.5/lib/active_record/base.rb:615:in `find'

Try this:
ee=Employee.all(
:select=>"employees.*,favorites.favorite_id",
:joins=>"LEFT OUTER JOIN favorites AS favorites
ON employees.id=favorites.favorite_id AND
favorites.employee_id = #{session[:user_id]}")
Or to be exact:
joins = Employee.send(:sanitize_sql_array,
["LEFT OUTER JOIN favorites AS favorites
ON employees.id=favorites.favorite_id AND
favorites.employee_id = ? ", session[:user_id]
])
ee=Employee.find(:all,
:select=>"employees.*,favorites.favorite_id",
:joins=> joins )
Second approach addresses the SQL injection issues.
Edit 1
To test these calls in irb do the following:
Simulate the session object by creating hash:
>> session = {:user_id => "1" }
session = {:user_id => "1" }
=> {:user_id=>"1"}
Execute the finder:
>> ee=Employee.find(:all,
:select=>"employees.*,favorites.favorite_id",
:joins=>"LEFT OUTER JOIN favorites AS favorites
ON employees.id=favorites.favorite_id AND
favorites.employee_id = #{session[:user_id]}")

I imagine both ways are possible, but normally, I'd stick the condition in the WHERE clause (:conditions):
ee = Employee.find(:all,
:select => 'employees.*, favorites.favorite_id',
:conditions => ['favorites.employee_id = ?', 1],
:joins => 'LEFT OUTER JOIN favorites ON employees.id = favorites.favorite_id'
)

:joins expects either a raw string or a symbol (association name), or an array of associations. So you can't have dynamic conditions there.
See parameters section here.

Related

Yii eager loading — Fatal error: Out of memory

I have a problem with Yii eager loading.
I open user profile page and use:
$model=User::model()->with('routes', 'likes', 'comments', 'questions', 'cityname')->findByPk($id);
Relations is:
public function relations()
{
return array(
'routes'=>array(self::HAS_MANY, 'Route', 'author_id', 'order'=>'routes.id DESC'),
'questions'=>array(self::HAS_MANY, 'Question', 'author_id', 'order'=>'questions.id DESC'),
'comments'=>array(self::HAS_MANY, 'Comment', 'author_id', 'order'=>'comments.id DESC',),
'likes'=>array(self::HAS_MANY, 'Like', 'author_id', 'order'=>'likes.id DESC'),
'cityname'=>array(self::BELONGS_TO, 'City', 'city'),
);
}
When i have around 70 (or more) comments in Comment table, i have error:
Fatal error: Out of memory (allocated 348651520) (tried to allocate 78 bytes) in /home/milk/kolyasya.ru/diplomyii/framework/db/CDbCommand.php on line 516
The interesting part of this problem, is if i comment any element of with(), for example:
$model=User::model()->with('routes', 'likes', 'comments', /* 'questions' */, 'cityname')->findByPk($id);
then all works as it should.
I checked all relations in all models and set ini_set('memory_limit', '512M'), but i can't find a source of the problem.
Maybe I need to use lazy loading?
You're suffering from exploding number of row combinations. Take a look at this question, it describes the same problem on a smaller scale. Basically, you are running a huge query with multiple one-to-many joins, similar to this:
SELECT ... FROM `User` `t`
LEFT JOIN `Route` routes ON t.id = routes.author_id
LEFT JOIN `Question` questions ON t.id = questions.author_id
LEFT JOIN `Comment` comments ON t.id = comments.author_id
LEFT JOIN `Like` likes ON t.id = likes.author_id
LEFT JOIN `City` city ON t.city = city.id
WHERE t.id = :id
ORDER BY routes.id DESC, questions.id DESC, comments.id DESC, likes.id DESC
You can take this query, modify it to SELECT COUNT(*) and run it in phpMyAdmin to see how many rows it returns. It will be equal to the number of routes multiplied by the number of questions multiplied by the number of comments multiplied by the number of likes created by this user.
In this situation, it would be a lot more efficient to fetch each HAS_MANY relation in a separate query. Yii can do that:
$model=User::model()
->with(array(
'routes' => array('together' => false),
'likes' => array('together' => false),
'comments' => array('together' => false),
'questions' => array('together' => false),
'cityname' => array(),
))
->findByPk($id);
If you do so, Yii will instead produce multiple SQL queries with less memory usage, similar to the following:
SELECT ... FROM `User` `t`
LEFT JOIN `City` `city` ON `t`.`city` = `city`.`id`
WHERE `t`.`id` = :id;
SELECT ... FROM `Route` `routes`
WHERE `author_id` = :id
ORDER by `routes`.`id` DESC;
SELECT ... FROM `Question` `questions`
WHERE `author_id` = :id
ORDER BY `questions`.`id` DESC;
SELECT ... FROM `Comment` `comments`
WHERE `author_id` = :id
ORDER BY `comments`.`id` DESC;
SELECT ... FROM `Like` `likes`
WHERE `author_id` = :id
ORDER BY `likes`.`id` DESC;
The results will be aggregated and returned to your code just like before.

Table Join in rails 2

I am working in rails 2, I have one table users and other table lms_users. In lms_users table, id from users table is coming as foreign key. I want to join two tables in such a way, so i get only those users, whose entry is not in lms_users.
class User
named_scope :not_in_lms_users, {
:conditions => [ "users.id NOT IN (SELECT user_id FROM lms_users)" ]
}
end
or
class User
named_scope :not_in_lms_users, {
:conditions => "lms_users.user_id IS NULL",
:joins => "LEFT OUTER JOIN lms_users ON lms_users.user_id = users.id"
}
end
Just check, but I think the second one is more efficient. This should work :
User.not_in_lms_users

How to make a join between 2 tables in Rails that returns the data of both tables

I'm trying to join two tables: Rooms and Room_Types (which have a relationship already). The thing is, I'm trying to do something like:
room = Room.all :conditions => ['rooms.id = ?', #room_id],
:joins => :room_type
room.to_json
..and this JSON is being sent to my view.
However, the JSON is only showing the fields of the Room table and is not including the Room_Type fields, and I need both tables' fields in this JSON. How can I accomplish this?
:joins only performs a JOIN. As in SQL, this does not add the JOINed table's columns to the results. If you want to do that you should use :include instead:
rooms = Room.all :conditions => [ 'rooms.id = ?', #room_id ],
:include => :room_type
rooms.to_json
Or, in Rails 3 parlance:
rooms = Room.where(:id => #room_id).include(:room_type).all
rooms.to_json
try
Room.joins(:room_type).where(:id => #room_id).select('room_types.foo as foo, room_types.bar as bar')

named_scope + average is causing the table to be specified more then once in the sql query run on postgresql

I have a named scopes like so...
named_scope :gender, lambda { |gender| { :joins => {:survey_session => :profile }, :conditions => { :survey_sessions => { :profiles => { :gender => gender } } } } }
and when I call it everything works fine.
I also have this average method I call...
Answer.average(:rating, :include => {:survey_session => :profile}, :group => "profiles.career")
which also works fine if I call it like that.
However if I were to call it like so...
Answer.gender('m').average(:rating, :include => {:survey_session => :profile}, :group => "profiles.career")
I get...
ActiveRecord::StatementInvalid: PGError: ERROR: table name "profiles" specified more than once
: SELECT avg("answers".rating) AS avg_rating, profiles.career AS profiles_career FROM "answers" LEFT OUTER JOIN "survey_sessions" survey_sessions_answers ON "survey_sessions_answers".id = "answers".survey_session_id LEFT OUTER JOIN "profiles" ON "profiles".id = "survey_sessions_answers".profile_id INNER JOIN "survey_sessions" ON "survey_sessions".id = "answers".survey_session_id INNER JOIN "profiles" ON "profiles".id = "survey_sessions".profile_id WHERE ("profiles"."gender" = E'm') GROUP BY profiles.career
Which is a little hard to read but says I'm including the table profiles twice.
If I were to just remove the include from average it works but it isn't really practical because average is actually being called inside a method which gets passed the scoped. So there is some times gender or average might get called with out each other and if either was missing the profile include it wouldn't work.
So either I need to know how to fix this apparent bug in Rails or figure out a way to know what scopes were applied to a ActiveRecord::NamedScope::Scope object so that I could check to see if they have been applied and if not add the include for average.
Looks like ActiveRecord is generating some bad SQL:
SELECT avg("answers".rating) AS avg_rating,
profiles.career AS profiles_career
FROM "answers"
LEFT OUTER JOIN "survey_sessions" survey_sessions_answers
ON "survey_sessions_answers".id = "answers".survey_session_id
LEFT OUTER JOIN "profiles"
ON "profiles".id = "survey_sessions_answers".profile_id
INNER JOIN "survey_sessions"
ON "survey_sessions".id = "answers".survey_session_id
INNER JOIN "profiles"
ON "profiles".id = "survey_sessions".profile_id
WHERE ("profiles"."gender" = E'm')
GROUP BY profiles.career
Presumably it's generated the left joins as part of getting the projected property, and the inner joins as part of getting the criteria: this wouldn't be invalid (just inefficient) if it assigned aliases to those tables, but it doesn't. Is there a way to specify an alias name from your app?

Find all objects with no associated has_many objects

In my online store, an order is ready to ship if it in the "authorized" state and doesn't already have any associated shipments. Right now I'm doing this:
class Order < ActiveRecord::Base
has_many :shipments, :dependent => :destroy
def self.ready_to_ship
unshipped_orders = Array.new
Order.all(:conditions => 'state = "authorized"', :include => :shipments).each do |o|
unshipped_orders << o if o.shipments.empty?
end
unshipped_orders
end
end
Is there a better way?
In Rails 3 using AREL
Order.includes('shipments').where(['orders.state = ?', 'authorized']).where('shipments.id IS NULL')
You can also query on the association using the normal find syntax:
Order.find(:all, :include => "shipments", :conditions => ["orders.state = ? AND shipments.id IS NULL", "authorized"])
One option is to put a shipment_count on Order, where it will be automatically updated with the number of shipments you attach to it. Then you just
Order.all(:conditions => [:state => "authorized", :shipment_count => 0])
Alternatively, you can get your hands dirty with some SQL:
Order.find_by_sql("SELECT * FROM
(SELECT orders.*, count(shipments) AS shipment_count FROM orders
LEFT JOIN shipments ON orders.id = shipments.order_id
WHERE orders.status = 'authorized' GROUP BY orders.id)
AS order WHERE shipment_count = 0")
Test that prior to using it, as SQL isn't exactly my bag, but I think it's close to right. I got it to work for similar arrangements of objects on my production DB, which is MySQL.
Note that if you don't have an index on orders.status I'd strongly advise it!
What the query does: the subquery grabs all the order counts for all orders which are in authorized status. The outer query filters that list down to only the ones which have shipment counts equal to zero.
There's probably another way you could do it, a little counterintuitively:
"SELECT DISTINCT orders.* FROM orders
LEFT JOIN shipments ON orders.id = shipments.order_id
WHERE orders.status = 'authorized' AND shipments.id IS NULL"
Grab all orders which are authorized and don't have an entry in the shipments table ;)
This is going to work just fine if you're using Rails 6.1 or newer:
Order.where(state: 'authorized').where.missing(:shipments)

Resources