In Rails how do I build a has_many association that has a scope
Asked Answered
R

3

10

I have something like the following:

class Project < ActiveRecord::Base
  has_many :project_people
  has_many :people, :through => :project_people
end

class Person < ActiveRecord::Base
  has_many :project_people
  has_many :projects, :through => :project_people
end

class ProjectPerson < ActiveRecord::Base
  belongs_to :project
  belongs_to :person
  scope :lead, where(:is_lead => true)
  scope :member, where(:is_lead => false)
end

When adding a "lead" ProjectPerson to a new Project, it appears to build correctly, but when calling "@project.project_people" the array is empty:

@project = Project.new
 => #<Project id: nil, name: nil>
@project.project_people.lead.build
 => #<ProjectPerson id: nil, project_id: nil, person_id: nil, is_lead: true>
@project.project_people
 => []

When I try this without the scope, the ProjectPerson shows up in the array:

@project.project_people.build
 => #<ProjectPerson id: nil, project_id: nil, person_id: nil, is_lead: false>
@project.project_people
 => [#<ProjectPerson id: nil, project_id: nil, person_id: nil, is_lead: false>]

How can I get it so that built scoped association records are also included?

UPDATE: This is an old question that's recently gained some attention. Originally I included a simple example of two scopes that use a boolean. A couple of the recent answers (Feb 2014) have focused on my specific examples instead of the actual question. My question was not for the "lead" and "member" scopes specifically (sometimes scopes are a lot more complex than this), but rather, if it's possible to use a scope and then the build method on an ActiveRecord model. I'm hoping I'm wrong, but there currently doesn't seem to be support for this.

Renwick answered 14/7, 2011 at 14:38 Comment(2)
just got this too - would be very nice to have this work with scopes!Teodora
just an observation but since you're operating on a boolean, you can avoid the need for the second scope if you add a default: false to your database schema for the is_lead column. Essentially then you're just opting that person out of the default case which will be easier to maintain and save you having to check the blank or nil cases.Northwards
P
5

TLDR: Build won't add the built lead to the association until you actually save the built lead.

I've made a simple rails app with the associations for you to check out if you're curious using rails 4.0. https://github.com/TalkativeTree/challenges-learning/tree/master/scope_has_many

In my opinion, I think Member would be a better name that ProjectPerson. That way you could just do Project.first.members and Project.first.members.lead. If you want a project's non-lead members, you could do Project.first.members.where(is_lead: false)

Models:

class Member < ActiveRecord::Base
  belongs_to :project
  belongs_to :person

  scope :lead,   -> { where(is_lead: true) }
end

class Project < ActiveRecord::Base
  has_many :members
  has_many :people, through: :members
end

class Person < ActiveRecord::Base
  has_many :members
  has_many :projects, through: :members
end

and how to create the lead.

> p = Project.new
=> #<Project id: nil, created_at: nil, updated_at: nil>
> p.save
=> true
> p.members
=> []
> p.members.lead.create
=> #<Member id: 1, is_lead: true, person_id: nil, project_id: 1, created_at: "2014-02-16 06:18:59", updated_at: "2014-02-16 06:18:59">
> p
=> #<Project id: 1, created_at: "2014-02-16 06:18:51", updated_at: "2014-02-16 06:18:51">
> p.members.create
=> #<Member id: 2, is_lead: false, person_id: nil, project_id: 1, created_at: "2014-02-16 06:19:07", updated_at: "2014-02-16 06:19:07">
> p.members
=> [#<Member id: 2, is_lead: false, person_id: nil, project_id: 1, created_at: "2014-02-16 06:19:07", updated_at: "2014-02-16 06:19:07">]

Build won't update the association until you actually save the association.

> l = p.members.lead.build
=> #<Member id: nil, is_lead: true, person_id: nil, project_id: 1, created_at: nil, updated_at: nil>
> l
=> #<Member id: nil, is_lead: true, person_id: nil, project_id: 1, created_at: nil, updated_at: nil>
> l.save
=> true
> p.members.lead
=> [#<Member id: 1, is_lead: true, person_id: nil, project_id: 1, created_at: "2014-02-16 06:18:59", updated_at: "2014-02-16 06:18:59">,
 #<Member id: 3, is_lead: true, person_id: nil, project_id: 1, created_at: "2014-02-16 06:23:04", updated_at: "2014-02-16 06:23:04">]
> l2 = p.members.lead.build
=> #<Member id: nil, is_lead: true, person_id: nil, project_id: 1, created_at: nil, updated_at: nil>
> p.members.lead
=> [#<Member id: 1, is_lead: true, person_id: nil, project_id: 1, created_at: "2014-02-16 06:18:59", updated_at: "2014-02-16 06:18:59">,
 #<Member id: 3, is_lead: true, person_id: nil, project_id: 1, created_at: "2014-02-16 06:23:04", updated_at: "2014-02-16 06:23:04">]
> l2.save
=> true
> p.members.lead
=> [#<Member id: 1, is_lead: true, person_id: nil, project_id: 1, created_at: "2014-02-16 06:18:59", updated_at: "2014-02-16 06:18:59">,
 #<Member id: 3, is_lead: true, person_id: nil, project_id: 1, created_at: "2014-02-16 06:23:04", updated_at: "2014-02-16 06:23:04">,
 #<Member id: 4, is_lead: true, person_id: nil, project_id: 1, created_at: "2014-02-16 06:23:34", updated_at: "2014-02-16 06:23:34">]

Also, if your data isn't showing for p, trying reloading the model will reflect the changes to the database.

> p
=> #<Project id: 2, created_at: "2014-02-14 03:21:55", updated_at: "2014-02-14 03:21:55">
> p.members
=> []
> p.reload
=> #<Project id: 2, created_at: "2014-02-14 03:21:55", updated_at: "2014-02-14 03:21:55">
> p.members
=> [#<Member id: 6, is_lead: true, person_id: nil, project_id: 2, created_at: "2014-02-14 03:22:24", updated_at: "2014-02-14 03:22:24">]
Pheni answered 14/2, 2014 at 3:31 Comment(4)
The alphabetical convention is for habtm relationships only and since the question is clearly using a join model that is not the case.Magnum
Yep, you're definitely right about the habtm. It looked so much like one with the name I just assumed it was when I wrote that. Decided to edit to a better suggestion.Pheni
thanks for your answer, even the answer is "it is not possible" :-)Teodora
Well it is possible. You just have to save it to access it from the Project model. If you did lead = Project.project_person.lead.build you could then work on the built lead, but you'd have to save lead for it to show up with project.project_person.leadPheni
G
3

You can do this, but it tends to produce MANY associations on your models.

If you're using Rails 3 (see farther down for the Rails 4 version):

class PeopleProject < ActiveRecord::Base
  belongs_to :project
  belongs_to :person

  scope :lead,   -> { where(is_lead: true) }
  scope :member, -> { where(is_lead: false)}
end

class Project < ActiveRecord::Base
  has_many :people_projects_as_lead, conditions: { is_lead: true }, class_name: 'PeopleProject'
  has_many :people_projects_as_member, conditions: { is_lead: false }, class_name: 'PeopleProject'

  has_many :leads, through: :people_projects_as_lead, source: :person
  has_many :members, through: :people_projects_as_member, source: :person

  has_many :people_projects
  has_many :people, through: :people_projects
end

class Person < ActiveRecord::Base
  has_many :people_projects_as_lead, conditions: { is_lead: true }, class_name: 'PeopleProject'
  has_many :people_projects_as_member, conditions: { is_lead: false }, class_name: 'PeopleProject'

  has_many :lead_projects, through: :people_projects_as_lead, source: :project
  has_many :member_projects, through: :people_projects_as_member, source: :project

  has_many :people_projects
  has_many :projects, through: :people_projects
end

With this setup, doing @project.people_projects_as_lead.build will do what you'd expect. Whether the additional association names add clarity or remove it is pretty much dependent on your problem domain.

The duplication between conditions above and the scopes is not so good. Rails 4 makes it possible to avoid the duplicate conditions:

class Project < ActiveRecord::Base
  has_many :people_projects_as_lead, -> { lead }, class_name: 'PeopleProject'
  has_many :people_projects_as_member, -> { member }, class_name: 'PeopleProject'

  has_many :leads, through: :people_projects_as_lead, source: :person
  has_many :members, through: :people_projects_as_member, source: :person

  has_many :people_projects
  has_many :people, through: :people_projects
end

class Person < ActiveRecord::Base
  has_many :people_projects_as_lead, -> { lead }, class_name: 'PeopleProject'
  has_many :people_projects_as_member, -> { member }, class_name 'PeopleProject'

  has_many :lead_projects, through: :people_projects_as_lead, source: :project
  has_many :member_projects, through: :people_projects_as_member, source: :project

  has_many :people_projects
  has_many :projects, through: :people_projects
end

NOTE: you may need additional inverse_of options to make sure that everything saves correctly, particularly on the relations between Project/Person and PeopleProject. If everything's set up correctly with this code, you'll be able to do things like @project.leads << some_person and have the join record built correctly.

Gimble answered 15/2, 2014 at 22:34 Comment(2)
using rails 4 version; this does not work for me: with @project.people_projects_as_lead.new, everything (leads, members, peope) is always empty for a new unsaved project. With @project.leads << Person.new, only @project.leads has an entry but @project.people is empty. Keep in mind: Just talking about unsaved records.Teodora
@markus - manually adding a join record (to the people_projects relation) isn't going to fill in the corresponding value in people. In the second case, it's not (in general) possible for ActiveRecord to determine that a record added to one association (leads) is actually going to show up in another (people) without going to the database.Gimble
P
1

I don't think scopes and build are meant to work together. Scopes are for searches and build is to build/create new associated records.

# this should do the trick
@project.project_people.build(:is_lead=>true)
Polyvinyl answered 14/7, 2011 at 14:54 Comment(2)
Thanks for the reply. The last example of this section (api.rubyonrails.org/classes/ActiveRecord/NamedScope/…) states that "Scopes can also be used while creating/building a record." But the code examples don't include any associations. Hmm, I'm still wondering if this is possible... My example above is simple in that the scope only modifies one attribute, but this would be useful with more complex scopes/chaining. Any ideas?Renwick
You are definitely right about the usefulness! The code you have would be a simple use case and since it doesn't work I assume it wasn't made to work with associations. Sorry, I'm out of ideas!Polyvinyl

© 2022 - 2024 — McMap. All rights reserved.