Rails Many-to-many association with correct validation and save all

In my Rails 4.2.5 application I have the following two models (Spell, Category) which have a many-to-many relation with association model (Spellcategory):

class Spell < ActiveRecord::Base
    has_many :spellcategories, class_name: "Spellcategory", foreign_key: "spell_id", dependent: :destroy
    has_many :categories, through: :spellcategories, source: :category
end

class Category < ActiveRecord::Base
    has_many :spellcategories, class_name: "Spellcategory", foreign_key: "category_id", dependent: :destroy
    has_many :spells, through: :spellcategories, source: :spell
end

class Spellcategory < ActiveRecord::Base
    belongs_to :spell, class_name: "Spell"
    belongs_to :category, class_name: "Category"
end

I have already an existing list of categories and want to import a list of spells. Because manual input is possible in the import process I want to first create everything, display it, and then save everything (spells + links to the categories) altogether. I store the objects in the activerecord-session_store from the first to the second step.

I load the object with the following code from a XML file in the first step (a little bit pseudo-code because I don't want to bore you with all the XML parsing):

spells = xml_doc.path(...).map do |xml_spell|
    new_spell = @user.spells.build
    xml_spell.path('cats/cat').each do |node|
       new_spell.categories << Category.find_by(name: node.text) 
    end
    new_spell
end

and save all the spells in the second step.

if spells.map(&:valid?).all?
    spells.each(&:save!)
end

The last lines fail because validation fails with the error Spellcategories is invalid. My guess is it has something to do with the unsaved child elements from the association table. I tried to use :autosave but it didn't solve the problem.

What am I doing wrong which prevents me from saving all in one go?

Upvotes: 1

Views: 673

Answers (1)

Aleksey Gureiev
Aleksey Gureiev

Reputation: 1759

If you don't need access to Spellcategory model per se, you can use has_and_belongs_to_many instead of two has_many and it will simplify things quite a bit.

If you do need it, the reason for the problem was outlined above in comments to the question -- you don't have an ID for Spell you are creating to be able to save relations with it. Here's what I would try -- two options:

Option 1. If you have just a few records:

Spell.transaction do
  spells = xml_doc.path(...).map do |xml_spell|
    new_spell = @user.spells.create
    xml_spell.path('cats/cat').each do |node|
       new_spell.categories << Category.find_by(name: node.text) 
    end
    new_spell
  end
end

Wrapping it into transaction guarantees the consistency. Replacing "build" with "create" initializes the Spell record before adding categories.

Option 2. If you have many records. I used this method for importing millions of records. Keep the join table class and then use bulk import plugin (like https://github.com/zdennis/activerecord-import) to load data.

Upvotes: 1

Related Questions