Explicit Many to Many Relationships in RW

I previously posted a question here about creating a list of Strings in a database. @Tobbe suggested a many-to-many relationship. This way one could add other properties to this relationship. I also talked with @dthyresson during the office hour last Thursday and he made some good points about testing the many-to-many relational tables and validating the logic inside seed.js before making any database call. I finally was able to add, remove, and modify entries on the table. I am going to post the database and the way I am running my mutations and queries so that I can get some feedback. Please take it from someone who has almost zero experience in databases.
Hopefully others can also benefit in the future.

First off, the database schema is defined as

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

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

model User {
  id                  Int                    @id @default(autoincrement())
  email               String                 @unique
  hashedPassword      String                 @default("")
  salt                String                 @default("")
  resetToken          String?
  resetTokenExpiresAt DateTime?
  favoriteMetrics     FavoriteMetricOnUser[]
}

model FavoriteMetric {
  id    Int                    @id @default(autoincrement())
  name  String                 @unique
  users FavoriteMetricOnUser[]
}

model FavoriteMetricOnUser {
  id               Int            @id @default(autoincrement())
  favoriteMetricId Int
  favoriteMetric   FavoriteMetric @relation(fields: [favoriteMetricId], references: [id])
  userId           Int
  user             User           @relation(fields: [userId], references: [id])

  @@unique([userId, favoriteMetricId])
}

FavoriteMetricOnUser is the table that connects the User model to the FavoriteMetric model.

Next step: Migration with yarn rw prisma migrate dev
Next step: Creating sdl and service scripts with

  • yarn rw g sdl User

  • yarn rw g sdl FavoriteMetric

  • yarn rw g sdl FavoriteMetricOnUser

The generated service and sdl scripts will have 80% of the necessary utilities to interact with the database. The challenging part was creating/updating the relationship between User and FavoriteMetric. Here are three of the main difficulties I had in regards to querying and mutating in the database.

  1. Query a user’s id and get back all user properties along with their favoritemetrics.

User sdl needs to be modified to

type favorite {
    id: Int!
    name: String!
  }
type User {
    id: Int!
    email: String!
    hashedPassword: String!
    salt: String!
    resetToken: String
    resetTokenExpiresAt: DateTime
    // Adding favorites as a new property so that I can fetch it from user service
    favorites: [favorite]
  }

In the users’ service, the request happens as

export const user = async ({ id }) => {
  const userInfo = await db.user.findUnique({
    where: { id },
    include: { favoriteMetrics: { include: { favoriteMetric: true } } },
  })
  // post processing the result
  const result = {
    id: userInfo.id,
    email: userInfo.email,
    favorites: userInfo.favoriteMetrics.map((item) => item.favoriteMetric),
  }

  return result
}
  1. Create a new relationship between User and FavoriteMetric.

The favoriteMetrics sdl gets updated as follows

  input CreateFavoriteMetricInput {
    name: String!
    userId: Int!
  }
type Mutation {
    createFavoriteMetric(input: CreateFavoriteMetricInput!): FavoriteMetric!
      @skipAuth}

Another update needs to happen inside favoritemetrics’ service section. This will create a new favoritemetric row if it doesn’t exit. If it already exists, it just connects that row to the user with id specified in the input.

