「名前をよく聞くが実態がよくわかっていないものリスト」にいたPrismaだが、official tutorialが5分で終わるというのでやってみた。
"5分"!? と思ったがPrisma SchemaとPrisma Clientの説明が中心で、ORMの書き味を見てみる程度の内容なのでそんなものかもしれない。
補足説明を読んだりしながら進めたので5分以上かかったが、まったく追っていないJS/TS界のORMの進化をキャッチアップできてだいぶ面白かった。
学んだこと
Prisma 2はただのORM
まず、Hasuraと同列のプロダクトだと誤解していたけど違っていた。Prisma 2はただのORM。
同列に語られがちだったのはどうやら今はメンテナンスモードに入っているPrisma 1の頃の話で、GraphQL DSLでデータモデルを定義したり、GraphQL APIサーバとしてCRUDする機能があったかららしい。
Prisma 1の時代のHasura official blogの記事Hasura vs Prisma (2018年10月) にも以下の記述がある。
Over the last few weeks, many people have asked me what the difference between Hasura & Prisma is.
Prismaのコンセプト
Prisma 2はどういうコンセプトなのか?を知るには以下のページが最もわかりやすかった。
Why Prisma? Comparison with SQL query builders & ORMs | Prisma Docs
Prisma 2のトッププライオリティは開発者の生産性へのフォーカス。その中には型安全性やエディタでの入力補完なども含まれる。
開発者はSQLそのものではなく、機能を開発するために必要なデータモデルを考えるべき。そういう意味では生SQL書くのも、SQL起点でデータを考えなければならないクエリビルダもPrisma的には生産性が低い。
(Prisma自身も含めて)ORMは高い生産性をうむがobject-relational impedance mismatchの問題がある。「リレーショナルデータは簡単にオブジェクトにマッピングできる」という間違った前提に基づくと、オブジェクト指向では自然なコードがN+1のような問題をかんたんに引き起こしてしまう。
Prismaはcommon antipatternやpitfallを避けるための適切な制約を設けることで従来のORMに比して高い生産性を生むという、スタンス。
https://www.prisma.io/docs/concepts/overview/why-prisma より引用
適切な制約の一例はチュートリアルにも現れていたので後述する。
Prismaの構成要素
Prismaのプロダクトは以下の3つで構成されている。すべてをオールインワンで提供しているわけではなく使いたいものを選んで使う。
- Prisma Client
- Prisma Migrate (preview)
- Prisma Schemaに基づいてdatabase migrationを行えるツール
- Prisma Studio
- DBのviewer / editor
What is Prisma? (Overview) | Prisma Docs
Prisma Schema
Prismaのすべての中心。
モデルを定義するデータモデリング言語であり、データソースの定義やgeneratorの定義も含む。
データソースにはPostgreSQL, MySQL, SQLiteが使える。SQL Serverもすでにpreviewが出ているのでそのうち対応が完了しそう。
環境によってはハイライトされないが、VSCodeのPrisma extensionを使うと良い感じ。
datasource db { provider = "sqlite" url = env("DATABASE_URL") } generator client { provider = "prisma-client-js" } 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[] }
Database features matrix (Reference) | Prisma Docsを見ながらこのようなDSLをガリガリ書いていく。DBの機能としては存在するがPrisma Schemaではまだ対応していない機能もあるので留意する。
Prisma Client
こんな感じのコードを書く。
import { PrismaClient } from "@prisma/client" const prisma = new PrismaClient() async function main() { const allUsers = await prisma.user.findMany() console.log(allUsers) } main() .catch(e => { throw e }) .finally(async () => { await prisma.$disconnect() })
ORMとして期待する通りこんな配列が得られる。
[ { id: 1, email: "sarah@prisma.io", name: "Sarah" }, { id: 2, email: "maria@prisma.io", name: "Maria" }, ]
型
面白いことに、PrismaClient
の返すデータにはすべて、発行するクエリに応じた適切な型が付いている。
(推論してくれるので自前で書く必要はないが)Prisma Schema DSLで定義したモデルをimportできる。
いわゆるeager load的に、関連するテーブルのオブジェクトを取得したときはどうなるだろうか。
const allUsers = await prisma.user.findMany({ include: { posts: true }, });
これもちゃんとIntersection Typesとして型がつく。(User & { posts: Post[] })[]
のような感じ。
select *
をやめてcolumnを指定すると
const allUsers = await prisma.user.findMany({ select: { id: true, email: true }, });
column nameでPick
した型になる。
こりゃあすげぇ…!
型定義の出力場所
このすげぇ型たちはいったいどこにいるのかと調べたら、node_modules/.prisma/client/index.d.ts
にいた。
ユーザーが定義したDSLに基づく成果物をnode_modules
配下に吐くこともあるのか。
読めないことはないがなかなか厳しい、というか、開発者が頻繁に読むものではない。
prisma.user.findMany
の後ろにはこんなのが控えている。
export type UserGetPayload< S extends boolean | null | undefined | UserArgs, U = keyof S > = S extends true ? User : S extends undefined ? never : S extends UserArgs | FindManyUserArgs ?'include' extends U ? User & { [P in TrueKeys<S['include']>]: P extends 'posts' ? Array < PostGetPayload<S['include'][P]>> : never } : 'select' extends U ? { [P in TrueKeys<S['select']>]: P extends keyof User ?User [P] : P extends 'posts' ? Array < PostGetPayload<S['select'][P]>> : never } : User : User
上述のたった少しのPrisma Schemaに対してこの雰囲気の型定義が3,000行。これを出力するPrismaのengineの実装はすごいことになっていそうだ。
Prisma Clientが課す制約
上述の型を見てわかるようにPrisma Clientが返すのはただのオブジェクトである。クラスのインスタンスではないのでモデルに関する操作を持つことはできずfat modelを作れないし、associationを辿ってN+1を発生させることはできない。
// posts を include していないので返り値は `User[]` const allUsers = await prisma.user.findMany(); allUsers.forEach((user) => { // 型エラーになる // Property 'posts' does not exist on type 'User'. console.log(user.posts); })
common antipatternやpitfallを避けるための適切な制約を設ける、という思想の一端が見える。
感想
やはり型の生成がすさまじく便利そうだ。
チュートリアルをやるときもあえてコピペではなく写経をしてみたのだが、入力補完や型チェックなどの支援が心強い。
複雑なクエリをどこまで組み立てられるのか?とか気になる点はまだあるが、Node.js + TypeScriptでサーバサイドアプリケーションを書く機会があれば積極的に検討してみたい。
This article is for ohbarye Advent Calendar 2020.