46

I am having a case which is getting around my head.

I have an Image model which I only want to save if it gets uploaded. I also need some information coming from the upload to validate the image(like height and width). But I want only the upload to happen if somebody is trying to save the file the image for the first time.

So I thought the best option would be to have a before_validation, but I would like it to run only on save!

My code is on this gist https://gist.github.com/andreorvalho/b21204977d2b70fdef83

So the weird part is this on: :save and on: :create have really weird behaviours or at least not what I expected.

When I put it as on: :save If I try to do an image.save on a test I can see my before_validation callbacks are not ran!

If I put on: :create is ran in every situation is does not matter if I ran image.save, image.create or image.valid?

So I am guessing this is either not working or I am misunderstanding the goal of those on settings.

p.s. my validation on create, also occurs in every situation save, create or valid?

let me know if anybody ran into the same or knows why is not supposed to work like this.

uday
  • 8,544
  • 4
  • 30
  • 54
andre.orvalho
  • 1,034
  • 2
  • 11
  • 17

2 Answers2

86

The before_validation callback allows restrictions to particular events within the lifecycle of an object.

You can use the following syntax:

before_validation :set_uuid, on: :create
before_validation :upload_to_system, on: [:create, :update]

Here is the documentation of this option for Rails 4.0, Rails 5.2, Rails 6.0 and Rails 7.0.

Jon
  • 10,678
  • 2
  • 36
  • 48
  • Well there is 3 things I have to say about your answer: 1. the validation validates :source_file_path, presence: true, on: :create is also being run on every single time not only on create 2. I cant upload after save because I need what's being done there for my validations. 3. Why doesnt it fail or give an error then, if I cant use on: something here? I mean it's weird that when I pass on on: :save doesnt run for any situation and when I pass on: :create it runs for everything! – andre.orvalho Jan 28 '14 at 08:49
  • 1
    1. Something else must be wrong then. Those callbacks are an integral part of Rails and are well tested. 2. Why would you want to upload the image to your CDN before you've validated that you're actually going to save it? 3. No idea, but if you read the documentation you'll see that those options are not supported on `before_validation` – Jon Jan 28 '14 at 16:22
  • I guess you were right about the first one, it was working I was just not testing it right! thanks for your clarifications! – andre.orvalho Feb 12 '14 at 15:45
  • 1
    Hmm, the current Ruby on Rails Guides specifically offer this as an example of how to limit `before_validation` callbacks to only run on certain operations: "`before_validation :normalize_name, on: :create`" (from http://guides.rubyonrails.org/active_record_callbacks.html#callback-registration). Are you *sure* they are supposed to execute on every action? – Jazz Nov 18 '14 at 21:29
  • Nearly a year has passed since I wrote this answer. At the time of writing it was accurate. Thanks for the heads up though - I'll add an update shortly. – Jon Nov 20 '14 at 07:16
  • I had a look back through the commits today to find out and looking at the source code this should have worked in the first place. However I remember testing it at the time and looking through a bunch of other documentation at the time and it definitely didn't work. Perhaps there was a bug somewhere else which prevented it from working? It's nice that it works properly now though! – Jon Dec 15 '14 at 21:33
  • This has been possible since at least Rails 2.3 (https://guides.rubyonrails.org/v2.3/activerecord_validations_callbacks.html#on). I shortened your answer a little to not confuse readers (like me). – slhck Sep 05 '19 at 13:50
6

I came across the same issue and here's what I found.

#valid? is a method which also accepts an optional parameter called context (which for some reason a lot of people don't know about, including me before I stumbled upon this question and did some research). Passing in the context when the #valid? method is called will result in only those before_validation callbacks being run for which the same context is set using the :on key.

Example

Let's say we have the following code:

class Model < ActiveRecord::Base
  before_validation :some_method, on: :create
  before_validation :another_method, on: :update
  before_validation :yet_another_method, on: :save
  before_validation :also_another_method, on: :custom
end

Now, calling:

Model.new.valid?(:create)

will only run :some_method. Likewise calling:

Model.new.valid?(:update)
Model.new.valid?(:save)
Model.new.valid?(:custom)

will only run :another_method, :yet_another_method, and :also_another_method respectively. But, if we do:

Model.new.valid?(:unknown)

then it will not call any callbacks because we did not specify :unknown as a context while creating callbacks.

Also, one other thing to note is that if we do not pass a context when calling #valid?, then ActiveRecord will internally use the new_record? method to figure it out. That is, if new_record? returns true, then the context will be set to :create, but if it returns false, then the context will be set to :update.

Coming back to your question

When I put it as on: :save If I try to do an image.save on a test I can see my before_validation callbacks are not ran!

That's because ActiveRecord's #save method internally calls #valid? without passing an explicit context, which means now the #valid? method will have to decide whether to use :create or :update as a context based on the boolean returned by #new_record?. And since you've specified on: :save, it doesn't run. Yes, that's right, :save context doesn't exist internally in ActiveRecord.

If I put on: :create is ran in every situation is does not matter if I ran image.save, image.create or image.valid?

This is true, but only if image is a new record. Try doing the same for an existing record and it will not run.

radiantshaw
  • 535
  • 1
  • 5
  • 18