🦅

【Flutter x Supabase】Supabase Databaseを使ったCRUD処理とクエリ

2023/07/17に公開

これは何か?

FlutterとSupabaseを使ったDB機能についての簡単な整理です。簡単なTODOアプリをサンプルにCRUD処理と関連するクエリについて紹介していきます。

https://github.com/heyhey1028/flutter_supabase_crud

サンプルではSupabase Authorizationを使ってユーザー管理を行なっていますが、今回の趣旨とはズレてしまうので、解説を省略します。

以前寄稿した記事の実装を踏襲していますので、参考にされたい方は以下を、

https://zenn.dev/flutteruniv_dev/articles/auth_with_supabase

またテーブル作成で使うPostgresSQLについて知りたい方は以下をどうぞ

https://zenn.dev/flutteruniv_dev/articles/plpqsql_for_supabase_beginner

話さないこと

  • Supabaseプロジェクトの作成方法
  • RDBMSに関する基礎概念
  • Supabase Authenticationについて
  • PostgreSQLの文法について

環境

バージョン
Flutter 3.10.2
Dart 3.0.2
Xcode 14.3
Android Studio Flamingo (2022.2)
supabase_flutter 1.10.4

Supabase Database

今更説明も不要かもしれませんが、Supabaseが提供するデータベースサービスです。PostgresをベースとしたRDBMSです。リアルタイム更新にも対応しています。

https://supabase.com/docs/guides/database/overview

料金に関して言えば、無料プランでは2プロジェクト、500MBまでのデータベースサイズが割り当てられ、APIは無制限に呼ぶ事が可能です。DBへの処理に応じて課金されるFirestoreと一番大きな違いはこのAPIコールが無制限なところでしょう

https://supabase.com/pricing

導入

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

こちらは既に作成している前提で進めていきます。まだの方は以下の記事を参考に作成してみてください。

https://zenn.dev/dshukertjr/books/flutter-supabase-chat/viewer/page2

1. パッケージをインストール

https://pub.dev/packages/supabase_flutter

flutter pub add supabase_flutter

2. Supabaseを初期化

mainメソッド内で、

  1. WidgetsFlutterBinding.ensureInitialized(); でFlutterが初期化されている事を確認
  2. Supabase.initilize() にて連携するSupabaseプロジェクトのURLAnon Keyを渡す

URLAnon key はプロジェクトごとに提供されます。

main.dart
Future<void> main() async {
  WidgetsFlutterBinding.ensureInitialized(); // Flutterの初期化を確認
  await Supabase.initialize(
    url: 'https://<your_project_id>.supabase.co',// プロジェクトURL
    anonKey: '<your_anon_key>', // プロジェクトAnon key
  );
  runApp(const MyApp());
}

3. Supabaseクライアントを取得

SupabaseクライアントはSupabase.instance.clientで取得可能です。このクライアントを通して全ての機能を使います。

final supabase = Supabase.instance.client;

4. Todoテーブルを作成

Supabaseコンソール上のSQL Editorから以下のPostgreSQLを実行し、TODOテーブルを作成します。

create table if not exists public.todos (
  id bigint generated by default as identity primary key,
  user_id uuid references auth.users on delete cascade not null,
  title text not null,
  description text,
  is_completed boolean default false not null,
  created_at timestamp with time zone default timezone('utc'::text, now()) not null,
  updated_at timestamp with time zone default timezone('utc'::text, now()) not null
);
comment on table public.todos is 'ユーザーのTODOタスクを管理';

基本的なCRUD

基本

CRUD処理全てに通じる基本としてデータベースへの操作を行う際はfromメソッドで操作を行うテーブルを指定します。fromメソッドに続ける形でCRUD処理のメソッドを呼び出します。

await supabase.from([テーブル名])

Filter

詳しくは事項で紹介しますが、Filterと呼ばれるクエリを使って対象のテーブルを絞る事ができます。selectupdatedeletestreamに対して実行する事ができます。

サンプルコード内に登場するmatchもFilterの1つです。

