Closed31

[キャッチアップ] Prisma

shingo.sasakishingo.sasaki

話題の Prisma を使ってみる。
Next-generation ORM っていう文章を見て、 Next.js に関係するライブラリだと思ったけど全然違った模様

shingo.sasakishingo.sasaki

公式ドキュメントのトップページで世界観を確認

見出しの通り、Node 用の ORM らしい。 ORM は Rails の ActiveRecord と、 Laravel のアレしか知らないので、 node / typescript でどう表現できるのかは楽しみ。

Next-generation ORM for Node.js and TypeScript

多分単体と言うよりは、Next.js みたいなフロントエンドフレームワークにサーバサイドを載せた開発体験がすごいんだろうけど。

shingo.sasakishingo.sasaki

JavaScript というと、どうしてもフロントエンドツールってイメージがまだまだあるけど、DB直接触れるコードを書くんだから、当然サーバサイドJS(≒ Node) の話か。

そもそもサーバサイドJS(express) みたいなのもあんまり書いたこと無いから、基本を抑えたらその辺含めてアプリ開発を試してみるのが良さそう。

shingo.sasakishingo.sasaki

schema.prisma っていうスキーマで、データモデルとその関連を定義できるらしい。
むっちゃくちゃ柔軟な型推論や補完が効いてくれそうな予感がする。

shingo.sasakishingo.sasaki

Prisma Client でDB操作を行う。この辺は ActiveRecord みたいなもんか。 vscode拡張やTSと合わせることで、より型安全な開発体験は得られそうだけど。

shingo.sasakishingo.sasaki

PRISMA MIGRATE でDBMSへのマイグレーションも可能(プレビュー版)
これがまだプレビューってのが意外。DBMS側とスキーマで一切の差異が無いようにするのが第一だと思うけど。

shingo.sasakishingo.sasaki

PRISMA STUDIO でDBをブラウザ上で確認できる。
ORMがここまで関与するのは面白いな。こういうのはそれ専用ツール任せで良いと思うけど、ORM側との連携とかどこまでできるんだろ。

shingo.sasakishingo.sasaki

Easy to integrate into your framework of choice, Prisma simplifies database access, saves repetitive CRUD boilerplate and increases type safety. It's the perfect database toolkit for building robust and scalable web APIs.

まぁフレームワークと合わせて使うことが概ね想定されてそう。
フレームワーク例の筆頭に Next.js があるしそういうことっぽい。

shingo.sasakishingo.sasaki

サンプルプロジェクトのインストール

Quickstart では、 SQLite を使って構築したローカルデータベースに対して Prisma を使っていく。

体験用のプロジェクトを簡単にダウンロードできた。

curl -L https://pris.ly/quickstart | tar -xz --strip=2 quickstart-master/typescript/starter
shingo.sasakishingo.sasaki

サンプルプロジェクトの確認

こういうのは、ちゃんと中身を見ないと気持ち悪い性分なので、サンプルプロジェクトをざっくり見る。

package.json は TS系とprisma系が入ってるだけ

package.json
{
  "name": "script",
  "license": "MIT",
  "devDependencies": {
    "@prisma/cli": "2.13.1",
    "ts-node": "9.1.1",
    "typescript": "4.1.3"
  },
  "scripts": {
    "dev": "ts-node ./script.ts"
  },
  "dependencies": {
    "@prisma/client": "2.13.1"
  },
  "engines": {
    "node": ">=10.0.0"
  }
}

tsconfig も最小限

tsconfig.json
{
  "compilerOptions": {
    "sourceMap": true,
    "outDir": "dist",
    "strict": true,
    "lib": ["esnext", "dom"],
    "esModuleInterop": true
  }
}
shingo.sasakishingo.sasaki

prisma/schema.prisma に、スキーマが定義されてる。専用の拡張子だからエディタの設定が必要そう。
zenn で最適なシンタックスハイライトがかかりそうな言語はどれだろ。とりあえずJSにしておくか。

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[]
}
shingo.sasakishingo.sasaki

で、 prisma/script.ts に、DB操作するコードを書いていく感じかな。
既にそれっぽいコードが書かれてる。

