【React Native】WatermelonDB を使って Supabase 連携
こんにちは。かきおです。
個人的にモバイルアプリケーション開発を行いたい思いからデータ管理/セキュリティ周りについて調査しました。そして、実際に動かすことで理解が深まると思い、POCを行いました。この記事ではその軌跡をまとめます。
本記事では WatermelonDB を用いております。WatermelonDB は優れたデータベースライブラリだと個人的に思うのですが、インターネット上には情報があまりないです。そのため、エコシステムの成熟という観点から寄与できたら幸いです。
POCを行ったアーキテクチャ
今回、POCを行ったのは以下のアーキテクチャです。
要点としては以下のものです。
Item | Detail |
---|---|
FW | React Native |
DB | SQLite |
DB Adapter | Watermelon DB |
Cloud | Supabase(Database/Authentication/Edge Functions) |
挙動確認をしながらのコミットで少し見づらくて恐縮なのですが、完成系のリポジトリは以下となります。
アーキテクチャの採用理由
今回考えていたアーキテクチャの要件が以下の通りです。
- データ表示速度改善と通信回数削減の観点からモバイルアプリはローカルDBを持つこと
- Webアプリからもデータにアクセスできること(=クラウドDBの利用)
- クラウドDBを用いるため、認可・認証機能を実装できること
- 個人開発のためトータル工数をできるだけ削減したい。そのため、任せられることはできるだけPaas側に任せられること
上記を実現するべく、今回のアーキテクチャを採用しました。それぞれの採用理由を述べていきます。
(1) 【SQLite/PostgreSQL】モバイルアプリ/WebアプリのDB要件充足
開発予定のプロダクトは、モバイルアプリ・Webアプリの双方で使用できるものを予定しております。そのため、ローカルDBだけではなく、クラウドDBもアーキテクチャに含みたいです。
そのため、以下3つの条件をすべて満たすことが求められます。
- ローカルDB:モバイルアプリの要件にフィットすること
- クラウドDB:Webアプリの要件にフィットすること
- ローカルDB&クラウドDB:同期しやすいこと
ローカルDB:SQLite
モバイルアプリ開発するにあたり、データベースはSQLite
とRealm(MongoDB)
に分かれる理解です。今回、クラウドDBとの同期を行いたい中で、MongoDBがオンライン同期のサポートを終了することを踏まえると、Realmは採用対象からは除外となりました。
クラウドDB:PostgreSQL
ローカルDBがSQLiteとなると、基本的にクラウドDBは RDB 系になります。この時点では積極的な理由はないですが、後述の通り、Supabase を使用したいため、PostgreSQLを採用することとなりました。
(2) 【WatermelonDB】同期の容易性
ローカルDBとクラウドDBの同期を行うにあたり、同期ステータスの管理がポイントとなります。CRUD処理によるステータス管理/差分管理をご自身で実装するのはかなり大変です。
WatermelonDBにはこちらのステータス管理の大枠を担ってくれる機能があります。クラウドDB側でCUD操作しないならば、WatermelonDB の機能だけで完結します。(クラウドDB側でCUD操作するならば、last_modifiedなどのカラムが必要となりますが、それだけで十分です)
以下が WatermelonDB を経由して作成したテーブルなのですが、自動的に _status
カラムを作成し、同期ステータスの管理をしてくれます。そのため、無駄にデータをやり取りする必要がなくなり、通信時間や通信回数の削減に繋がります。
(3) 【WatermelonDB】SQLiteよりも優れたパフォーマンス
当然のことですが、特にデメリットがない場合にはパフォーマンスを求めたいです。
素のSQLiteを利用するよりも、WatermelonDBを利用した方がパフォーマンスが優れております。WatermelonDBはSQLiteをベースとしつつも、アクセスの最適化やメモリキャッシュの活用などにより、SQLiteよりも優れた速度でデータベース操作を行うことができます。
先述の通り、WatermelonDBはエコシステムが成熟しておらず、情報は少ないですが、機能的なデメリットは感じられないので、WatermelonDBを採用する方針としました。
(4) 【Supabase】Cloudでのセキュリティ実装(Paasに任せることが可能)
セキュリティはアプリ開発の中でも最も大切な部分です。一方で、個人開発において工数削減も重要な課題です。
AuthenticationやDatabase、Edge Functionなどの多様な機能が All In One で提供されているSupabaseを利用することによって、低工数で認可認証機能を用いた Database 操作や API 実行などを実現することができます。
特に実現したいのが RLS(Row Level Security) です。行レベルでのデータアクセス制御が比較的簡単に実現できます。
(5) 【Supabase/WatermelonDB】クラウドコストの削減
モバイルアプリで常にクラウドDBに対してCRUD処理を実行しているとCloud側への負荷が上がります。すると、レスポンス待ち時間が増加によるユーザー体験悪化や、Cloudコスト上昇にもつながります。
WatermelonDB の同期機能を利用することにより、普段はローカルDBへのCRUD処理を行い、必要に応じて、クラウドDBへアクセスする作りにできます。
その際にもSupabaseが提供する Edge Functions と Authentication を使用することで、セキュアに、かつ、必要性最低限の通信にとどめることができます。
この手法により、ユーザー体験がよりよいものになり、なおかつ、Cloudコスト上昇も防ぐことができます。(Supabase は従量課金ではなく月額課金なので、ダイレクトにコスト削減になるわけではないですが、CPU/Memory 負荷起因によるプランアップグレードは避けられるかなと思います。)
この構成のデメリット
POCするなかでデメリットも見つけたのでまとめます。
(1) WatermelonDB が Expo との親和性が悪い
Expo に対応するための WatermelonDB の npm パッケージもあるのですが、私の場合、参照エラーとなり、開発が進まなくなりました。
もっと粘り強く改修することでExpoを利用しての WatermelonDB 使用もできたかもしれないですが、どんどん複雑になっていく感覚を覚えたので、バニラな React Native を利用することとしました。
(2) エコシステムが成熟していないため、情報量が少ない
WatermelonDB に関する情報は少ないです。あくまでライブラリなので公式を見ればある程度の情報は載っていますが、第三者の考察記事などを重宝する場合には、採用が難しいかもしれません。
手順(1) Supabase 側の準備
実装手順をまとめました。まずは Supabase 側の準備からです。
(1-1) Supabase にてプロジェクトを作成する
以下の公式サイトの Start your project
よりプロジェクトを作成してください。
(1-2) 以下の情報を控えておく
① ANON Key
Project Settings
> API Keys
から取得可能です。
こちらはクライアント側に持たせるKeyです。サーバー側にアクセスした際に匿名ユーザーとしてのロールとして機能します。RLSを実装すれば、クライアント側に持たせることに関してのセキュリティ的な問題はないです。(本記事ではRLSの実装まで取り扱います)
② Project ID
Project Settings
> General
から取得可能です。
https://{ProjectID}.supabase.co
がこの後用意するAPIのドメインになるので控えておきます。
手順(2) ローカルDB側の実装
ローカル側の React Native のアプリケーション側での準備をしていきます。
(2-1)ReactNative のプロジェクトを作成する
先述の通り、WatermelonDB は Expo との相性が悪いです。私は React Native CLI を使用することとしました。以下のコマンドで React Native のアプリケーションを作成します。
npx @react-native-community/cli init study-app
(2-2) ローカルDBの構築を行う
以下の WatermelonDB 公式ページに従って、インストールしていきます。この手順では npm を用いて構築していきます。
WatermelonDB の npm パッケージインストール
以下をコマンド実行します。
npm install @nozbe/watermelondb
Babel pulugin のインストール
以下をコマンド実行します。
npm install -D @babel/plugin-proposal-decorators
Babel の設定ファイルを作成する
Babel の設定に関しては、公式で提示されている .babelrc
よりも babel.config.js
の方が推奨されているので、ファイルを追加し、以下のコードを記載すると良いと思います。
iOS/Android/Web に必要なパッケージをインストール
それぞれ公式ページに記載がございます。
SQLite の npm パッケージをインストールする
WatermelonDB では SQLite を使用するので、npm パッケージをインストールします。以下のコマンドを実行します。
npm install -D better-sqlite3
(2-3) ローカルDBにてテーブルを作成する
テーブルを作成していきます。基本的には公式ページの通りに実施すればよいです。
私が詰まったところを特筆してお伝えすると以下の通りです。
DB のテーブル/カラム定義で重要なのは schema.js と migrations.js
schema.js
で最新の状態を定義。 migrations.js
にてその状態までのステップを定義します。そして、schema.js のバージョンと migrations.js の最新バージョンは一致する必要があります。
おそらく私のコードを見るのが一番手っ取り早いです。
以下の通り、双方が同じバージョンを指し示しています。
【schema.js(schema.ts)】
【migrations.js(migrations.ts)】
各テーブルに user_id カラムを含めることを忘れないこと
お好みにテーブル定義すればよいですが、RLS実装のために user_id をテーブルに保持する必要があるので、各テーブルに忘れずに含めるようにしましょう。
【例】
DB Browser for SQLite が便利
ローカルDBを参照する際には、 DB Browser for SQLite
を活用するとよいです。以下のように簡単にDBに格納されたデータ/テーブル情報を確認することができます。
(2-4) ローカルDBへの CRUD 処理を実装する
公式の通りに進めていただければ問題ないです。モデルクラスを用意し、それに対してCRUD処理を書く形です。
【モデルクラス】
【Register処理のサンプル】
(2-5) Supabase を用いて Auth を実装する
Supabase を用いた Auth は比較的実装しやすいです。以下2つを実装するだけで事足ります。
- Supabase client の作成
- Supabase client を利用するUI
【Supabase client の作成】
【Supabase client を利用するUI】
(2-6) Supabase Database にテーブルを作成する
Supabase Database 側にテーブルを作成します。基本的にはローカルDBと同じ構成にします。Supabase Studio から直接テーブル作成しても良いですが、個人的には migration の軌跡を残すためにも Supabase CLI を用いた Supabase Database 操作がおすすめです。
手順は以下の通りです。
migration ファイルを作成する
以下のコマンドで migrartion ファイルを作成します。
supabase migration new xxxxxxx
DDL を記載する
直前の手順で自動作成されたファイルにテーブル定義のDDLなどを記載します。以下がサンプルです。
各種ロールが持つ権限を剥奪する
各種ロールが持つ権限を剥奪します。現状のままだと各種ロールが作成したテーブルに対してのアクションについて全ての権限を持っているので、剥奪します。migration ファイル経由の実行をおすすめしておりまして、以下がサンプルファイルです。
authenticated ロールがCRUD操作可能にする
前操作にて authenticated ロールもテーブルに対する操作ができなくなりましたので、改めて、CRUD操作に絞り権限を付与していきます。以下がサンプルファイルです。
(2-7) 【超重要】 RLS を Enable にする
現状だとAuthenticated ロールがすべての行に対して、CRUDが可能な状態となっております。なので、RLSを Enable にすることで、行単位での制御が可能となります。
RLSを Enable にすると、すべての行に対してアクセスすることができなくなります。次のステップで行ごとにアクセスできるポリシーを作成し、authenticated ロールにアタッチします。
authenticated ロールに対してCRUD権限を付与する
ユーザーIDを参照し、一致するものに対してCRUD処理を許容する Policy を作成し、authenticated ロールに対してアタッチします。以下がサンプルコードです。
(2-8) Supabase Edge Functions に同期用関数を作成する
WatermelonDB において Backend 側のResponse要件は以下のページに記載されております。
こちらをSupabase Edge Functions を利用した場合のコードのサンプルを以下に示します。
Pull(クラウドDB -> ローカルDB)用の関数
Push(クラウドDB <- ローカルDB)用の関数
手順(3) ローカルアプリ側の実装(同期処理)
WatermelonDB において Frontend 側の実装サンプルは以下のページに記載されております。
以下が私の作成したサンプルコードになります。
最後に
本構成を使用したアプリケーションはこれからの作成になります。アプリケーションを作成する中で気づいた点などがあれば、アップデートしていきたいと思います。
Discussion