export const createFavoriteMetric = ({ input }) => {
  const connetCreateMetric = db.favoriteMetric.upsert({
    where: { name: input.name },
    create: {
      name: input.name,
      users: {
        connectOrCreate: [
          {
            create: { userId: input.userId },
            where: { id: input.userId },
          },
        ],
      },
    },
    update: {
      users: {
        connectOrCreate: [
          {
            create: { userId: input.userId },
            where: { id: input.userId },
          },
        ],
      },
    },
  })
  1. Remove a relationship between User and FavoriteMetric.

This one took me a bit longer to figure out. The favoriteMetricOnUsers sdl script gets modified as:

input DeleteFavoriteMetricOnUser {
    name: String!
    userId: Int!
  }

  type Mutation {
deleteFavoriteMetricOnUser(
      input: DeleteFavoriteMetricOnUser!
    ): FavoriteMetricOnUser! @skipAuth
  }

On the service side, we first fetch the id of the favoritemetric (since we just know the name of the metric) and then delete the corresponding row in the relationship table.

export const deleteFavoriteMetricOnUser = async ({ input }) => {
  const favMetric = await db.favoriteMetric.findUnique({
    where: { name: input.name },
  })

  return db.favoriteMetricOnUser.delete({
    where: {
      userId_favoriteMetricId: {
        userId: input.userId,
        favoriteMetricId: favMetric.id,
      },
    },
  })
}

Lastly, if someone wants to check the database before writing the service script, as @dthyresson pointed, try to run the queries and mutations inside seed.js via yarn rw prisma db seed. It’s much faster and keeps you moving.

2 Likes

This example is super helpful, @rouzbeh! Can you clarify how you’re calling the connectCreateMetric variable inside your createFavoriteMetric function?

1 Like

@rouzbeh Thanks for posting this.
I have one question.
If I need to remove some favoriteMetrics from a User, do I have to use

db.favoriteMetric.delete({
    where: {id}
});

or

db.favoriteMetricOnUser.delete({
    where: {
    userId_favoriteMetricId: {
        userId: input.userId, 
        favoriteMetricId: favMetric.id,
    }
}

?

I will modify the model FavoriteMetricOnUser a little bit like this.

model FavoriteMetricOnUser {
  id               Int            @id @default(autoincrement())
  favoriteMetricId Int
  favoriteMetric   FavoriteMetric @relation(fields: [favoriteMetricId], references: [id], onDelete: Cascade)
  userId           Int
  user             User           @relation(fields: [userId], references: [id], onDelete: Cascade)

  @@unique([userId, favoriteMetricId])
}

If I delete favoriteMetric, will it affect relevant User and FavoriteMetrics?
Or do I have to use later method?

Please let me know.

Hi RedwoodJS team, please answer this question.
@thedavid, @Tobbe , @ajcwebdev

Thanks in advance.

1 Like

There is no connectCreateMetric variable, is there?
I call createFavoriteMetric like the following in the front end:

const UPDATE_FAVORITES = gql`
  mutation addmetric($input: CreateFavoriteMetricInput!) {
    createFavoriteMetric(input: $input) {
      id
      name
    }
  }
`
const [updateFavoriteDB] = useMutation(UPDATE_FAVORITES, {
    onCompleted: (_data) => {
      //pass
    },
  })
//
updateFavoriteDB({ variables: { input: {name:'test', userId:123} } })

The above is the shortened version of what I do in the front end side.

If you use

db.favoriteMetric.delete({
    where: {id}
});

you’d need to know the id of the metric you’re removing? What if you just know the name of the favoriteMetric and not the id?

With

export const deleteFavoriteMetricOnUser = async ({ input }) => {
  // First find the metric's id and then delete the relationship row
  // between favorite metric and the user.
  const favMetric = await db.favoriteMetric.findUnique({
    where: { name: input.name },
  })

  return db.favoriteMetricOnUser.delete({
    where: {
      userId_favoriteMetricId: {
        userId: input.userId,
        favoriteMetricId: favMetric.id,
      },
    },
  })
}

you can delete it by knowing the name of it. No?

I will modify the model FavoriteMetricOnUser a little bit like this.
model FavoriteMetricOnUser {
id Int @id @default(autoincrement())
favoriteMetricId Int
favoriteMetric FavoriteMetric @relation(fields: [favoriteMetricId], references: [id], onDelete: Cascade)
userId Int
user User @relation(fields: [userId], references: [id], onDelete: Cascade)

@@unique([userId, favoriteMetricId])
}

Can you please clarify what difference it would make? I do see you’re using cascade but we are not deleting anything from the User and FavoriteMetrics table. We are deleting a row from FavoriteMetricOnUser table.

If I delete favoriteMetric, will it affect relevant User and FavoriteMetrics?
Or do I have to use later method?

It will not affect User and FavoriteMetrics tables, it will just remove the relationship in the FavoriteMetricOnUser Table. For example the following row will be removed from FavoriteMetricOnUser table.

Did you try? The best way to learn is probably to experiment and see what happens, instead of just being served an answer from us :slight_smile:

1 Like