Photo by Steve Johnson / Unsplash

A many-to-Many relationship occurs when an entity A can have n relations to entity B, and inversely, entity B can m relations to entity A. n and m are numbers greater or equal to 1. Handling this relationship in a web application is not straightforward.

Fortunately, with an ORM like Prisma, we will see how to implement it in a Node.js application.

The use case

We have a video-on-demand (VOD) platform where users can browse movie catalogs and watch a movie. In the end, they can give a rate over 10. In addition to the rate, the user can add a review. We must also store the in which the user rated the movie.

A user can rate many movies, and a movie can be rated by many users. This is a Many-To-Many relationship. We must create an intermediary table to store each combination of a user and a movie. As the table's primary key, we will use the userId and MovieId because they are only a single occurrence of a user rating a specific movie.

Below is a simple database structure of the movie platform

Database schema of the application

Prerequisites

You must install the following tools on your computer to follow this tutorial.

Run the command below to start the Docker container from the MySQL image:


docker run -d -e MYSQL_ROOT_PASSWORD=secret -e MYSQL_DATABASE=moviedb --name mysqldb -p 3307:3306 mysql:8.0

Set up the project

In this tutorial, we built a starter project for Node.js with Typescript and will use it to initialize the project.

Run the command below:

git clone https://github.com/tericcabrel/node-ts-starter.git node-prisma-many-to-many

cd node-prisma-many-to-many

yarn install

yarn start

Set up Prisma

Inside the project directory, install the Prisma CLI.


yarn add -D prisma

Initialize Prisma in the project:


yarn prisma init

This command will create a Prisma schema and a .env file to set the environment variables to inject into the application.

Replace the content of the file prisma/schema.prisma with the code below to set the provider to MySQL:

// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "mysql"
  url      = env("DATABASE_URL")
}

Open the .env file and update the database URL to the connection string of the MySQL database instance running in Docker.

The connection string is: mysql://root:secret@localhost:3307/moviedb

Design the Prisma schema

Update the file prisma/schema.prisma with the code below:

// This is your Prisma schema file,
// learn more about it in the docs: https://pris.ly/d/prisma-schema

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "mysql"
  url      = env("DATABASE_URL")
}

model Movie {
  id            String   @id @default(cuid())
  name          String   @unique
  description   String?
  releaseDate   DateTime @updatedAt @map("release_date")
  ratings       MovieRating[]

  @@map("movies")
}

model User {
  id          String   @id @default(cuid())
  email       String   @db.VarChar(100) @unique
  name        String   @db.VarChar(100)
  createdAt   DateTime @default(now()) @map("created_at")
  updatedAt   DateTime @updatedAt @map("updated_at")

  ratings     MovieRating[]

  @@map("users")
}

model MovieRating {
  movie     Movie    @relation(fields: [movieId], references: [id])
  movieId   String   @map("movie_id")
  user      User     @relation(fields: [userId], references: [id])
  userId    String   @map("user_id")
  rate      Int
  review    String?
  addedAt   DateTime @default(now()) @map("added_at")

  @@id([movieId, userId])
  @@map("movies_rating")
}

The model MovieRating represents the intermediary table; inside, we define relationships with Movie and User model.

To set two columns as a primary key of a table in Prisma use @@id().

Create a migration and execute it against the database with the command below:


yarn prisma migrate dev --name init-db-table

Seed the database

We will load the database with some movies and users to focus on how to work with Many-to-Many relationships. Prisma offers an easy way to load the database with some predefined data.

Create a file called seed.ts in the folder prisma and add the code below:

import { Prisma, PrismaClient } from '@prisma/client';

const prisma = new PrismaClient({ errorFormat: 'pretty' });

