dom lizarraga

dominiclizarraga@hotmail.com

Rails code guide through with Kasper Timm Hansen | notes

13 minutes to read

Next session with Kasper (luma link)

Last Friday I attended a session with Kasper where he shared with us how he usually explores Rails codebase, he’s got a lot of experience doing this therefore I applied what I learned here.

I’ve always been curious how callbacks and model validations work in Rails, so I decided to explore this topic.

  1. Setting up the app
  2. Possible error with bundle
  3. First exploration with ActiveRecord lib
  4. super keyword for .valid?
  5. super keyword for .save
  6. Ruby call stack
  7. Recap with flow chart
  8. Shortcut for Ruby method lookup

Always have a question in mind that you want to answer otherwise it may be pretty easy to get lost.💡

Setting up the app

# create a new rails app
rails new callbacks && cd callbacks
# create a new model
rails g model Post title
rails db:migrate
# add the following code to the model
class Post < ApplicationRecord
  validates :title, presence: true, allow_nil: false

  before_validation :titleize_title
  after_create :print_out_title

  private
    def titleize_title
      return unless title.present?
      self.title = title.downcase.titleize
      puts "before_validation- Title changed to #{title}"
    end

    def print_out_title
      puts "after_create- Title was saved as: #{title}"
    end
end

After setting up a very basic app with 2 callbacks, and one validation, we can see that both callbacks are running corrrectly.

  callbacks git:(main)  rc
Loading development environment (Rails 8.0.1)
callbacks(dev)> p = Post.new
=> #<Post:0x00000001205322c8 id: nil, title: nil, created_at: nil, updated_at: nil>
callbacks(dev)> p.valid?
=> false
callbacks(dev)> p.errors.full_messages
=> ["Title can't be blank"]
callbacks(dev)> p.title = "HOLA"
=> "HOLA"
callbacks(dev)> p.save
before_validation- Title changed to Hola 👈
  TRANSACTION (0.1ms)  BEGIN immediate TRANSACTION /*application='Callbacks'*/
  Post Create (10.0ms)  INSERT INTO "posts" ("title", "created_at", "updated_at") VALUES ('Hola', '2025-02-03 01:09:45.881379', '2025-02-03 01:09:45.881379') RETURNING "id" /*application='Callbacks'*/
after_create- Title was saved as: Hola 👈
  TRANSACTION (0.3ms)  COMMIT TRANSACTION /*application='Callbacks'*/
=> true

Pretty standard, now what does trigger ‘active_record_callbacks’? Was it after calling “.valid?” or “.save”?

Possible error with bundle

Let’s go and open active_record library with ‘bundle open activerecord’, it might throw you an error, I fixed it by typing:

# in your console, it will work for one session
EDITOR=code bundle open activerecord
# or permanently set it in your .zshrc file
export BUNDLER_EDITOR=code
# close the .zshrc file
source ~/.zshrc

Docs for ‘bundle open’ and setting your editor for opening gems 🪄

First exploration with ActiveRecord lib

Within lib/active_record/validations.rb:69 and after reading a bit we can see the following chain of method calls:

save → perform_validations → valid?.

Let’s add a puts statement and test it out.

def valid?(context = nil)
  puts "you are calling 'valid?' :)" # added for testing 👈
  context ||= default_validation_context
  output = super(context) # <--- calls ActiveModel::Validations#valid? (parent method)
  errors.empty? && output
end

Close the editor and ‘reload!’ rails console

  callbacks git:(main) reload!
Loading development environment (Rails 8.0.1)
callbacks(dev)> p = Post.new
=> #<Post:0x0000000120b394c8 id: nil, title: nil, created_at: nil, updated_at: nil>
callbacks(dev)> p.valid?
you are calling 'valid?' :) 👈
=> false
callbacks(dev)> p.title = "HOLA!"
=> "HOLA!"
callbacks(dev)> p.save
you are calling 'valid?' :) 👈
before_validation- Title changed to Hola!
  TRANSACTION (0.1ms)  BEGIN immediate TRANSACTION /*application='Callbacks'*/
  Post Create (3.0ms)  INSERT INTO "posts" ("title", "created_at", "updated_at") VALUES ('Hola!', '2025-02-03 01:32:16.234929', '2025-02-03 01:32:16.234929') RETURNING "id" /*application='Callbacks'*/
after_create- Title was saved as: Hola!
  TRANSACTION (0.5ms)  COMMIT TRANSACTION /*application='Callbacks'*/
=> true

We can conclude with this inspection that each time we call “.save” or “save!” we call in the end “.valid?”.

Now, let’s explore the 2 ‘super’ keywords following the chain of methods up to its ancestor.

super’ keyword for .valid?

Let’s open the gem with ‘bundle open activemodel’.

‘.valid?’ will invoke ActiveModel::Validations#valid? lib/active_model/validations.rb:361 this method will call ‘run_validations!’ which is defined in lib/active_model/validations.rb:459

def valid?(context = nil)
  current_context = validation_context
  context_for_validation.context = context
  errors.clear
  run_validations! # <--- calls all validation callbacks (returns true if the record is valid, false otherwise)
ensure
  context_for_validation.context = current_context
end

run_validations! → _run_validate_callbacks

The latter comes from ActiveSupport::Callbacks, it is dynamically generated via ‘define_callbacks’, inside of this ActiveSupport module you’ll find very interesting classes as Before, After, Around, CallbackSequence, CallbackChain (where the callbacks are stored in a [])

