Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Invalidating parent model save through child before_save callback

I have two models, a Parent and a Child (as outlined below). The child model has a before_save callback to handle some external logic, and if it encounters any errors, the callback invalidates that model being saved.

class Parent < ActiveRecord::Base
  has_one :child
  accepts_nested_attributes_for :child

  validates :child, :presence => true
  validates_associated :child
end

class Child < ActiveRecord::Base
  belongs_to :parent

  before_save :external_logic
  validates :parent, :presence => true

  def external_logic
    begin
      # Some logic
    rescue
      #Invalidate child model
      errors.add(:base, "external logic failed")
      return false
    end
  end
end

The problem that I'm running into is that the Child model instance is created as through the nested attributes of the Parent model. When the external logic fails, I want the child model AND the parent model to not be saved, but instead the parent model is being saved on its own. How can I achieve this?

Please note, I am aware of validation callbacks, but they are not suitable in this case. The child model callback has to be a before_save.

EDIT #1

I already know about transactions, and don't consider someone telling me "hey, wrap it around a transaction externally" to be a valid response. This question is explicitly about how to solve this issue through a before_save call.

Why I can't use validations on create - as mentioned in the comments, the external bit of logic needs to be guaranteed to run ONLY before a database save. Validation calls can happen multiple times with or without altering the database record, so that's an inappropriate place to put this logic.

EDIT #2

Ok, apparently having the before_save return false does prevent the parent being saved. I have verified that through the console and actually inspecting the database. However, my rspec tests are telling me otherwise, which is just odd. In particular, this is failing:

describe "parent attributes hash" do
  it "creates new record" do
    parent = Parent.create(:name => "name", :child_attributes => {:name => "childname"})
    customer.persisted?.should be_false
  end
end

Could that be an rspec/factory_girl bit of weirdness?

EDIT #3

The test error is because I'm using transactional fixtures in Rspec. That was leading to tests that incorrectly tell me that objects were being persisted in the database when they really weren't.

config.use_transactional_fixtures = true
like image 732
Bryce Avatar asked Dec 10 '25 23:12

Bryce


1 Answers

Okay so your problem is with the ActiveRecord::Callbacks order.

As you can see on the linked page first validation is processed and if validation was successful then before_save callbacks are run. before_save is a place where you can assume every validation passed so you can manipulate a bit data or fill a custom attribute based on other attributes. Things like that.

So what you could do is just say for the Child model:
validate :external_logic and just remove the before_save :external_logic callback.

It's equivalent with what you want to do. When a Parent instance is created it will just error out if the Child object fails to validate, which will happen in your :external_logic validation method. This is a custom validation method technique.

After OP update:

Still you can use :validate method. You can set it to only run on create with:
validate :external_logic, :on => :create.

If you are running into issue that you need this to run on update as well, that is the default behavior. Validations are run on .create and .update only.

OR If you want to stick to before_save:

The whole callback chain is wrapped in a transaction. If any before callback method returns exactly false or raises an exception, the execution chain gets halted and a ROLLBACK is issued; after callbacks can only accomplish that by raising an exception.

I see you did return false so it should work as expected. How do you use Parent.create! method? What are the arguments there?

Make sure you are using it like (supposing .name is an attribute of Parent and Child):

Parent.create!{
  :name => 'MyParent'
  # other attributes for Parent
  :child_attributes => { 
    :name => 'MyChild'
    # other attributes for Child
  } 
}

This way it both the Parent and Child object will be created in the same transaction, so if your before_save method returns false Parent object will be rolled back.

OR

If you cannot use this format you could just try using pure transactions (doc, example in guides):

Parent.transaction do
  p = Parent.create
  raise Exception if true # any condition
end

Anything you do inside of this transaction will be rolled back if there is an exception raised inside the block.

like image 170
p1100i Avatar answered Dec 12 '25 13:12

p1100i



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!