【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 |
テーブルの定義
サンプルとして、以下のデータモデルとします。
中間テーブルは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 ?COMMIT
mysql> 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 ?COMMIT
Prisma 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 ?COMMIT
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においています。