📌

Prismaの概要とTypeORMとの比較を少し

2023/03/12に公開

Prisma

Prismaは、従来のORMの多くの問題、例えばモデルインスタンスの肥大化、ビジネスロジックとストレージロジックの混在、そして型の安全性やレイジーローディングの課題を軽減するために開発された新しいORM

以下のツールで構成されている
Prisma Client: タイプセーフなデータベースクライアントで、自動生成されます。
Prisma Migrate: 宣言的なデータモデリングとカスタマイズ可能なマイグレーションを提供します。
Prisma Studio: データを閲覧・編集するためのモダンなUIを備えています。

Prismaスキーマでのデータモデリング

Prismaを使用する場合、データモデルをPrisma schemaで定義します。Prismaを使ったモデルのサンプル

model Post {
  id        Int     @id @default(autoincrement())
  title     String
  content   String?
  published Boolean @default(false)
  author    User?   @relation(fields: [authorId], references: [id])
  authorId  Int?
}

model User {
  id    Int     @id @default(autoincrement())
  email String  @unique
  name  String?
  posts Post[]
}

これらのモデルは、それぞれ基礎となるデータベースのテーブルにマッピングされ、Prisma Clientによって提供され、生成されたデータアクセスAPIの基礎となる
VSCodeを使えばシンタックスハイライト、自動補完、クイックフィックスが効く

Prisma Clientによる直観的で型安全なデータベースアクセス

Prisma Clientを使用する主な利点は、開発者がオブジェクトで考えることができ、データを推論するための身近で自然な方法を提供することです。

Prisma Clientには、モデル・インスタンスの概念がありません。
その代わりに、常にプレーンなJavaScriptオブジェクトを返すデータベースクエリを形成することができます。
生成された型のおかげで、これらのクエリの自動補完も可能です。

Prisma Clientのクエリの結果はすべて完全に型付けされる。
TypeScript ORMの中で最も強力な型安全性を保証している
https://www.prisma.io/docs/concepts/more/comparisons/prisma-and-typeorm#type-safety

// すべての投稿を検索
const posts = await prisma.post.findMany()
// すべてのpostsと紐づくauthorsを検索
const postsWithAuthors = await prisma.post.findMany({
  include: { author: true },
})
// userとpostを同時に生成
const userWithPosts: User = await prisma.user.create({
  data: {
    email: 'ada@prisma.io',
    name: 'Ada Lovelace',
    posts: {
      create: [{ title: 'Hello World' }],
    },
  },
})
// `@prisma`を含む全てのユーザーを検索
const users = await prisma.user.findMany({
  where: {
    email: { contains: '@prisma' },
  },
})

Prisma Migrateによるデータベースのマイグレーション

Prisma Migrateは、Prismaのスキーマを、データベースのテーブルを作成・変更するために必要なSQLに変換する
Prisma Migrateは、Prisma CLIで提供されるprisma migrateコマンドで使用できます。

自動生成されたマイグレーションファイル

-- CreateTable
CREATE TABLE "User" (
    "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
    "email" TEXT NOT NULL,
    "name" TEXT
);

-- CreateTable
CREATE TABLE "Post" (
    "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
    "title" TEXT NOT NULL,
    "content" TEXT,
    "published" BOOLEAN NOT NULL DEFAULT false,
    "authorId" INTEGER NOT NULL,
    CONSTRAINT "Post_authorId_fkey" FOREIGN KEY ("authorId") REFERENCES "User" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
);

-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "User"("email");

Prisma Studio

phpMyAdminのようなデータベース内のデータのビジュアルエディタ

TypeORMとの比較

PrismaはORMを名乗っているが、DBから返ってくるのはプレーンなJavaScriptのObjectで、モデルクラスにマッピングすると言った概念がない。
軽く触った感じだと開発体験はすごく良さそう。
Schemaファイルの定義さえ管理すれば、DBもClientコードも同期できるところがよさそう。

