はじめに
本記事はPostgreSQLの同期エンジンであるElectricSQLをPhoenixに組み込んで、
APIを構築することなくモバイルアプリからリアルタイムにデータを取得する方法を解説します
ElectricSQLとは?
ElectricSQLは、リアルタイムで同期可能なローカルファーストデータベースを提供するプロダクトです
以下のような特徴があります:
✅ 主な機能と特徴
機能 | 内容 |
---|---|
📖 リアルタイム読み取り同期 | PostgreSQL → クライアントへの リアルタイムデータ同期に対応(書き込み同期は未対応) |
📦 PostgreSQL ネイティブ対応 | 既存の Postgres スキーマ・データ・SQL をそのまま利用可能 |
🧩 部分レプリケーション(Shapes / DDLX) | クエリルール(shape)や DDL 拡張で必要な行だけを選んで同期 |
🌍 マルチクライアント同期 | 複数の端末に対して即時に読み取りデータを共有 |
🌐 PGLite(WASM Postgres)対応 | ブラウザ / Node.js 上で動作する Postgres と同期できる軽量クライアント |
☁️ Electric Cloud(β) | 30秒でセットアップ可能なマネージドクラウドサービス(現在β公開中) |
🛠️ 幅広いクライアント SDK | TypeScript、React、React Native、Expo、Phoenix などをサポート(書き込み非対応) |
📡 CDNベースの高速配信 | スケーラブルなアーキテクチャで数百万ユーザーにリアルタイム配信 |
⚠️ 現時点での制限事項
- 書き込み(Insert / Update / Delete)の同期には対応していません
- 書き込みは通常の API 呼び出し等で別途対応する必要があります
🧭 今後の展望
- 双方向同期(read/write)のサポートがロードマップに含まれています
- CRDT による競合解決の仕組みも検討されています
by chatgpt
更に要約すると、モバイル用途に特化した同期をリアルタイムに行うエンジンで、Elixirで記述されています
まだ実装はReadだけですが、APIを実装せずにEctoクエリを書くだけで従来のコードをほとんど変更することなくリアルタイム同期を行えるやばいやつです
どっちかというとReactNative等TSなどのフロントエンド環境のバックエンドとして使用することを想定しているようです
(Elixirのモバイルはニッチなのでしょうがない)
Phoenix.Sync
Phoenix.SyncとはElecticSQLを簡単にPhoenixに組み込むライブラリでクライアント、サーバー両方で使います
使い方は既存のControllerやPlug、LiveViewで専用の関数を使うだけでほとんど既存の書き方を変えることなく同期を行えます
今回はLiveViewを例を紹介します
LiveView streamの同期
Phoenix.LiveView.stream/3
をPhoenix.Sync.LiveView.sync_stream/4
に入れ替えると、LiveViewが自動的にPostgresデータベースの最新の状態に保たれます:
Postgresの更新が入るとhandle_info
で受け取り、sync_stream_update
でデータが同期されます
defmodule MyWeb.MyLive do
use Phoenix.LiveView
import Phoenix.Sync.LiveView
def mount(_params, _session, socket) do
{:ok, sync_stream(socket, :todos, Todos.Todo)}
end
def handle_info({:sync, event}, socket) do
{:noreply, sync_stream_update(socket, event)}
end
end
この例だと全件取得されるので、絞り込みをするときは通常のEcto.Queryを使います
未完了のTodoを取得する場合は以下のような関数を作って、第3引数にわたすと未完了のTodo一覧が同期されます
def incomplete() do
from(todo in Todo, where: todo.status == :incomplete)
end
サンプルの構築
ElectricSQLはまだクライアントからの書き込みを同期する機能はないので、Readオンリーなアプリとして
ニュース配信アプリを作ろうかと思います
Postgresqlの設定
Electricですが論理レプリケーションを使うためにwal_levelをreplicaからlogicalに変更する必要があります
203 # - Settings -
204
205 wal_level = logical # minimal, replica, or logical
構成
配信サーバーとしてPhoenix、クライアントアプリとしてElixirDesktopを構築します
配信サーバーにはElecrticをいれ、サーバー、クライアント双方にPhoenix.Syncを組み込み同期を行います
構築は基本こちらに沿って行います
配信サーバーの構築
通常のPhoenixアプリを作成します
mix phx.new newstand
cd newstand
mix ecto.create
次に必要なライブラリを追加します
defp deps do
[
...
{:electric, "~> 1.0"},
{:phoenix_sync, "~> 0.4"}
]
end
mix deps.get
Phoenix.Syncの設定
httpサーバーと同じにするとうまく動かないので別のポートで起動します
config :phoenix_sync,
env: config_env(),
mode: :http,
http: [
port: 3000
],
repo: Newstand.Repo,
url: "http://localhost:3000"
CRUD画面
記事作成画面を作ります
mix phx.gen.live Articles Article articles title:string content:string category:string
mix ecto.migrate
routerに以下のように追加します
scope "/", NewstandWeb do
pipe_through :browser
get "/", PageController, :home
+ live "/articles", ArticleLive.Index, :index
+ live "/articles/new", ArticleLive.Form, :new
+ live "/articles/:id", ArticleLive.Show, :show
+ live "/articles/:id/edit", ArticleLive.Form, :edit
end
配信サーバーは以上で終わりです
クライアントアプリの作成
通常のPhoenixアプリを作成し、destkop_setupでElixirDesktopのアプリ化します
DBは必要ないのですがphx.gen.liveが実行できなくなるので sqliteを指定しています
mix phx.new newstand_mobile --database sqlite3
cd newstand_mobile
mix ecto.create
ライブラリの追加とデスクトップアプリ化
defp deps do
[
...
{:desktop_setup, github: "thehaigo/desktop_setup", only: :dev},
{:phoenix_sync, "~> 0.4"}
]
end
mix deps.get
mix desktop.install
Phoenix.Syncの設定
config.exs
に以下を追加します
config :phoenix_sync,
env: config_env(),
mode: :http,
url: "http://localhost:3000"
CRUDの作成
以下のコマンドを実行します
実行後はマイグレーションは不要なのでpriv/repo/migration/xxx_create_articles.exs
は削除します
mix phx.gen.live Articles Article articles title:string content:string category:string
routerに以下のリンクを追加します
scope "/", NewstandMobileWeb do
pipe_through :browser
get "/", PageController, :home
+ live "/articles", ArticleLive.Index, :index
+ live "/articles/new", ArticleLive.Form, :new
+ live "/articles/:id", ArticleLive.Show, :show
+ live "/articles/:id/edit", ArticleLive.Form, :edit
end
起動後に開くページを変更します
{:ok, _} =
Supervisor.start_child(sup, {
Desktop.Window,
[
app: @app,
id: NewstandMobileWindow,
title: "newstand_mobile",
size: {400, 800},
- url: "http://localhost:#{port}"
+ url: "http://localhost:#{port}/articles"
]
})
sync_streamに置き換え
stream/4をsync_stream/4に差し替えます
defmodule NewstandMobileWeb.ArticleLive.Index do
use NewstandMobileWeb, :live_view
alias NewstandMobile.Articles
+ import Phoenix.Sync.LiveView
@impl true
def render(assigns) do
~H"""
...
"""
end
@impl true
def mount(_params, _session, socket) do
{:ok,
socket
|> assign(:page_title, "Listing Articles")
- |> stream(:articles, Articles.list_artciles())}
+ |> sync_stream(:articles, Articles.Article)}
end
+ @impl true
+ def handle_info({:sync, event}, socket) do
+ {:noreply, sync_stream_update(socket, event)}
+ end
@impl true
def handle_event("delete", %{"id" => id}, socket) do
article = Articles.get_article!(id)
{:ok, _} = Articles.delete_article(article)
{:noreply, stream_delete(socket, :articles, article)}
end
end
動作確認
クライアント側はDBに繋いでないのでReadのみ配信サーバーのデータの同期になります
配信サーバー側の変更が即時同期されるのを確認できました
クエリの例
条件をつけて取得、一件のみ取得の場合は以下のようになります
sync_stream
に直でもいいですが、Articles.list_articles()など既存を書き換える方が従来と変わりないがないので良いかと思います
# articles.ex
def list_articles(category) do
from(article in Article, where: article.category == ^category)
end
def get_article(id) do
from(article in Article, where: article.id == ^id, limit: 1)
end
# index.ex
def handle_event(%{"category" => category }, socket) do
{:noreply, sync_stream(socket,:articles, Articles.list_article(category)}
end
# show.ex
def mount(%{"id" => id}, session, socket) do
{:ok, sync_stream(socket,:article, Articles.get_article(id))}
end
@impl true
def render(assigns) do
~H"""
<Layouts.app flash={@flash}>
<%= {_id, article} <- @stream.article do %>
<.header>
Article {article.id}
<:subtitle>This is a article record from your database.</:subtitle>
<:actions>
<.button navigate={~p"/articles"}>
<.icon name="hero-arrow-left" />
</.button>
<.button variant="primary" navigate={~p"/articles/#{article}/edit?return_to=show"}>
<.icon name="hero-pencil-square" /> Edit article
</.button>
</:actions>
</.header>
<.list>
<:item title="Title">{article.title}</:item>
<:item title="Content">{article.content}</:item>
<:item title="Category">{article.category}</:item>
</.list>
<% end %>
</Layouts.app>
"""
end
運用に関して
今回はPhoenixにPhoenix.Syncを組み込んで同期サーバーとしましたが
Electric Cloudという同期サービスのみのホスティングがあるのでそちらを使うとより簡単に運用できるかと思います
最後に
クライアントからのWriteはまだAPI経由でしかできませんが、Readに関しては素晴らしい開発体験ができるかと思います
また従来のWebアプリケーションのLiveViewでもリアルタイム更新は従来はPubSubで行う必要があり別の複数のサーバーの場合はRedisを使用する必要がありましたが、これを使うことで簡単に実装ができると思われます
今後の発展が非常に楽しみなライブラリなため引き続き情報を追っていきたいと思います
本記事は以上になりますありがとうございました