【Flutter x Supabase】Supabase Databaseを使ったCRUD処理とクエリ
これは何か?
FlutterとSupabaseを使ったDB機能についての簡単な整理です。簡単なTODOアプリをサンプルにCRUD処理と関連するクエリについて紹介していきます。
サンプルではSupabase Authorizationを使ってユーザー管理を行なっていますが、今回の趣旨とはズレてしまうので、解説を省略します。
以前寄稿した記事の実装を踏襲していますので、参考にされたい方は以下を、
またテーブル作成で使うPostgresSQL
について知りたい方は以下をどうぞ
話さないこと
- 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です。リアルタイム更新にも対応しています。
料金に関して言えば、無料プランでは2プロジェクト、500MBまでのデータベースサイズが割り当てられ、APIは無制限に呼ぶ事が可能です。DBへの処理に応じて課金されるFirestoreと一番大きな違いはこのAPIコールが無制限なところでしょう
導入
0. Supabaseプロジェクトを作成
こちらは既に作成している前提で進めていきます。まだの方は以下の記事を参考に作成してみてください。
1. パッケージをインストール
flutter pub add supabase_flutter
2. Supabaseを初期化
main
メソッド内で、
-
WidgetsFlutterBinding.ensureInitialized();
でFlutterが初期化されている事を確認 -
Supabase.initilize()
にて連携するSupabaseプロジェクトのURLとAnon Keyを渡す
URLとAnon key はプロジェクトごとに提供されます。
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
と呼ばれるクエリを使って対象のテーブルを絞る事ができます。select
、update
、delete
、stream
に対して実行する事ができます。
サンプルコード内に登場するmatch
もFilterの1つです。
Insert
)
Create (-
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) {
...
}
Select
)
Read (- 指定したテーブル内のレコードを返却します
- 引数なしでを呼ぶとテーブル内の全てのレコードの全てのフィールドを取得します
-
引数にフィールド名を渡す とそのテーブル内の全てのレコードの指定したフィールドの値だけを
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
/Upsert
)
Update (
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
と呼ばれるクエリでselect
、update
、delete
、stream
の処理を行う対象レコードを絞る事が出来ます。
これらのシンタックスはPostgREST
と呼ばれるPostgreSQLデータベースを操作するRESTful APIを提供するWebサーバーのシンタックスを踏襲しています。
数が多いので全ては紹介しきれませんが、代表的なものだけここで紹介したいと思います。
等号(イコール)
まずは等号の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
が実行されているのでより詳しく知りたい方は以下をご参照ください
不等号
次は大小比較など不等号の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
という処理も用意されています。
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について全ては扱っていませんが、サンプルアプリ内でtextSearch
やorder
を使っているので、参考にしてみていただければと思います。
Firebaseを長らく使ってきた自分としては、少し勝手が違う部分もありましたが、一度分かってしまえば操作感も手軽さもFirebaseに近く非常にとっつきやすいと感じました。
Discussion