14

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!

geoboy
  • 1,172
  • 1
  • 11
  • 25
  • which version of rails you are using? – dnsh Oct 06 '16 at 12:56
  • @Dinesh Rails 4.2.6 – geoboy Oct 06 '16 at 19:43
  • 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 Oct 06 '16 at 22:15
  • 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 Oct 09 '16 at 15:27
  • @slowjack2k I am essentially saving Image or Video, and before saving, setting up the association for Content. So its not being saved before. – geoboy Oct 09 '16 at 17:09
  • Sorry I only want to clarify it. Do you `video.content = Content.new; video.save!` or `content.video = video; video.save!`? – slowjack2k Oct 09 '16 at 20:14
  • @slowjack2k `video.build_content` and then `video.save`. – geoboy Oct 09 '16 at 22:09

3 Answers3

5

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"]
infused
  • 24,000
  • 13
  • 68
  • 78
  • thanks for taking out the time to respond. unfortunately, that doesn't work. it still continues to save the record ( if i insert `validates_presence_of : content`, then it will say that `content cannot be blank`). does nesting cause that? also, from the documentation: "NOTE: This validation will not fail if the association hasn't been assigned. If you want to ensure that the association is both present and guaranteed to be valid, you also need to use validates_presence_of" – geoboy Oct 04 '16 at 03:19
4

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.

David
  • 3,510
  • 3
  • 21
  • 39
0

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
  • Interesting idea. Shouldn't I be able to do it just via validation and errors object though, the more conventional rails way? – geoboy Sep 21 '16 at 03:13
  • I would not implement logic to update the children inside my validate. Because that would be misleading. Hence why I would prefer to update using before_save instead and handle it there. You can always put the two requests inside a transactiwould not be commited aswell. – Mudit Dalmia Sep 22 '16 at 01:17
  • @geoboy I just came across a similar issue, errors from child model are not passed to parent model. In your case since the validation is inside the content model. The parent models video and image won't be able to see errors from content. You might want to take a look at below link suggesting how you can merge errors from child models to parents http://stackoverflow.com/questions/2680016/ruby-on-rails-how-to-get-error-messages-from-a-child-resource-displayed – Mudit Dalmia Oct 04 '16 at 23:06