こんにちは。pregum_foxです。
今回の記事は Supabase Advent Calendar 2023 の6日目の記事となっています。
前回は いせりゅー🥳 さん の【Flutter x Supabase】SNS系のアプリで自分の投稿だけを取得する方法 でした!
それではSupabaseでの中間テーブルの作り方とFlutterからの使い方について書いていきます。
以下目次です。
- 背景
- 想定読者
- この記事を読むとできるようになること
- この記事を読んでもできないこと
- 開発環境
- 今回作成するテーブル一覧
- 今回作成するアプリの操作フロー
- 完成図
- 手順
- うまくいかない場合に、確認する点
- 宣伝
- RLSについて再度ご連絡
- 雑感
- 参考サイト
背景
今回なぜ中間テーブルについて書こうかと考えたかというと、React.jsやNext.jsではSupabaseを使った中間テーブルの記事を見かけることはありましたが、Flutterではあまり記事がなかったので、少しでも自分のような初学者の助けになればと思って書こうと思いました。
想定読者
想定読者としては以下のような方です。
- RDBというワードは知っていて、中間テーブルなどもみたことはあるが、自分でテーブル設計などはあまりしたことはない
- 中間テーブルというものは知っているが、Supabase上ではどのように設定すれば良いかわからない
- RDBもPostgresの中間テーブルの作り方も知っているが、Flutterからどうやって中間テーブルに含まれるデータを引っ張ってくるかわからない...
この記事を読むとできるようになること
- Supabase上での中間テーブルの作り方がわかる
- Flutterから中間テーブルに含まれるデータの取得処理のコードが書ける
この記事を読んでもできないこと
- Supabase上で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 URL
とAPI Key
をコピー後、メモに取っておいてください。
2. .env
ファイルの設定
GitHub - Pregum/supabase_intermediate_table_pracetice_flutter
上記のリポジトリをクローンします。
リポジトリ直下にある .env.example
ファイルのファイル名を .env
に変更します。
SUPABASE_URL
にProject URL
を、SUPABASE_ANON_KEY
にAPI Key
を設定します。
下記のようなイメージです。
SUPABASE_URL=https://xxxxxxxxxxxxxxxxxxxx.supabase.co SUPABASE_ANON_KEY=aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb.ccccccccccccccccccccccccccccccccccccccccccc
3. DBの初期設定
menusテーブルの作成
まずは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_name
、created_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
テーブルを作成します。
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_id
とallergen_id
の二つにチェックがついていること - menus_allergensの
menu_id
とallergen_id
の外部キー制約の設定ができていること - Supabaseの.envファイルの設定ができていること
宣伝
もう少しレコードを増やして、アレルゲンのアイコンを表示させたり、メニューのタイプでフィルタリングする機能を載せたサイトを以下URLにデプロイしておりますので、もしよろしければご覧ください。
saizeriya-menu-lottery.pages.dev
リポジトリはこちらです。
また、Supabase Authentication + Flutterのサンプルもいくつか書いていますので、よろしければご覧ください。
RLSについて再度ご連絡
改めてRLSの説明ですが、今回はあくまでテストで試す敷居を下げるために無効化しています。
実運用時はRLSの設定をお願いいたします。
雑感
Advent Calendarには初めて参加しましたが、締切があると否が応でも書かないといけないので、早いペースで書くことができました。
次回はもう少し余裕をもってより深い内容について書いてみたいと思います。
またこの記事で少しでも中間テーブルで詰まる人が減ることを願っています 🙏
2回ぐらい実装したはずなのに、今回のサンプルを作る際にRLSの設定ミスで1時間ぐらい溶けました...
時間を見つけ次第、ローカル環境での中間テーブルの作り方なども追記できれば良いなと考えています。
明日は No Name さんの「PrismaとAuth絡めてなんかする」です!
※ 更新されましたら、こちらも合わせてリンク追加予定です。
ここまで読んでいただきありがとうございました。