その他

【Supabase】 Firebase Realtime Database を Supabase に移行した話

その他
この記事は約7分で読めます。

個人開発においては、費用負担をせずにデータを保存したいことが多いかと思います。私も例に漏れず、Firebaseができた当時より、Realtime Databaseを無料枠の範囲内で使用していました。最近、FirebaseのNoSQLからSupabaseのSQLに移行しましたので、その話をさせていただきます。

はじめに

個人開発で家計簿WEBアプリを作っていました。

以下の要件を満たすものをイメージいただければと思います。(実際にはもっと複雑です)

  • 1つの会計ごとに支出の記録ができる
    • 「記録」の際、ユーザは、金額、カテゴリ、支払い方法、メモを入力する
    • 「カテゴリ」と「支払い方法」は、あらかじめ自分で設定した選択肢から選ぶ
  • 以下の情報をみることができる
    • 任意の年月に、その月全体の支出に対して、どのカテゴリがどの程度の割合を占めているか(円グラフで表示)
  • 「支払い方法」、「カテゴリ」は設定画面から追加・編集・削除できる

説明の便宜上、3つの要件をそれぞれ満たす画面を、上から「記録画面」「集計画面」「設定画面」として説明することとします。

Firebase Realtime Database

Firebase Realtime Database は、JSON 形式で値を保存できるデータベースになります。

Realtime Database では、SQL でできるような、テーブルの外部結合や、複雑なクエリ操作ができません。

そのため、要件を満たすために以下のような構造としていました。

{
    "records": {
        "0": { // 0 から始まるインデックス
            "price": 500,
            "category_id": 1,
            "method_id": 100,
            "note": "弁当代",
            "created_at": "2022-01-01 12:00:00"
        },
        "1": {
            "price": 210,
            "category_id": 2,
            "method_id": 100,
            "created_at": "2022-01-01 19:00:00"
        },
    },
    "categories": {
        "0": { // 0 から始まるインデックス
            "id": 1,
            "name": "食費",
        },
        "1": {
            "id": 2,
            "name": "交通費",
        }
    },
    "methods": {
        "0": { // 0 から始まるインデックス
            "id": 100,
            "name": "現金",
        }
    },
    "summary": {
        "2022-10": {
            "1": 500, // key は category_id に対応する
            "2": 210,
        }
    }
}

このデータを、画面側では以下のように使っていました。

  • 記録画面
    • 初期表示として、”categories”や”methods” を取得する
    • ユーザが記録を行うたびに、以下を行う
      • “records” に新しい要素を追加する。
        • このとき、”method_id”や”category_id”には、”categories”や”method”から取得してきた”id”を割り振る
      • 集計用のデータ”summary”を更新する。
        • このとき、既存の”records”をもとに、期間ごと、カテゴリごとに集計し階層構造を決定する
  • 集計画面
    • 初期表示として、”summary”を取得する。
  • 設定画面 – 初期表示として、”categories”と”methods”を取得する。
    • “categories”の要素と”methods”の要素を、それぞれ登録・編集・削除できるようにする
      • このときも、集計用のデータ”summary”を更新する

集計画面については、「”created_at”が該当月である”records”の要素を取得し、フロント内部でソート・グループ化を行い、画面に表示させる」こともできましたが、「データベースの状態が変わるタイミングで集計データ”summary”を更新する。集計画面では表示のみを行う」という方針をとっていました。

私が「家計簿WEBアプリを作ろう」と思った段階では、無料でSQLを使用できるサービスがなく、イマイチだと思いながら、以下のように運用していました。

  • データに対しての簡単な取得操作、登録操作、更新操作、削除操作は、用意されたメソッド呼び出し()で行う
  • 複雑な絞り込みやソート、グループ化が施されたデータを取得したい場合は、JavaScript 内で行う

Supabase PostgreSQL

Supabase が最近注目され、重たい腰をあげてSQLに移行しました。

具体的には、データベースを以下の構造にするようにしました。

CREATE TABLE categories (
    id SERIAL NOT NULL,
    name TEXT NOT NULL
);
CREATE TABLE methods (
    id SERIAL NOT NULL,
    name TEXT NOT NULL
);
CREATE TABLE records (
    price INT NOT NULL,
    category_id INT NOT NULL,
    method_id INT NOT NULL,
    note TEXT,
    created_at DATETIME NOT NULL DEFAULT NOW(),
    FOREIGN KEY (category_id) REFERENCES categories(id),
    FOREIGN KEY (method_id) REFERENCES methods(id)
);

簡単なCRUD処理は、Supabase が用意してくれるメソッド呼び出しで解決できます。例としては以下です。

await supabase.from('todos').insert({
    'price': 500,
    'category_id': 1,
    'method_id': 1,
    'note': null,
    });

Firebase Realtime Database で使用していた”summary”については、RPCを用いて以下のクエリを呼び出すことにより、代替することができました。

WITH summarized_records AS (
    SELECT DISTINCT
        records.category_id,
        sum(price) AS sum
    FROM records
    WHERE
        -- input_year と input_month にはそれぞれ、集計したい年と月を指定する
        TO_CHAR(CAST(created_at AS DATE), 'YYYY-MM') = input_year || '-' || input_month
    GROUP BY category_id
)
SELECT
    categories.name AS category_name,
    summarized_records.sum
FROM summarized_records
INNER JOIN categories ON
    categories.id = summarized_records.category_id
ORDER BY summarized_records.sum DESC
;

その他の複雑なデータ操作についてもRPCで行うことができます。

おわりに

Supabase を採用することで、当初フロント側で持っていたデータ操作ロジックを、バックエンド側に持たせることができました。

もともとNoSQLで管理する必要がないデータ構造であったため、SQLを用いてデータ操作・移行作業ができるようになったのもメリットです。

また、`firebase/database` のサイズよりも、`supabase/supabase-js` のサイズの方が小さいため、バンドルサイズの削減にも寄与しました。