In Rails 4, changing the primary key of a model to something other than the default ‘id’ is fairly trivial. While this is technically fine, it is generally discouraged because it requires additional explicit configuration in the models and elsewhere, where Rails expects primary keys to be named ‘id’. Some gems and extensions do not handle custom primary keys well, so be sure to test thoroughly when changing them.

Migrations

First, in our ActiveRecord migrations, we can create tables in the database with a primary key other than the default ‘id’…

class CreateParents < ActiveRecord::Migration
  def change
    create_table  :parents,
    {
      :id           => false,
      :primary_key  => :parent_name
    } do |t|
      t.string :parent_name
      #...
    end
  end
end

Models

Now we need to state that primary key in our model classes…

class Parent < ActiveRecord::Base
  self.primary_key = "parent_name"
  #...
end

Associations

Associations in models expect the foreign key field referencing a table ‘t’ to be named ‘t_id’, but that name is not very practical if t’s primary key is something other than ‘id’. We can name the foreign key field something that matches the related model’s primary key…

class CreateChildren < ActiveRecord::Migration
  def change
    create_table :children do |t|
      #foreign-key field
      t.string :parent_name
      #...
    end
  end
end

…and then explicitly state the name of the foreign key field to use for the association…

class Child < ActiveRecord::Base
  belongs_to  :parent,
              :foreign_key => 'parent_name'</p>
  #...
end

Routes

The resources method, which generates routes corresponding to CRUD operations for a particular model, maps end-point URLs to actions in the controller. Expecting the primary key of models to be ‘id’, resources will name the identifying parameter of a resource (taken from the matched URL) as :id. In the case of nested resources, each parent resource in the hierarchy is identified by a parameter with ‘_id’ appended to the name of the resource, such as :parent_id, while the bottom-level resources are still identified by the **:id ** parameter…

Example::Application.routes.draw do
  resources :parents do
    resources :children
  end
end

Results in these routes:

  parent_children GET        /parents/:parent_id/children(.:format)             children#index
                  POST       /parents/:parent_id/children(.:format)             children#create
 new_parent_child GET        /parents/:parent_id/children/new(.:format)         children#new
edit_parent_child GET        /parents/:parent_id/children/:id/edit(.:format)    children#edit
     parent_child GET        /parents/:parent_id/children/:id(.:format)         children#show
                  PATCH      /parents/:parent_id/children/:id(.:format)         children#update
                  PUT        /parents/:parent_id/children/:id(.:format)         children#update
                  DELETE     /parents/:parent_id/children/:id(.:format)         children#destroy</p>
          parents GET        /parents(.:format)                                 parents#index
                  POST       /parents(.:format)                                 parents#create
       new_parent GET        /parents/new(.:format)                             parents#new
      edit_parent GET        /parents/:id/edit(.:format)                        parents#edit
           parent GET        /parents/:id(.:format)                             parents#show
                  PATCH      /parents/:id(.:format)                             parents#update
                  PUT        /parents/:id(.:format)                             parents#update
                  DELETE     /parents/:id(.:format)                             parents#destroy</pre></p>

This means our ParentController will have the param :id availble to identify the correct parent for an action, and our ChildrenController will have the param :parent_id available to it to identify the associated Parent when restricting our actions to children of a particular parent. Then we would have to retrieve our models with code like this:

@parent   = Parent.find(:id)
@children = Child.where(:parent_name => params[:parent_id])

That works, but it is misleading and suggests that the primary key of a Parent is ‘id’, and presumably an integer, neither of which is the case. This can cause confusion for other developers or our future-selves, especially if looking at the enummerated routes as documentation for our RESTful API. Luckily, though, Rails 4 has a very convenient way of specifying a unique name for our URL when using resources: the :param option. Passing that along to the resource method will cause Rails to generate the appropriate routes.

Example::Application.routes.draw do
  resources :parents,
            :param => :parent_name do
    resources :children
  end
end

This will result in these routes:

  parent_children GET        /parents/:parent_name/children(.:format)           children#index
                  POST       /parents/:parent_name/children(.:format)           children#create
 new_parent_child GET        /parents/:parent_name/children/new(.:format)       children#new
edit_parent_child GET        /parents/:parent_name/children/:id/edit(.:format)  children#edit
     parent_child GET        /parents/:parent_name/children/:id(.:format)       children#show
                  PATCH      /parents/:parent_name/children/:id(.:format)       children#update
                  PUT        /parents/:parent_name/children/:id(.:format)       children#update
                  DELETE     /parents/:parent_name/children/:id(.:format)       children#destroy</p>
          parents GET        /parents(.:format)                                 parents#index
                  POST       /parents(.:format)                                 parents#create
       new_parent GET        /parents/new(.:format)                             parents#new
      edit_parent GET        /parents/:name/edit(.:format)                      parents#edit
           parent GET        /parents/:name(.:format)                           parents#show
                  PATCH      /parents/:name(.:format)                           parents#update
                  PUT        /parents/:name(.:format)                           parents#update
                  DELETE     /parents/:name(.:format)                           parents#destroy

Now our routes, controllers, models and associations are able to gracefully operate on models which have custom primary keys. Happy coding!