简体   繁体   中英

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:

@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)

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM