electric-schema-shapes について
Electric と @electric-sql/client を使う開発者向けに、Shape 同期を想定したスキーマ設計や複数テーブルの扱い、楽観的書き込みを解説するスキルです。単一テーブルの Shape、クライアント側の結合、トランザクション内の txid 連携を扱います。
主な機能
- 01単一テーブル向けの Shape 設計
- 02複数 Shape のクライアント側結合
- 03レプリカモードの選択
- 04トランザクション内での txid 取得
- 05列挙型のフィルター処理
活用シーン
- 01Shape 同期向けのテーブル設計
- 02クライアント側での複数テーブル結合
- 03楽観的書き込みと同期結果の整合
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ electric-sql/ electric --skill electric-schema-shapesインストール後、Claude Code で/electric-schema-shapesを入力して読み込まれたか確認します。
詳細
このスキルは electric-shapes を基盤としています。ShapeStream の設定については、先にそちらをお読みください。
Electric — スキーマとシェイプ
セットアップ
各シェイプが同期するテーブルは1つなので、それを踏まえてテーブルを設計してください。テーブルをまたぐデータには、複数のシェイプを使い、クライアント側で結合してください。
-- Schema designed for Electric shapes
CREATE TABLE todos (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
org_id UUID NOT NULL,
text TEXT NOT NULL,
completed BOOLEAN DEFAULT false,
created_at TIMESTAMPTZ DEFAULT now()
);
ALTER TABLE todos REPLICA IDENTITY FULL;
import { ShapeStream } from '@electric-sql/client'
const todoStream = new ShapeStream({
url: '/api/todos', // Proxy sets: table=todos, where=org_id=$1
})
コアパターン
複数のシェイプを使ったテーブル間のデータ
// Each shape syncs one table — join client-side
const todoStream = new ShapeStream({ url: '/api/todos' })
const userStream = new ShapeStream({ url: '/api/users' })
// With TanStack DB, use .join() in live queries:
// q.from({ todo: todoCollection })
// .join({ user: userCollection }, ({ todo, user }) => eq(todo.userId, user.id))
レプリカモードの選択
// Default: only changed columns sent on update
const stream = new ShapeStream({ url: '/api/todos' })
// Full: all columns + old_value on updates (more bandwidth, needed for diffs)
const stream = new ShapeStream({
url: '/api/todos',
params: { replica: 'full' },
})
楽観的書き込みのためのバックエンド txid ハンドシェイク
変更と同じトランザクション内で pg_current_xact_id()::xid::text を呼び出してください。トランザクション外でクエリすると、異なる txid が取得されるため、クライアント側で調整されません。
// API endpoint — txid MUST be in the same transaction as the INSERT
app.post('/api/todos', async (req, res) => {
const client = await pool.connect()
try {
await client.query('BEGIN')
const result = await client.query(
'INSERT INTO todos (id, text, org_id) VALUES ($1, $2, $3) RETURNING id',
[crypto.randomUUID(), req.body.text, req.body.orgId]
)
const txResult = await client.query(
'SELECT pg_current_xact_id()::xid::text AS txid'
)
await client.query('COMMIT')
// txid accepts number | bigint | `${bigint}`
res.json({ id: result.rows[0].id, txid: parseInt(txResult.rows[0].txid) })
} finally {
client.release()
}
})
// Client awaits txid before dropping optimistic state
await todoCollection.utils.awaitTxId(txid)
よくある間違い
HIGH 複数のテーブルにまたがるシェイプの設計
誤り:
const stream = new ShapeStream({
url: '/api/data',
params: {
table: 'todos JOIN users ON todos.user_id = users.id',
},
})
正しい例:
const todoStream = new ShapeStream({ url: '/api/todos' })
const userStream = new ShapeStream({ url: '/api/users' })
シェイプは単一テーブルのみを対象とします。テーブルをまたぐデータには、複数のシェイプを使い、TanStack DB のライブクエリでクライアント側から結合する必要があります。
出典:AGENTS.md:104-105
MEDIUM WHERE で enum カラムを使う際に text へキャストしない
誤り:
// Proxy route
originUrl.searchParams.set('where', "status IN ('active', 'done')")
正しい例:
originUrl.searchParams.set('where', "status::text IN ('active', 'done')")
WHERE 句で enum 型を使うには、明示的な ::text キャストが必要です。キャストしないと、クエリが失敗したり、予期しない結果を返したりする場合があります。
出典:packages/sync-service/lib/electric/replication/eval/env/known_functions.ex
HIGH 楽観的書き込みのための txid ハンドシェイクを設定しない
誤り:
// Backend: just INSERT, return id
app.post('/api/todos', async (req, res) => {
const result = await db.query(
'INSERT INTO todos (text) VALUES ($1) RETURNING id',
[req.body.text]
)
res.json({ id: result.rows[0].id })
})
正しい例:
// Backend: INSERT and return txid in same transaction
app.post('/api/todos', async (req, res) => {
const client = await pool.connect()
try {
await client.query('BEGIN')
const result = await client.query(
'INSERT INTO todos (text) VALUES ($1) RETURNING id',
[req.body.text]
)
const txResult = await client.query(
'SELECT pg_current_xact_id()::xid::text AS txid'
)
await client.query('COMMIT')
res.json({ id: result.rows[0].id, txid: parseInt(txResult.rows[0].txid) })
} finally {
client.release()
}
})
txid がないと、Electric から同期済みのバージョンが届く前に楽観的な状態が破棄され、UI がちらつきます。クライアントは awaitTxId(txid) を使い、実際のデータが同期されるまで楽観的な状態を保持します。
出典:AGENTS.md:116-119
関連項目:electric-shapes/SKILL.md — シェイプは不変です。動的フィルターには、新しい ShapeStream インスタンスが必要です。 関連項目:electric-orm/SKILL.md — スキーマ設計はシェイプ(読み取り)と ORM クエリ(書き込み)の両方に影響します。
バージョン
@electric-sql/client v1.5.10 を対象としています。