Skip to content

【Prisma】 データをupdateしたとき、関連するテーブルも同時にupdateまたはupsertする

はじめに

あるデータをupdateで更新するのと一緒に、それと関連するデータ(中間テーブルにあるデータ)も更新したい場合、どのようにPrismaを書けば良いのでしょうか?

今回は、記事に指定したタグを付け外しする、というケースを想定し、中間テーブルのデータも一緒に更新する方法を考えます。

blog-post-tags

成果物

https://github.com/kntks/blog-code/tree/main/2023/03/prisma-upsert-with-delete

バージョン

バージョン
npm8.19.3
Prisma4.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

BEGIN
INSERT 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
Terminal window
mysql> select postId, tagId from PostTags;
+--------+-------+
| postId | tagId |
+--------+-------+
| 1 | 1 |
| 1 | 2 |
+--------+-------+

以下のようにtagIdを 2 から 4 に変更したい場合、どのようにprisma clientを使えば良いでしょうか?

Terminal window
mysql> select postId, tagId from PostTags;
+--------+-------+
| postId | tagId |
+--------+-------+
| 1 | 1 |
| 1 | 4 |
+--------+-------+

さらに今回の場合は、tagId:4となるデータは作成されていません。

Terminal window
mysql> select * from tags;
+----+------+
| id | name |
+----+------+
| 1 | tag1 |
| 2 | tag2 |
+----+------+

そのためpostの情報を更新するタイミングで、tagが存在しない場合はtagの作成 & 中間テーブルにもデータを作成する必要があります。

結論

updateメソッドの中でdeleteManyupsertを使えば可能です。

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])
}
Terminal window
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が実行されます。

BEGIN
SELECT `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が実行されます。

BEGIN
SELECT `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にNOTnotInどちらを使っても同じ結果が得られることがわかりました。

今回使ったコードはblog-code/prisma-upsert-with-delete - GitHubにおいています。

参考