PGliteのLive Queries機能を試す

IT

前回は、PGliteの導入から基本的なクエリの実行までを試しました。
今回は、PGliteの機能「Live Queries」を中心に、さらに掘り下げていきます。
(※まだ前回の記事を読んでいない方は、そちらを先に読むと理解しやすいと思います。)

前提

以下の環境で動作確認を行っています。

oswindows11(4cpu、16GB)
PGlite0.2.17
typescript5.8.2
vue3.5.13
IDEVisual Studio Code

npm create vue@latest で作成したプロジェクトで検証しています。事前にお好みの方法で環境を用意してください。

Live Queries

Live Queriesは、クエリ結果を購読(サブスクライブ)し、データの追加・更新・削除があった際に自動的に変更を受け取ることができる仕組みです。
一度購読してしまえば、都度クエリを再発行する必要がありません。

queryメソッド

queryメソッドは、少量データの購読に適したシンプルなLive Query機能です。

以下のコードでは、PGliteインスタンス生成時にliveオプションを指定し、Live Queries機能を有効にしています。テーブル作成と初期データの挿入後、live.queryメソッドを使ってクエリを購読します。データの変化があるたびに、コールバックが呼ばれresultに最新データが格納されます。

<script setup lang="ts">
import { PGlite } from '@electric-sql/pglite'
import type { Results } from '@electric-sql/pglite'
import { live, type LiveNamespace } from '@electric-sql/pglite/live'
import { onMounted, ref } from 'vue'

let db: PGlite & { live: LiveNamespace }

onMounted(async () => {
  db = await PGlite.create({
    dataDir: 'idb://testdb',
    extensions: {
      live,
    }
  })

  await db.exec(`
    CREATE TABLE IF NOT EXISTS todo (
      id SERIAL PRIMARY KEY,
      task TEXT,
      done BOOLEAN DEFAULT false
    );
    INSERT INTO todo (task, done) VALUES ('Install PGlite from NPM', true);
    INSERT INTO todo (task, done) VALUES ('Load PGlite', true);
    INSERT INTO todo (task) VALUES ('Update a task');
  `)

  db.live.query<Todo>('SELECT * FROM todo', [], (res) => {
    result.value = res
  })
})

type Todo = {
  id: number
  task: string
  done: boolean
}

const result = ref<Results<Todo>>({
  rows: [],
  affectedRows: 0,
  fields: [],
})

const getAsync = async () => {
  result.value = await db.query<Todo>('SELECT * FROM todo')
}

const task = ref('')
const addAsync = async () => {
  await db.exec(`INSERT INTO todo (task) VALUES ('${task.value}')`)
}

const deleteAsync = async (task: string) => {
  await db.exec(`DELETE FROM todo WHERE task = '${task}'`)
}
</script>

todoリスト表示部分のHTMLは以下のとおりです。

    <ul>
      <li v-for="todo in result.rows" :key="todo.id">
        <input type="checkbox" v-model="todo.done" />
        {{ todo.task }}
      </li>
    </ul>

<ul>要素内でリアルタイムにデータが更新される様子を確認できます。
なお、TRUNCATE操作は購読結果に反映されませんでした。

incrementalQuery

incrementalQueryは、大量のデータやReactとの相性が良いメソッドです。内部的に一時テーブルを利用しており、クエリにおいて一意のキー(通常は主キー)を指定する必要があります。

このキーをもとに差分検知が行われるため、例えばtask列のように重複する可能性があるカラムをキーにしてしまうと、同じタスク名の登録や削除は正しく検知されません。

// idをキーとして指定  
db.live.incrementalQuery<Todo>('SELECT * FROM todo', [], 'id', (res) => {
    result.value = res
})

基本的な動作はqueryとほぼ同じです。

changesメソッド

changesメソッドは、incrementalQueryの裏側で使用されている低レベルAPIで、差分だけを扱いたい場合に便利です。

このメソッドでは、操作の種類(INSERT / UPDATE / DELETE)と変更内容を含んだChange型の情報がコールバックで受け取れます。

 const newResult = ref<Change<Todo>[]>()
  db.live.changes<Todo>(
    'SELECT * FROM todo', [], 'id', (res) => {
      newResult.value = res
    },
  )

以下のように表示することで、操作ごとの変更内容を確認します。

    <ul>
      <li v-for="res in newResult" :key="res.id">
        <div>Operation: {{ res.__op__ }}, id: {{ res.id }},task: {{ res.task }}</div>
      </li>
    </ul>

まずは登録です。登録を示すINSERTと登録した情報(IDとtask)が確認できます。

次に更新です。登録と同じ情報が確認できます。

そして削除です。削除はid以外の情報は取得できません。

なお、queryメソッドでは、Truncateしてもコールバックは呼ばれませんでした。changesメソッドもTruncate時はコールバックが呼ばれません。しかし、内部的には削除操作の情報を保持しており、Truncateしてから登録すると、INSERTだけでなくDELETEの情報も受け取ります。

まずはタスクが3つある状態でTruncateします。

この状態で1つタスクを登録します。すると、今回登録した情報だけでなく、先ほど実行したTruncateによって削除された情報も取得できます。

まとめ

PGliteLive Queries機能を使えば、非常にシンプルな実装でリアルタイムにデータ同期を行うことができます。特に、ローカルDBとのスムーズな連携や、小〜中規模のデータストアで手軽にライブ感を出したい場面に最適です。

コメント