狐好きぷろぐらまー

狐好きプログラマーのブログです。

【Flutter】Supabaseでの中間テーブルの作り方とFlutterからの使い方について【Supabase】

こんにちは。pregum_foxです。

今回の記事は Supabase Advent Calendar 2023 の6日目の記事となっています。

前回は いせりゅー🥳 さん の【Flutter x Supabase】SNS系のアプリで自分の投稿だけを取得する方法 でした!

それではSupabaseでの中間テーブルの作り方とFlutterからの使い方について書いていきます。

以下目次です。

背景

今回なぜ中間テーブルについて書こうかと考えたかというと、React.jsやNext.jsではSupabaseを使った中間テーブルの記事を見かけることはありましたが、Flutterではあまり記事がなかったので、少しでも自分のような初学者の助けになればと思って書こうと思いました。

想定読者

想定読者としては以下のような方です。

  • RDBというワードは知っていて、中間テーブルなどもみたことはあるが、自分でテーブル設計などはあまりしたことはない
  • 中間テーブルというものは知っているが、Supabase上ではどのように設定すれば良いかわからない
  • RDBもPostgresの中間テーブルの作り方も知っているが、Flutterからどうやって中間テーブルに含まれるデータを引っ張ってくるかわからない...

この記事を読むとできるようになること

  • Supabase上での中間テーブルの作り方がわかる
  • Flutterから中間テーブルに含まれるデータの取得処理のコードが書ける

この記事を読んでもできないこと

  • Supabase上でSQLを実行しての中間テーブルの作成方法
    • SQLは記載しておりますが、SQLの実行エディタの開き方や実行方法は記載していません。
  • ローカル環境でself-hostingしているSupabase上での中間テーブルの設定方法

開発環境

項目 バージョン
flutter 3.16.2
supabase_flutter 1.10.25
fvm flutter doctor
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 3.16.2, on macOS 14.1.1 23B81 darwin-arm64, locale ja-JP)
[✓] Android toolchain - develop for Android devices (Android SDK version 34.0.0)
[✓] Xcode - develop for iOS and macOS (Xcode 15.0.1)
[✓] Chrome - develop for the web
[✓] Android Studio (version 2022.3)
[✓] VS Code (version 1.84.2)
[✓] Connected device (4 available)
[✓] Network resources

• No issues found!

今回作成するテーブル一覧

今回作成するテーブルは以下の3つです。

  • menus
    • 料理のメニューが保存されているテーブルです。
  • allergens
    • 特定原材料が保存されているテーブルです。
  • menus_allergens
    • menusとallergensの中間テーブルです。

図も合わせて添付します。

今回作成するアプリの操作フロー

今回作成するFlutter Webアプリは以下のフローとなっています。

  • menusを選択する
  • 紐づくallergensのレコードを取得し、表示する

上記の紐づくallergensのレコードを取得する箇所で中間テーブルを使用します。

Flutter Webを使用する理由は、準備が簡単な為です。

完成図

完成したものは以下のようなものになります。

リポジトリはこちらです。

GitHub - Pregum/supabase_intermediate_table_pracetice_flutter

手順

1. Supabaseプロジェクトを作成

まずは、Supabaseのダッシュボード を開いて、「New project」をクリックします。

「Name」、「Database Password」、「Region」を設定し、「Create new project」をクリックします。

作成しましたら、Project URLAPI Key をコピー後、メモに取っておいてください。

2. .env ファイルの設定

GitHub - Pregum/supabase_intermediate_table_pracetice_flutter

上記のリポジトリをクローンします。

リポジトリ直下にある .env.example ファイルのファイル名を .env に変更します。

SUPABASE_URLProject URLを、SUPABASE_ANON_KEYAPI Keyを設定します。

下記のようなイメージです。

SUPABASE_URL=https://xxxxxxxxxxxxxxxxxxxx.supabase.co
SUPABASE_ANON_KEY=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb.ccccccccccccccccccccccccccccccccccccccccccc

3. DBの初期設定

まずはSupabaseのダッシュボード を開いて、先ほど作成したプロジェクトを開きます。

そのあと左側のメニューから「Table Editor」をクリックします。

初期状態ですと、テーブルがないと思いますので、「New table」をクリックして、テーブル作成画面へ移ります。

「Name」にmenusを設定、新規カラムとしてcreated_atと、nameを作成します。

また今回は検証を簡易にする為、「Enable Row Level Security (RLS)」のチェックを外します。

※ 本番環境などの運用時はチェックをつけてください。

「Save」をクリックします。

すると、menusテーブルが作成されます。

いくつか、レコードを追加しますので、「Insert」 > 「Insert row」をクリックします。

そこからいくつかデータを作成します。

これでmenusテーブルの設定は完了です。

SQLから作成したい方は以下にSQLを記載致しますのでそちらを参照してください。

create table
  public.menus (
    id bigint generated by default as identity,
    created_at timestamp with time zone not null default now(),
    name text null default ''::text,
    constraint menus_pkey primary key (id)
  ) tablespace pg_default;

次にallergensテーブルの作成に移ります。

allergensテーブルの作成

menusテーブルと同様に、「New table」をクリックしてテーブルを作成します。

「Name」がallergens 、新規カラムとしてtype_namecreated_atを設定します。

こちらも、「Enable Row Level Security (RLS)」のチェックを外します。

※ 本番環境などの運用時はチェックをつけてください。

こちらもSQLでテーブルを作成する方用にSQLを記載致します。

create table
  public.allergens (
    id bigint generated by default as identity,
    created_at timestamp with time zone not null default now(),
    type_name text null default ''::text,
    constraint allergens_pkey primary key (id)
  ) tablespace pg_default;

では、アレルゲンのレコードを追加します。

私のDBで設定したデータをcsvで以下に記載します。

id,created_at,type_name
1,2023-12-04 00:35:32.675205+00,えび
2,2023-12-04 00:35:42.416896+00,かに
3,2023-12-04 00:35:50.105787+00,くるみ
4,2023-12-04 00:35:57.525895+00,小麦
5,2023-12-04 00:36:07.64438+00,そば
6,2023-12-04 00:36:15.297054+00,卵
7,2023-12-04 00:36:27.14979+00,乳
8,2023-12-04 00:36:34.627851+00,落花生

上記のデータをcsv形式で保存しましたら、Supabaseの機能でcsvのimport機能がありますので、そちらからimportすることができます。

それでは中間テーブルであるmenus_allergensテーブルを作成します。

先ほどと同様に「New table」をクリックしテーブル作成画面へ移ります。

Nameに「menus_allergens」 を設定します。

先ほどと同様に「Enable Row Level Security (RLS) 」のチェックを外します。

このあと先にidの「Primary」のチェックボックスを外し、新規カラムとして「menu_id」と「allergen_id」を作成します。

そして、上記2つのカラムの「Primary」にチェックを入れます。

また「menu_id」と「allergen_id」にはそれぞれ外部キー制約を設定します。

まず「menu_id」のリンクのマークをクリックします。

その後、menu_idの外部キー制約の設定画面が表示されますので、「Select a table to reference to」にmenus を選択します。

選択すると、どのカラムか聞かれますので、id を選択します。

その下は2つは、外部キーが削除された時、こちらのレコードへの反映の種類ですので、今回は任意のもので良いです。

私はCascade (参照先の変更に追従する) 設定にしております。

各選択肢の挙動を知りたい方は下記記事をご覧ください。

MySQLの外部キー制約RESTRICT,CASCADE,SET NULL,NO ACTIONの違いは? #MySQL - Qiita

「allergen_id」も同様にリンクのマークをクリックして、「allergens」の「id」を外部キー制約に設定します。

設定後、カラムのマークが下記のようになっていましたら良いです。

こちらもSQLを記載致します。

create table
  public.menus_allergens (
    menu_id bigint not null,
    allergen_id bigint not null,
    id bigint generated by default as identity,
    constraint menus_allergens_pkey primary key (menu_id, allergen_id),
    constraint menus_allergens_allergen_id_fkey foreign key (allergen_id) references allergens (id) on update cascade on delete cascade,
    constraint menus_allergens_menu_id_fkey foreign key (menu_id) references menus (id) on update cascade on delete cascade
  ) tablespace pg_default;