import { PrismaClient } from '@prisma/client'

const prisma = new PrismaClient()

// A `main` function so that you can use async/await
async function main() {
  // ... you will write your Prisma Client queries here
}

main()
  .catch(e => {
    throw e
  })
  .finally(async () => {
    await prisma.$disconnect()
  })
shingo.sasakishingo.sasaki

スキーマの確認

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[]
}

直感的に読む限り、 User テーブルと Post テーブルがあって、ユーザごとの投稿物が0個以上あるってイメージか。

shingo.sasakishingo.sasaki

DBのread

async function main() {
  const allUsers = await prisma.user.findMany();
  console.log(allUsers);
}
[
  { id: 1, email: 'sarah@prisma.io', name: 'Sarah' },
  { id: 2, email: 'maria@prisma.io', name: 'Maria' }
]

↑ SQLite に初期データが入ってて、それを取得できてるのがわかる。

めっちゃシンプルな例だけど、補完がスッと効くのと、 Promise ベースで直感的に書けるので第一印象は良い感じ。

shingo.sasakishingo.sasaki

関連モデルも取得

ActiveRecord の eagler_load みたいな感じかな…?
発行されるSQLを確認したいな。

  const allUsers = await prisma.user.findMany({
    include: { posts: true },
  });
shingo.sasakishingo.sasaki

発行されるSQL確認

多分この先で出てくると思うけど、調べたらインスタンス生成時にロギング設定できた。

const prisma = new PrismaClient({
  log: ["query"],
});

こんな感じでログがでる。(eagler_load で良さそう)

prisma:query SELECT `dev`.`User`.`id`, `dev`.`User`.`email`, `dev`.`User`.`name` FROM `dev`.`User` WHERE 1=1 LIMIT ? OFFSET ?
prisma:query SELECT `dev`.`Post`.`id`, `dev`.`Post`.`title`, `dev`.`Post`.`content`, `dev`.`Post`.`published`, `dev`.`Post`.`authorId` FROM `dev`.`Post` WHERE `dev`.`Post`.`authorId` IN (?,?) LIMIT ? OFFSET ?
shingo.sasakishingo.sasaki

型はどうなってるのか

  const allUsers = await prisma.user.findMany({
    include: { posts: true },
  });

の場合、 allUsers の型は (User & { posts: Post[] })[] になる。すごいな、 ActiveRecord もたいがいだけど、これも TSの魔法を感じてしまう。内部でとんでもない実装してそう。

shingo.sasakishingo.sasaki

データの作成

  const post = await prisma.post.create({
    data: {
      title: "Prisma makes database easy",
      author: {
        connect: {
          email: "sarah@prisma.io",
        },
      },
    },
  });
prisma:query BEGIN
prisma:query SELECT `dev`.`User`.`id` FROM `dev`.`User` WHERE `dev`.`User`.`email` = ? LIMIT ? OFFSET ?
prisma:query INSERT INTO `dev`.`Post` (`title`, `published`, `authorId`) VALUES (?,?,?)
prisma:query SELECT `dev`.`Post`.`id`, `dev`.`Post`.`title`, `dev`.`Post`.`content`, `dev`.`Post`.`published`, `dev`.`Post`.`authorId` FROM `dev`.`Post` WHERE `dev`.`Post`.`id` = ? LIMIT ? OFFSET ?
prisma:query COMMIT
  • 必須パラメータ省略しちゃうとちゃんと型検査で引っかかる体験は、Railsに慣れてるとやっぱり気持ちいい
  • 関連モデルの参照を connect でできるのちょっと新鮮
  • 一連の手続きがちゃんとトランザクションになってて、失敗時(emailに外用するユーザが見つからないとか)にちゃんとロールバックできてた
  • 最後の select が余計な気がするけど、これはオプションで制御できる気がする
shingo.sasakishingo.sasaki

データの更新

  await prisma.post.update({
    where: {
      id: 2,
    },
    data: {
      published: true,
    },
  });
