seemingly complex ActiveRecord associations problem

Hi,

I want to specify relationships between two classes like that
expressed in the following pseudocode:

class User < ActiveRecord::Base
   has_many :projects
end

class Project < ActiveRecord::Base

has_one :primary_record_owner, :<pseudo_option_for_referring_to>=>"User"
   has_many :record_owners, :<pseudo_option_for_referring_to>=>"User"
   has_many :users # this refers to User, too
end

From the above I seek the following kind of results:

@user.projects # returns all projects associated with the user from
any of the three User associations in Project
@project.primary_record_owner # returns the User instance
@project.record_owners # returns the collection of User instances
@project.users # returns User instances, not necessarily overlapping
with the other associations

The intent I mean to express is that a Project can have any of three
levels of relationship with a User, while a User has only one level of
association with a Project.

I was thinking HABTM, but I don't see how to get my desired
'@user.projects' result from that.

Any helpful comment would be greatly appreciated.

Thanks,

Lille

I’m thinking about 4 models

  • User
  • Project
  • RecordOwner
  • UserProject -> this is the join table for user and project
    You won’t need RecordOwner if record_owners is a subset of the users association.

Give us an example so we can help out with the options you want.

Jim,

Thanks, but your proposed RecordOwner would have no behavior distinct
from User and would not describe the relationship from the Project
model's perspective. My User shouldn't care about the kind of relation
it has to Project, rather the reverse.

My pseudocode is about as specific as I know how to get.

To recap in its most distilled form, my problem is what relationships
to include in two models, User and Project, where a User may be
associated with Project in any of three ways -- User,
'primary_record_owner', or 'record_owner, such that the following
expressions are possible:

1) @user.projects # returns all projects associated with a User
instance
2) @project.primary_record_owner # returns a User instance
3) @project.record_owners # returns a collection of User instances
4) @project.users # returns a collection of User instances

I think I see how to accomplish 2-4, using HABTM, but I don't see how
to accomplish 1) at the same time.

Thanks,

Lille

Jim,

Thanks, but your proposed RecordOwner would have no behavior distinct
from User and would not describe the relationship from the Project
model's perspective. My User shouldn't care about the kind of relation
it has to Project, rather the reverse.

My pseudocode is about as specific as I know how to get.

To recap in its most distilled form, my problem is what relationships
to include in two models, User and Project, where a User may be
associated with Project in any of three ways -- User,
'primary_record_owner', or 'record_owner, such that the following
expressions are possible:

1) @user.projects # returns all projects associated with a User
instance
2) @project.primary_record_owner # returns a User instance
3) @project.record_owners # returns a collection of User instances
4) @project.users # returns a collection of User instances

I think I see how to accomplish 2-4, using HABTM, but I don't see how
to accomplish 1) at the same time.

Have you thought about a polymorphic join model between the two, like maybe a Role? That's how I ended up structuring a recent project.

Walter

Walter,

Yes, Role sounds promising. If it's not too much to ask, could you
please paste some of what you ended up with?

Lille

Sure. Strictly speaking, what I ended up with was not polymorphic, but it suited my project all right. Polymorphic is the fully buzzword-compliant way to go here, because it would let you have different validations for each user type, etc.

class Role < ActiveRecord::Base
   belongs_to :campaign
   belongs_to :user
   has_many :approvals, :dependent => :destroy
   validates_presence_of :user_id, :on => :create, :message => "can't be 0"
   validates_presence_of :role_name, :on => :create, :message => "can't be blank"
   # ...
end

class User < ActiveRecord::Base
   devise :database_authenticatable, :recoverable, :rememberable, :trackable, :validatable, :invitable
   has_many :notes, :dependent => :destroy
   has_many :roles, :dependent => :destroy
   has_many :campaigns, :through => :roles
   default_scope :order => 'name'
   validates_presence_of :name, :on => :update, :message => "can't be blank"
   attr_accessible :email, :password, :password_confirmation, :name, :initials
   # ...
end

class Campaign < ActiveRecord::Base
   has_attached_file :header, :styles => {}
   attr_accessible :name, :campaign_date, :header, :header_fill_color, :page_fill_color
   has_many :roles, :order => "position ASC", :dependent => :destroy
   has_many :users, :through => :roles
   default_scope :order => 'position'
   # ...
end

This was Rails 2.3.10, in case that matters to you.

Walter

Jim,

Thanks, but your proposed RecordOwner would have no behavior distinct

from User and would not describe the relationship from the Project

model’s perspective. My User shouldn’t care about the kind of relation

it has to Project, rather the reverse.

My pseudocode is about as specific as I know how to get.

To recap in its most distilled form, my problem is what relationships

to include in two models, User and Project, where a User may be

associated with Project in any of three ways – User,

‘primary_record_owner’, or 'record_owner, such that the following

expressions are possible:

  1. @user.projects # returns all projects associated with a User

instance

Can’t you use a condition for the has many while eager loading the other models? Something like

has_many :projects, :include => [:record_owners, :project_users], :include => ‘primary_record_owner = #{self.id} OR record_owners.user_id = #{self.id} OR project_users.user_id = #{self.id}’

Guys,

Good stuff.

@Walter - what bothers me about the Role approach is that I will need
to write accessor methods on Project -- #primary_record_owner,
#record_owners -- to retrieve the desired Role players by finding on
attributes like is_record_owner or is_primary_record_owner in Role.

That's not too great a burden, but...

@Jim - maybe I can get what I need from something along the lines of
the following?

class Project < ActiveRecord::Base
  has_one :primary_record_owner ... # see discussion farther below...
  has_and_belongs_to_many :record_owners, :class_name =>
"User", :join_table => :projects_record_owners
  has_and_belongs_to_many :users, :join_table => :projects_users
end

class User < ActiveRecord::Base
   has_many :projects, :include =>
[:record_owners, :users, :primary_record_owner], :include =>
'primary_record_owner = #{self.id} OR record_owners.user_id =
#{self.id} OR project_users.user_id = #{self.id}'
end

I'm in over head...I think this dead-ends in at least one place, with
the has_one association in Project. Seems like I would have to
describe that as a has_one :through association.

Lille

This seems like a good place to use a decorated join model with
has_many :through. Code:

class User < AR::Base
  has_many :project_links
  has_many :projects, :through => :project_links
end

class ProjectLink < AR::Base
  belongs_to :user
  belongs_to :project
  # other fields here:
  # primary: boolean, default false
  # owner: boolean, default false
end

class Project < AR::Base
  has_many :project_links

  has_many :users, :through => :project_links, :conditions =>
{ :project_links => { :owner => false } }
  has_many :record_owners, :through => :project_links, :source
=> :user, :conditions => { :project_links => { :owner =>
true, :primary => false } }
  has_one :primary_owner, :through => :project_links, :source
=> :user, :conditions => { :project_links => { :owner =>
true, :primary => true } }
end

You'll probably need some callbacks to handle the "only one primary
owner" restriction, but everything else should work as written.

--Matt Jones