EY-Office ブログ

ConvexというリアルタイムDBを中心にしたバックエンドが興味深い

Convexという興味深いバックエンド・サービス/開発プラットフォームを知ったので試してみました。

Convexは、リアルタイムDBを中心にしたバックエンド・サービスです、Firebaseのようなサービスと言えば伝わりやすいでしょうか。ただし、

  • JavaScriptレベルのAPI提供ではなく、Reactから使いやすいHooksをサポートしています
  • バックエンドソフトもOSSで自前のサーバーでも運用できるようです

Convex Convexホームページから

リアルタイムDB

実はリアルタイムDBを扱うのははじめてで、とても新鮮です! 下の画像(アニメーションGIF)はいつものジャンケンアプリをです、2つのウィンドウで同じアプリを動かしています。右のウインドウのアプリのボタンを押すと左のアプリの結果も同時に更新されています。

コード説明

Convexを使って驚いたのは、Reactとの親和性の高さです!

src/App.tsx

このコードの基になった純粋なReactのコードではジャンケンの結果はStateで持っていました → Ⓐ 。

export default function App() {
 const [scores, setScores] = useState<ScoreType[]>([]);    // ← Ⓐ

  const pon = (human: Te) :void => {
    const computer = randomHand();
    const judgment = judge(computer, human);
    setScores([{human, computer, judgment}, ...scores]);   // ← Ⓐ
  }

  // ・・・

これをConvex用に書き換えたのが以下のコードです。

  • ① useQuery Hooksでデータベースから取得した値をscores変数に代入しています
    • api.scores.getは後で説明するConvexデータベースの取得関数です
    • Convexデータベースが更新されると、このHooksがAppコンポーネントを再描画します
    • useQueryはテーブルが空でデータが取得できない場合はundefinedを戻すので ?? [] で空配列にしています
    • また型アサーションでScoreType[]型を指定しています
  • ② useMutation Hooksでデータベース更新関数を取得しcreateScore変数に代入しています
    • api.scores.createは後で説明するConvexデータベースの挿入関数です
    • 結果としてデータベースが更新されるので、Appコンポーネントは再描画されます
import { useQuery, useMutation } from "convex/react";
import { api } from "../convex/_generated/api";
import { Judgment, Te, ScoreType } from "./JyankenType";
import JyankenBox from "./JyankenBox";
import ScoreBox from "./ScoreBox";

export default function App() {
  const scores = (useQuery(api.scores.get) ?? []) as ScoreType[];   // ← ①
  const createScore = useMutation(api.scores.create);               // ← ②

  const pon = (human: Te) => {
    const computer = Math.floor(Math.random() * 3) as Te;;
    const judgment = (computer - human + 3) % 3 as Judgment;;
    createScore({human, computer, judgment});
  }

  return (
    <div className="md:ml-8">
      <h1 className="my-4 ml-4 text-3xl font-bold">じゃんけん ポン!</h1>
      <div className="p-3 md:p-6 bg-white md:w-3/5">
        <JyankenBox pon={pon} />
        <ScoreBox scores={scores} />
      </div>
    </div>
  )
}
convex/scores.ts

このファイルはConvexデータベースのscoresテーブルの操作関数の集まりです。

  • get関数はscoresテーブルの全データを取得する関数です
    • 引数argsは無いです
  • ② ORM的なメソッドチェインでデータが取得(SELECT)できます
    • ConvexのデータベースはRDBではないのでSQLは使えません
  • create関数はscoresテーブルにデータを挿入する関数です
    • 引数argsは人間、コンピューターの手、勝敗で、いずれもJavaScriptのnumber型です
    • データベースの扱える型の情報はData Typesにあります
  • ④ これもORM的なメソッドですね
import { query, mutation } from "./_generated/server";
import { v } from "convex/values";

export const get = query({                                        // ← ①
  args: {},
  handler: async (ctx) => {
    return await ctx.db.query("scores").order("desc").collect();  // ← ②
  },
});

export const create = mutation({                                  // ← ③
  args: { human: v.number(), computer: v.number(), judgment: v.number() },
  handler: async (ctx, args) => {
    await ctx.db.insert("scores",                                 // ← ④
      { human: args.human, computer: args.computer, judgment: args.judgment });
  },
});

Convex対応コードはこれだけです❗

Convexの使い方

Convexの使い方はReact Quickstartを見るとわかります、まずはこれを試してみると良いと思いますが、今回のアプリを作った手順を書いておきます。

Reactアプリを作る

ReactアプリはNext.jsやRemixなども使えますが、ここでは久しぶりにViteを使いました。

$ npm create vite@latest convex-jyanken -- --template react-ts
$ cd convex-jyanken
$ npm install

この後Tailwind CSSをインストールし、いつものジャンケンアプリのコードを書きました。

Convexにインストール

いよいよConvexです、Convexをインストールし実行すると、GitHub等での認証が行われConvexのアカウントが作成されます。

$ npm install convex
$ npx convex dev

上が終了するとConvexのアクセス情報が.env.localに書かれます。この情報をプロジェクトにコピーする事で他のアプリでもつかえます。

.env.local
CONVEX_DEPLOYMENT=dev:xxxxxxxxx-yyyy # team: yuumi-yoshida, project: convex-app1
VITE_CONVEX_URL=https://xxxxxxxxx-yyyy .convex.cloud
convex/schema.ts

Convextに作成するテーブル定義ファイルを作成します。書き方はSchemasを参照してください。

ここではnumber(Float64)型のカラムを持つscoresテーブルの定義です。制限付きですが配列やオブジェクト型のカラムも作成できます。
指定してませんが、プライマリーキー(_id)と作成日付(_creationTime)カラムが自動的に作られます。_idはハッシュ文字列です。また、関連キーやインデックスも設定出来ます。

import { defineSchema, defineTable } from "convex/server";
import { v } from "convex/values";

export default defineSchema({
  scores: defineTable({
    human: v.number(),
    computer: v.number(),
    judgment: v.number(),
  }),
});
convex/scores.ts

この後、最初に示したconvex/scores.tsを作りました。書き方はReading DataWriting Dataを参考にしてください。

ドキュメントは少し不親切ですが、Convex DemosのGitHubも参考になります。 ネットを検索しても(英語を含めても)まだ情報は少ないですね。

Action

Convexは当然バックエンドにビジネスロジックなどの関数を作り、それをReactアプリから呼び出す事ができます。
ここではジャンケンを行うpon関数をバックエンドに置いてみました、Convexではこのような関数をActionsと呼んでいます。

src/App.tsx
  • useAction HooksでActionを呼出だすための関数をponに代入しています
  • ② Actionへの引数はオブジェクトなので、無名関数の中かから呼出しました
xport default function App() {
  const pon = useAction(api.scores.pon);   // ← ①
  const scores = (useQuery(api.scores.get) ?? []) as ScoreType[];

  return (
    <div className="md:ml-8">
      <h1 className="my-4 ml-4 text-3xl font-bold">じゃんけん ポン!</h1>
      <div className="p-3 md:p-6 bg-white md:w-3/5">
        <JyankenBox pon={(te) => pon({human: te})} />   {/* ← ② */}
        <ScoreBox scores={scores} />
      </div>
    </div>
  )
}
convex/scores.ts

Actionの定義はqueryやmutationと同様です。

  • ① ジャンケンを行うActionの定義です
    • 引数は人間の手で、型はnumberです
    • 処理は概ね最初のsrc/App.tsxと同じです
    • ただし、Action内部からは直接データベース操作関数を呼び出せないので、内部関数(Internal Functions)を呼び出す必要があります
  • scoresテーブルにデータを挿入する内部関数です
    • mutationではなくinternalMutationで定義しているだけで、最初に出てきたconvex/scores.tsと同じです
    • 内部関数はクライアント(Reactアプリ)からは呼び出せません。クライアントから呼び出させるActionは認証等を行い、安全な環境内から内部関数と呼出すように作る必要があります(こではやってませんが)
  • ③ Action内で内部関数の呼出しています
import { internal } from "./_generated/api";
import { query, internalMutation, action } from "./_generated/server";
import { v } from "convex/values";

export const get = query({
  args: {},
  handler: async (ctx) => {
    return await ctx.db.query("scores").order("desc").collect();
  },
});

export const create = internalMutation({              // ← ②
  args: { human: v.number(), computer: v.number(), judgment: v.number() },
  handler: async (ctx, args) => {
    await ctx.db.insert("scores",
      { human: args.human, computer: args.computer, judgment: args.judgment });
  },
});

export const pon = action({                           // ← ①
  args: { human: v.number() },
  handler: async (ctx, args) => {
    const computer = Math.floor(Math.random() * 3);
    const judgment = (computer - args.human + 3) % 3;

    await ctx.runMutation(internal.scores.create,    // ← ③
      { human: args.human, computer, judgment });
  },
});

まとめ

Firebaseを使ったiOSアプリを作り今でも使っています。 しかし、Reactでフロントエンドを作るならバックエンドはFirebaseではなくConvexがお薦めです。

今回の例でもわかるように、とてもReactととの親和性が高くState管理をuseStateを、useQuery, useMutation, useActionに置き換えてデータベースアクセスやロジックを書けばバックエンドが出来てしまいます。

もちろんConvexを仕事で使って大丈夫なのかは、Googleが運営するFirebaseに比べると不安はあるかも知れませんね・・・
ただしConvexはBackendも含めオープンソース(FSLライセンス)なので、最悪の場合は自前サーバーで運用すれば良さそうです。

リアルタイムDBが活かせるようなアプリがあったら使ってみたいですね。

- about -

EY-Office代表取締役
・プログラマー
吉田裕美の
開発者向けブログ