valid,invalid

関心を持てる事柄について

5分で終わるPrisma 2 Tutorial

「名前をよく聞くが実態がよくわかっていないものリスト」にいたPrismaだが、official tutorialが5分で終わるというのでやってみた。

www.prisma.io

"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に比して高い生産性を生むという、スタンス。

image

https://www.prisma.io/docs/concepts/overview/why-prisma より引用

適切な制約の一例はチュートリアルにも現れていたので後述する。

Prismaの構成要素

Prismaのプロダクトは以下の3つで構成されている。すべてをオールインワンで提供しているわけではなく使いたいものを選んで使う。

  • Prisma Client
    • Prisma Schemaに基づいて自動生成されるtype-safeなquery builder
    • Go実装もあるがEarly Accessという位置づけ
  • 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が出ているのでそのうち対応が完了しそう。

環境によってはハイライトされないが、VSCodePrisma 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.