KabologHomeXGitHub

PocketBaseハンズオン

7/7 頃に公開されました。
概要は下のツイートのとおりです。

GitHub のスターがたったの数日間に数千に増え、一ヶ月ちょっと経った今では 8 k を超えています。
日本でほとんど話題になっていないのが不思議です。

この記事について

7 月末に Dart の SDK がリリースされたので、ごく基本的なところ、特にデータベースの基礎だけでも触ってつかんでおこうということで試してまとめたものです。

  • PocketBase 0.4.2、Dart SDK 0.2.0 時点の情報です
  • ほぼデータベース(非リアルタイム)のことのみです
  • Go のサーバのフレームワークとしても使えますが、今回は BaaS っぽい使い方に限っています
  • コード例には Dart を用い、実行結果も Dart の場合のものです

かわいいのも魅力

個人により開発されていますが、デザインがプロフェッショナルです。
ホームページも管理画面も堅苦しくなくて、ほんのりとかわいらしさがあります。

どんなプロダクトも惹きつける見た目が大事だなと改めて思いました。

かわいい管理画面

導入

ホームページの Introduction のページ や GitHub の Releases に各 OS(Linux / Windows / macOS)用の ZIP ファイルがあるので、ダウンロードして解凍(展開)します。

中身のファイルは二つだけです。

Mode                 LastWriteTime         Length Name  
----                 -------------         ------ ----  
-a----        2022/08/13     14:15           1080 LICENSE.md  
-a----        2022/08/13     14:15       37131264 pocketbase.exe  

このうちの実行ファイルのほうをターミナルで起動するだけでサーバが動きます。
次のスクショは Windows の場合です。

PocketBase の起動

書かれているとおり、Admin UI の URL は https://localhost:8090/_/ です。
初めてなら管理用アカウントを画面上で作成しましょう。
メールアドレス等のアカウント情報の保存先は SQLite 内です。
気になる人は SQLite を扱えるクライアントツールなどで覗いてみてください。

準備は以上です。
同種のツールでこんなにも簡単に導入できるものがかつてあったでしょうか!
Docker もいらず、あまりに手軽なので、もうこの時点で虜になりそうです。

ユーザ

管理画面で作成

冒頭に書いたとおりデータベースを少し使ってみて把握することが主旨なので、ユーザ作成は手間を省いて管理画面で行います。

ユーザ作成

「Edit Profile」でユーザの名前も設定しておきます。
これは、どんなふうに取得できるのかを確認したいからです。
使わなければ設定不要です。

ユーザ一覧

アバターを設定できる点もいいですね。
アプリで使うアバターの機能がこれに依存すると、その機能が存在しない別サービスに移行するときに困るかもしれませんが。

※プロフィールやカラムのデータに日本語は使えますが、Enter キーで保存するようになっているようで、変換確定のための Enter でも保存して閉じられてしまいます。今後の改善に期待…。(改善されていました)

Dart でユーザ認証

設定したメールアドレスとパスワードでログインしてみます。

import 'package:pocketbase/pocketbase.dart';  
  
...  
  
Future<void> main() async {  
  final client = PocketBase('http://127.0.0.1:8090');  
  
  final response = await client.users.authViaEmail(  
    'user1@example.com',  
    'password',  
  );  
  
  print(response.user?.id);  // 3DKCttAcBXBaoE4  
  print(response.user?.email);  // user1@example.com  
  print(response.user?.profile?.data);  // {avatar: , name: ユーザ1, userId: 3DKCttAcBXBaoE4}  
}  

とても簡単でした。
ユーザ ID や先ほど設定した名前を得ることができています。
プロフィールが Map になっているのはちょっと残念です。

引数は改善の余地あり

authViaEmail() のように同じ型の複数の引数がある場合には、順番を間違えてもエラーにならなくて安全でないので名前付きの引数が望ましいですが、ただの positional な引数になっています。
他のメソッドでも同じ傾向なので、誰かが提案して改善を促すのが良さそうです。

コレクション

PocketBase ではデータベースのテーブルは Collection、その中のカラムは Field と呼ばれています。

ここでは ToDo アプリを作る想定でコレクションを「tasks」という名前で作ります。

フィールド

フィールドは二つだけにします。
その他に自動的に idcreatedupdated が作られます。

task フィールド

ToDo のタスクのテキストを入れるフィールドなので Text 型にします。

taskフィールド

テキストがないとタスクとして成り立たないので Required を有効にしました。
Unique は無効のままにしましたが、同一テキストのタスクを複数作れないようにするなら有効にすればいいです。

他に最短/最長文字数の制約を設定でき、正規表現によって形式を制限することもできます。
SQL では可能なデフォルト値の設定はなぜか見当たりません。

user フィールド

タスクを他の人が参照/更新できてはいけないので、それを制限するルールに用いるフィールドを作っておきます。