それでは確認用にいくつかレコードを追加します。

「Insert」 > 「Insert row」をクリックしますと、「menu_id」と「allergen_id」に紐づくidが選択できるボタンがありますので、そこから設定すると 選択するだけで済むので、楽で良いかと思います。

いくつか設定後、以下のような感じでレコードが設定されていましたら良いです。

これで、テーブルの設定は完了ですので、次にFlutterのコードへ移ります。

4. Flutter側の呼び出し側の実装

先ほどクローンしたプロジェクトにすでに中間テーブル経由でデータを取得する処理が実装されている為、そのコードを抜粋します。

// menus_util.dart

Future<List<Allergen>> getAllergensById(int menuId) async {
  try {
    final response = await supabase
        .from('menus')
        .select('*, allergens (id, type_name, created_at)')
        // このidはmenusテーブルのidをしているので注意
        .eq('id', menuId);
      
    if (response.isEmpty) {
      return [];
    }

    debugPrint('response: $response');
    final record = response[0] as Map<String, dynamic>;
    final allergens = record['allergens'] as List<dynamic>;
    final results = allergens.map(
      (allergen) {
        debugPrint('allergen: ${allergen.toString()}');
        return Allergen.fromJson(allergen);
      },
    ).toList();
    return results;
  } catch (e) {
    debugPrint('error!: $e');
    return [];
  }
}

上記のコードで、以下の2行が重要なコードです。

  • .select('*, allergens (id, type_name, created_at)')
  • .eq('id', menuId);

1つ目は、sqlを実行しているのですが、中間テーブルであるmenus_allergensテーブルはここには記載なく、allergensテーブルのレコードが取得できるようになっています。 また、2つ目のeqメソッドが指定されていない場合は、紐づくallergens テーブルのレコードが正しく取得できませんでした。 ここで注意しておきたいこととしては第1引数の 'id' はallergensの id ではなく fromで指定しているテーブルのidが比較対象となりますので、その点だけ注意してください。

  • 今回の例ですと、menusのidが比較対象です。

うまくいかない場合に、確認する点

うまくデータが取得できない場合、以下の点を確認してください。

  • 全てのテーブルのEnable Row Security (RLS)がのチェックが外れていること
  • menus_allergensのprimary keyがmenu_idallergen_id の二つにチェックがついていること
  • menus_allergensの menu_idallergen_id の外部キー制約の設定ができていること
  • Supabaseの.envファイルの設定ができていること

宣伝

もう少しレコードを増やして、アレルゲンのアイコンを表示させたり、メニューのタイプでフィルタリングする機能を載せたサイトを以下URLにデプロイしておりますので、もしよろしければご覧ください。

saizeriya-menu-lottery.pages.dev

リポジトリはこちらです。

github.com

また、Supabase Authentication + Flutterのサンプルもいくつか書いていますので、よろしければご覧ください。

pregum-fox.hatenablog.jp

RLSについて再度ご連絡

改めてRLSの説明ですが、今回はあくまでテストで試す敷居を下げるために無効化しています。

実運用時はRLSの設定をお願いいたします。

雑感

Advent Calendarには初めて参加しましたが、締切があると否が応でも書かないといけないので、早いペースで書くことができました。

次回はもう少し余裕をもってより深い内容について書いてみたいと思います。

またこの記事で少しでも中間テーブルで詰まる人が減ることを願っています 🙏

2回ぐらい実装したはずなのに、今回のサンプルを作る際にRLSの設定ミスで1時間ぐらい溶けました...

時間を見つけ次第、ローカル環境での中間テーブルの作り方なども追記できれば良いなと考えています。

明日は No Name さんの「PrismaとAuth絡めてなんかする」です!

※ 更新されましたら、こちらも合わせてリンク追加予定です。

ここまで読んでいただきありがとうございました。

参考サイト