prisma:query BEGIN
prisma:query SELECT `dev`.`Post`.`id` FROM `dev`.`Post` WHERE `dev`.`Post`.`id` = ?
prisma:query UPDATE `dev`.`Post` SET `published` = ? WHERE `dev`.`Post`.`id` IN (?)
prisma:query SELECT `dev`.`Post`.`id`, `dev`.`Post`.`title`, `dev`.`Post`.`content`, `dev`.`Post`.`published`, `dev`.`Post`.`authorId` FROM `dev`.`Post` WHERE `dev`.`Post`.`id` = ? LIMIT ? OFFSET ?
prisma:query COMMIT
  • ActiveRecord みたいに変にメソッドチェインよりも、こっちのほうが生SQLに近くて直感的に感じる
  • 更新の場合は必須の属性もオプショナルになるみたいな、当たり前なんだけど細かいところが良く出来てる
shingo.sasakishingo.sasaki

Quickstart を終えて

Node(+TS) の ORM 自体触るの初めてだったので、やっぱり型周りの体験はかなり良い感じ。
とはいえ、これだけなら他のNode系のORMでも最低限持ってる仕組みと思える(多分) ので、もう少し Prisma 特有の開発体験に触れていきたいところ。

どうやら Quickstart の次は Introduction が用意されてるようなので、これも引き続きハンズオンしてみてから、具体的なドキュメントを読んでいくか、ものづくりに入っていくか判断しよう。

https://www.prisma.io/docs/concepts/overview/what-is-prisma/

shingo.sasakishingo.sasaki

What is Prisma?

Prisma は以下から構成されてるよ

Prisma Client

自動生成かつ型安全なクエリビルダー。クイックスタートやっただけでこの辺の気持ちよさは感じた。

Prisma Migrate

DBMSに対するマイグレーションシステム。まだ使えてない。AWS みたいなクラウドサービスにマイグレーションしてみたいな。

Prisma Studio

DB読み書き用のGUIビュー。まだ使えてない。Prisma にGUIビューが内包されてる理由を早く知りたい。

Prisma は Node(TS) が動く環境なら何にでも使えるよという話。
まぁやっぱりバックエンドのAPIサーバが主軸になるんじゃないかな。GraphQLもイケるぞみたいに書いてあるけど、これは謎だし今は触れないでおく。

shingo.sasakishingo.sasaki

How does Prisma work?

Prisma を使ったプロジェクトは、 Prisma schema file でスキーマを定義し、それに基づいてクエリ作成、データモデル構築、型生成が行われる。

The Prisma data model

  • データモデルはモデルの集合である
  • モデルは、テーブルのオブジェクト表現と、クエリの提供が主な役割 (ORMとはそういうもの)
  • スキーマを一度定義してmigrateしてしまえば、自動でデータモデルが生成されて、型安全に扱える

ちょっと複雑なクエリも簡単にかけちゃうんだぜの例

  const posts = await prisma.post.findMany({
    where: {
      OR: [
        { title: { contains: "prisma" } },
        { content: { contains: "prisma" } },
      ],
    },
  });
prisma:query SELECT `dev`.`Post`.`id`, `dev`.`Post`.`title`, `dev`.`Post`.`content`, `dev`.`Post`.`published`, `dev`.`Post`.`authorId` FROM `dev`.`Post` WHERE (`dev`.`Post`.`title` LIKE ? OR `dev`.`Post`.`content` LIKE ?) LIMIT ? OFFSET ?

(ActiveRecord はやたらとLike が書きづらかったので contains は良き)

shingo.sasakishingo.sasaki

Typical Prisma workflows

Prisma と DBMS の連携には2通りのやり方が提供されてる

DBMS 更新を手動で行って、Prisma スキーマを追従する

  • コレまで通り、DMBS側を別途マイグレーション (SQL直接叩くとか)
  • CLI の introspect 機能を使って、DBスキーマを元にPrismaスキーマを更新
  • CLI の generate 機能を使って、 Prismaスキーマを元にデータモデルを更新

Prisma Migrate を使って、 Prisma から DMBSをマイグレーションする

  • Prisma 側のスキーマ、データモデルを手動で更新する
  • CLI で prisma migrate を実行する
  • 良い感じにDBMS側にマイグレーションされる

