Skip to main content

Step 7

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.

iddepartment_idname
11The A Team
21The B Team
32The C Team

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

idteam_idemployee_id
111
221
332

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 the Hopping Relationships recipe.

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.

NEXT - Step 8: Polymorphic Relationships ยป