Prismaはクラウドサービスだと思っていましたが、Next-generation ORMとのこと。 もともとそこまで興味はなかったのですが、Discordで話題に出たので私もやってみたいと思いました。 公式サイトのドキュメントのGetting startedQuickstartSet up Prismaを一通り終えたばかりなのですが、とても直感的だったのでブログにも残そうと思いました。

Prismaの特徴

Node.jsでSQLを扱う選択肢でいえばKnex.js、最近ではObjection.jsなどがありました。 Knex.jsはクエリビルダーでしばらく愛用していました。これ単体でも十分に使いやすいのですが、特に複雑なリレーションシップやjoin配列をオブジェクトに変換するのが若干厄介です。 Objection.jsはKnex.jsの使いにくい部分を改良してこちらも同様に使いやすいのですが、このライブラリを使い始めたときにTypeScriptがまだ使えなかったこともありあまり使い込んでいません。

Knex.jsはクエリビルダーですがmigrationの機能も持ち合わせているのでこれを利用してデータベースを構築していくことになります。 OAuth2の認証サーバーをMongoDBで実装していて思ったのはSQLのようにmigrationがないのが良くも悪くもこれまでと異なります。 テーブルの変更に毎回migrationを実行する必要がないのは良いのですが、既存のデータを変換したいときはやはりmigrationがあったらいいのにと思っていました。

Introspect workflow

PrismaはSQLなのでmigrationは必要なのですが、prisma introspectコマンドを実行するとスキーマが生成されます。 まるで魔法のようですが、このスキーマをもとにprisma generateを実行すると型ファイルも定義されるのでエディタの補完機能を使うこともできます。 このアプローチは今までになかったのでNext-generationなのかも。

既存のプロジェクトを置き換えてみる

このブログの初期に取り組んでいたRailsでEleventyという静的サイトジェネレーターを使うためのプロジェクトを作っていました。 現在はRailsの運用をやめて単純なJekyllに戻しているのでしばらく放置されていたのですが、題材としては良さそうなので再び取り組んでみることにしました。

まずはプロジェクトをcloneしてrails db:structure:dumpを実行してstructure.sqlファイルを生成しました。 現時点ではこのファイルを使うことはなかったのですが、念の為残しておくことにします。 Dockerで書いてあったので、rails db:setup実行後にPostgresのコンテナ以外は消しました。 docker-compose down --remove-orphansを実行するとリンクが切れたコンテナを消してくれるので便利ですね。

続いてちょっともったいない気もしますがRailsはもう使わないのでdbディレクトリ以外はすべて消してしまいました。 チュートリアルにかかれている通りデータベースの接続をします。 SCHEMAは普段意識することがないのですが、ここは?schema=publicのままで問題ありませんでした。 先程書き出したSQLファイルに書かれているのでやはり書き出しておいてよかったのかも。

prisma introspectを実行するとschema.prismaファイルが生成されます。 VSCodeでエクステンションが用意されているのでシンタックスハイライトできるのが有り難いですね。

model articles {
  id               Int             @id @default(autoincrement())
  slug             String?         @unique
  title            String?
  description      String?
  body             String?
  favorites_count  Int?            @default(0)
  user_id          Int
  created_at       DateTime
  updated_at       DateTime
  date             DateTime?
  users            users           @relation(fields: [user_id], references: [id])

  @@index([user_id], name: "index_articles_on_user_id")
}

articlesテーブルを抜粋してみました。 これを以下のように書き換えます:

model Article {
  id             Int       @id @default(autoincrement())
  slug           String?   @unique
  title          String?
  description    String?
  body           String?
  favoritesCount Int?      @default(0) @map("favorites_count")
  userId         Int       @map("user_id")
  createdAt      DateTime  @map("created_at")
  updatedAt      DateTime  @map("updated_at")
  date           DateTime?
  user           User      @relation(fields: [userId], references: [id])

  @@index([userId], name: "index_articles_on_user_id")
  @@map("articles")
}

ここでのポイントは@map()@@map()です。 ドキュメントにも書かれていましたが、Railsはsnake_caseでテーブルが定義されているのでcamelCaseに変換します。 この書き換えが最も時間がかかりましたが、新規で用意したプロジェクトならこの書き換え作業は少なくて済みそうですね。

const prisma = new PrismaClient()

async function main() {
  const articles = await prisma.article.findMany({
    include: { user: true },
  })
  console.log(articles)
}

main()
  .catch((e) => {
    throw e
  })
  .finally(async () => {
    await prisma.$disconnect()
  })

あとは実際にプログラムを動かしてみました。 本当にこれだけでいいのかと思ってしまうほど簡潔なコードです。 KnexではKnexfileで定義していましたが、環境変数にすることでDBの接続すら書かなくていいのかと思うと感動です。

チュートリアルでは単純なCRUDしか見ていませんが、複数のモデルの作成もとても簡潔で直感的です。 極端なことを言えば言語の指定がなければ今後はすべてPrismaでいいんじゃないかと思えるほど。 こういったライブラリが登場するとWebアプリケーションを作るのに生のSQLを書いていたことが一種の語りぐさになっていくでしょうね。