Silblings in has_many relationship - ruby-on-rails

A user has many employments.
What do you think?
Is this a valid and clear way to fetch all siblings (belonging to the same user) of a given employment object?
class Employment < ActiveRecord::Base
belongs_to :user
has_many :silblings,
:primary_key => :user_id,
:foreign_key => :user_id,
:class_name => 'Employment'
end
This can be extended with the following named scope:
named_scope :except, lambda {|id| {:conditions => ["id != ?", id]} if id}
Now I can do stuff like:
self.silblings.except(self.id).each do |silbling|
puts silbling
end
The resulting SQL statement looks like:
SELECT * FROM `employments`
WHERE (`employments`.user_id = 49)
AND ((id != 46) AND (`employments`.user_id = 49))
Comments like 'no, you abuse XY, rather use this XZ' are very welcome!
Reto

Looks fine. Except that the SQL doubles ('employments'.user_id = 49) in the query. Which is nothing major. If it's something you really don't want, you could go about defining siblings like this:
class Employment < ActiveRecord::Base
belongs_to :user
named_scope :for_user, lambda { |user|
{ :conditions => {:user_id => user} }
}
named_scope :except, lambda {|employment|
{:conditions => ["id != ?", employment}
}
def siblings
Employment.for_user(user_id).except(id)
end
end
Believe it or not you can still call named scopes on #employment.siblings. Although doing things this way means you can't assign to siblings. The siblings call comes out a little cleaner. There may be a performance improvement, but it probably won't be significant to make a difference.

Related

ActiveRecord find NOT(has_many)

I have a very simple model like this:
class User < ActiveRecord::Base
has_many :cookies
has_many :fortunes, :through => :cookies
end
class Cookie < ActiveRecord::Base
belongs_to :user
belongs_to :fortune
end
class Fortune < ActiveRecord::Base
has_many :cookies
has_many :users, :through => :cookies
end
For a given user, u, I can do
u.fortunes
This will give me all the fortunes associated with this user via Cookies table. What I want to do is get all Fortunes not returned by u.fortunes.
I tried
Fortune.all(:limit => 5, :conditions => {:user => {:id._ne => u.id} })
but that doesn't work :(. I am new to ActiveRecord.
Thanks
try this:
Fortune.limit(5).where("id not in (?)", u.fortunes.map(&:id))
(I tried it on my own tables)
Or try this
Fortune.includes(:cookies).limit(5).where([ 'cookies.user_id != ? OR cookies.user_id IS NULL', u.id ])
Or with the syntax You use
Fortune.all(:include => :cookies, :limit => 5, :conditions => [ 'cookies.user_id != ? OR cookies.user_id IS NULL', u.id ])
The reason to not use include :users is to avoid one extra join.
EDIT:
The other suggestions are shorter, and I think also a little bit quicker when finding (no joins), I only wanted to show how to use associations.
You can do
ids_to_reject = u.fortunes.map(&:id)
Fortune.all(:limit => 5, :conditions => ["id not in (?)", ids_to_reject])
try this
#fortune=Fortune.find(:all).delete_if{|fortune| !fortune.user.nil? }
It will delete the fortunes which are belongs to user, and give us the remaining.

Find condition needs refactoring/optimising on polymorphic association

I'm building a recommendation method for users in my project. Users generate interest records whenever they view, create, comment or interact with objects (weighted depending on the action).
I've written a find method that looks at a user's interests, and then finds users who are also interested in those items. However, it is horrendously inefficient, making as many db calls as the user has interests (up to 50).
Here's a chopped down version of what's going on:
#User.rb
...
has_many :interests, :as => :interestable, :dependent => :destroy
def recommendations
recommendations = []
Interest.for(self).limit(50).each do |item|
recommendations << Interest.other_fans_of(item)
end
user_ids = recommendations.flatten.map(&:user_id).uniq
end
...
#interest.rb
...
belongs_to :user
belongs_to :interestable, :polymorphic => true
named_scope :for, lambda { |user| { :conditions => { :user_id => user.id } } }
named_scope :limit, lambda { |num| { :limit => num } }
named_scope :other_fans_of, lambda { |interest| { :conditions => { :interestable_type => interest.interestable_type, :interestable_id => interest.interestable_id } } }
default_scope :order => "weight DESC"
...
Are there any sql geniuses out there who can turn that into one nice clean db call?
Something like this should do the job. There might be prettier ways…
class User < ActiveRecord::Base
#...
def recommendations
# get a list of the relevant interests
the_interests = Interest.for(self).limit(50).map{|x| [x.interestable_type, x.interestable_id]}
# make some sql
conditions = the_interests.map{|x| "(`interestable_type`=? AND `interestable_id`=?)"}.join(" OR ")
# use ruby magic to make a valid finder and get the other user_ids
user_ids = Interest.all(:select => '`user_id`', :conditions => [conditions, *(the_interests.flatten)]).map(&:user_id).uniq
end
#...
end

named_scope dependent on existence of association is breaking tests

User model:
class User < ActiveRecord::Base
named_scope :clients,
:conditions => "roles_users.role_id = #{Role.find_by_name('client').id}"
end
When testing, throws error:
Called id for nil, which would mistakenly be 4 -- if you really wanted (etc.)
Role fixtures:
client:
name: client
user:
name: user
Apparent problem: Rails is loading this class before it loads fixtures. When it loads the class it evaluates the named_scope. There are no roles at that point, so it blows up.
Possible solution:
named_scope :clients,
lambda { { :conditions => "roles_users.role_id = #{Role.named('client').id}" } }
However, I am not pleased with this solution, seeing as it introduces additional complexity and presumably a (small?) performance hit, just so that tests run properly. I'd like an alternative. Can you help?
The solution you propose is the correct solution. I would also recommend changing your code to:
named_scope :clients, lambda { { :conditions => ['roles_users.role_id = ?', Role.named('client').id } }
An alternative might be:
named_scope :clients, :joins => :role, :conditions => ['roles.name = ?', 'client']
You might also want to think about doing:
named_scope :with_role, lambda { |r| { :conditions => ['roles_users.role_id = ?', r.id] } }
Or even (for extra points)
Role.find_by_name('client').users
Anyway, I hope this helps.

Change a finder method w/ parameters to an association

How do I turn this into a has_one association?
(Possibly has_one + a named scope for size.)
class User < ActiveRecord::Base
has_many :assets, :foreign_key => 'creator_id'
def avatar_asset size = :thumb
# The LIKE is because it might be a .jpg, .png, or .gif.
# More efficient methods that can handle that are OK. ;)
self.assets.find :first, :conditions =>
["thumbnail = '#{size}' and filename LIKE ?", self.login + "_#{size}.%"]
end
end
EDIT: Cuing from AnalogHole on Freenode #rubyonrails, we can do this:
has_many :assets, :foreign_key => 'creator_id' do
def avatar size = :thumb
find :first, :conditions => ["thumbnail = ? and filename LIKE ?",
size.to_s, proxy_owner.login + "_#{size}.%"]
end
end
... which is fairly cool, and makes syntax a bit better at least.
However, this still doesn't behave as well as I would like. Particularly, it doesn't allow for further nice find chaining (such that it doesn't execute this find until it's gotten all its conditions).
More importantly, it doesn't allow for use in an :include. Ideally I want to do something like this:
PostsController
def show
post = Post.get_cache(params[:id]) {
Post.find(params[:id],
:include => {:comments => {:users => {:avatar_asset => :thumb}} }
...
end
... so that I can cache the assets together with the post. Or cache them at all, really - e.g. get_cache(user_id){User.find(user_id, :include => :avatar_assets)} would be a good first pass.
This doesn't actually work (self == User), but is correct in spirit:
has_many :avatar_assets, :foreign_key => 'creator_id',
:class_name => 'Asset', :conditions => ["filename LIKE ?", self.login + "_%"]
(Also posted on Refactor My Code.)
Since there are actually multiple avatar_assets ( one for each size ), you have to keep it as a has_many association.
class User < AR::B
has_many :avatar_assets, :conditions => ['filename like ?' '%avatar%'], :class_name => 'Asset'
named_scope :avatar_size, lambda { |size|
{ :conditions => [ "thumbnail = ?", size ] }
}
end
An alternative would be to put all the work in the named scope:
class User < AR::B
named_scope :avatar_for, lambda { |user, options|
if options[:size]
{ :conditions => [ "filename like ? AND thumbnail = ?", user.login, options[:size] ] }
else
{ :conditions => [ "filename like ?", user.login ] }
end
}
end
this allows you to say
Asset.avatar_for(current_user, :size => :medium)
but is less cool when you find yourself saying
current_user.avatar_for( current_user, :size => :medium )
you could add some :avatar, :avatar?, etc methods to User to clean this up.
Personally I advise you to check out the Paperclip plugin and avoid these issues entirely.
EDIT:
Per your comment, to create a condition like "show me comments by avatar-having users", I'm not sure that will do it. You'd could make a relationship like so:
class Comment
named_scope :with_avatars, :include => { :user => :avatar_assets }, :conditions => [ 'assets.thumbnail = ?', :thumb ]
end
EDIT:
Since you're only interested in caching, rather than conditions, we can drop the condition array:
named_scope :with_avatars, :include => { :user => :avatar_assets }
I revised the code above to be more workable. The key difference is to make the 'avatar'-ness of the assets easily queryable. If you can update your existing avatar_assets to have a filename including the pattern 'avatar-[login]', you can make the condition set static which is much cleaner than always having to search for the avatar based on the user login. Association extensions are another way to resolve this, however I don't think you'll be able to chain them or combine them with named scopes.

Rails, ActiveRecord: how do I get the results of an association plus some condition?

I have two models, user and group. I also have a joining table groups_users.
I have an association in the group model:
has_many :groups_users
has_many :users, :through=> :groups_users
I would like to add pending_users which would be the same as the users association but contain some conditions. I wish to set it up as an association so that all the conditions are handled in the sql call. I know there's a way to have multiple accessors for the same model, even if the name is not related to what the table names actually are. Is it class_name?
Any help would be appreciated, thanks
Use named_scopes, they're your friend
Have you tried using a named_scope on the Group model?
Because everything is actually a proxy until you actually need the data,
you'll end up with a single query anyway if you do this:
class User < ActiveRecord::Base
named_scope :pending, :conditions => { :status => 'pending' }
and then:
a_group.users.pending
Confirmation
I ran the following code with an existing app of mine:
Feature.find(6).comments.published
It results in this query (ignoring the first query to get feature 6):
SELECT *
FROM `comments`
WHERE (`comments`.feature_id = 6)
AND ((`comments`.`status` = 'published') AND (`comments`.feature_id = 6))
ORDER BY created_at
And here's the relevant model code:
class Feature < ActiveRecord::Base
has_many :comments
class Comment < ActiveRecord::Base
belongs_to :feature
named_scope :published, :conditions => { :status => 'published' }
This should be pretty close - more on has_many.
has_many :pending_users,
:through => :groups_users,
:source => :users,
:conditions => {:pending => true}
:pending is probably called something else - however you determine your pending users. As a side note - usually when you see a user/group model the association is called membership.
In the User model:
named_scope :pending, :include => :groups_users, :conditions => ["group_users.pending = ?", true]
That's if you have a bool column named "pending" in the join table group_users.
Edit:
Btw, with this you can do stuff like:
Group.find(id).users.pending(:conditions => ["insert_sql_where_clause", arguments])

Resources