Create (Insert)

  • insertメソッドにjsonデータを渡す事でその値が追加される
try {
  await supabase.from('todos').insert({
    'title': _titleController.text,
    'description': _descriptionController.text,
    'is_completed': false,
    'user_id': supabase.auth.currentUser!.id,
  });
} catch (e) {
  ...
}

Read (Select)

  • 指定したテーブル内のレコードを返却します
  • 引数なしでを呼ぶとテーブル内の全てのレコードの全てのフィールドを取得します
  • 引数にフィールド名を渡す とそのテーブル内の全てのレコードの指定したフィールドの値だけをjson形式で返却します
// レコードの全てのフィールドを取得
final data = await supabase
  .from('cities')
  .select();

// 指定したフィールドの値だけを取得
final data2 = await supabase
  .from('cities')
  .select('id,created_at');

print(data2);// ex. [{id: 1, created_at: ....}, {id: 2, created_at: ....}]

Update (Update/Upsert)

Update

  • Filterで対象レコードの条件を指定する必要あり
  • 条件に合致するレコード全てが対象になる為、複数のレコードを一度に更新する事も可能
try {
  await supabase.from('todos').update({
    'title': _titleController.text,
    'description': _descriptionController.text,
    'updated_at': DateTime.now().toIso8601String(),
  }).match({'id': widget.todo.id});
} catch (e) {
  ...
}

Upsert

  • primary keyフィールドの指定が、あればUpdateとして動作。なければInsertとして動作。
try {
  await supabase.from('todos').upsert({
    'id': widget.todo?.id, // 今回primary keyとなっているフィールド
    'title': _titleController.text,
    'description': _descriptionController.text,
    'is_completed': false,
    'user_id': supabase.auth.currentUser!.id,
  });
} catch (e) {
  ...
}

Delete

  • Filterで対象レコードの条件を指定する必要あり
  • 条件に合致するレコード全てが対象になる為、複数のレコードを一度に削除する事も可能
try {
  await supabase.from('todos').delete().match({'id': todo.id});
} catch (e) {
  ...
}

リアルタイム取得

  • Supabaseではテーブルデータのリアルタイム取得が可能
  • stream メソッドで指定のテーブルのStreamを受け取る
  • 引数として primary key の指定が必要
  • Filterで対象レコードを絞ることも可能
  • List<Map<String,dynamic>>で受け取るのでmapメソッドで展開してモデルクラスなどに変換しましょう
final stream = Supabase.instance.client
      .from('todos')
      .stream(primaryKey: ['id'])
      .map(
        (events) {
          // モデルクラスへの変換など
        },
      );

Filters

前項で軽く触れましたが、Filter と呼ばれるクエリでselectupdatedeletestreamの処理を行う対象レコードを絞る事が出来ます。

https://supabase.com/docs/reference/dart/using-filters

これらのシンタックスはPostgRESTと呼ばれるPostgreSQLデータベースを操作するRESTful APIを提供するWebサーバーのシンタックスを踏襲しています。

https://postgrest.org/en/stable/references/api/tables_views.html#operators

数が多いので全ては紹介しきれませんが、代表的なものだけここで紹介したいと思います。

等号(イコール)

まずは等号のFilterです。

eq

  • イコール(equal)を意味するeqメソッド
// eq(フィールド名,値)
final data = await supabase
  .from('cities')
  .select('name, country_id')
  .eq('name', 'The shire');

neq

  • イコールでない(not equal)を意味するneqメソッド
// neq(フィールド名,値)
final data = await supabase
  .from('cities')
  .select('name, country_id')
  .neq('name', 'The shire');

match

  • eq同様に同じ値のレコードに絞る
  • json形式で複数の条件を同時に指定する事が可能
// match({フィールド名:値,フィールド名:値,...})
final data = await supabase
  .from('cities')
  .select('name, country_id')
  .match({'name': 'Beijing', 'country_id': 156});

textSearch

  • 該当フィールドの文字列検索
  • 第二引数に検索したい文字列の条件を記載
  • 条件式自体を文字列にしなければならないので注意
