Skip to main content

Rails Callbacks and Boolean Values

Rails has a lovely feature as part of ActiveRecord - Callbacks; to wit:

Callbacks are hooks into the life cycle of an Active Record object that allow you to trigger logic before or after an alteration of the object state

Basically, that means that you can instruct Rails to execute your own code throughout the process of creating, updating and destroying objects/records. You can, for example, use them to set some default values on create, or perhaps do some last minute validation, or even fire an event just after you've saved a record. The available callbacks are:

  • after_initialize
  • after_find
  • after_touch
  • before_validation
  • after_validation
  • before_save
  • around_save
  • after_save
  • before_create
  • around_create
  • after_create
  • before_update
  • around_update
  • after_update
  • :before_destroy
  • around_destroy
  • after_destroy
  • after_commit
  • after_rollback

At the same time, Ruby methods have the concept of implicit return values. That is to say, any method will return the value of the last evaluated statement. This is neat - you don't need to write an explicit "return" at the end of your methods.

When we mangle these two features together, we can create subtle bugs in our code. Well not so subtle, because your code will just stop working the way you expect, and it can be fairly difficult to debug because no error is thrown.

Consider the Rails Callbacks again:

If a callback returns false, all the later callbacks and the associated action are cancelled

And I'm sure you'll see where I'm going with this now.....Take an example where you have a boolean value as one of your fields in your model. You want to set this boolean in a callback, either as a default value or because it represents some state based on some other logic.

Let's contrive an example - a blog application which allows comments. We want comments to be moderated of course, because spam is bad. We create a quick boolean on our comment model called "isinmoderation". Every time a new comment is submitted, we want to set this variable to true. We have an added piece of logic to trust any comments from readers who have submitted five good (non-spammy) comments.

Given the above, we might end up with something like the following:

class Comment < ActiveRecord::Base
  before_create :moderate

  protected
    def moderate
      unless is_from_trusted_reader
        this.isinmoderation = false
      end
    end
end

We run some tests and find that new comments from untrusted readers are not being saved to the database. We notice that our transaction is being rolled back, but there's no error. For a while, we are confused. And then we remember this blog post and realise that we are returning a falsey value from one of our callbacks....and, as the Rails docs say,

all the later callbacks and the associated action are cancelled

And the mystery is solved. So is there an elegant way to ensure we don't hit this condition? Well one way out is to explicitly declare a return of true from our moderate method. It is a little less elegant than the normal implicit return, but c'est la vie

Comments

Popular posts from this blog

Getting started with Ruby on Rails 3.2 and MiniTest - a Tutorial

For fun, I thought I would start a new Ruby on Rails project and use MiniTest instead of Test::Unit. Why? Well MiniTest is Ruby 1.9s testing framework dejour, and I suspect we will see more and more new projects adopt it. It has a built in mocking framework and RSpec like contextual syntax. You can probably get away with fewer gems in your Gemfile because of that. Getting started is always the hardest part - let's jump in with a new rails project rails new tddforme --skip-test-unit Standard stuff. MiniTest sits nicely next to Test::Unit, so you can leave it in if you prefer. I've left it out just to keep things neat and tidy for now. Now we update the old Gemfile: group :development, :test do gem "minitest" end and of course, bundle it all up.....from the command line: $ bundle Note that if you start experiencing strange errors when we get in to the generators later on, make sure you read about rails not finding a JavaScript runtime . Fire up

Getting started with Docker

Docker, in the beginning, can be overwhelming. Tutorials often focus on creating a complex interaction between Dockerfiles, docker-compose, entrypoint scripts and networking. It can take hours to bring up a simple Rails application in Docker and I found that put me off the first few times I tried to play with it. I think a rapid feedback loop is essential for playing with a piece of technology. If you've never used Docker before, then this is the perfect post for you. I'll start you off on your docker journey and with a few simple commands, you'll be in a Docker container, running ruby interactively. You'll need to install Docker. On a Mac, I prefer to install Docker Desktop through homebrew: brew cask install docker If you're running Linux or Windows, read the official docs for install instructions. On your Mac, you should now have a Docker icon in your menu bar. Click on it and make sure it says "Docker desktop is running". Now open a terminal and ty

Rails 3.2, MiniTest Spec and Capybara

What do you do when you love your spec testing with Capybara but you want to veer off the beaten path of Rspec and forge ahead into MiniTest waters? Follow along, and you'll have not one, but two working solutions. The setup Quickly now, let's throw together an app to test this out. I'm on rails 3.2.9. $ rails new minicap Edit the Gemfile to include a test and development block group :development, :test do gem 'capybara' gem 'database_cleaner' end Note the inclusion of database_cleaner as per the capybara documentation And bundle: $ bundle We will, of course, need something to test against, so for the sake of it, lets throw together a scaffold, migrate our database and prepare our test database all in one big lump. If you are unclear on any of this, go read the guides . $ rails g scaffold Book name:string author:string $ rake db:migrate $ rake db:test:prepare Make it minitest To make rails use minitest , we simply add a require