In Rails how do I build a has_many association that has a scope
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:
开发者_如何学JAVA@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.
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">]
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.
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)
精彩评论