// 単一文字列の検索
final data = await supabase
  .from('quotes')
  .select('catchphrase')
  .textSearch('catchphrase', "'fat'");

// 複数文字列の検索
final data = await supabase
  .from('quotes')
  .select('catchphrase')
  .textSearch('catchphrase', "'fat' & 'cat'");

裏側ではPostgresのtsqueryが実行されているのでより詳しく知りたい方は以下をご参照ください

https://www.postgresql.jp/document/9.6/html/functions-textsearch.html#textsearch-operators-table

不等号

次は大小比較など不等号のFilterです。

gt

  • 以上(greater than)を意味するgtメソッド
  • 指定された下限値を含まないそれ以上の値のレコードに絞ります
// gt(フィールド名,下限値)
final data = await supabase
  .from('cities')
  .select('name, country_id')
  .gt('country_id', 250);

gte

  • 超過(greater than or equal)を意味するgteメソッド
  • 指定された下限値とそれ以上の値のレコードに絞ります
// gte(フィールド名,下限値)
final data = await supabase
  .from('cities')
  .select('name, country_id')
  .gte('country_id', 250);

lt

  • 以下(less than)を意味するltメソッド
  • 指定された上限値を含まないそれ以下の値のレコードに絞ります
// lt(フィールド名,上限値)
final data = await supabase
  .from('cities')
  .select('name, country_id')
  .lt('country_id', 250);

lte

  • 未満(less than or equal)を意味するlteメソッド
  • 指定された上限値とそれ以下の値のレコードに絞ります
// lte(フィールド名,上限値)
final data = await supabase
  .from('cities')
  .select('name, country_id')
  .lte('country_id', 250);

含む

配列型やJSON型、範囲型のフィールド持つレコードに対して実行できるFilterです。

contains

  • 合致条件の値は複数指定できる
  • 複数指定した場合はすべての値を含むレコードのみを返す
// contains(フィールド名,[値,値,...])
final data = await supabase
  .from('countries')
  .select('name, id, main_exports')
  .contains('main_exports', ['oil']);

Modifiers

Filtersが取得したレコードを条件で絞るのに対して、取得した結果に変更を加えるModifiersという処理も用意されています。
https://supabase.com/docs/reference/dart/using-modifiers

order

  • 取得したレコードの順序を変更します
final data = await supabase
  .from('cities')
  .select('name, country_id')
  .order('id', ascending: true);

limit

  • 取得するレコードの数を限定します
final data = await supabase
  .from('cities')
  .select('name, country_id')
  .limit(1);

range

  • Filterや操作の対象となるレコードをインデックスで限定します
  • 指定した範囲のインデックスのレコードを処理やFilterの対象にします
  • あくまでもインデックスの範囲で、値の範囲ではないことに注意です
final data = await supabase
  .from('cities')
  .select('name, country_id')
  .range(0,3);

おまけ

Postgres Functionを呼び出す

最後にCRUD処理とは異なりますが、もう1つ便利な機能として、データベースに登録したPostgres Functionを直接呼び出すことが可能です。

.rpc(登録した関数名、params:{引数名:引数})の形で引数を渡すこともできます。rpcはRemote Procedure Callを意味しています。

// .rpc(登録した関数名、params:{引数名:引数})
final data = await supabase
  .rpc('echo_city', params: { 'name': 'The Shire' });

より複雑な処理を定義する事ができるPostgres Functionが直接呼び出せるのはなかなか強力だと思います。

以上

以上簡単ですが、FlutterでSupabase databaseを使ったCRUD処理について整理してみました。

FilterとModifierについて全ては扱っていませんが、サンプルアプリ内でtextSearchorderを使っているので、参考にしてみていただければと思います。

https://github.com/heyhey1028/flutter_supabase_crud

Firebaseを長らく使ってきた自分としては、少し勝手が違う部分もありましたが、一度分かってしまえば操作感も手軽さもFirebaseに近く非常にとっつきやすいと感じました。

Flutter大学

Discussion