Rails 3 和 has_many :through: 自动设置/初始化连接模型上的属性

发布于 2024-11-28 10:05:54 字数 5505 浏览 3 评论 0原文

我深入搜索了网络,以便找到一种干净、简单的方法来处理 has_many :through 关系的连接模型上的属性初始化,但我没有找到满足我需要的最佳解决方案。

在下面提供的示例中,当我创建或更新 Course 对象时,我需要自动设置 Training 连接模型的属性 role

这是我的模型:

QUALIFICATIONS = ["Theoretical Instructor", "Practical Instructor"]

class Course < ActiveRecord::Base
  has_many :trainings, dependent: :destroy

  has_many :theoretical_instructors, through: :trainings, source: :trainer, conditions: { "trainings.role" => "Theoretical Instructor" }
  accepts_nested_attributes_for :theoretical_instructors

  has_many :practical_instructors, through: :trainings, source: :trainer, conditions: { "trainings.role" => "Practical Instructor" }
  accepts_nested_attributes_for :practical_instructors
end

class Trainer < ActiveRecord::Base
  has_many :trainings, dependent: :destroy
  has_many :courses, through: :trainings
end

class Training < ActiveRecord::Base
  belongs_to :trainer
  belongs_to :course

  # Join model has the :role attribute, that I wish I could validate this way:  
  # validates :role, presence: true, inclusion: { in: QUALIFICATIONS }
end

这个模型背后的基本原理是我想将 Training 对象保存在单个表中。我不想创建 TheoreticalInstructor 和 PracticalInstructor 连接模型(可能会导致表数量激增)来解决此问题。

此视图提供了提交新课程的表单:

<%= form_for @course do |course_form| %>
  <%- # fields for course attributes, as usual... %>

  <%= course_form.label :theoretical_instructor_ids %><br />
  <%= course_form.select :theoretical_instructor_ids, Trainer.all.map { |x| [[x.name, x.surname].join(" "), x.id] }, {  }, { multiple: true } %>

  <%= course_form.label :practical_instructor_ids %><br />
  <%= course_form.select :practical_instructor_ids, Trainer.all.map { |x| [[x.name, x.surname].join(" "), x.id] }, {  }, { multiple: true } %>

  <%= course_form.submit %>
<% end%>

问题是:我该怎么做才能使@course = Course.new(params[:course]) Course 控制器中需要在提交上一个表单时保存此关联的唯一代码行吗?

这个问题不同,我不知道想要在创建新的 Course 时创建新的 Trainer 对象:我想从数据库中已存在的对象中选择它们(通过多选输入字段)。

我需要的是像 @course.theoretical_instructor_ids = [1, 2] 这样的东西创建两个 Training 对象,并将 role 属性设置为 理论讲师

我正在考虑Training上的after_initialize回调,根据关系名称设置role:theoretical_instructors:practical_instructors),但我真的不知道该怎么做。有什么建议吗?我错过了一些要点吗?

谢谢你们!

编辑 oli-g 1

这个问题处理类似的问题:不同的是,当我创建新的课程Trainer对象代码>,但是我只是想将现有的 Trainer 对象关联到新的 Course

来自oli-g的编辑2

基于this(5年前的帖子)和这个博客文章,我以这种方式更改了 Course 模型:

class Course < ActiveRecord::Base
  has_many :trainings, dependent: :destroy

  has_many :theoretical_instructors, through: :trainings, source: :trainer, conditions: ["trainings.role = ?", "Theoretical Instructor"] do
    def <<(theoretical_instructor)
      Training.send(:with_scope, create: { role: "Theoretical Instructor" }) { self.concat theoretical_instructor }
    end
  end
  accepts_nested_attributes_for :theoretical_instructors

  has_many :practical_instructors, through: :trainings, source: :trainer, conditions: ["trainings.role = ?", "Practical Instructor"] do
    def <<(practical_instructor)
      Training.send(:with_scope, create: { role: "Practical Instructor" }) { self.concat practical_instructor }
    end
  end
  accepts_nested_attributes_for :practical_instructors
end

此代码使我能够做这样的事情

