Using Constants in Rails Models

One of my pet peeves in code is the absence of constants and thus the prevalence of “magic numbers” in their place. Constants add a touch of readability and semantic meaning and magnify the maintainability of the code.

As a simple example, here’s a partial model featuring constants to aid in clear validation specifications:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Email < ActiveRecord::Base
  
  MAX_FROM_LENGTH   = 30
  MAX_TO_LENGTH     = 100
  FROM_LENGTH_RANGE = 1..MAX_FROM_LENGTH
  TO_LENGTH_RANGE   = 1..MAX_TO_LENGTH
  TO_FORMAT         = /^[_a-z0-9+.-]+@[_a-z0-9-]+\.[_a-z0-9.-]+$/i
  NOTE_LENGTH_RANGE = 1..DB_TEXT_MAX_LENGTH
  
  # ---- Attributes ----
  
  # Attributes protected from mass-assignment (as through forms) 
  attr_protected :sent_at
  
  
  # ---- Validations ----
  
  validates_length_of   :from, :within => FROM_LENGTH_RANGE
  validates_length_of   :to,   :within => TO_LENGTH_RANGE
  validates_format_of   :to,   :with   => TO_FORMAT
  validates_length_of   :note, :within => NOTE_LENGTH_RANGE

As you can see the model defines constants to enforce length maximums and in turn the ranges. You could argue that the range constants are less useful since you have to refer back to the constants section to see them, but that’s a matter of taste. I like having them.

With constants like these properly placed in your model, you can take advantage of them in your migration. Here’s a partial example:

1
2
3
4
5
6
7
8
9
10
class CreateEmails < ActiveRecord::Migration
  def self.up
    create_table :emails do |t|
      t.column :emailable_id, :integer, :null => false
      t.column :to,         :string,  :null => false, :limit => Email::MAX_TO_LENGTH
      t.column :from,       :string,  :null => false, :limit => Email::MAX_FROM_LENGTH
      t.column :note,       :text,    :null => false
      t.column :sent_at,    :datetime
    end
  end

The advantage here is that your model and your migration are now in synch for field maximums. If that’s not enough, then consider what you can now do in your test code:

1
2
3
def test_invalid_with_long_to
    assert_value_for_attribute_invalid(Email.new, :to, filled_str("X", Email::MAX_TO_LENGTH + 1))
  end

So now, if you change your mind and decide you need to lengthen your fields, you need only change your model constants. Your migration could be rerun or you could add an additional migration which applies the new field length. And lastly, your test code doesn’t have to change.

I think this is simple, yet very much worth doing.

This entry was posted in Ruby, Ruby on Rails. Bookmark the permalink.

6 Responses to Using Constants in Rails Models

  1. Marcin Olak says:

    I like this technique. BTW. GeoKit rocks! :)

  2. Marcin Olak says:

    From Programming Ruby:

    [...] a period (.0) appearing outside brackets represents any character except a newline [...]

    Thus we need a backslash:

    TO_FORMAT = /^[_a-z0-9+.-]+@[_a-z0-9-]+\.[_a-z0-9.-]+$/i

    I know the code is for demonstration purposes only but still, we don’t want bugs to propagate throughtout the Internet from this magnificent blog :)

  3. Thanks for your great tips!

    By the way: GeoKit has helped me a lot building Waschstrasse.info—thanks!

  4. Marcin,

    I actually went back to my original code and it was correct there. This means that it is either being stripped out by CodeRay or that there’s a combination of characters that make the slash not render.

    Sorta makes you want to be cautious about the code you lift from a blog, eh? As always, one should know all of the code they deploy.

    Thanks for the tip.

    Bill

  5. Tom-Eric says:

    You really shouldn’t use your models in your migrations. It’s now possible to break the migration file by modifying the model. You could solve this by placing the model in your migration file, but I’m not sure if that’s the best solution.

  6. Yeah, I’ve had situations where I’ve added a model, then later deleted it, and thus broken the migration when it runs from an early version. However, you can easily add the model into your migration to fix the migration.

    I think this is a matter of style and my preference is still to use model constants in the migration. It just seems more DRY and adds accuracy to your migration.

    Thanks for your feedback!

Leave a Reply

Your email address will not be published. Required fields are marked *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>