User 型については後述します。

userフィールド

Supabase で DEFAULT auth.uid() にしたときのようにレコード作成時にユーザ情報が自動で入るかと思ったのですが、そうなりませんでした。

このフィールドの値を指定しないで作成しようとしても、後で設定する API ルールによって弾かれるので安全ですが、念のために Required は有効にしました。

フィールドの型

TextNumberBool のような基本的な型の他に JSONFile なども選べます。
PocketBase のデータベースは SQLite で、調べると SQLite 3.9 からは JSON が使えるようになっているそうなので、JSON 型にはその機能を使っているのかもしれません。

さらに、RelationUser という型もあります。

Relation 型

「TYPE」を Relation にすると、コレクションを選択するプルダウンが現れます。
想像ですが、内部的には外部キーの設定なのかなと思いました。
SQL に不慣れな人にとっては SQL を意識せずに使えて良いのかもしれません。

ON DELETE CASCATE のように他方のレコードが削除されたときに一緒に消す「DELETE RECORD ON RELATION DELETE」の設定も可能です。

User 型

名前のとおりユーザ情報の型ですが、この設定の時点ではどんな値が入るのかわかりませんでした。
使っているうちにわかったので、後で説明します。

これも Relation の削除設定に似た「DELETE RECORD ON USER DELETE」のオプションがあります。
内部的には同じなのかもしれません。

API ルール

Supabase/PostgreSQL の RLS(Row Level Security)のポリシー、Firestore のセキュリティルールに相当するものです。

Supabase では RLS があるからこそ可能なわけですが、そういったものが存在しない SQLite で同様の機能を実現しているのは偉業だなと思います。

ルール設定

APIルール

このように設定しました。
入力補完があって使いやすいです。

比較対象である右辺が @request.user.id なので左辺は @collection.tasks.user.id かと思ったのですが、補完では .id は出てこなくて、それを付けたままではルール保存時にエラーが出ました。
それから試行錯誤してうまくいった設定がスクショの内容です。

User 型に入る値が判明

先ほど、User 型のフィールドにどんな値が入るかわからなかった旨を書きましたが、これでユーザ ID が入るのだとわかりました。

ユーザ ID はテキストですが、Text 型ではなく User 型にすることで ID の存在確認まで行われます。

レコード追加のルール

レコード追加時用の「CREATE ACTION」は作ろうとしているレコードのルールであって、そのレコードはまだ存在しないため、@collection.tasks.user を使うと正しいルールになりません。
そうすると、自分以外のユーザの ID を使ってタスクを追加できてしまいます。

@request.data.user は API で追加しようとしているレコードデータのうちの user フィールドの値を表すので、それを使うことで API を実行したユーザの ID に限ることができました。

CRUD

ここまでで大事な部分は概ね終わっています。
ここからは、操作の仕方や実行結果を見ていきます。

レコードを追加

final response = await client.users.authViaEmail(...);  
  
final record = await client.records.create(  
  'tasks',  
  body: {'task': 'タスク1', 'user': response.user!.id},  
);  
  
print(record.id); // fqCY05ud8RDoPBl  
print(record.created); // 2022-08-13 09:06:13.491  
print(record.data); // {task: タスク1, user: 3DKCttAcBXBaoE4}  

追加したレコードの情報が返ってくるのが便利です。

別のユーザの ID で追加

body: {'task': 'タスク1', 'user': 'zCDIdLMnCtarKMt'},  

先ほど設定した API ルールに反するので、阻止されて例外(ClientException)が発生します。

