🤔

Supabase・Prisma.jsを利用したチャット機能のアーキテクチャについて

2023/08/22に公開

実装途中の状態なのでここから変わっていくかもしれないが現時点での考えを書いていきたい。主に現状の頭の整理です。

なお、フロント側はNext.jsを利用しています。

要件の整理

  • Realtime通信(Supabase)
  • チャットにはスタンプによるリアクションを可能にする

技術選定理由

要件の通りではあるのですが、まずRealtimeに通信するアプリケーションの、特にWebSocket部分を自分で作りたくないという物がありました。

なるべくあり物でPOCを行いたいという理由からSupabaseを選択し、RealtimeDatabase + Authの範囲をSupabaseに担ってもらっています。

また、Prisma.jsについてはやはりテーブル定義を楽にしたい、型をそのまま使いたいという理由が強いです。

DB構造

model Message {
  id           String         @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
  body         Json?
  createdAt    DateTime       @default(now())
  updatedAt    DateTime       @default(now()) @updatedAt
  channelId    String         @db.Uuid
  Channel      Channel?       @relation(fields: [channelId], references: [id])
  userId       String         @db.Uuid
  User         User?          @relation(fields: [userId], references: [id])
  ChatStamp    ChatStamp[]

  @@index([channelId], name: "channelId")
}

model ChatStamp {
  id        String   @id @default(dbgenerated("gen_random_uuid()")) @db.Uuid
  emoji     String   @db.VarChar(4)
  createdAt DateTime @default(now())
  updatedAt DateTime @default(now()) @updatedAt
  userId    String   @db.Uuid
  User      User?    @relation(fields: [userId], references: [id])
  messageId String   @db.Uuid
  Message   Message? @relation(fields: [messageId], references: [id], onDelete: Cascade)

  @@index([messageId], name: "ChatStampMessageId")
}

考えていた部分として、まずはMessageの本文部分はJsonカラムを利用しようと考えていました。 必ず大規模なテーブルになる部分で、Alter Tableは効かなくなるだろうという予想と、ある程度はフロントで柔軟に行えるような構造を選択したかったという理由です。

正直、全文検索等はElasticSearchなど専用のものに任せる形になるだろうというのもあるので、負荷周りはそれほど気にしていませんでした。
同様に、それなりのカーディナリティを確保できるため、ChannelId的なものにIndexを貼っておけばなんとかなるといいなぁ・・・と、現時点では思っています。

Prisma.jsにも絡みますが、このJsonカラムはアプリケーション内に別途型を用意し、Prisma.jsが用意するMessage型を拡張した、CustomMessage型を受け取る形で利用しています。

具体的には以下のような型です。

export type CustomMessage = Message & {
  User: User;
  ChatStamp?: ChatStamp[];
  body: MessageBody;
};

/**
 * Messageのbodyの型
 *
 * json型のカラムに対してPrisma.jsでは型を定義できないため、別途用意する
 * 必要に応じて追加していき、あとから追加された値の場合は?で定義する
 */
export type MessageBody = {
  text: string;
};

これにより、APIから返却されてきたJson型の部分も、型の推論が効くようになっています。

また、uuidについても可能であれば時系列を持ったuuid v7ほしかったとは思っていますが、CreatedAtをつかうことでほぼ問題ないだろうという結論にはなっています。

API周りの処理について

まだ足りていないのですが、カスタムフックでuseChatを作成し、そこですべて管理しています。

useChatの中身としては

  • 初回ロード(最新のn件を取得)
  • MessageテーブルのSubscribe(変更検知)
    • 変更検知後のStateの更新

以上が主な役割です。

Realtime部分はMessageテーブルの変更をSubscribeし、変更されたmessageIDをもとに単体の投稿を取得、それをStateに反映させるような処理にしています。
理由はMessageテーブルの中身だけではフロントで必要な情報が集まらないというものです(ChatStampの情報やUserの情報が欠ける)

関連し、ChatStampテーブルの変更はそのままではMessageテーブル変更として認識しないためSupabaseのRealtimeDatabaseの機能により検知できません。
ここはChatStampの変更API部分で、Message.UpdatedAtを更新する処理をAPI側にはさみ変更を検知しています。

実装中

諸々実装しているが、スタンプ部分は少し冗長な結果が返ってきていて、それをフロント側でパースしなくてはいけないのがだいぶ微妙かなぁという気持ちがあります。
Component内に隠蔽されているのですが、結構スタンプ部分今後影響範囲が大きくなる可能性があるなぁと思っていて、この辺は後々大規模なマイグレーション、リファクタリングが発生しそうで憂鬱ですがなんとも言えない感じですね・・・。

また、チャット内部を保持しているJson型の部分は今後どのような魔改造がされるかが見当がついていません。
ひとえにChatに関するアーキテクチャをそれほど経験していなかったり情報を調べきれていないことが原因ですが、これから悩みながら実装していくことになるだろうというところで、一旦ここまでとします。

誰かの参考と、どなたかのご意見がいただけたら嬉しいです!

Discussion