型安全性

色々な場面でTypeORMよりも優れた型安全性がある
https://www.prisma.io/docs/concepts/more/comparisons/prisma-and-typeorm#selecting-fields

  • Prismaが問い合わせ結果の型に対してより強い保証を提供できるシナリオ

TypeORM

const postRepository = getManager().getRepository(Post)
const publishedPosts: Post[] = await postRepository.find({
  where: { published: true },
  select: ['id', 'title'],
})

返されるpublishedPosts配列の各オブジェクトは、実行時には選択されたidとtitleのプロパティしか持たないが
TypeScriptコンパイラはこのことについて何の知識も持っていない。
クエリの後にPostエンティティに定義された他のプロパティにアクセスすることができてしまう。

const post = publishedPosts[0]

// The TypeScript compiler has no issue with this
if (post.content.length > 0) {
  console.log(`This post has some content.`)
}

上記は実行時エラーになる

TypeError: Cannot read property 'length' of undefined

Prisma

PrismaClientは同じ状況で完全な型安全性を保証し、データベースから取得されなかったフィールドへのアクセスから保護できる。

const publishedPosts = await prisma.post.findMany({
  where: { published: true },
  select: {
    id: true,
    title: true,
  },
})
const post = publishedPosts[0]

// The TypeScript compiler will not allow this
if (post.content.length > 0) {
  console.log(`This post has some content.`)
}

上記の場合TypeScriptコンパイラはコンパイル時にエラーを投げる。

リレーション

Prismaでは、取得するリストのモデルだけでなく、そのモデルのリレーションに適用される基準に基づいてリストをフィルタリングすることができる。
TypeORMでは専用のAPIは提供していない。QueryBuilderを使用するか、手作業によりクエリを記述する必要がある。

ネストする感じで書ける。型安全にかける。

const posts = await prisma.user.findMany({
  where: {
    Post: {
      some: {
        title: {
          contains: 'Hello',
        },
      },
    },
  },
})

関連するテーブルの作成もネストして書くことができる。

// Create a user and two posts
const userWithPosts: User = await prisma.user.create({
  data: {
    email: 'elsa@prisma.io',
    name: 'Elsa Prisma',
    country: 'Italy',
    age: 30,
    posts: {
      create: [
        {
          title: 'Include this post!',
        },
        {
          title: 'Include this post!',
        },
      ],
    },
  },
})

https://www.prisma.io/client

Schema

Typeormはスキーマをクラスで扱い、モデルの定義に加えてデコレータにより制約を付与してくスタイル。
スキーマ定義とエンティティを同一クラスで使いまわしているため定義が複雑になってしまう場合に煩雑になる。
DBの型がデコレータに依存しているためクラスの型と異なるなど型安全性が担保されていない問題がある。

Prismaでは、スキーマファイルを使ってデータベースを設計する
独自のDSLを起用していて、スキーマで定義されているモデルのデータを読み書きするためカスタマイズされた完全に型安全なAPIを公開する軽量なクライアントを生成できる
わかりやすく、読み込みもきれい。Intellijではサポートされておらず、推奨プラグインも機能しない
スキーマから型を生成することもできる

https://www.prisma.io/docs/concepts/more/comparisons/prisma-and-typeorm#data-modeling-and-migrations

懸念点

  • サブクエリとか使うような複雑なクエリを書こうとしたときに生のSQLを書かないといけなくなるかも
// Select users by email
const email = 'edna@prisma.io'
const users: User[] = await prisma.$queryRaw`
  SELECT * FROM "User" WHERE email = ${email};
`

GraphQLとの相性

Prismaの出自GraphQLなのもあって相性はよさそう
https://www.prisma.io/graphql

N + 1問題

GraphQLを扱うにあたってN + 1問題が発生することが多い
Prisma では独自のアプローチにより N+1 問題を解決していて、Prisma で提供される findUnique には N+1 を防ぐための機構が組み込まれている

Discussion