:001 > c = Course.first
=> #<Course id: 1>
:002 > t1 = Trainer.first
=> #<Trainer id: 1, name: "Tom">
:003 > c.theoretical_instructors << t1
=> #<Trainer id: 1, name: "Tom">
:004 > Training.all
=> [#<Training id: 1, role: "Theoretical Instructor", trainer_id: 1, course_id: 1>]

这是一个可以接受的解决方法,即使在我的控制器中我仍然不能只做 <代码>@课程= Course.new(params[:course]),但我必须创建在 params[:course][:theoretical_instructor_ids]上迭代的 Training 对象>params[:课程][:practical_instructor_ids]

但我很好奇,所以问题仍然悬而未决:我该怎么做才能使 @course = Course.new(params[:course]) 构建 Training 对象与课程一起?

现在...我想我发现了 Rails 中的一个错误:

:005 > c.practical_instructors
=> []        # correct
:006 > c.practical_instructor_ids
=> []        # obviously
:007 > c.reload
=> #<Course id: 1>
:008 > c.practical_instructor_ids
=> [1]       # WRONG!!!
:009 > c.practical_instructors
=> []        # now it's correct...
:010 > c.practical_instructor_ids
=> []        # WTF!?

我想我会在 github issues 上报告这个问题...

由 oli-g 编辑 3

github

I deeply searched the web in order to find a clean and simple way to deal with attributes initialization on the join model of a has_many :through relation, but I did not find a best solution for my need.

In the exaple I provide below, I need to automatically set the attribute role of the Training join model when I create or update a Course object.

This is my model:

QUALIFICATIONS = ["Theoretical Instructor", "Practical Instructor"]

class Course < ActiveRecord::Base
  has_many :trainings, dependent: :destroy

  has_many :theoretical_instructors, through: :trainings, source: :trainer, conditions: { "trainings.role" => "Theoretical Instructor" }
  accepts_nested_attributes_for :theoretical_instructors

  has_many :practical_instructors, through: :trainings, source: :trainer, conditions: { "trainings.role" => "Practical Instructor" }
  accepts_nested_attributes_for :practical_instructors
end

class Trainer < ActiveRecord::Base
  has_many :trainings, dependent: :destroy
  has_many :courses, through: :trainings
end

class Training < ActiveRecord::Base
  belongs_to :trainer
  belongs_to :course

  # Join model has the :role attribute, that I wish I could validate this way:  
  # validates :role, presence: true, inclusion: { in: QUALIFICATIONS }
end

The rationale behind this model is that I want to save Training objects in a single table. I don't want to create the TheoreticalInstructor and the PracticalInstructor join models (potentially exploding the number of tables) to solve this problem.

This view provides the form to submit a new Course:

<%= form_for @course do |course_form| %>
  <%- # fields for course attributes, as usual... %>

  <%= course_form.label :theoretical_instructor_ids %><br />
  <%= course_form.select :theoretical_instructor_ids, Trainer.all.map { |x| [[x.name, x.surname].join(" "), x.id] }, {  }, { multiple: true } %>

  <%= course_form.label :practical_instructor_ids %><br />
  <%= course_form.select :practical_instructor_ids, Trainer.all.map { |x| [[x.name, x.surname].join(" "), x.id] }, {  }, { multiple: true } %>

  <%= course_form.submit %>
<% end%>

The question is: what can I do in order to make @course = Course.new(params[:course]) the only line of code in the Course controller needed to save this association on submit of the previous form?

Differently from this question I don't want to create new Trainer objects when I create a new Course: I want to choose them from those already present in the DB (through a multiselect input field).

What I need is that something like @course.theoretical_instructor_ids = [1, 2] creates two Training objects with the role attribute set to Theoretical Instructor

I'm thinking on an after_initialize callback on Training that set role basing on the relation name (:theoretical_instructors and :practical_instructors), but I really don't know how to do it. Any advice? Am I missing some point?

Thank you guys!

EDIT 1 from oli-g

This question deals with a similar problem: the difference is that I don't want to build Trainer objects when I create a new Course, but I simply want to associate existing Trainer objects to a new Course.

EDIT 2 from oli-g

Basing on this (a 5 years old post) and this blog posts, I've changed the Course model in this way:

class Course < ActiveRecord::Base
  has_many :trainings, dependent: :destroy

  has_many :theoretical_instructors, through: :trainings, source: :trainer, conditions: ["trainings.role = ?", "Theoretical Instructor"] do
    def <<(theoretical_instructor)
      Training.send(:with_scope, create: { role: "Theoretical Instructor" }) { self.concat theoretical_instructor }
    end
  end
  accepts_nested_attributes_for :theoretical_instructors

  has_many :practical_instructors, through: :trainings, source: :trainer, conditions: ["trainings.role = ?", "Practical Instructor"] do
    def <<(practical_instructor)
      Training.send(:with_scope, create: { role: "Practical Instructor" }) { self.concat practical_instructor }
    end
  end
  accepts_nested_attributes_for :practical_instructors
end

This code enables me to do a thing like this

:001 > c = Course.first
=> #<Course id: 1>
:002 > t1 = Trainer.first
=> #<Trainer id: 1, name: "Tom">
:003 > c.theoretical_instructors << t1
=> #<Trainer id: 1, name: "Tom">
:004 > Training.all
=> [#<Training id: 1, role: "Theoretical Instructor", trainer_id: 1, course_id: 1>]

This is an acceptable workaround, even if in my controller I still can't do just @course = Course.new(params[:course]), but I have to create Training objects iterating on params[:course][:theoretical_instructor_ids] and params[:course][:practical_instructor_ids].

But I am curious, so the question remains open: what can I do in order to enable @course = Course.new(params[:course]) to build Training objects along with the Course?

Now... I think I discovered a bug in Rails:

:005 > c.practical_instructors
=> []        # correct
:006 > c.practical_instructor_ids
=> []        # obviously
:007 > c.reload
=> #<Course id: 1>
:008 > c.practical_instructor_ids
=> [1]       # WRONG!!!
:009 > c.practical_instructors
=> []        # now it's correct...
:010 > c.practical_instructor_ids
=> []        # WTF!?

I think I will report this at github issues...

EDIT 3 by oli-g

Bug reported at github

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(1

人海汹涌 2024-12-05 10:05:54

您的问题是,在您的记录创建之后,您将无法添加关联。在这种情况下,培训关联是使用课程记录 ID 来存储的,并且直到第一次保存课程后才定义课程 ID。您需要做的是使用 after_create 回调在创建记录后调用函数。

将其添加到课程模型的末尾:

# Use attr accessors to store the initial values so they won't conflict with the *_instructor_ids methods defined above 
attr_accessor :create_theoretical_instructors
attr_accessor :create_practical_instructors
# This will call the create_training_records function after the record is created
after_create :create_training_records

private
def create_training_records
  create_theoretical_instructors.each do |instructor_id|
    self.theoretical_instructors << Instructor.find(instructor_id)
  end
  create_practical_instructors.each do |instructor_id|
    self.practical_instructors << Instructor.find(instructor_id)
  end
  save!
end

并更改视图中的表单以使用新的 attr_accessors:

<%= course_form.label :create_theoretical_instructors %><br />
<%= course_form.select :create_theoretical_instructors, Trainer.all.map { |x| [[x.name, x.surname].join(" "), x.id] }, {  }, { multiple: true } %>

<%= course_form.label :create_practical_instructors %><br />
<%= course_form.select :create_practical_instructors, Trainer.all.map { |x| [[x.name, x.surname].join(" "), x.id] }, {  }, { multiple: true } %>

现在,当您提交表单时,它会将讲师 ID 写入新的课程实例变量;课程经过验证并保存后,它将自动创建新的关联。

Your issue is that you won't be able to add associations until after your record has been created. In this case, the Training associations are stored using the Course record id, and the Course id isn't defined until after the Course is saved for the first time. What you'll want to do is to use the after_create callback to call a function after the record has been created.

Add this to the end of your Course model:

# Use attr accessors to store the initial values so they won't conflict with the *_instructor_ids methods defined above 
attr_accessor :create_theoretical_instructors
attr_accessor :create_practical_instructors
# This will call the create_training_records function after the record is created
after_create :create_training_records

private
def create_training_records
  create_theoretical_instructors.each do |instructor_id|
    self.theoretical_instructors << Instructor.find(instructor_id)
  end
  create_practical_instructors.each do |instructor_id|
    self.practical_instructors << Instructor.find(instructor_id)
  end
  save!
end

And change the form in your view to use the new attr_accessors:

<%= course_form.label :create_theoretical_instructors %><br />
<%= course_form.select :create_theoretical_instructors, Trainer.all.map { |x| [[x.name, x.surname].join(" "), x.id] }, {  }, { multiple: true } %>

<%= course_form.label :create_practical_instructors %><br />
<%= course_form.select :create_practical_instructors, Trainer.all.map { |x| [[x.name, x.surname].join(" "), x.id] }, {  }, { multiple: true } %>

Now when you submit the form, it will write the instructor ids to the new Course instance variables; after the Course has been validated and saved, it will automatically create the new associations.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文