Many to Many Relationships in GraphQL & Prisma

In this post we’ll cover “Many to Many” relationships in GraphQL and Prisma using the “explicit” design. More details on relations can be found here. Our project in the tutorial series we’ve been working on doesn’t have any need for many-to-many yet, so let’s use a different project that I’ve already setup for us.

Starting code is at startingPoint branch.

Typically “Many to Many” relationships in SQL have a “Join” table that includes an ID for each side of the relationship.

Class

StudentsOnClasses

Student

idname
1Math101
2Psychology202
class_idstudent_id
12
23
11
21
idname
1Susan
2Bob
3Sandy

This allows us to see which students are in each class by passing through the join table. We can see that class_id of 1 (Math101) has two students in it with the student_id’s of 2 and 1 (Bob and Susan)

In order to model this via Prisma, we’ll need to update our schema.prisma to look as follows:

model Class {
    id        Int                 @id @default(autoincrement())
    name      String
    createdAt DateTime            @default(now())
    students  StudentsOnClasses[]
}

model Student {
    id        Int                 @id @default(autoincrement())
    createdAt DateTime            @default(now())
    firstName String
    email     String
    classes   StudentsOnClasses[]
}

model StudentsOnClasses {
    class     Class    @relation(fields: [classId], references: [id])
    classId   Int // relation scalar field (used in the `@relation` attribute above)
    student   Student  @relation(fields: [studentId], references: [id])
    studentId Int // relation scalar field (used in the `@relation` attribute above)
    createdAt DateTime @default(now())
    @@id([classId, studentId])
}

The most important bit is the model StudentsOnClasses. In here we use the @relation twice. For “class” we’re saying that the “classId” is a reference to the “id” in the Class model. We do the same thing for Student.

I’m honestly not sure what the @@id([classId, studentId]) does. Based on the documentation it appears that it forces a unique combination between those two fields, but I’m too lazy to verify it.

I’m also too lazy to figure out why exactly the StudentsOnClasses[] portions are needed on the Class and Student models if they don’t actually alter the database. All we need for a successful join is the join table and ids. But I couldn’t find much info in the docs so I left it alone.

Now we need to run the migrations in the terminal:

$ npx prisma migrate save --experimental
$ npx prisma migrate up --experimental
$ npx prisma generate

Add the Many to Many “type”:

In order to actually add a “student to class” record, we need to add a row to our StudentsOnClasses table. So let’s update our schema.graphql file as follows:

type Mutation {
  // ...
  createStudentsOnClasses(studentId: ID!, classId: ID!): StudentsOnClasses
}

//...

type StudentsOnClasses {
  classId: ID!
  studentId: ID!
}

Nothing new in the above code. We’re just allowing GraphQL to add records to our StudentsOnClasses table.

Now we need our mutation resolver for the createStudentsOnClasses. Add the following function to your Mutation resolvers in index.js

const resolvers = {
  Mutation: {
    createStudentsOnClasses: (parent, args, context, info) => {
      return context.prisma.studentsOnClasses.create({
        data: {
          class: { connect: { id: parseInt(args.classId) } },
          student: { connect: { id: parseInt(args.studentId) } },
        },
      });
    },
  }
}

This is nearly identical to what you would do with a one-to-many relationship, only you’re making two connections. At this point the connection has officially been made. You can now run queries like the following to enroll students into classes: (the below query will need to be swapped out with your own data. Create a new class and student first and enter the query with the new class/student.

mutation {
  createStudentsOnClasses(studentId: 1, classId: 2) {
    studentId
    classId
  }
}

But we still need to setup in a way that we can see each class that a student is in, and each student in a class.

First side of Many to Many – Accessing classes from the Student

At this point in our code we have the relationships working, but GraphQL still doesn’t know how to link the students to the classes, or the classes to the students. A query like this will break:

query {
  student(studentId: 3) {
    id
    email
    firstName
    classes {
      id
      name
    }
  }
}

We need to update our schema.graphql to define that a Student has a list of “classes” they’re enrolled in.

type Student {
  id: ID!
  firstName: String!
  email: String!
  classes: [Class]!
}

With the above code, GraphQL knows that each student has a list of Classes, but it doesn’t know how to FIND the classes… We need a field level resolver to define how that list of Class objects gets returned.

In your index.js file add the following code:

async function classes(parent, args, context, info) {
  const studentsOnClasses = await context.prisma.studentsOnClasses.findMany({
    where: {
      studentId: parent.id,
    },
  });
  const classIds = studentsOnClasses.map((elem) => {
    return elem.classId;
  });

  return await context.prisma.class.findMany({
    where: {
      id: {
        in: classIds,
      },
    },
  });
}

const resolvers = {
  Mutation {
    //...
  },
  Student: {
    classes,
  },
}

What we’re doing here is creating an asynchronous resolver function “classes”. This function first gets all the records in StudentsOnClasses that matches the Student of the current query. (The student is the parent query) Once we get all the classes that match, we put all the class ids in a const classIds array. Finally we return all the classes that match the id of the student, and return that as the list of classes.

Save everything and restart your server. Run the query again and you should get results back:

Now we just need to do the same exact thing on the class side, which will allow us to see all the Students in a class.

In schema.graphql adjust your type Class

type Class {
  id: ID!
  name: String!
  students: [Student!]!
}

Now your index.js:

async function students(parent, args, context, info) {
  const studentsOnClasses = await context.prisma.studentsOnClasses.findMany({
    where: {
      classId: parent.id,
    },
  });
  const studentIds = studentsOnClasses.map((elem) => {
    return elem.studentId;
  });
  return await context.prisma.student.findMany({
    where: {
      id: {
        in: studentIds,
      },
    },
  });
}

const resolvers = {
  Mutation: {
    //...
  },
  Class: {
    students,
  }
}

It’s pretty much the same thing as before. Let’s save and restart the server, and the following query should now work:

query {
  classes {
    id
    name
    students {
      firstName
      id
    }
  }
}

That’s all for Many to Many relationships.

Final code is in done-many-to-many branch

Want More Tutorials?

Get our best crash courses and tutorials

(Source Code Provided)

Leave a Comment

Your email address will not be published. Required fields are marked *

want more details?

Fill in your details and we'll be in touch

%d bloggers like this: