投稿日: 2022-10-29
Prismaは、Node.jsとTypeScriptのためのデータベースの周辺ツールです。具体的には、データベースクライアントのPrisma Client、データベースのマイグレーションを行うPrisma Migrateなどがあります。この2つは、どちらもPrisma schemaという独自のスキーマファイルを用います。
Prisma schemaでは、例えば以下のようにスキーマを定義します。generator
は生成するPrisma Clientの設定で、datasource
は接続するデータベースの設定です。model
は作成するテーブル(モデル)の定義です。
generator client { provider = "prisma-client-js" } datasource db { provider = "mysql" url = env("DATABASE_URL") } model User { id Int @id @default(autoincrement()) username String @unique email String @unique password String createdAt DateTime @default(now()) updatedAt DateTime @updatedAt }
Prisma Clientの使い心地を試してみました。お題には、Realworldという記事投稿サービスのテーブルを使いました。
コードはこちら: https://github.com/tekihei2317/prisma-playground
リレーション以外の部分は、型のついたSQLのように書けるのでとっつきやすかったです。一方で、ActiveRecord系のフレームワークと比較するとコードの記述量は多くなると感じました。
Prismaで非依存のリレーション(多対多など)を表現するには、中間テーブルのモデルを明示的に定義するExplicit relationと、定義しないImplicit relationの2つがあります。Implicit relationのほうが、ネストが1段階少なくなるのでクエリがシンプルに記述できます。
しかしImplicit relationには、中間テーブルのカラム名が「A」「B」になったり、それ以外のカラムを追加できないなどの制約があります。そのため、例えば中間テーブルにタイムスタンプが必要な場合は、Explicit relationを使う必要があります。
記事とタグが多対多の関係です。以下の例ではImplicit relationを使っています。
// 属性は省略 model Article { id Int @id @default(autoincrement()) tags Tag[] } model Tag { id Int @id @default(autoincrement()) articles Article[] }
記事とタグを同時に取得するには、以下のようにincludesを使ってリレーションを指定します。
const articleAndTags = await prisma.article.findUnique({ where: { id: 1 }, include: { tags: true }, });
次はフォローの実装です。フォロー順にソートするためにフォロー日時を登録したいため、Explicit relationを使います。同じモデル間に複数のリレーションを定義する場合は、リレーションにnameをつける必要があります。
model User { id Int @id @default(autoincrement()) followers Follow[] @relation("Followee") // Followのうち、自分がフォローされているもの followingUsers Follow[] @relation("Follower") // Followのうち、自分がフォローしているもの } model Follow { id Int @id @default(autoincrement()) followerId Int followeeId Int follower User @relation(name: "Follower", fields: [followerId], references: [id]) followee User @relation(name: "Followee", fields: [followeeId], references: [id]) @@unique([followerId, followeeId]) }
const user = await prisma.user.findFirstOrThrow({ include: { followingUsers: { include: { followee: true }, }, followers: { include: { follower: true } } } }); // userがフォローしているユーザー一覧 const followingUsers = user.followingUsers.map((follow) => follow.followee); // userをフォローしているユーザー一覧 const followers = user.followers.map((follow) => follow.follower);
リレーション先を同時に作成したり、既存のデータに関連付けることができます。
// 記事を作成し、ユーザーと関連付ける await prisma.article.create({ data: { title: "Prismaを使ってみた", description: "Prismaを使ってみた", slug: "prisma-wo-tukatte-mita", body: "Prismaを使ってみました", author: { connect: { id: user.id }, }, }, }); // 記事に新しくタグを関連付ける await prisma.article.update({ where: { id: article.id }, data: { tags: { connect: [{ id: tag1.id }, { id: tag2.id }], }, }, });
Implicit relationの場合は、上記のようにconnectを使って関連付けるのがシンプルだと思います。Explicit relationで同じようにしようとすると、リレーションが2重になって分かりづらいです。そのため、以下のように素直に中間テーブルにInsertするのがよいと思います。
// userがanotherUserをフォローする const follow = await prisma.follow.create({ data: { followerId: user.id, followeeId: anotherUser.id, }, });
試した範囲では、Eloquentのほうがシンプルに書けました。以下のコードは雰囲気で書いたので、間違っているところがあるかもしれません。
// 記事とタグを取得する $articleAndTags = Article::with('tags')->findOrFail($articleId); $user = User::findOrFail($userId); // フォローしているユーザー一覧を取得する $followingUsers = $user->followingUsers; // フォローされているユーザー一覧を取得する $followers = $user->followers; // 記事に新しくタグを関連付ける $article = Article::findOrFail($articleId); $article->tags()->attach([$tag1, $tag2]); // ユーザーをフォローする $user->followingUsers()->attach($anotherUser);
返ってくるオブジェクトがクラスのインスタンスなので、object->method
の記法を使って直感的に操作できます。クラスを使うことにはデメリットもあると思います。例えばORMのAPIが肥大化することや、モデルクラスに色々詰め込まれがちなことです。
また、Eloquentではモデルにsetterが生えているので無法地帯だったり、SQLの戻り値に正確な型がつけにくいというデメリットがあります。
こう比較してみると、ActiveRecordとPrismaは一長一短だと思いました。Prismaを実践に投入してしばらく使ってみてから、また感想を書きたいと思います。
シーダーとデータベーステスト周りを調べようと思います。シーダーはドキュメントに記述があるので、途中で失敗したときにロールバックしてくれるかなどを確認しようと思います。
データベースのテストで調べることは、テスト前にトランザクションを貼ってロールバックする方法、データベースのファクトリを使う方法などです。これらは以下の記事やライブラリを参考にしてみようと思います。