Prisma 'set' with explicit many-to-many relation?

Viewed 467

As in this document of Prisma, set can be used to override the value of a relation.

const user = await prisma.user.update({
  where: { email: 'alice@prisma.io' },
  data: {
    posts: {
      set: [{ id: 32 }, { id: 42 }],
    },
  },
})

But when I tried it with explicit many-to-many relation, it does not work.

model Product {
  id        String   @id @default(cuid())
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  name        String
  description String?
  price       Decimal
  sku         String  @unique
  published   Boolean @default(false)

  tags ProductTag[]
}

model Tag {
  id   Int    @id @default(autoincrement())
  name String

  products ProductTag[]
}

model ProductTag {
  productId String
  tagId     Int

  createdAt DateTime @default(now())

  product Product @relation(fields: [productId], references: [id])
  tag     Tag     @relation(fields: [tagId], references: [id])

  @@id([productId, tagId])
}

My code to update Product

update(id: string, updateProductDto: UpdateProductDto) {
    const tags = updateProductDto.tags.map((tag) => ({
      productId_tagId: {
        productId: id,
        tagId: tag.id,
      },
    }));

    console.log(JSON.stringify(tags));

    return this.prisma.product.update({
      where: { id: id },
      data: {
        ...updateProductDto,
        tags: {
          set: [...tags],
        },
      },
    });
  }

I want to update the product's tag with the product's information.

How could I implement this correctly?

3 Answers

I try a reply, but i would ask you to add the error messages, and your ProductTagWhereUniqueInput from node_modules/.prisma/client/index.d.ts

update(id: string, updateProductDto: UpdateProductDto) {
    const tags = updateProductDto.tags.map((tag) => ({
        // no need to encapsulate  
        productId: id,
        tagId: tag.id,
    }));
     // prevent the tags property to be update in product table, because you want to use set. 
    delete updateProductDto.tags;

    console.log(JSON.stringify(tags));

    return this.prisma.product.update({
      where: { id: id },
      data: {
        ...updateProductDto,
        tags: {
          set: tags, // no need to rebuild the array
        },
      },
    });
  }

You could delete existing productTag records and then create new ones, in two separate queries. Then you could run the two queries as a transaction.

This is what it would look like

    // do other updates to Tags with values from updateProductDto. 

    const deleteOldtags = prisma.productTag.deleteMany({
        where: { productId: "__PRODUCT_ID__" },
    });

    const addNewTags = prisma.productTag.createMany({
        data: [
            {
                productId: "__PRODUCT_ID__",
                tagId: TAG_ID_VALUE

            },
            // ...array of productID and tagId objects. 
        ]
    })

    let updateTags = await prisma.$transaction([deleteOldtags, addNewTags])

This is a workaround, that is slightly different from set as it will create new records every time.

Preamble

I came across this question when I was struggling with this myself. I assume that you've probably figured it out by now, but I wanted to post the solution I came up with in case anyone else comes across this in the future like me.

I found your GitHub discussion, which then led me to this other GitHub issue that finally helped me figure out what I think is a pretty good solution.

Problem Explanation

The problem here is that with an explicit many-to-many relationship, the relation is represented by creating/deleting records within a relation table, described in the Prisma docs here. set does not create or delete records, it simply disconnects existing relationships and connects the provided models.

Solution

Because set doesn't create or delete records, we need to do both of these steps. Luckily, you can do both of these steps in a single query. I've adapted your code to show what I do in my code:

// BEFORE -- NOT WORKING
update(id: string, updateProductDto: UpdateProductDto) {
    const tags = updateProductDto.tags.map((tag) => ({
      productId_tagId: {
        productId: id,
        tagId: tag.id,
      },
    }));

    return this.prisma.product.update({
      where: { id: id },
      data: {
        ...updateProductDto,
        tags: {
          set: [...tags],
        },
      },
    });
  }

// AFTER -- WORKING
update(id: string, updateProductDto: UpdateProductDto) {
    return this.prisma.product.update({
      where: { id },
      data: {
        ...updateProductDto,
        tags: {
          deleteMany: {},
          create: updateProductDto.tags.map((t) => ({ tag: { connect: { t.id } } }))
        },
      },
    });
  }

Notes

A couple things to take into account with this approach:

  • This will completely reset the relationships between the two models, even if your new set of tags contains tags that were already present. So if you have extra properties in your relation table records that you want to keep intact (such as the createdAt property), you'll need to account for copying those properties over.
  • I have only tested this using Prisma 4.1.0 and higher, I'm not sure what version you were using then or now.

I hope this helps anyone else that makes it to this post!

Related