Summer Rails Plugin Series #3: acts_as_referenced
(by Erik Peterson on July 23rd 2008)

Next up in my summer series of plugins is acts_as_referenced. This one is a bit small, but I think it can be extremely useful. This is a versatile and reasonably well-tested plugin that I'm using every day. On my main project, I have about 8 models that use reference numbers- five of them use the same numbering scheme, and the other three use different ones. I needed a way to DRY up the assignment of these reference numbers while creating a central search index (GREAT for barcoded reference numbers), allowing for legacy reference numbers AND having them be valuable to a human reader by including the date and object type in the reference number.

Installation

If you are on edge rails:

script/plugin install git://github.com/subwindow/acts_as_referenced.git

If you are not on edge rails:

git clone git://github.com/subwindow/acts_as_referenced.git vendor/plugins/acts_as_referenced

Usage

To make use of acts_as_referenced, you first need to create the Reference model & its table:

script/generate reference reference
rake db:migrate

You also need a column 'reference' in all of the tables you plan on using acts_as_referenced with. (You can change this with the :referenced_column option)

Enable the functionality by declaring acts_as_referenced on your model

class Order < ActiveRecord::Base
  acts_as_referenced
end

Advanced Usage

Advanced usage is enabled through a set of options. Here they are:

# Separator
acts_as_referenced # Defaults to '-', e.g.: '080722-1'
acts_as_referenced :separator => "" #e.g.: '0807221'
acts_as_referenced :separator => "_" #e.g.: '080722_1'

# Prefix.  A string that is appended at the front of the reference number.  Does not affect incrementing.  Accepts a proc.
acts_as_referenced # Defaults to '', e.g.: '080722-1'
acts_as_referenced :prefix => "OR" # e.g.: "OR-080722-1"
acts_as_referenced :prefix => Proc.new {|o| o.category.first.upcase } # e.g.: "M-080722-1"

# Increment base.  A string that is used as the base for incrementing- all reference numbers with the same base will be incremented together.  Accepts strftime arguments.  
acts_as_referenced # Defaults to "%y%m%d", e.g.: "080722-1"
acts_as_referenced :increment_base => "#{(65+(Time.now.year-2003)).chr}%m%d" # e.g.: "F0722-1"

# Increment size.  An integer that is used to pad incrementers.  Useful if you require reference numbers with a consistent legnth.
acts_as_referenced # Defaults to 0, e.g.: "080722-1", "080722-9999"
acts_as_referenced :increment_size => 4 #e.g.: "080722-0001", "080722-9999"

# Referenced Column.  Change the name of the "reference" column that is stored on the model.
acts_as_referenced # Defaults to 'reference'
acts_as_referenced :referenced_column => "order_number"

A Note on Racing

There's a racing condition inherent in the plugin as it is shipped. If there are two processes creating references at approximately the same time, it is entirely possible that they will collide and create reference numbers at the same time. Rails may or may not catch this, so the possibility for duplicate reference numbers exists, and the possibility for confusing end-user errors is even greater. To solve this, you have to lock the table from reads while creating a reference number. It should only take < 50ms, so in most cases it is not that big of a deal. The locking procedures are different for different DBMSs. For PostgreSQL, uncomment lib/acts_as_referenced.rb line #91. For MySQL, uncomment lib/acts_as_referenced.rb lines #93 and #99. For other systems, simply write your own table locking/unlocking SQL and place it in the same spots.

Conclusion

As always, I hope someone can find this plugin useful. Please let me know if there are any bugs or you are confused by something. My normal use-case is a bit esoteric, so if you have questions on how this might be useful, let me know in a comment and I can try to explain it better. For any other questions or comments, email me at erik [at] subwindow (dot} com. To contribute code, fork it at github.