Rails 3 и has_many: through: автоматически устанавливают / инициализируют атрибуты в модели соединения - PullRequest
4 голосов
/ 07 августа 2011

Я глубоко искал в Интернете, чтобы найти чистый и простой способ справиться с инициализацией атрибутов в модели соединения has_many :through relation, но я не нашел лучшего решения для своих нужд.

В приведенном ниже примере я должен автоматически установить атрибут role модели объединения Training при создании или обновлении объекта Course.

Это моя модель:

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 (что может привести к разрыву числа таблиц) для решения этой проблемы.

В этом представлении представлена ​​форма для отправки нового 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%>

Вопрос в том, что я могу сделать, чтобы сделать @course = Course.new(params[:course]) единственной строкой кода в контроллере Course, необходимой для сохранения этой ассоциации при отправке предыдущей формы?

В отличие от этого вопроса Я не хочу создавать новые Trainer объекты при создании нового Course: я хочу выбирать их из тех, которые уже присутствуют в БД (через мультиселекторполе ввода).

Мне нужно, чтобы что-то вроде @course.theoretical_instructor_ids = [1, 2] создавало два Training объекта с атрибутом role, установленным в Теоретический инструктор

I 'Я думаю о after_initialize обратном вызове Training, который устанавливает role на основе имени отношения (:theoretical_instructors и :practical_instructors), но я действительно не знаю, как это сделать.Любой совет?Я что-то упустил?

Спасибо, ребята!

РЕДАКТИРОВАТЬ 1 из oli-g

Этот вопрос имеет отношение кпохожая проблема: разница в том, что я не хочу создавать Trainer объекты при создании нового Course, но я просто хочу связать существующие Trainer объекты с новым Course.

РЕДАКТИРОВАТЬ 2 от oli-g

Основываясь на этом (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 = Course.new(params[:course]), но мне нужно создать Training объектов, повторяющихся на params[:course][:theoretical_instructor_ids] и params[:course][:practical_instructor_ids].

Но мне любопытно, поэтому вопрос остается открытым: что я могу сделатьчтобы разрешить @course = Course.new(params[:course]) строить Training объекты вместе с Course?

Теперь ... Я думаю, что обнаружил ошибку в 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выпуски ...

РЕДАКТИРОВАТЬ 3 от oli-g

Ошибка сообщена в github

1 Ответ

1 голос
/ 11 августа 2011

Ваша проблема в том, что вы не сможете добавлять ассоциации до после вашей записи. В этом случае ассоциации обучения сохраняются с использованием идентификатора записи курса, а идентификатор курса не определяется до тех пор, пока курс не будет сохранен в первый раз. Вам нужно использовать обратный вызов 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 } %>

Теперь, когда вы отправляете форму, она записывает идентификаторы инструктора в новые переменные экземпляра курса; после того, как курс был проверен и сохранен, он автоматически создаст новые ассоциации.

...