I'm trying to tidy up my code by using named_scopes in Rails 2.3.x but where I'm struggling with the has_many :through associations. I'm wondering if I'm putting the scopes in the wrong place...
Here's some pseudo code below. The problem is that the :accepted named scope is replicated twice... I could of course call :accepted something different but these are the statuses on the table and it seems wrong to call them something different. Can anyone shed light on whether I'm doing the following correctly or not?
I know Rails 3 is out but it's still in beta and it's a big project I'm doing so I can't use it in production yet.
class Person < ActiveRecord::Base
has_many :connections
has_many :contacts, :through => :connections
named_scope :accepted, :conditions => ["connections.status = ?", Connection::ACCEPTED]
# the :accepted named_scope is duplicated
named_scope :accepted, :conditions => ["memberships.status = ?", Membership::ACCEPTED]
end
class Group < ActiveRecord::Base
has_many :memberships
has_many :members, :through => :memberships
end
class Connection < ActiveRecord::Base
belongs_to :person
belongs_to :contact, :class_name => "Person", :foreign_key => "contact_id"
end
class Membership < ActiveRecord::Base
belongs_to :person
belongs_to :group
end
I'm trying to run something like person.contacts.accepted and group.members.accepted which are two different things. Shouldn't the named_scopes be in the Membership and Connection classes?
However if you try putting the named scopes in the Membership and Connection classes then you get this error (because Person.find(2).contacts returns an array of Persons which doesn't have an 'accepted' method:
>> Person.find(2).contacts.accepted
NoMethodError: undefined method `accepted' for #<Class:0x108641f28>
One solution is to just call the two different named scope something different in the Person class or even to create separate associations (ie. has_many :accepted_members and has_many :accepted_contacts) but it seems hackish and in reality I have many more than just accepted (ie. banned members, ignored connections, pending, requested etc etc)
You answered your own question:
Shouldn't the named_scopes be in the Membership and Connection classes?
Yes, they should be. This will let you call them as you wanted. It's also logically where they belong.
If you want something on Person that checks both, you can do something like:
named_scope :accepted, :conditions => ["connections.status = ? OR memberships.status = ?", Connection::ACCEPTED, Membership::ACCEPTED]
Maybe you want this to be an AND? not sure.
I'm sure this is not the best way and I believe you can do this on person and group models, but I also believe the following will work for you:
# models
class Person < ActiveRecord::Base
has_many :connections
has_many :contacts, :through => :connections
has_many :memberships
has_many :groups, :through => :memberships
end
class Group < ActiveRecord::Base
has_many :memberships
has_many :members, :through => :memberships
end
class Connection < ActiveRecord::Base
belongs_to :person
belongs_to :contact, :class_name => "Person", :foreign_key => "contact_id"
named_scope :accepted, :conditions => ["status = ?", Connection::ACCEPTED]
end
class Membership < ActiveRecord::Base
belongs_to :person
belongs_to :group
named_scope :accepted, :conditions => ["status = ?", Membership::ACCEPTED]
end
# controller
# get person's accepted contacts
#person = Person.first
#person.connections.accepted.map(&:contact)
# get group's accepted members
#group = Group.first
#group.memberships.accepted.map(&:person)
Related
This question already has an answer here:
Losing an Attribute When Saving Through an Association w/ Scope (Rails 4.0.0)
(1 answer)
Closed 9 years ago.
I want to model such a relationship between the models User and Event.
Therefore I have started with the following classes:
class User < ActiveRecord::Base
...
end
class Attendance < ActiveRecord::Base
# with columns user_id and event_id
...
end
class Event < ActiveRecord::Base
has_many :attendances
has_many :users, :through => :attendances
...
end
So far everything is okay: I can assign users and access attendances. But now I want to bring the state into play, such that I can distinguish e.g. between "attending", "unexcused absent", ... users. My first try was:
class Event < ActiveRecord::Base
has_many :attendances
has_many :users, :through => :attendances
has_many :unexcused_absent_users, -> { where :state => 'unexcused' },
:through => :attendances,
:source => :user
...
end
(:source has to be specified since otherwise it would search for a belongs to association named 'unexcused_absent_users')
The problem here is, that the where-predicate is evaluated on table 'users'.
I am clueless how to solve this 'correctly', without introducing new join tables/models for every state. Especially since every user can be just in one state for every event, I think a solution with one Attendance-model makes sense.
Have you an idea, how to get this right?
You can simply narrow the scope to look at the correct table:
has_many :unexcused_absent_users, -> { where(attendances: {state: 'unexcused'}) },
:through => :attendances,
:source => :user
Evem better, add this scope to the Attendance model and merge it in:
class Attendance < ActiveRecord::Base
def self.unexcused
where state: 'unexcused'
end
end
class Event < ActiveRecord::Base
has_many :unexcused_absent_users, -> { merge(Attendance.unexcused) },
:through => :attendances,
:source => :user
end
I have found a workaround, but I still think, this is ugly.
class Event < ActiveRecord::Base
has_many :user_attendances, :class_name => 'Attendance'
has_many :users, :through => :user_attendances, :source => :user
has_many :unexcued_absent_user_attendances, -> { where :state => 'unexcused'}, :class_name => 'Attendance'
has_many :unexcused_absent_users, :through => :unexcued_absent_user_attendances, :source => :user
end
In general: For every state that I want, I have to introduce a new has_many relationship with a scope and on top of that and an according has_many-through relationship.
this might work for you?
class Event < ActiveRecord::Base
has_many :attendances
has_many :users, :through => :attendances
def unexcused_absent_users
User.joins(:attendances)
.where(:state => 'unexcused')
.where(:event_id => self.id)
end
end
in rails 3+ methods are basically the same as scopes, just less confusing (in my opinion), they are chainable
event = Event.find(xxxx)
event.unexcused_absent_users.where("name LIKE ?", "Smi%")
I have the following models:
class Publication < ActiveRecord::Base
has_many :reviews
has_many :users, :through => :owned_publications
has_many :owned_publications
end
class User < ActiveRecord::Base
has_many :publications, :through => :owned_publications
has_many :owned_publications
end
class OwnedPublication < ActiveRecord::Base
belongs_to :publication
belongs_to :user
has_one :review, :conditions => "user_id = #{self.user.id} AND publication_id = #{self.publication.id}"
end
In the third model, I'm trying to set a condition with a pair of variables. It seems like the syntax works, except that self is not an instance of OwnedPublication. Is it possible to get the current instance of OwnedPublication and place it into a condition?
The solution requires the use of :through and :source options, as well as a proc call:
has_one :review, :through => :publication, :source => :reviews,
:conditions => proc { ["user_id = ?", self.user_id] }
Proc is the trick to passing in dynamic variables to ActiveRecord association conditions, at least as of Rails 3.0. Simply calling:
has_one :conditions => proc { ["publication_id = ? AND user_id = ?",
self.publication_id, self.user_id] }
will not work, though. This is because the association will end up searching the reviews table for a 'reviews.owned_publication_id' column, which does not exist. Instead, you can find the proper review through publication, using publication's :reviews association as the source.
I think your best bet is to just have the Review record belong_to an OwnedPublication, and setup your Publication model to get the reviews via a method:
def reviews
review_objects = []
owned_publications.each do |op|
review_objects << op
end
review_objects
end
Might be a more efficient way if you use a subquery to get the information, but it removes the concept of having unnecessary associations.
I've extended many of my has_many declarations to filter / join / preload associations. I'd like to re-use some of these extensions when I declare has_many :through relationships. Is this possible? Should I take a different approach?
Example:
I have this in my Library Model:
class Library < ActiveRecord::Base
has_many :meals, :dependent => :destroy do
def enabled
where(:enabled => true)
end
end
end
My Meal Model has this:
class Meal < ActiveRecord::Base
has_many :servings, :inverse_of => :meal, :dependent => :destroy
end
I'd like my library to have many servings, but only from the enabled meals. There are a couple ways I can do this:
# repeat the condition in the has_many :servings declaration
class Library < ActiveRecord::Base
has_many :servings, :through => :meals, :conditions => ["meals.enabled = ?", true]
end
# declare a different meals association for only the enabled meals
class Library < ActiveRecord::Base
has_many :enabled_meals, :class_name => "Meals", :conditions => [:enabled => true]
has_many :servings, :through => :enabled_meals
end
Is there any way to re-use the extension to my existing :meals declaration? (def enabled in the first code block)
Looks a lot like you want to use activerecord-association-extensions, as described here http://blog.zerosum.org/2007/2/8/activerecord-association-extensions.html.
I haven't tried it, but I think you could do:
module LibraryMealExtensions
def enabled?
where(:enabled=>true)
end
def standard_includes
includes(:servings)
end
end
class Library < ActiveRecord::Base
has_many :meals, :dependent => :destroy, :extend=>LibraryMealExtensions
has_many :servings, :through => :meals, :extend=>LibraryMealExtensions
end
Not sure about the "enabled=>true" there - you might have to say
where("meals.enabled=true")
b/c of confusion with aliases.
I'm still having trouble building complex joins in ActiveRecord.
I have a User model that is using the HasManyFriends plugin by Steve Ehrenberg (http://dnite.org).
Then I have a UserFeedEvent model that links users to a FeedEvent model.
What I'd like to achieve is to find all the FeedEvents linked to the friends of a User.
How should I write my ActiveRecord query?
Here are my models:
class User < ActiveRecord::Base
has_many_friends
has_many :feed_events, :through => :user_feed_events, :dependent => :destroy
has_many :user_feed_events, :dependent => :destroy
end
class UserFeedEvent < ActiveRecord::Base
belongs_to :feed_event, :dependent => :destroy
belongs_to :user
end
class FeedEvent < ActiveRecord::Base
has_many :user_feed_events, :dependent => :destroy
has_many :users, :through => :user_feed_events
serialize :data
end
Thanks in advance!
Augusto
Digging through HasManyFriends source leads me to believe that the following should work (or be half-way through):
EDIT: found out that source cannot point to another :has_many :through association. So you could try the updated version.
class User < ActiveRecord::Base
#...
has_many :user_feed_events_of_friends_by_me, :through => :friends_by_me,
:source => :user_feed_events
has_many :feed_events_of_friends_by_me, :through => :user_feed_events_by_me
has_many :user_feed_events_of_friends_for_me, :through => :friends_for_me,
:source => :user_feed_events
has_many :feed_events_of_friends_for_me, :through => :user_feed_events_for_me
# A wrapper to return full list of two-way friendship events
def feeds_events_of_my_friends
self.feed_events_of_friends_by_me + self.feed_events_of_friends_for_me
end
end
Unfortunately the HMF plugin has two one-way friendship links, which means full list requires 2 DB queries.
I found a working and more traditional SQL solution:
friends_id = current_user.friends.collect {|f| f.id}.join(",")
sql = "SELECT feed_events.*, user_feed_events.user_id FROM feed_events LEFT JOIN user_feed_events ON feed_events.id = user_feed_events.feed_event_id WHERE user_feed_events.user_id IN (#{friends_id}) GROUP BY feed_events.id ORDER BY feed_events.created_at DESC"
friend_feed_events = FeedEvent.paginate_by_sql(sql, :page => params[:page], :per_page => 30)
If you have a more efficient / more elegant way of doing the same, please let me know!
Can't wrap my head around this...
class User < ActiveRecord::Base
has_many :fantasies, :through => :fantasizings
has_many :fantasizings, :dependent => :destroy
end
class Fantasy < ActiveRecord::Base
has_many :users, :through => :fantasizings
has_many :fantasizings, :dependent => :destroy
end
class Fantasizing < ActiveRecord::Base
belongs_to :user
belongs_to :fantasy
end
... which works fine for my primary relationship, in that a User can have many Fantasies, and that a Fantasy can belong to many Users.
However, I need to add another relationship for liking (as in, a User "likes" a Fantasy rather than "has" it... think of Facebook and how you can "like" a wall-post, even though it doesn't "belong" to you... in fact, the Facebook example is almost exactly what I'm aiming for).
I gathered that I should make another association, but I'm kinda confused as to how I might use it, or if this is even the right approach. I started by adding the following:
class Fantasy < ActiveRecord::Base
...
has_many :users, :through => :approvals
has_many :approvals, :dependent => :destroy
end
class User < ActiveRecord::Base
...
has_many :fantasies, :through => :approvals
has_many :approvals, :dependent => :destroy
end
class Approval < ActiveRecord::Base
belongs_to :user
belongs_to :fantasy
end
... but how do I create the association through Approval rather than through Fantasizing?
If someone could set me straight on this, I'd be much obliged!
Keep your first set of code, then in your User Model add:
has_many :approved_fantasies, :through => :fantasizings, :source => :fantasy, :conditions => "fantasizings.is_approved = 1"
In your Fantasizing table, add an is_approved boolean field.