14
votes

I have been stuck with this problem for a while and have thoroughly confused myself as to how nested models and validations work together.

In the code below, my aim is to have the creation of the parent model (Image or Video) fail if the validation of the child model(Content) fails. Currently, the parent model is being saved while the child model is not, and the validation errors are going unheard. If there are no validation errors, then everything works as expected.

#Image.rb
has_one     :content,
as:         :contentable,
inverse_of: :contentable,
dependent:  :destroy

#Video.rb
has_one     :content,
as:         :contentable,
inverse_of: :contentable,
dependent:  :destroy

#Content.rb
belongs_to   :contentable,
inverse_of:  :content,
polymorphic: true

validate     :all_good?

def all_good?
  errors.add(:base, "Nope!")
  return false
end

Any leads or insights are much appreciated!

3
which version of rails you are using?dnsh
@Dinesh Rails 4.2.6geoboy
Why exactly are you setting this up like this - "recommended approach" isn't really doable unless you present an end goal - what exactly is the reasoning for separating items like this - what do the models / schema look like in your application that require this separation?MageeWorld
I think the important part of your code is missing. You have to show us how you persist thoose objects. For instance if you first create your Image and save it and then add a child, Rails can't undo this save. Then you have to wrap every thing in a transaction and raise an error to trigger rollback.slowjack2k
@slowjack2k I am essentially saving Image or Video, and before saving, setting up the association for Content. So its not being saved before.geoboy

3 Answers

5
votes

Rails has a special validation called validates_associated that ensures that the associated record(s) is valid. If the associated record is invalid then the parent record will also be invalid and an error for the association will be added to it's list of errors.

In both your Image and Video classes add the following:

validates_associated :content

Now if the content association is invalid the video or image will not be saved.

video = Video.new
video.content = Content.new
video.save #=> false
video.valid? #=> false
video.errors #=> [:content => "is invalid"]
4
votes

Short Answer

Add to image and video model:

accepts_nested_attributes_for :content

The Proof

I was quite sure I knew the answer to this but wasn't sure if it worked with polymorphic associations (which I haven't used before) so I set up a small test.

Created the models the same way as you have yours setup but with a name attribute and with a validation that I can use to test for failure.

class Image < ActiveRecord::Base
  has_one     :content,
              as:         :contentable,
              inverse_of: :contentable,
              dependent:  :destroy

  validates_length_of :name, maximum: 10
end

class Content < ActiveRecord::Base
  belongs_to   :contentable,
               inverse_of:  :content,
               polymorphic: true

  validates_length_of :name, maximum: 10
end

Next setup the migrations as so:

class CreateImages < ActiveRecord::Migration
  def change
    create_table :images do |t|
      t.string :name

      t.timestamps null: false
    end
  end
end


class CreateContents < ActiveRecord::Migration
  def change
    create_table :contents do |t|
      t.string :name
      t.references :contentable, polymorphic: true, index: true

      t.timestamps null: false
    end
  end
end

Next write an RSpec to test that the parent isn't saved if child can't be saved and that validation errors perculate up.

  it 'should not save image if content is invalid' do
    image = Image.new()
    image.name = 'this is ok'
    expect(image).to be_valid

    content = Content.new()
    content.name = 'a string that should fail validation'
    image.content = content

    expect(image).to_not be_valid
    image.save

    expect(image).to_not be_persisted
    expect(content).to_not be_persisted

    expect(image.errors.count).to eq(1)
    expect(image.content.errors[:name][0]).to include('is too long')
  end

Ran the test and sure enough it fails.

Next add the following line to image (and video)

  accepts_nested_attributes_for :content

The tests now pass - i.e., if the child fails validation the parent will also fail validation and will not save.

0
votes

You need to raise exception in your custom validation. do something like

before_save :ensure_all_good

def ensure_all_good
 try saving stuff to chile
 failed? raise nope
end