📦

ElectronでPrismaを使う

2023/06/09に公開

Dev modeで起動するだけなら何も考えず使えるんですが、ビルド時の設定が必要なのと、アプリ起動時にマイグレーションしようとすると少々面倒です。

検証に使ったバージョン

  • electron@25.0.1
  • prisma@4.15.0
  • electron-builder@23.6.0
  • electron-vite@1.0.23

今回はバンドルにelectron-vite、ビルドにelectron-builderを使用しましたが、他のツールを使った場合でも似たような問題があると思われる。

macOS 13.3.1(arm64)でしか検証してません。Win/Linux/Intel Mac対応する場合には、この記事に書かれていることだけでは不足するかもしれません。

SQLiteを使う場合のファイルの場所

ユーザデータの保存先ディレクトリは、Electronのapp.getPath('userData')で取得できます。

このような感じで、devとprodで分けてやるとよいでしょう。

import { is } from "@electron-toolkit/utils";
const dbUrl = is.dev
    ? "file:./dev.db"
    : `file:${path.join(app.getPath("userData"), "app.db")}`;

ビルドマシンと異なるプラットフォームに対応させる

Prisma clientはネイティブライブラリを使用しているため、クライアント生成時に各プラットフォーム用のバイナリを含める必要があります。

schema.prisma
generator client {
  provider      = "prisma-client-js"
  // Intel MacとARM Macに対応させる例
  binaryTargets = ["native", "darwin", "darwin-arm64"]
}

クライアントの設定は npx prisma generateで反映させてください。

ビルド時の設定

バンドルしただけだとなぜかprismaのimportに失敗するので、extraResourcesで関連ファイルを指定する必要があります。

electron-build.yml
extraResources:
  - './node_modules/.prisma/**'
  - './node_modules/@prisma/**'

アプリ起動時にマイグレーションする

現在のPrismaはprisma migrateコマンド経由でマイグレーションを実施することを前提としており、マイグレーションAPIは提供されているものの、仕様はまだ不安定です。
(APIが必要な方はアンケートにご協力ください)

さて、真面目にマイグレーション機能を作る場合は、必要なファイルをバンドル時に含めた上でprisma migrateコマンドをアプリから呼ぶことになります(紹介記事およびelectron-prisma-trpc-exampleリポジトリ参照)。

しかしちょっとSQLを実行するために大がかりな仕掛けを用意するのは美意識に反する!!!頻繁にスキーマ変わるわけでもないしこんなもんでええやろ。と言って書いたコードがこちら:

migrate.ts
// LICENSE: WTFPL

type Migration = {
  readonly id: string;
  sql: string;
};
const migrations: readonly Migration[] = [
  {
    id: '20230609_initial',
    sql: `create table foo(id integer not null); create table bar(name string);`
  },
  // ...
]

async function execute(
  query: string,
  exec: (singleQuery: string) => Promise<void>
): Promise<void> {
  for (const q of query.split(";")) {
    if (q.trim() === "") continue;
    await exec(q);
  }
}

export async function migrate<P extends PrismaClientTypeArgs>(
  prisma: PrismaClientFor<P>
): Promise<void> {
  await prisma.$executeRaw`create table if not exists _migrations(
    id text primary key not null
  )`;

  const rawResult =
    await prisma.$queryRaw`select id from _migrations order by id`;
  const appliedIds = new Set(
    (rawResult as unknown[]).map((row) => {
      const typed = row as { id: string };
      return typed.id;
    })
  );

  for (const { id, sql } of migrations) {
    if (appliedIds.has(id)) continue;
    console.log(`Migration: ${id}`);
    await execute(sql, async (q) => {
      console.log(`Execute: ${q}`);
      await prisma.$executeRawUnsafe(q);
    });
    await prisma.$executeRaw`insert into _migrations(id) values (${id})`;
    console.log("Applied: ${id}");
  }
}

あとはmainプロセスで await migrate(prisma) を呼んでやればマイグレートされます。良かったですね。

Discussion