Introduction
Electric は先日TechRadarで知ったフレームワークです。
このフレームワークはWebアプリ/モバイル用のlocal-first syncフレームワークです。
local-firstとは、Web/モバイルアプリがローカルにある組み込みDB(SQLite など)
とやり取りし、大元となるDBとレプリケーションでデータが同期されるアーキテクチャです。
Electricは、ローカルDBにSQLite、大元のセントラルDBにPostgreSQLを使用します。
今回はElectric SQLでサンプルコードを動かしてみます。
Electric SQL?
Electricは、Local-first のアプリを構築するためのシステムです。
ここにあるように、Electricを使うと、
リアクティブ、リアルタイム、Local-first なアプリをPostgres上で
構築することが可能になります。
Local-firstアプリでは、ローカルの組み込みデータベースと直接通信します。
ネットワーク経由の場合と比べて、常に高速に動作します。
Local-first?
「Local-first」とは、データをローカルに保持し、後でサーバーと同期するアプローチです。
この手法によりオフラインでも動作するようになったり、
ユーザーの体感する通信が高速になったりします。
この「Local-first」という言葉は、
Martin Kleppmann氏をはじめとするその筋では有名な人たちが
2019年のマニフェストで作った造語とのことです。
なお、ここで Local-firstとCloud-first を比較したデモを動かせます。
Local-firstのほうが圧倒的に速いのがわかります。
このアプローチの場合、データの整合性ってどうなるの?という疑問には
このへんで解説されてます。
Electric をつかったワークフロー
Electric はクラウド経由でローカルとのデータを同期します。
(現時点では)クラウドにあるセントラル DB は、Postgres を使用します。
Electric を使用した基本的なワークフローは下記です。
1.Postgres に対して Electric の migration 機能を使用してスキーマ定義
2.CLI コマンドを実行して、型安全なクライアントライブラリを生成
3.生成したライブラリを import して使う
4.複数ユーザー/デバイスの同期が必要な場合、ローカルアプリを同期サービスに接続する
Electric の機能でクライアントを生成することで、そのままローカルの
SQLite/PGlite を使用するアプリを構築できます。
バックグラウンド同期を有効にした場合にのみ、データがネットワーク経由で送信されます。
データ同期の詳細についてはこのあたりを確認してみてください。
Environments
- MacBook Pro (13-inch, M1, 2020)
- OS : MacOS 14.3.1
- Node : v20.8.1
Setup yourself
Electricが用意するコマンドを使えば簡単に動かせるのですが、
手動でセットアップすることも可能です。
まずは自分でやってみます。
↓ のような compose.yml を作成し、dockerで起動します。
version: "3.1"
volumes:
pg_data:
services:
postgres:
image: postgres:14-alpine
environment:
POSTGRES_PASSWORD: pg_password
command:
- -c
- wal_level=logical
ports:
- 5432:5432
restart: always
volumes:
- pg_data:/var/lib/postgresql/data
electric:
image: electricsql/electric
depends_on:
- postgres
environment:
DATABASE_URL: postgresql://postgres:pg_password@pg/postgres
DATABASE_REQUIRE_SSL: false
LOGICAL_PUBLISHER_HOST: electric
PG_PROXY_PASSWORD: proxy_password
AUTH_MODE: insecure
ports:
- 5133:5133
- 65432:65432
restart: always
% docker compose -f compose.yaml up
これにより、electric コンテナと postgres コンテナが起動します。
コンテナが起動したら、テーブルを作成して「electrifying」(electric に対応させる処理)します。
直接 postgres に接続するのではなく、proxy の electric 経由で接続します。
% psql -h <electricコンテナのip> -p 65432 -U postgres -W
database・テーブルを作成して ENABLE ELECTRIC コマンドで electrifying します。
postgres=# create database hoge_db;
postgres=# \c hoge_db
You are now connected to database "hoge_db" as user "postgres".
hoge_db=#
CREATE TABLE users (
id int PRIMARY KEY,
name TEXT NOT NULL,
email TEXT NOT NULL
);
#electric proxy経由で接続すると↓のコマンドが使える
hoge_db=# ALTER TABLE users ENABLE ELECTRIC;
ELECTRIC ENABLE
こんな感じで DB のセットアップを手動で実施できます。
詳しくはこのあたりをご確認ください。
クライアントコードの生成も npx で実行できます。
% npx electric-sql generate
Generating Electric client...
Service URL: http://localhost:5133
Proxy URL: postgresql://prisma:**\*\*\*\***@localhost:65432/electric
Successfully generated Electric client at: ./src/generated/client
Building migrations...
Successfully built migrations
こうすることで、src/generated/client に先ほど定義したテーブルに対応する
クライアントが生成されます。
Try
↑ では手動でセットアップしましたが、Githubの example にはセットアップコマンドも含め、
デモがいくつか用意されています。
動かしてみましょう。
electric のリポジトリを clone します。
% cd /path/your/electric-sql
% git clone https://github.com/electric-sql/electric.git
ここではwa-sqliteを使ったデモを動かしてみます。
examples の web-wa-sqlite ディレクトリには、
ローカルの SQLite とセントラル DB の PostgreSQL を sync させるデモが用意されています。
先程は docker compose でコンテナを起動しましたが、
backend:up コマンドを実行すると、DB コンテナと Electric コンテナが起動します。
% cd examples/web-wa-sqlite/
% npm run backend:up
> npx electric-sql start --with-postgres --detach
Starting ElectricSQL sync service with PostgreSQL
Docker compose config: {
SERVICE: 'http://localhost:5133',
・
・
・
}
[+] Running 2/4
✔ Container postgres-1 Started 0.4s
✔ Container electric-1 Started 0.5s
Waiting for PostgreSQL to be ready...
/var/run/postgresql:5432 - accepting connections
PostgreSQL is ready
Electric is ready
ps でコンテナが 2 つ起動していることを確認できます。
% docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
aabeee7f2082 electricsql/electric:0.9 "/app/bin/entrypoint…" 9 seconds ago Up 9 seconds 0.0.0.0:5133->5133/tcp, :::5133->5133/tcp, 0.0.0.0:65432->65432/tcp, :::65432->65432/tcp electric-1
85de8abedae4 postgres:14-alpine "docker-entrypoint.s…" 9 seconds ago Up 9 seconds (health: starting) 0.0.0.0:5432->5432/tcp, :::5432->5432/tcp postgres-1
db:psql コマンドで、postgres に接続できます。
% npm run db:psql
> npx electric-sql psql
psql (14.11)
Type "help" for help.
hoge-#
db/migrations/01-create_items_table.sql には、
migrate コマンドで PostgreSQL にアクセスするための SQL が記述してあります。
ここでは、items テーブルを作成して Electrify するためのコマンドが書いてあります。
-- Create a simple items table.
CREATE TABLE IF NOT EXISTS items (
value TEXT PRIMARY KEY NOT NULL
);
-- ⚡
-- Electrify the items table
ALTER TABLE items ENABLE ELECTRIC;
migrate コマンドを実行して Electic 化したテーブルを作成しましょう。
% npm run db:migrate
> db:migrate
> npx electric-sql with-config "npx pg-migrations apply --database {{ELECTRIC_PROXY}} --directory ./db/migrations"
Applying 01-create_items_table.sql
Applied 01-create_items_table.sql
1 migrations applied
client:generate で、さきほど作成したテーブルに対応した
prisma クライアントが src/generated 下に生成されます。
% npm run client:generate
> client:generate
> npx electric-sql generate
Generating Electric client...
Service URL: http://localhost:5133
Proxy URL: postgresql://prisma:**\*\*\*\***@localhost:65432/hoge
Successfully generated Electric client at: ./src/generated/client
Building migrations...
Successfully built migrations
実際にTypeScriptでElectricのクライアントでアクセスしているのは↓のような感じです。
コード内容についてはこのあたり参照。
<br />・・・
export const App = () => {
const [electric, setElectric] = useState()
useEffect(() => {
const init = async () => {
const config = {
debug: import.meta.env.DEV,
url: import.meta.env.ELECTRIC_SERVICE
}
const { tabId } = uniqueTabId()
const scopedDbName = `basic-${LIB_VERSION}-${tabId}.db`
const conn = await ElectricDatabase.init(scopedDbName)
const electric = await electrify(conn, schema, config)
await electric.connect(authToken())
setElectric(electric)
}
・・・
}, [])
・・・
}
const ElectricComponent = () => {
const { db } = useElectric()
const { results } = useLiveQuery(
db.items.liveMany()
)
useEffect(() => {
const syncItems = async () => {
const shape = await db.items.sync()
await shape.synced
}
syncItems()
}, [])
const addItem = async () => {
await db.items.create({
data: {
value: genUUID(),
}
})
}
・・・
}
vite でビルドしてアプリを実行してみましょう。
% npm run build
> build
> vite build
・
・
・
% npm run dev
> dev
> vite
VITE v5.2.8 ready in 110 ms
➜ Local: http://localhost:5173/
➜ Network: use --host to expose
➜ press h + enter to show help
ブラウザでデモアプリにアクセスしてみます。
Add ボタンで item を追加します。
クライアントから直接アクセスしているのはローカルの SQLite です。
Summary
2024/04時点ではPublicアルファですが、
Local-firstアプローチはUXにおいて使えそうな技術なので、
いまのうちから触れておきたいところです。