意外とどっちも筋悪く無さそうだけど、 Prisma で完結できる後者のほうがやっぱり良いのかな。スキーマのバージョン管理もしやすそうだし、DBmigrateをデプロイまで先延ばし出来ることを考えると開発ワークフローとしても良さそう

shingo.sasakishingo.sasaki

Why Prisma?

最後にコレを確認して、なんで Prisma を使うのか良いのかを理解する。
NodeのORM自体始めてなので、Prisma 特有の良さが書いてあると嬉しい。

https://www.prisma.io/docs/concepts/overview/why-prisma

RDMSのボトルネックは、SQLや、複雑なORMオブジェクトに対するデバッグの困難さがあるが、 Prisma は型安全で、ピュアなJSオブジェクトを提供することで、これを解決している。

TLDR

Prisma のゴールは、DBに関わる開発生産性を高めることで、そのために以下のような特徴を持っている。

  • モデルの関連とかを意識する代わりに、JSオブジェクトだけを考えれば済むようになっている
  • クエリがクラスではなくオブジェクトを使って構築される
  • スキーマに基づく Single source of truth が実現されている
  • あるあるな落とし穴、アンチパータンを避けるための制約
  • An abstraction that make the right thing easy (何を言いたいかよくわかんなかったけど、抽象化がちゃんとしてるってことかな)
  • 型安全なクエリ
  • 少ないボイラーテンプレート
  • 充実したコード補完

↑はほとんど、クイックスタートレベルのハンズオンでも体験できたのでよくわかる。

SQL と ORM(などのDBツール) の問題点

これまでの Node 系の ORM は、生産性とコントロール性がトレードオフだった

  • 生SQL(node用のDBMSドライバ) は、フルコントロールだが生産性が低い
    • SQLを自由に送れるから何でも出来てGood
    • DBの細かい都合を考える必要あってBad
    • 型安全性が全く無くてBad
  • クエリビルダーライブラリ(e.g. knex.js) は、高いコントロール性と、まずまずの生産性
    • SQLの構築をある程度抽象化してくれるのでGood
    • まだまだSQL寄りなので、開発者はSQLの観点からデータを考える必要があってBad
  • 既存ORM は、生産性は高いがコントロール性が低い
    • ORMはSQLを抽象化し、リレーションをモデルクラスで表現できるようにすることで生産性を高めてGood
    • ORMによって、開発者のメンタルモデルはテーブルからクラスに変わったため、インピーダンスミスマッチが生じるようになり、多くの落とし穴を生み出した
      • N+1 を容易に産み出すなど

上記のトレードオフを考えるに、アプリケーション開発者は、データを気にする必要はあるが、SQLを直接考えるべきではないと言える。

Prisma はその結論に基づいた開発生産性を産み出している。

shingo.sasakishingo.sasaki

感想

  • Why Prisma? を読んで、確かに Prisma は ActiveRecord 並に抽象化した ORM ではないなと感じた
    • SQLというかデータに意識が向くようなクエリビルダー
    • データモデルがプレインオブジェクトで、メソッドは持ってない(多分)
  • アーキテクチャ云々より、型安全とコード補完の体験がやっぱり一番良い
  • ActiveRecord のような魔法はそこまで感じない(型生成は別) ので、使っててあまり不安にならない

まだまだドキュメントとかを読み進めても良いけど、やっぱり実際にアプリケーション開発で使ってみてどうかってのを体験したいので、お勉強はここまでにして、次の個人開発で試しに採用してみようと思う。

qaynamqaynam

とても参考にできる記事です!
一つprismaに関して質問してよろしいでしょうか?

activeRecordのようなtry catchのtransaction張りたくて、どこ探しても出てこないです。
一応公式には$transactionというヘルパーがあるみたいですが、引数として配列のPromiseを渡さなければいけないみたいです。

公式ではこういう風に👇書いてます。


const prisma = new PrismaClient();

await prisma.$transaction([
   PromiseA,
   PromiseB
]);

やりたいことは、PromiseAで成功して帰ってくるuser_idをPromiseBの引数に入れたいですが、どうやって実現すればいいのかわからないです。

お力貸していただけないでしょうか?

このスクラップは2020/12/30にクローズされました