const loadMovies = async () => {
  const moviesInput: Prisma.MovieCreateManyInput[] = [
    { name: 'Avatar', releaseDate: new Date(2009, 11, 16) },
    {
      description: "A soldier try to revenge his family's death",
      name: 'Gladiator',
      releaseDate: new Date(2000, 5, 20),
    },
    {
      description: 'Our here united to save the world',
      name: 'Avengers',
      releaseDate: new Date(2012, 3, 25),
    },
  ];

  await prisma.movie.createMany({
    data: moviesInput,
  });
};

const loadUsers = async () => {
  const usersInput: Prisma.UserCreateManyInput[] = [
    { name: 'Bob', email: 'bob@email.com' },
    { name: 'Alice', email: 'alice@email.com' },
  ];

  await prisma.user.createMany({
    data: usersInput,
  });
};

const main = async () => {
  await loadUsers();

  await loadMovies();
};

main().then();

We load three movies and two users. It will be enough for this post.

Run the command below to load the data:


yarn ts-node prisma/seed.ts

Insert data

Bob just watched the movie Gladiator and wanted to rate the movie. Create a file src/insert-data.ts and add the code below:

import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient({ errorFormat: 'pretty' });

const main = async () => {
  const movieGladiator = await prisma.movie.findUniqueOrThrow({
    where: {
      name: 'Gladiator',
    },
  });
  const userBob = await prisma.user.findUniqueOrThrow({
    where: {
      email: 'bob@email.com',
    },
  });

  const gladiatorRatingByBob = await prisma.movieRating.create({
    data: {
      movieId: movieGladiator.id,
      userId: userBob.id,
      rate: 9,
      review: 'I was expecting a better ending.',
      addedAt: new Date(2022, 6, 10),
    },
  });

  console.log(gladiatorRatingByBob);
};

main().then();

Run the command yarn src/insert-data.ts

Movie rating created successfully.

If you try to rerun this code, Prisma will throw an error because a key with the combination of the movieId and the userId already exists.

Retrieve data

For a Many-to-Many relationship, retrieving data is easy with Prisma. The code below shows some requests you could want to make:

import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient({ errorFormat: 'pretty' });

const main = async () => {
  // Retrieve all ratings
  await prisma.movieRating.findMany();

  // Find rating with a rate greater or equal to 5
  await prisma.movieRating.findMany({
    where: {
      rate: {
        gte: 5,
      },
    },
  });

  // Calculate overall average movie rating
  await prisma.movieRating.aggregate({
    _avg: {
      rate: true,
    },
  });

  // Get the average rate of the movie Gladiator
  const movieGladiator = await prisma.movie.findUniqueOrThrow({ where: { name: 'Gladiator' } });

  await prisma.movieRating.aggregate({
    _avg: {
      rate: true,
    },
    where: {
      movieId: movieGladiator.id,
    },
  });
};

main().then();

You can find this code in the file src/retrieve-data.ts.

Delete data

The code below deletes some ratings from the database:

import { PrismaClient } from '@prisma/client';

const prisma = new PrismaClient({ errorFormat: 'pretty' });

const main = async () => {
  const movieAvatar = await prisma.movie.findUniqueOrThrow({ where: { name: 'Avatar' } });
  const userBob = await prisma.user.findUniqueOrThrow({ where: { email: 'bob@email.com' } });

  // Delete 'Avatar' rating made by Bob
  await prisma.movieRating.delete({
    where: {
      movieId_userId: {
        movieId: movieAvatar.id,
        userId: userBob.id,
      },
    },
  });

  // Delete all the ratings for the movie 'Avatar'
  await prisma.movieRating.deleteMany({
    where: {
      movieId: movieAvatar.id,
    },
  });
};

main().then();

You can find this code in the file src/retrieve-data.ts.

Wrap up

Managing a Many-to-Many relationship with Prisma is simple, as you can see. Prisma also gives the ability to create an implicit relationship between the entities. To learn more about it, check out this link.

You can find the code source on the GitHub repository.

Follow me on Twitter or subscribe to my newsletter to avoid missing the upcoming posts and the tips and tricks I occasionally share.