Rails 3 and has_many: via: automatic setting / initialization of attributes in the join model

I searched the website deeply to find a clean and easy way to handle attribute initialization in the has_many :through relation join model, but I did not find a better solution for my need.

In the example below, I should automatically set the role attribute of the Training connection model when creating or updating the 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 for this model is that I want to store Training objects in one table. I do not want to create models for joining TheoreticalInstructor and PracticalInstructor (potentially explosive numbers of tables) to solve this problem.

This view provides a form for submitting 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%> 

Question: what can I do to make @course = Course.new(params[:course]) only line of code in the Course controller that is necessary to maintain this association when submitting the previous form?

Unlike this question, I do not want to create new Trainer objects when creating a new Course : I want to select them from existing ones in the database (via the input field in multimode mode).

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

I am thinking of the after_initialize on Training , which sets the role based on the relation name ( :theoretical_instructors and :practical_instructors ), but I really don't know how to do this. Any advice? Am I missing a point?

Thanks guys!

EDIT 1 of oli-g

This question deals with a similar problem: the difference is that I do not want to create Trainer objects when creating a new Course , but I just want to associate existing Trainer objects with the new Course .

EDIT 2 of oli-g

Based on this (5 years post) and this on blogs, I modified the Course model as follows:

 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 allows me to do something 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 only @course = Course.new(params[:course]) , but I need to create Training objects, iterate over params[:course][:theoretical_instructor_ids] and params[:course][:practical_instructor_ids] .

But I'm curious, so the question remains open: what can I do so that @course = Course.new(params[:course]) can create Training objects along with Course ?

Now ... I guess I found an error 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 correct... :010 > c.practical_instructor_ids => [] # WTF!? 

I think I'll talk about this in github questions ...

EDIT 3 oli-g

Github error reported

+4
source share
1 answer

Your problem is that you cannot add associations until your post has been created. In this case, learning associations are stored using the course record identifier, and the course identifier is not determined until the course is saved for the first time. You will need to use the after_create callback to call the function after creating the record.

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 instructor identifiers for the new course instance variables; after the course has been verified and saved, it will automatically create new associations.

+1
source

All Articles