super’ keyword for .save

The second ‘super’ keyword is in ‘.save’ and goes up to ActiveRecord::Persistence module.

Just for making this more practical I have added a puts statement to lib/active_record/persistence.rb:390

def save(**options, &block)
  create_or_update(**options, &block)
  puts "you saved it :)" # added for testing 👈
rescue ActiveRecord::RecordInvalid
  false
end

rails console 🎮

callbacks(dev)> p = Post.new
=> #<Post:0x000000011d5b92a8 id: nil, title: nil, created_at: nil, updated_at: nil>
callbacks(dev)> p.title = "HOLA?"
=> "HOLA?"
callbacks(dev)> p.valid?
you are calling 'valid?' :)
before_validation- Title changed to Hola?
=> true
callbacks(dev)> p.save
you are calling 'valid?' :)
before_validation- Title changed to Hola?
  TRANSACTION (0.1ms)  BEGIN immediate TRANSACTION /*application='Callbacks'*/
  Post Create (8.7ms)  INSERT INTO "posts" ("title", "created_at", "updated_at") VALUES ('Hola?', '2025-02-03 23:16:20.758330', '2025-02-03 23:16:20.758330') RETURNING "id" /*application='Callbacks'*/
after_create- Title was saved as: Hola?
you saved it :) 👈
  TRANSACTION (0.0ms)  ROLLBACK TRANSACTION /*application='Callbacks'*/
=> nil

‘.save’ will call ‘create_or_update’ and then depending on whether the object is new or not, it can be about an ‘insert’ or ‘update’ operation.

How ruby call up its ancestor objects

Something that was difficult to wrap my head around was why ‘valid?’ and ‘save’ point to different modules and how they get overridden? After some lookups I figured that this is because in ‘lib/active_record/base.rb’ we have the following:

Module ActiveRecord
 class Base
   include Persistence 
   include Validations
   include Callbacks
   ...
 end
end

Since Validations is included after Persistence, it overrides ‘save’, adding validation checks before calling ‘super’ to go back to ‘Persistence#save’.

Recap with visual aid

When we call ‘.save’, Ruby follows this lookup order:

First, it checks the model’s own class (Post in this case). The Post model does not define ‘save’, so Ruby looks in the included modules.

If not found, it looks in Callbacks, which does not define ‘save’ either.

Third, it checks ‘ActiveRecord::Validations#save’. Since this method exists, it runs, calling ‘perform_validations’. If validation passes, it calls ‘super’, which means “find the next save method in the lookup chain.”

Finally, super calls ‘ActiveRecord::Persistence#save’. This method handles inserting/updating the record.

Persistence

Validations

Callbacks

Transactions

First, Ruby looks in the Post class itself

Here a flow chart as recap of what we explored:

validations_rails_flow_cart

Shorcut - Ruby method lookup

I got some feeback from Kasper and he suggested the following commands as alternatives, they seem to be more efficient than looking up the code in the gem.

The code will tell you where the method is defined, and you can keep chaining .super_method to go up the inheritance chain.

  callbacks git:(main) rc
Loading development environment (Rails 8.0.1)
callbacks(dev)> Post.instance_method(:save).super_method
=> #<UnboundMethod: ActiveRecord::Transactions#save(**) /Users/dominiclizarraga/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/activerecord-8.0.1/lib/active_record/transactions.rb:361>

# 2 times '.super_method'
callbacks(dev)> Post.instance_method(:save).super_method.super_method
=> #<UnboundMethod: ActiveRecord::Validations#save(**options) /Users/dominiclizarraga/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/activerecord-8.0.1/lib/active_record/validations.rb:47>

# 3 times '.super_method'
callbacks(dev)> Post.instance_method(:save).super_method.super_method.super_method
=> #<UnboundMethod: ActiveRecord::Persistence#save(**options, &block) /Users/dominiclizarraga/.rbenv/versions/3.4.1/lib/ruby/gems/3.4.0/gems/activerecord-8.0.1/lib/active_record/persistence.rb:390>

And also for seeing the module hierarchy and suplerclasses you can use the following command:

Post.ancestors
=> 
[Post (call 'Post.load_schema' to load schema informations),
 Post::GeneratedAssociationMethods,
 Post::GeneratedAttributeMethods,
 ApplicationRecord(abstract),
 ApplicationRecord::GeneratedAssociationMethods,
 ApplicationRecord::GeneratedAttributeMethods,
 ActionText::Encryption,
 ActiveRecord::Base,
 Turbo::Broadcastable, 🤯
 ActionText::Attribute,
 ActiveStorage::Reflection::ActiveRecordExtensions,
 ActiveStorage::Attached::Model,
 GlobalID::Identification,
 ActiveRecord::Marshalling::Methods,
 ...
 ...
 ...
 ActiveRecord::ReadonlyAttributes,
 ActiveRecord::Persistence, 👈
 ActiveSupport::Callbacks, 👈
 ActiveModel::Validations, 👈
 ActiveSupport::Dependencies::RequireDependency,
 Object,
 PP::ObjectMixin,
 ActiveSupport::ToJsonWithActiveSupportEncoder,
 ActiveSupport::Tryable,
 JSON::Ext::Generator::GeneratorMethods::Object,
 Kernel,
 BasicObject]