Saturday 12 July 2008

Primary Keys that mean something in Rails

I like Rails, I really do, but you like me may need to migrate a data-model that doesn't use locally generated integers as the primary keys (PKs) of entities.

Rails, by default, seeds model (entity) data with unique identifiers it uses as PKs from database specific mechanisms (say serial columns in PostgreSQL).

Personally, I think locally generated integers used as identifiers for "things", are most poor for two main reasons:

  1. They don't scale.  Your local postgreSQL instance may just be part of a wider data architecture.  If you use locally generated PKs as Rails will get you to, you'll clash with other locally (but possibly the same value) PKs from other local DBs when synchronization with the "master" takes place.  If you don't know what I'm talking about then go look at scaling databases, its not just a postgreSQL issue.  If you really really have/want/need to use an identifier, then there is a good article of using Universal Unique Identifiers (UUID) for IDs at GUID-as-Primary-in-Rails although the article doesn't follow through on how in precise detail, so we'll cover that in a later blog.
  2. They don't mean anything.  Say for example you have an entity type HillType, whose PK should really be a unique and meaningful name.   If you really wanted to find out about the details of a HillType of name gnarly you'd want to enter a RESTful-like URL of http://localhost:3000/hilltypes/gnarly.  You really wouldn't want to know the mapping between "gnarly" and some internal ID that Rails generated for you via a DB sequence.  Yes, I know there are ways of RESTfulizing the internal ID to some other attribute on the model/table, but avoid the complexity in the first place, and under-populate your database with data it actually needs to be readable by people not frameworks.

So ..........  After those contentious points, how do we do it?

Three steps to RAILS CRUD with meaningful PKS

1) Unfortunately, I've not found a way round of avoiding an integer PK in Rails, unless either you specify your migration without a PK, or you avoid the Rails meta-DDL completely and go native in your migration as in here:-

class CreateHillTypes < ActiveRecord::Migration
  def self.up
      # -------------------------------
      # This is postgreSQL specific DDL
      # -------------------------------
      execute <<-EOF
        create table public.hill_types (
            typename varchar(255) not null unique,
            description varchar(2000),
            primary key (typename)
        );
      EOF
  end

  def self.down
    drop_table "hill_types"
  end
end

2) We also need to ensure our Rails "knows" we've provided a non-standard PK, so we need to amend our model:-

class HillType < ActiveRecord::Base
    set_primary_key "typename"
end

3) Next we need to amend the templated controller that script/generate scaffold HillType generated for us to avoid attempting to mass-assign what is now a protected attribute (typename) on our model.  So, on the create method Rails makes for you by default in your model controller, you'll need something like:

def create
      @hill_type = HillType.new
      got_details = params[:hill_type]
      @hill_type.typename = got_details["typename"]
      @hill_type.description = got_details["description"]
      respond_to do |format|
          if @hill_type.save
              flash[:notice] = 'HillType was successfully created.'
              format.html { redirect_to(@hill_type) }
              format.xml  { render :xml => @hill_type, :status => :created, :location => @hill_type }
          else
              format.html { render :action => "new" }
              format.xml  { render :xml => @hill_type.errors, :status => :unprocessable_entity }
          end
      end
  end

That is it.  Go gambol in the fields of  meaningful URLs, and leave out meaningless framework convenience identifiers from your models, they are embarrassing.

No comments: