【Prisma】 データをupdateしたとき、関連するテーブルも同時にupdateまたはupsertする
あるデータをupdateで更新するのと一緒に、それと関連するデータ(中間テーブルにあるデータ)も更新したい場合、どのようにPrismaを書けば良いのでしょうか?
今回は、記事に指定したタグを付け外しする、というケースを想定し、中間テーブルのデータも一緒に更新する方法を考えます。

https://github.com/kntks/blog-code/tree/main/2023/03/prisma-upsert-with-delete
| バージョン | |
|---|---|
| npm | 8.19.3 | 
| Prisma | 4.10.1 | 
テーブルの定義
Section titled “テーブルの定義”サンプルとして、以下のデータモデルとします。
中間テーブルはExplicit many-to-many relationsに書いているように明示的に中間テーブルを定義します。
model Tags {  id       Int        @id @default(autoincrement())  name     String     @unique  PostTags PostTags[]
  @@map("tags")}
model PostTags {  tag    Tags  @relation(fields: [tagId], references: [id], onDelete: Cascade)  tagId  Int  post   Posts @relation(fields: [postId], references: [id], onDelete: Cascade)  postId Int
  @@id([postId, tagId])}
model Posts {  id       Int        @id @default(autoincrement())  url      String  title    String  PostTags PostTags[]
  @@map("posts")}PostTags中間テーブルにデータを入れます。
post, tagのデータを作成したコード
async function createPosts() {  await prisma.posts.create({    data: {      url: "https://xxxxx",      title: "example",      PostTags: {        connectOrCreate: [          {            where: {              postId_tagId: {                postId: 1,                tagId: 1,              },            },            create: {              tag: {                create: {                  name: "tag1",                },              },            },          },          {            where: {              postId_tagId: {                postId: 1,                tagId: 2,              },            },            create: {              tag: {                create: {                  name: "tag2",                },              },            },          },        ],      },    },  });}実行されるSQL
BEGININSERT INTO `example`.`posts` (`id`,`url`,`title`) VALUES (?,?,?)SELECT `example`.`PostTags`.`postId`, `example`.`PostTags`.`tagId` FROM `example`.`PostTags` WHERE ((`example`.`PostTags`.`postId` = ? AND `example`.`PostTags`.`tagId` = ?) AND 1=1)INSERT INTO `example`.`tags` (`id`,`name`) VALUES (?,?)INSERT INTO `example`.`PostTags` (`tagId`,`postId`) VALUES (?,?)SELECT `example`.`PostTags`.`postId`, `example`.`PostTags`.`tagId` FROM `example`.`PostTags` WHERE ((`example`.`PostTags`.`postId` = ? AND `example`.`PostTags`.`tagId` = ?) AND 1=1)INSERT INTO `example`.`tags` (`id`,`name`) VALUES (?,?)INSERT INTO `example`.`PostTags` (`tagId`,`postId`) VALUES (?,?)SELECT `example`.`posts`.`id`, `example`.`posts`.`url`, `example`.`posts`.`title` FROM `example`.`posts` WHERE `example`.`posts`.`id` = ? LIMIT ? OFFSET ?COMMITmysql> select postId, tagId from PostTags;+--------+-------+| postId | tagId |+--------+-------+|      1 |     1 ||      1 |     2 |+--------+-------+以下のようにtagIdを 2 から 4 に変更したい場合、どのようにprisma clientを使えば良いでしょうか?
mysql> select postId, tagId from PostTags;+--------+-------+| postId | tagId |+--------+-------+|      1 |     1 ||      1 |     4 |+--------+-------+さらに今回の場合は、tagId:4となるデータは作成されていません。
mysql> select * from tags;+----+------+| id | name |+----+------+|  1 | tag1 ||  2 | tag2 |+----+------+そのためpostの情報を更新するタイミングで、tagが存在しない場合はtagの作成 & 中間テーブルにもデータを作成する必要があります。
updateメソッドの中でdeleteManyとupsertを使えば可能です。
async function upsertPostTags(postId: number, tagIds: number[]) {  await prisma.posts.update({    where: { id: postId },    data: {      PostTags: {        upsert: tagIds.map((tagId) => ({          where: { postId_tagId: { postId, tagId } },          create: { tag: { create: { id: tagId, name: `tag${tagId}` } } },          update: { tag: { connect: { id: tagId } } },        })),        deleteMany: {          NOT: tagIds.map((tagId) => ({ tagId })),        },      },    },  });}async function main() {  // postId:1に対してtagId:1,4 を関連づける  await upsertPostTags(1, [1,4])}mysql> select * from tags;+----+------+| id | name |+----+------+|  1 | tag1 ||  2 | tag2 ||  4 | tag4 |+----+------+
mysql> select postId, tagId  from PostTags;+--------+-------+| postId | tagId |+--------+-------+|      1 |     1 ||      1 |     4 |+--------+-------+ちなみにコードを実行すると、以下のようなSQLが実行されます。
BEGINSELECT `example`.`posts`.`id` FROM `example`.`posts` WHERE (`example`.`posts`.`id` = ? AND 1=1)SELECT `example`.`PostTags`.`postId`, `example`.`PostTags`.`tagId` FROM `example`.`PostTags` WHERE (((`example`.`PostTags`.`postId` = ? AND `example`.`PostTags`.`tagId` = ?) AND 1=1) AND `example`.`PostTags`.`postId` IN (?))SELECT `example`.`tags`.`id` FROM `example`.`tags` WHERE (`example`.`tags`.`id` = ? AND 1=1)UPDATE `example`.`PostTags` SET `tagId` = ? WHERE ((`example`.`PostTags`.`postId`,`example`.`PostTags`.`tagId`) IN ((?,?)) AND ((`example`.`PostTags`.`postId` = ? AND `example`.`PostTags`.`tagId` = ?) AND 1=1))SELECT `example`.`PostTags`.`postId`, `example`.`PostTags`.`tagId` FROM `example`.`PostTags` WHERE (((`example`.`PostTags`.`postId` = ? AND `example`.`PostTags`.`tagId` = ?) AND 1=1) AND `example`.`PostTags`.`postId` IN (?))INSERT INTO `example`.`tags` (`id`,`name`) VALUES (?,?)INSERT INTO `example`.`PostTags` (`tagId`,`postId`) VALUES (?,?)SELECT `example`.`PostTags`.`postId`, `example`.`PostTags`.`tagId` FROM `example`.`PostTags` WHERE (((NOT `example`.`PostTags`.`tagId` = ?) AND (NOT `example`.`PostTags`.`tagId` = ?)) AND `example`.`PostTags`.`postId` IN (?))DELETE FROM `example`.`PostTags` WHERE ((`example`.`PostTags`.`postId`,`example`.`PostTags`.`tagId`) IN ((?,?)) AND 1=1)SELECT `example`.`posts`.`id`, `example`.`posts`.`url`, `example`.`posts`.`title` FROM `example`.`posts` WHERE `example`.`posts`.`id` = ? LIMIT ? OFFSET ?COMMITPrisma Clientの型定義を読んでみると、deleteManyに書いたNOTの箇所にはnotInもあるので、これを使っても実現できます。
export type Enumerable<T> = T | Array<T>;
export type PostTagsUpdateManyWithoutPostNestedInput = {  create?: XOR<Enumerable<PostTagsCreateWithoutPostInput>, Enumerable<PostTagsUncheckedCreateWithoutPostInput>>  connectOrCreate?: Enumerable<PostTagsCreateOrConnectWithoutPostInput>  upsert?: Enumerable<PostTagsUpsertWithWhereUniqueWithoutPostInput>  createMany?: PostTagsCreateManyPostInputEnvelope  set?: Enumerable<PostTagsWhereUniqueInput>  disconnect?: Enumerable<PostTagsWhereUniqueInput>  delete?: Enumerable<PostTagsWhereUniqueInput>  connect?: Enumerable<PostTagsWhereUniqueInput>  update?: Enumerable<PostTagsUpdateWithWhereUniqueWithoutPostInput>  updateMany?: Enumerable<PostTagsUpdateManyWithWhereWithoutPostInput>  deleteMany?: Enumerable<PostTagsScalarWhereInput>}
export type PostTagsScalarWhereInput = {  AND?: Enumerable<PostTagsScalarWhereInput>  OR?: Enumerable<PostTagsScalarWhereInput>  NOT?: Enumerable<PostTagsScalarWhereInput>  tagId?: IntFilter | number  postId?: IntFilter | number}
export type IntFilter = {  equals?: number  in?: Enumerable<number>  notIn?: Enumerable<number>  lt?: number  lte?: number  gt?: number  gte?: number  not?: NestedIntFilter | number}async function upsertPostTags(postId: number, tagIds: number[]) {  await prisma.posts.update({    where: { id: postId },    data: {      PostTags: {        upsert: tagIds.map((tagId) => ({          where: { postId_tagId: { postId, tagId } },          create: { tag: { create: { id: tagId, name: `tag${tagId}` } } },          update: { tag: { connect: { id: tagId } } },        })),        deleteMany: {          tagId: {            notIn: tagIds          }        },      },    },  });}async function main() {  // postId:1に対してtagId:1,4 を関連づける  await upsertPostTags(1, [1,4])}ちなみにコードを実行すると、以下のようなSQLが実行されます。
BEGINSELECT `example`.`posts`.`id` FROM `example`.`posts` WHERE (`example`.`posts`.`id` = ? AND 1=1)SELECT `example`.`PostTags`.`postId`, `example`.`PostTags`.`tagId` FROM `example`.`PostTags` WHERE (((`example`.`PostTags`.`postId` = ? AND `example`.`PostTags`.`tagId` = ?) AND 1=1) AND `example`.`PostTags`.`postId` IN (?))SELECT `example`.`tags`.`id` FROM `example`.`tags` WHERE (`example`.`tags`.`id` = ? AND 1=1)UPDATE `example`.`PostTags` SET `tagId` = ? WHERE ((`example`.`PostTags`.`postId`,`example`.`PostTags`.`tagId`) IN ((?,?)) AND ((`example`.`PostTags`.`postId` = ? AND `example`.`PostTags`.`tagId` = ?) AND 1=1))SELECT `example`.`PostTags`.`postId`, `example`.`PostTags`.`tagId` FROM `example`.`PostTags` WHERE (((`example`.`PostTags`.`postId` = ? AND `example`.`PostTags`.`tagId` = ?) AND 1=1) AND `example`.`PostTags`.`postId` IN (?))INSERT INTO `example`.`tags` (`id`,`name`) VALUES (?,?)INSERT INTO `example`.`PostTags` (`tagId`,`postId`) VALUES (?,?)SELECT `example`.`PostTags`.`postId`, `example`.`PostTags`.`tagId` FROM `example`.`PostTags` WHERE (`example`.`PostTags`.`tagId` NOT IN (?,?) AND `example`.`PostTags`.`postId` IN (?))DELETE FROM `example`.`PostTags` WHERE ((`example`.`PostTags`.`postId`,`example`.`PostTags`.`tagId`) IN ((?,?)) AND 1=1)SELECT `example`.`posts`.`id`, `example`.`posts`.`url`, `example`.`posts`.`title` FROM `example`.`posts` WHERE `example`.`posts`.`id` = ? LIMIT ? OFFSET ?COMMITSQLの差分
Section titled “SQLの差分”先ほど紹介したコードは以下の部分しか変わりません。
deleteMany: {  NOT: tagIds.map((tagId) => ({ tagId })),},deleteMany: {  tagId: {    notIn: tagIds  }},SQLを比較すると
NOTを使ったコードは、WHERE句にNOTとANDでクエリを書いているのに対して
SELECT  `example`.`PostTags`.`postId`,  `example`.`PostTags`.`tagId`FROM  `example`.`PostTags`WHERE  (    (      (        NOT `example`.`PostTags`.`tagId` = ?      )      AND (        NOT `example`.`PostTags`.`tagId` = ?      )    )    AND `example`.`PostTags`.`postId` IN (?)  )notInを使ったコードは、WHERE句にNOT INになっていることがわかりますね。
SELECT  `example`.`PostTags`.`postId`,  `example`.`PostTags`.`tagId`FROM  `example`.`PostTags`WHERE  (    `example`.`PostTags`.`tagId` NOT IN (?, ?)    AND `example`.`PostTags`.`postId` IN (?)  )それ以外のINSERTやDELETE文には差分がないので、deleteManyにNOT、notInどちらを使っても同じ結果が得られることがわかりました。
今回使ったコードはblog-code/prisma-upsert-with-delete - GitHubにおいています。