Lighthouse has a new layout. Prefer the old one? Return to the old layout, and switch back any time from the link at the top of each page.

This project is archived and is in readonly mode.

Iterate over :has_many array with the join (:through) model

#82

It is often the case when I iterate over an array of objects in a has_many_through association that I want access to both the :has_many objects and the :through objects. This patch makes that easier.

Example:

class User < ActiveRecord::Base
   has_many :memberships
   has_many :groups, :through => :memberships
 end

user.groups.each_with_join do |group, membership|
  puts "#{group.name}, #{membership.status}"
end

This also works regardless of whether the :through association is joined by a :has_many or :belongs_to.

Reported by Daniel Lunde · May 1st, 2008 @ 05:28 PM

State: invalid
Milestone: none
Assigned to: nobody
Importance: none

Activity

  1. Michael Koziarski
    Michael Koziarski
    • State changed from new to invalid

    This should really just be:

    user.memberships.each do |membership|

    puts membership.status, membership.group.name

    end

    So I can't the need for this feature.

    May 1st, 2008 @ 09:10 PM

  2. Daniel Lunde
    Daniel Lunde

    What about the cases where you are sorting by groups

    class User < ActiveRecord::Base
       has_many :memberships
       has_many :groups, :through => :memberships, :order => "groups.name"
     end
    
    user.groups.each_with_join do |group, membership|
      puts "#{group.name}, #{membership.status}"
    end
    

    By only doing user.memberships, there's no knowledge of the groups to order it correctly.

    There's other instances where it makes sense to iterate over the groups rather then the memberships.

    May 1st, 2008 @ 09:22 PM

  3. Pratik
    Pratik

    Why can't you just do the following ?

    user = User.find(:first, :include => { :groups => :membership })
    user.groups.each do |group|
      puts "#{group.name}, #{group.membership.status}"
    end
    

    May 1st, 2008 @ 10:33 PM

  4. Daniel Lunde
    Daniel Lunde

    Pratik:

    It seems like the obvious thing to do, but there's a problem with it. Let me give a more complete example.

    >> pp frank.groups
    [#<Group id: 2, name: "Swim Team">]
    >> pp frank.memberships
    [#<Membership id: 4, user_id: 2, group_id: 2, status: "Starter">]
    
    >> pp john.groups
    [#<Group id: 2, name: "Swim Team">]
    >> pp john.memberships
    [#<Membership id: 6, user_id: 3, group_id: 2, status: "Finisher">]
    
    # Note, I'm using memberships instead of membership since the Group class declares "has_many :memberships"
    >> frank = User.find(2, :include => { :groups => :memberships })
    >> frank.groups.each do |group|
    ?> pp group
    >> pp group.memberships
    >> end
    #<Group id: 2, name: "Swim Team">
    [#<Membership id: 4, user_id: 2, group_id: 2, status: "Starter">, #<Membership id: 6, user_id: 3, group_id: 2, status: "Finisher">]
    

    So when iterating over frank's groups, it returned not only frank's membership, but all memberships from that group. Which is incorrect.

    With my patch, you only get the correct membership record that matches both the user and the group, plus you get it as a record that's not wrapped in an array.

    And in those instances where someone might have multiple memberships to a single group, it does give it back as an array of just those memberships.

    May 2nd, 2008 @ 04:14 PM

  5. Daniel Lunde
    Daniel Lunde

    Here's a real world example where I've needed to use each_with_join:

    class Feature < ActiveRecord::Base
      has_many :featured_items, :dependent => :destroy, :order => 'featured_items.position'
    
      has_many :featured_users,
               :source => :featureable,
               :through => :featured_items,
               :source_type => 'User',
               :order => 'featured_items.position'
    
      has_many :featured_media,
               :source => :featureable,
               :through => :featured_items, 
               :source_type => 'Medium',
               :order => 'featured_items.position'
    end
    
    # in view
    <% feature = Feature.find_by_group("users", :include => :featured_users, :order => 'featured_items.position') %>
    <% feature.featured_users.each_with_join do |user, featured_item| %>
      <%= image_tag(featured_item.photo.public_filename) %><br/>
      <%= user.name %>
    <% end %>
    

    In this case I couldn't do:

    feature.featured_items do |featured_item|
      featured_item.featured_user.name # featured_user is not a valid method
    end
    

    Wouldn't you say that using each_with_join in this example would be easier then alternative methods?

    May 2nd, 2008 @ 04:42 PM

  6. Michael Koziarski
    Michael Koziarski

    I certainly can see cases where this is required, but I'm just not convinced that it justifies a new method for us to have to maintain on an ongoing basis.

    If there's a join model, it should be exposed.

    May 4th, 2008 @ 01:25 AM