🐥

Supabase SDKでJSONB配列の検索の実装方法

2024/11/04に公開

課題

SELECTは全てPostgresのViewで定義し、検索はSupabaseのJavascript SDKを使用する運用をしている。
この運用を行うと、Supabase側が自動的にデータベースのスキーマを読み取り、Type Fileを生成してくれるため、フロントエンド側ではTypeを定義する必要がなくなり、開発コストが若干下がる。

また、DBのスキーマを見れば、返しているレスポンスを確認できるため、オレオレレスポンスが行われることがなくなる。

https://supabase.com/docs/guides/api/rest/generating-types

今回は、下記のTypeをレスポンスとして受け取れるViewを作成し、like_idが一致するUserだけを抽出する検索を実装しようとしたが、公式ドキュメント等を参照して苦戦をしたのでメモ。

受け取りたいレスポンス
type user_likes = {
    user_id: string
    user_name: string
    likes: {
        like_id: string
        like_name: string
    }[]
}

▼ テストで使用したリポジトリ
https://github.com/DevRyuki/supabase_jsob_agg_search/tree/main

DB構造

supabase/migrations/20241023103407_table.sql
CREATE TABLE users (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  name TEXT NOT NULL,
  created_at TIMESTAMP WITH TIME ZONE DEFAULT now(),
  updated_at TIMESTAMP WITH TIME ZONE DEFAULT now()
);

CREATE TABLE likes (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  name TEXT NOT NULL
);

CREATE TABLE user_likes (
  user_id UUID REFERENCES users(id),
  like_id UUID REFERENCES likes(id),
  PRIMARY KEY (user_id, like_id)
);

CREATE VIEW user_likes_view AS
SELECT
  users.id AS user_id,
  users.name AS user_name,
  jsonb_agg(
    jsonb_build_object(
      'like_id', likes.id,
      'like_name', likes.name
    )
  ) AS likes:jsonb
FROM
  users
LEFT JOIN users_likes ON users.id = users_likes.user_id
LEFT JOIN likes ON users_likes.like_id = likes.id
GROUP BY
  users.id;

検索してヒットするサイト

下記は同じような問題を抱えているが、解決するのか微妙な回答で解決に至らなかった。

https://github.com/orgs/supabase/discussions/7373

https://github.com/orgs/supabase/discussions/2102

https://www.reddit.com/r/Supabase/comments/10z753w/issues_with_jsonb_column_defined_as_array/

解決策

.contains()を使用し、valueをJSON.stringify()でJSON文字列にすることで検索の実装が出来た。
https://supabase.com/docs/reference/javascript/contains

RedditでJSON.stringifyをしているコードを見つけてそんなバナナと思いながら実行をしてみたら検索が出来た。

上記の同じ課題を抱えていて、解決している例では1件もJSON.stringfiy()を使用している例が存在しなかったので、まさかの解決。

AND検索

AND検索はcontainsに配列で入れることで実装可能。

typescript
const supabase = createClient();
const payload = supabase
.from("user_likes_view")
.select()
.contains("likes", JSON.stringify([{ like_name: "1" }, {like_name: "2"}]));

OR検索

OR検索は.or()を活用することで実装可能。

typescript
const likeNames = ["1", "2"];
const orContains = likeNames.map((name) => `likes.cs.${JSON.stringify([{ like_name: name }])}`).join(",");

const supabase = createClient();
const payload = supabase
.from("user_likes_view")
.select()
.or(orContains)

さいごに

これを解決するのに1週間ぐらいテストしてたけど、みんな同じ壁にぶちあたって無いのかな。。。

SasaTech Engineer Blog

Discussion