Tutorial
Step 9: Polymorphic Resources
In the last step, we covered polymorphic relationships: a single relationship can point to many different Resources. Polymorphic Resources are the same concept, without an association: a single Resource can resolve to many different sub-Resources. It’s a very similar to Single-Table Inheritance in ActiveRecord.
To illustrate this, we’ll add a tasks table and corresponding Task
superclass. Each record in this table will resolve to one of Bug,
Epic, or Feature.
| id | milestone_id | type | title |
|---|---|---|---|
| 1 | null | Bug | Incorrect Value! |
| 2 | null | Feature | Build great stuff! |
| 3 | 1 | Epic | Build TONS of great stuff! |
Why not just stick with a single Task model? Because each of these
types has specific behavior: only Features have a points attribute,
and only Epics have a milestones relationship.
The Rails Stuff 🚂
Let’s create our Task model:
$ bin/rails g model Task employee:belongs_to team:belongs_to type:string
title:string
$ bin/rails db:migrateAnd create models to reflect our STI logic:
# app/models/task.rb
class Task < ApplicationRecord
TYPES = %w(Bug Feature Epic)
belongs_to :team, optional: true
belongs_to :employee, optional: true
end
# app/models/bug.rb
class Bug < Task
end
# app/models/feature.rb
class Feature < Task
end
# Only Epics have Milestones
# app/models/epic.rb
class Epic < Task
has_many :milestones
end
# app/models/milestone.rb
class Milestone < ApplicationRecord
belongs_to :epic
endAdd the association:
# app/models/team.rb
has_many :tasks
has_many :bugs
has_many :features
has_many :epics
# app/models/employee.rb
has_many :tasks
has_many :bugs
has_many :features
has_many :epicsFinally view the diff to edit your seeds.rb file.
The Graphiti Stuff 🎨
Start by creating our Resource as normal:
$ bin/rails g graphiti:resource Task title:stringNow edit to support polymorphism and associations:
class TaskResource < ApplicationResource
self.polymorphic = %w(FeatureResource BugResource EpicResource)
attribute :employee_id, :integer, only: [:filterable]
attribute :team_id, :integer, only: [:filterable]
attribute :title, :string
belongs_to :employee
belongs_to :team
endThe point of this was to show how responses could be specific to type,
so let’s customize Features:
class FeatureResource < TaskResource
attribute :points, :integer do
rand(20)
end
endOnly Epics have milestones, but let’s support those as well:
$ bin/rails g graphiti:resource Milestone name:stringclass MilestoneResource < ApplicationResource
attribute :epic_id, :integer, only: [:filterable]
attribute :name, :string
# Customize the link to the Tasks endpoint, as we
# didn't create an Epics endpoint
belongs_to :epic do
link do |milestone|
helpers = Rails.application.routes.url_helpers
helpers.task_url(milestone.epic_id)
end
end
endDigging Deeper 🧐
We can now resolve Tasks, either as a relationship or through the
/tasks endpoint directly. When Task is type 'Feature' it will have
an extra attribute of points. When it’s an Epic, it will have an
additional relationship Milestone.
Graphiti is smart enough to fetch the appropriate relationships. A hit
to /tasks?include=milestones will only query for milestones when the
resulting Task records are Epics.