Rails 3 has_and_belongs_to_many association: how to assign related objects without saving them to the database
Asked Answered
V

1

0

Working with an has_and_belongs_to_many_association

class Category
  has_and_belongs_to_many :projects
end

I would would like to use a before_filter to set projects before saving categories

before_filter :set_projects, :only => [:create, :update]

def set_projects
    @category.assign_attributes({projects: Project.all})
end

This works well, except when the category cannot be saved and there is a rollback. The projects are still updated in database.

Why this line

@category.assign_attributes({projects: Project.all})

generate these database records immediately?

BEGIN
INSERT INTO "categories_projects" ("category_id", "project_id") VALUES (86, 1)
INSERT INTO "categories_projects" ("category_id", "project_id") VALUES (86, 2)
INSERT INTO "categories_projects" ("category_id", "project_id") VALUES (86, 3)
COMMIT

I would like to wait for @category.save before commiting these new categories_projects relations. How to postpone these commits?

Please note that I can't modify the main "update" action. I have to use before/after filters and callbacks in order to override current functionality of my app.

------ EDIT ----------

Ok, after reading the doc carefully here, I think I have a solution:

When are Objects Saved?

When you assign an object to a has_and_belongs_to_many association, that object is automatically saved (in order to update the join table). If you assign multiple objects in one statement, then they are all saved.

If you want to assign an object to a has_and_belongs_to_many association without saving the object, use the collection.build method.

I will try to use the collection.build method. Do you have any idea how to do that with existing projects?

Veach answered 21/10, 2014 at 16:44 Comment(0)
S
0

Why not move this into the Category model in an after_save call back? e.g.

class Category
  #based on comment need virtual attribute
  attr_accessor :assignable_projects

  after_save :set_projects

  private 
    def set_projects
      self.assign_attributes({projects: self.assignable_projects})
    end
end

Since you need to set this for specific projects only you will need to create a virtual attribute. This attribute will be stored in the instance but will not be saved to the database. To do this we add an attr_accessor line which will create bot the getter and setter methods needed.

Then in the controller

class CategoriesController < ApplicationContoller
  before_filter :set_assignable_projects, only: [:create,:update]


  private
    def set_assignable_projects
      @category.assignable_projects = params[:project_ids]
    end
end

This event will fire after category validations are run and the category is save successfully. It will then use the values assigned in the before_filter to create the associations. Since assign_attributes does not call save again it will avoid an infinite loop. You could also place this in an after_validation callback but make sure you check for self.errors.empty? before using assign_attributes or you will be in the same boat you are now.

If the category fails to save the assignable_projects will still be set for that instance so they will show up in the rendered view for a failed save.

Storebought answered 22/10, 2014 at 17:5 Comment(3)
Thanks, it makes sense. Homever I must say I simplified the problem in the question. I don't always associate all projects with the category, I actually get a params[:project_ids]. And I don't think I can access request params from the model...Veach
@Veach what you need is a virtual attribute to store these in the active instance. I have updated my answer to show you how to implement this. This attribute will have both getter and setter methods but the value will not be stored in the database. If you need further explanation please let me know.Storebought
You are right. It could work this way. Thank you. I will try to implement that.Veach

© 2022 - 2024 — McMap. All rights reserved.