ClientException: {url: http://127.0.0.1:8090/api/collections/tasks/records, isAbort: false, statusCode: 400, response: {code: 400, message: Failed to create record., data: {}}, originalError: null}  

ルール違反であることがメッセージでわからないです…。
ステータスコードは 400(Bad Request)です。

追記:
Admin UI 上のログのページを見ると具体的なエラー内容がわかるかもしれません。

存在しないユーザ ID で追加

User 型にしていることでチェックが効くようで、例外が発生します。
ステータスコードは同じ 400 ですがメッセージが異なります。

ClientException: {url: http://127.0.0.1:8090/api/collections/tasks/records, isAbort: false, statusCode: 400, response: {code: 400, message: Something went wrong while processing your request., data: {user: {code: validation_missing_users, message: Failed to fetch all users with the provided ids.}}}, originalError: null}  

時刻

追加したレコードを管理画面で見ると次のようになっています。

レコード一覧

created 等の日時は UTC になっているのがわかります。

UTC は ISO 8601 の形式に従うならば 2022-08-13T18:06:13.491Z などの表記になりますが、先ほどのレコード追加結果に含まれていた created では 2022-08-13 06:13.491 という文字列でした。

このままでは Dart で UTC として扱われなくて使えないので、面倒な変換が必要になります。
下記は一例です。

final utcString = '${DateTime.parse(record.created).toIso8601String()}Z';  
final createdAt = DateTime.parse(utcString).toLocal();  
print(createdAt);  

UTC の時刻が UTC として表現されていないのは不具合と呼べる動作です。
PocketBase 自体か Dart のパッケージかどこかの原因箇所を修正してもらわないと実用できません。
重大なのでそのうち修正されるでしょう。

レコードを取得

レコードの取得は複数のメソッドがあります。

  • getOne()
    • ID 指定で一件を取得
  • getFullList()
    • 全件(フィルタを指定した場合は該当する全件)を取得
  • getList()
    • getFullList() に似ていて、こちらはページ指定できる
final list = await client.records.getFullList('tasks');  
  
for (final record in list) {  
  print(record.data);  
}  

フィルタの他にソートも指定でき、さらに必要ならヘッダも指定できるようです。

query という引数もありますが、ドキュメントがないので詳細は不明です。
ソースコードを見ると、pagefilter 等の引数で指定した値が query の Map に入れられているので、普通の操作では使う必要はなさそうです。

getFullList() の内部的な取得方法

最終結果として全件が返りますが、ソースコードを見ると小分けにして取得しています。
たぶん負荷を考慮したものですね。
batch の引数に何も渡さなければ 100 件単位になります。

getFullList() と getList()

返り値の型が異なります。
getFullList()List<RecordModel> で、getList()ResultList<RecordModel> です。

異なっている理由は、後者では pagination のための情報などを含んでいるからだと思います。

別のユーザで取得

レコードを追加したユーザとは異なるもう一人でサインインして取得してみると、空のリストになっていました。
ルールがフィルタとして機能するという Supabase と同様の仕組みになっていると捉えると、例外が発生せずに空が返ってくるのは理解できます。

次に ID を指定して一件を取得してみます。

final record = await client.records.getOne('tasks', 'fqCY05ud8RDoPBl');  

例外が起こりました。
ステータスコードは 404(Not Found)です。

ClientException: {url: http://127.0.0.1:8090/api/collections/tasks/records/fqCY05ud8RDoPBl, isAbort: false, statusCode: 404, response: {code: 404, message: The requested resource wasn't found., data: {}}, originalError: null}  

レコードを更新

final record = await client.records.update(  
  'tasks',  
  'fqCY05ud8RDoPBl',  
  body: {'task': 'タスク1-改'},  
);  

これも更新後のレコードが返ってきます。

別ユーザで更新しようとした場合は、先ほどの一件取得のときと同じ例外でした。

レコードを削除

await client.records.delete('tasks', 'fqCY05ud8RDoPBl');  

削除では返り値はありません。
成否は例外の有無で判断することになりそうです。

別ユーザで実行したときはやはり一件取得や更新のときと同様の例外でした。

実用

ごく基本的な操作しかしていませんが、これで感覚はつかめました。
単純な ToDo アプリは tasks コレクションを使ってもう作れます。
あとは Relation 型の扱い方を確認しておきたいところですが、今回はここまでにします。

実用は…、個人的にはもう少し先です…。
v1.0 に達したときが無難ですが、フィードバックがある程度出て改善されてきた時点で個人開発には使い始めてもいいとは思っています。

PocketBase vs Supabase

PocketBase とクラウド の Supabase のどちらかを選ぶなら、費用面を無視できるなら Supabase を選びます。

自分でホストする方式に限れば、Supabase では Docker で複数のコンテナを動かす必要があるのに対して PocketBase ではシングルバイナリになっていて手軽であり、バックアップ/リカバリも SQLite のほうが手間が少ないです。

  • アプリを多数作る場合
    • PocketBase
    • 将来的にクラウドの Supabase に移行しそうなら Supabase を自分でホスト
  • 多数作らない場合
    • Supabase クラウド

PocketBase は SQL らしさが薄いので、SQL が苦手な人は Supabase より入りやすそうです。

二通りの使い方

  • a) Supabase や Firebase のようにフロントエンドから DB に直接アクセスする
  • b) Go のフレームワークとして用いて Web API を提供する

a は便利ですが、ルールをきっちりと設定して、そのためのテストも書いておかないと危険です。
また、アプリから直に利用するのでスキーマを変更したいときに困りそうです。

PocketBase はサーバレスではなくて、どうせ自分でホストするなら b が良い気もします。
バックエンドに処理をまとめたほうがクライアントがシンプルになるというのもあります。

でも、フロントエンドな人にはきっと a のほうが魅力ですね。

迷うところですが、選択肢があるのは嬉しいことなので、運用方法などを考えながら前向きに迷いましょう。

さいごに

PocketBase には可能性が感じられて期待できます。
こんなに素晴らしいものが OSS として提供されているのはありがたいですね。
しばらく見守りたいと思います。

Xに投稿するこのエントリーをはてなブックマークに追加