Simpler Polymorphic Selects in Rails 4 with Global ID

Polymorphic Relations in Rails

Rails allows you to define polymorphic relations, where one attribute can point to more than one type of record.

For example, if we have Tasks that should be assignable to Clients, but we have two types of Client, Company and Individual and we want to be able to reference either, then we could set up a polymorphic relation like this:

class Company < ActiveRecord::Base
  has_many :tasks, as: :client
end

class Individual < ActiveRecord::Base
  has_many :tasks, as: :client
end

class Task < ActiveRecord::Base
  belongs_to :client, polymorphic: true
end

Task should now have a client_id field (integer) and also a client_type field (string), which will for each record contain either “Company” or “Individual”. Rails will use the value in this field to infer the model to retrieve, as just the id on its own is ambiguous.

The Problem

The problem we are concerned with here emerges when we want to allow the user to select a client for a Task. Normally we would identify each item in the drop down by its id, but we now need to also pass the type of the record.

We could include an additional field, client_type, but this would need to be set using Javascript every time the client is selected which is not desirable.

What we would like is a way to encode the type of the record with the id so that in the controller we can infer the record type and set the client_type field appropriately.

A quick search on StackOverflow turns up various answers where instead of just an id, the model name is included in the value of the collection select, e.g.

- client_collection = Individual.all.map {|x| [x.name, "Individual:#{x.id}"]} + Company.all.map {|x| [x.name, "Company:#{x.id}"]}
= form.collection_select :client_id, client_collection, :id, :full_name

Other answers use slightly different formats, such as Model-id but the principle is the same.

This approach solves the problem and avoids the use of Javascript, but carries the disadvantage that it is brittle due to the hard-coding of model names (what happens if you add more models to this polymorphic relation, or change a model name?) and is also a scheme that you will need to reimplement in an ad-hoc manner in each of your projects.

An alternative, that will be baked into Rails 4.2 but can be used as a gem today, is Global ID.

A Quick Primer on Global ID

Global ID is a very simple library, soon to be built in to Rails, that produces URIs (Uniform Resources Identifiers) for records for your Rails apps, e.g.

gid://yourappname/Customer/1

It then also performs the reverse operation: give it a gid (Global ID) like the above and it will return you the model instance for that record (Customer with an id of 1).

This is basically a more complete implementation of the Model:id scheme discussed earlier, with the advantage that this is going to be baked into Rails, providing a standard way to refer to any record in your app.

Using Global ID with PolyMorphic Selects

In Rails 4.2, Global ID support will be automatically included in ActiveModel. For 4.1.x and below, we can install it as a gem and mix it in to our models directly.

First we add it to our Gemfile:

gem 'globalid', github: 'rails/globalid', tag: 'v0.2.2'

Run bundle install, and mix it in to the relevant models:

class Company < ActiveRecord::Base
  include GlobalID::Identification

  has_many :tasks, as: :client
end

class Individual < ActiveRecord::Base
  include GlobalID::Identification

  has_many :tasks, as: :client
end

Now when setting up our collection select, we can easily group the records into their respective type, and instead of a bare id, send along the selected record’s gid instead:

= form_for(@task) do |form|
  # Other fields, labels...
  = form.grouped_collection_select :client_id, [ Company, Individual ], :all, :model_name, :global_id, :name

This will create a grouped collection select for the field client, over models Company and Individual, it will take all such records and group them by their model_name, calling record.global_id to get the select value, and record.name for the record name in the drop down.

Your select will then look something like this:

Rails Polymorphic Grouped Collection Select

The last thing we need to do is set up our controller to use the gid, if it has been provided:

# POST /tasks
# POST /tasks.json
def create
  @task = Task.new(task_params)

  # Unless the client has been specified already (via a combination
  # of client_id and client_type, attempt to discover the client
  # via its global id.
  unless @task.client.present?
    @task.client = GlobalID::Locator.locate task_params[:client_id]
  end

  respond_to do |format|
    if @task.save
      format.html { redirect_to tasks_path, notice: 'Task was successfully created.' }
      format.json { render action: 'show', status: :created, location: @task }
    else
      format.html { render action: 'new' }
      format.json { render json: @task.errors, status: :unprocessable_entity }
    end
  end
end

Upon saving, ActiveRecord will automatically set the client_type for you, based on the record type identified by the global id.

And now the equivalent implementation for the update action:

# PATCH/PUT /tasks/1
# PATCH/PUT /tasks/1.json
def update
  params_for_update = task_params

  # The Locator will return nil if the URI is not valid (e.g. it is
  # a bare id) or if the record is not found, in which case we
  # fall back to the regular update procedure below.
  if GlobalID::Locator.locate task_params[:client_id]
    @task.client = GlobalID::Locator.locate params_for_update[:client_id]
    params_for_update.delete :client_id
  end

  respond_to do |format|
    if @task.update(params_for_update)
      format.html { redirect_to tasks_path, notice: 'Task was successfully updated.' }
      format.json { head :no_content }
    else
      format.html { render action: 'edit' }
      format.json { render json: @task.errors, status: :unprocessable_entity }
    end
  end
end

You can make use of this today in any Rails app running 4.1.0 or later. When 4.2 is released you will be able to drop the include statements and remove Global ID from your Gemfile.

You can also extend the use of this library to any other part of your application where records need to be passed around, such as when handling jobs to be executed asynchronously. The upcoming ActiveJob interface will be making use of global ids to help avoid the need to serialise model instances directly, which can lead to undesirable behaviour like database connections being serialised along with the record data.

For more information on Global ID, you can take a look at its repo on Github. Given its relative simplicity it has been highlighted by DHH as a good library to hack on for those looking to get into contributing to Rails development.

Getting URI::InvalidURIError?

If the name of your app is composed of multiple words, camel-cased, e.g. “MyApp” then Global ID will convert this name to “my_app”, which is an invalid hostname (due to the underscore), and will lead to the following exception being raised:

URI::InvalidURIError: the scheme gid does not accept registry part: my_app (or bad hostname?)

This has now been fixed in the latest version of Global ID, but it is also possible to get around it by specifying the app name explicitly, e.g. in config/application.rb:

module MyApp
  class Application < Rails::Application
    # GlobalID doesn't like underscores in the app name,
    # which would be my_app by default
    config.global_id.app = 'myapp'
  end
end

Even if this is not an issue for you, you may wish to set this if your app has an internal development name but you wish it to be known by something else in production.

But remember, good URIs don’t change, so this should be thought through carefully before deployment.