Search code examples
ruby-on-railsruby-on-rails-3callbackmodel-associations

Has Many Through Association Callbacks with multiple associations using the same join table


So this might be really bad form. I'm relatively new to rails. I'm not sure.

I have a project model and I want there to be many owners (who can read and write everything) and many collaborators (who can read and write some stuff).

In my project.rb file I have:

  has_many :project_user_relationships, :dependent => :destroy
  has_many :collaborators, :through => :project_user_relationships, :source => :user

  has_many :project_owners_relationships, :class_name => "ProjectUserRelationship", :foreign_key => "project_id", 
           :before_add => Proc.new { |p,owner_r| owner_r.owner = true }, :conditions => "`project_user_relationships`.owner = true"
  has_many :owners, :through => :project_owners_relationships, :source => :user

So this works reasonably well. If I add a new owner, that user is also a collaborator which is what I want. The issue I'm not sure how to solve is if I add a user that is already collaborator as an owner, I get two entries in the join table. I'd like for it to just amend the record that's already there. How do I do that?


Solution

  • Here's the data model I would suggest for this:

    class Project < ActiveRecord::Base
        has_many :memberships, :dependent => :destroy
        ...
    end
    
    class Membership < ActiveRecord::Base
        belongs_to :project
        belongs_to :user
        ...
    end
    
    class User < ActiveRecord::Base
        has_many :memberships, :dependent => :destroy
        has_many :projects, :through => :memberships
        ...
    end
    

    And then the membership table will have the following attributes:

    :id
    :user_id
    :project_id
    :is_owner (boolean)
    

    A scope defined on the membership class:

    scope :owner, where("is_owner")
    

    And a special method for User instances:

    def owned_projects
        memberships.owner.includes(:projects).inject([]) {|array, m| array << m.project; array}
    end
    

    will allow you to retrieve a user's owned projects with the user.owned_projects call.

    And just a call to user.projects to see a user's projects that they either collaborate on or own.

    You have better data normalization with this data model, and a simple boolean attribute to define whether or not a user is a project owner.

    This data model is used in this project, with the exception that s/Project/Group/, and there's some additional functionality to handle inviting users to the Project.

    This doesn't answer your "real question", but I think part of the issue is that a data model where collaborators are owners are stored in the same table is needed to minimize redundancies and the need to manage two separate tables.