Step 7: Many to Many

View the Diff

Let’s add a Team relationship: a Team can have many Employees, an Employee can have many Teams. Let’s also say a Team belongs to a Department.

id department_id name
1 1 The A Team
2 1 The B Team
3 2 The C Team

To satisfy this many-to-many use case, we’ll need a join model, TeamMembership:

id team_id employee_id
1 1 1
2 2 1
3 3 2

The Rails Stuff 🚂

$ bin/rails g model Team name:string department:belongs_to
$ bin/rails g model TeamMembership employee:belongs_to team:belongs_to
$ bin/rails db:migrate

Graphiti supports has_many :through:

# app/models/employee.rb
has_many :team_memberships
has_many :teams, through: :team_memberships
# app/models/department.rb
has_many :teams
class Team < ApplicationRecord
  belongs_to :department
  has_many :team_memberships
  has_many :employees, through: :team_memberships
end
class TeamMembership < ApplicationRecord
  belongs_to :team
  belongs_to :employee
end

Finally, we’ll need a new seed file to handle these new associations:

[
  Employee,
  Position,
  Department,
  TeamMembership,
  Team
].each(&:delete_all)

departments = []
def create_department(name)
  dept = Department.create! name: name
  dept.teams.create!(name: 'Engineering Team B')
  dept.teams.create!(name: 'Engineering Team C')
  dept
end

departments << create_department('Engineering')
departments << create_department('Safety')
departments << create_department('QA')

100.times do
  employee = Employee.create! first_name: Faker::Name.first_name,
    last_name: Faker::Name.last_name,
    age: rand(20..80)

  (1..2).each do |i|
    employee.positions.create! title: Faker::Job.title,
      historical_index: i,
      active: i == 1,
      department: departments.sample
  end

  employee.teams << employee.positions[0].department.teams.sample
end

The Graphiti Stuff 🎨

$ bin/rails g graphiti:resource Team name:string

Let’s flesh out our TeamResource:

# app/resources/team_resource.rb
class TeamResource < ApplicationResource
  attribute :department_id, :integer, only: [:filterable]
  attribute :name, :string

  belongs_to :department
  many_to_many :employees
end

The trick here is the many_to_many relationship. Let’s add the reverse as well:

# app/resources/employee_resource.rb
many_to_many :teams

And for good measure:

# app/resources/department_resource.rb
has_many :teams

We can now get all the usual functionality: fetch Employees and their Teams in a single request (or vice versa).

Digging Deeper 🧐

The many_to_many relationship is the only one where Graphiti modifies a separate Resource “under the hood”. When we said many_to_many :employees, the EmployeeResource got a team_id filter, and many_to_many :teams created an employee_id filter on TeamResource.

This is because the logic is more complex than the default use case. We don’t have a simple WHERE clause; we need to join tables and look at the appropriate primary/foreign keys. If the name of your API association doesn’t match the name of your ActiveRecord association, try has_many :things, as: :my_activerecord_relationship to make the introspection work correctly - or, write your own filter.

Sometimes you’ll have multiple levels of has_many :through. In this case, a simple many_to_many isn’t enough - check out our Hopping Relationships Cookbook.

Think hard before reaching for many_to_many. Imagine one Team is the “primary” Team for an Employee. We’d add a primary boolean column to the team_memberships table…but that table isn’t exposed to the API! Consider if there’s a hidden domain concept there.