tekiehei2317's blog

Prisma Clientを使ってみた感想

投稿日: 2022-10-29

Prismaとは

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段階少なくなるのでクエリがシンプルに記述できます。

Many-to-many relations

しかし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, }, });

LaravelのEloquentとの比較

試した範囲では、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を実践に投入してしばらく使ってみてから、また感想を書きたいと思います。

これから調べること

シーダーとデータベーステスト周りを調べようと思います。シーダーはドキュメントに記述があるので、途中で失敗したときにロールバックしてくれるかなどを確認しようと思います。

データベースのテストで調べることは、テスト前にトランザクションを貼ってロールバックする方法、データベースのファクトリを使う方法などです。これらは以下の記事やライブラリを参考にしてみようと思います。

;