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

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 ...

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...

Poor person's guide to managing Ruby versions

Understanding the guts of Ruby Version Management by rolling your own I've been tinkering with a fresh install of Ubuntu 12.10, setting up a nice clean development environment. One of the first things to do, of course, is implement some sort of Ruby version management. RVM and rbenv seem to be the clear winners in this arena, though there are a lot of tools out there that do a similar job . Writing your own version management for your Rubies isn't actually all that difficult. At it's core, we need need two things: A way to segregate the executables of the various versions A way to call the versions at will Segregating versions is trivial - working with files and folders, we can put the various versions into named directories. Actually executing our different versions is not all that difficult either. One way would be to create aliases with version numbers and explicitly call those when we want to use them. The more popular way, however, is to manipulate our PATH ...