🐈

【Prisma】followerとfollowee、逆だったかもしれねェ...

2023/04/30に公開

記事の概要

SNSアプリのDBスキーマをPrismaで定義するときに、フォロワーと被フォロワーの関係を表現しようとして直感でスキーマを書いたら逆になってしまったので、教訓として対策を含めて記事に残すことにしました。

前提知識

https://www.prisma.io/docs/concepts/components/prisma-schema/relations/one-to-many-relations

Prismaで1対多の関係を定義したいときは、以下のように書きます。

schema.prisma
model User {
  id    Int    @id @default(autoincrement())
  posts Post[]
}

model Post {
  id       Int  @id @default(autoincrement())
  author   User @relation(fields: [authorId], references: [id])
  authorId Int
}

これは以下のSQLに対応しています。

SQL
CREATE TABLE "User" (
    id SERIAL PRIMARY KEY
);
CREATE TABLE "Post" (
    id SERIAL PRIMARY KEY,
    "authorId" integer NOT NULL,
    FOREIGN KEY ("authorId") REFERENCES "User"(id)
);

ポイントは、postsはSQLには現れないという点です。Prismaの記法として、"1"側のモデルに"多"のモデルの配列を定義するルールになっています。

最初にやったこと

フォローと非フォローの関係を表現するFollowingというモデルを作成しました。 Userへの参照を定義した後、Prismaのルールに従ってUserモデルの中にFollowing[]型の変数を定義します。同じモデルへの参照が2つあるので、区別するために名前をつけておきます。(@relation("follower")@relation("followee"))
ここで思考停止した私はFollowing.followerに対応しているからUser.followers, Following.followeeに対応しているからUser.followeesとしました。

schema.prisma
model Following {
  followerId String
  followeeId String
  follower   User     @relation("follower", fields: [followerId], references: [id], onDelete: Cascade)
  followee   User     @relation("followee", fields: [followeeId], references: [id], onDelete: Cascade)
  createdAt  DateTime @default(now())

  @@id([followerId, followeeId])
}

model User {
  id            String       @id @default(cuid())
  name          String?
  followers     Following[]  @relation("follower")
  followees     Following[]  @relation("followee")
}

逆だったかもしれねェ...

上記のモデルで、「ユーザーID: AがユーザーID: Bをフォローしている」という関係を表すことを考えます。以下のようなデータを用意します。

User

id
A
B

Following

followerId followeeId
A B

この時ユーザーID: Bのフォロワー配列を取得します。Prisma clientでは以下のように記述します。includeはSQLでいうところのjoinのようなものです。

prisma.user.findUnique({
    where: {
        id: "B"
    },
    include: {
        followers: {
            select: {
                followerId: true
            }
        }
    }
});

Aを要素とする配列['A']が返ってくることを期待したのですが、結果は[]でした。

なぜ逆だったのか

上記の処理では「followerIdが'B'であるようなFollowingのレコード配列」が返ってきます。それはつまりBのfolloweeの配列ということになります。

対策

つまり、正しくはFollowing.followerUser.followees, Following.followeeUser.followersを対応づければよかったのです。

しかしそれは直感的でなく、そのソースを初めて見た人は「コーディングミスでは?」と思ってしまう可能性があります。そこで、relationの名前を工夫することにしました。

schema.prisma
model Following {
  followerId String
  followeeId String
-  follower   User     @relation("follower", fields: [followerId], references: [id], onDelete: Cascade)
-  followee   User     @relation("followee", fields: [followeeId], references: [id], onDelete: Cascade)
+  follower   User     @relation("asFollower", fields: [followerId], references: [id], onDelete: Cascade)
+  followee   User     @relation("asFollowee", fields: [followeeId], references: [id], onDelete: Cascade)
  createdAt  DateTime @default(now())

  @@id([followerId, followeeId])
}

model User {
  id            String       @id @default(cuid())
  name          String?
-  followers     Following[]  @relation("follower")
-  followees     Following[]  @relation("followee")
+  followers     Following[]  @relation("asFollowee")
+  followees     Following[]  @relation("asFollower")
}

これなら、「自分を被フォロワーとしてのフォロワーリスト」、「自分をフォロワーとしての被フォロワーリスト」であることが読み取れます。

宣伝

先日、本を書きました!200円で販売中です(無料部分あり)。

https://zenn.dev/ninjin_umigame/books/2619c009c452b6

T3 Stack(Next.js, tRPC, Prisma, NextAuth.js, TailindCSS)を使ったSNSのWebアプリケーション開発について、ユーザーとユースケースの定義から作ったWebアプリのデプロイまで、一通り説明しています。よかったら読んでみてください!

Discussion