Prisma ORMを2年運用して培ったノウハウを共有する (original) (raw)

ノウハウを共有する
Cloudbase株式会社
t...")

セッションの目的
")

© 2024 Cloudbase Inc.
")

しかし
")

リアルな運用事例が
足りてない!
")

培ったノウハウを共有する
© 2024 Cloudbas...")

Prismaは地位を確立しつつある
Cloudbaseについて
")

前提知識を整理したところで
本題です
")

© 2024 Cloudbase Inc.
")

Prismaは
「直感的なAPIを提供しSQLを...")

雰囲気で使えちゃうので嬉しい
")

でも結局、SQLは大事
")

運用する中で注意すべきと感じた
SQLを見ていく
")

{
,
,
,
[
{
,
,
,
,
,...")
, , }, ], }; id: email: name: posts: id: title: content: published: authorId: 1 1 11 '[email protected]' 'tockn' 'Cloudbase' 'Cloudbase is a cloud security platform.' false © 2024 Cloudbase Inc.

{
,
,
,
[
{
,
,
,
,
,...")
, , }, ], }; id: email: name: posts: id: title: content: published: authorId: 1 1 11 '[email protected]' 'tockn' 'Cloudbase' 'Cloudbase is a cloud security platform.' false © 2024 Cloudbase Inc. リレーション先のデータも取得

リレーションを取得ということは…
JOIN句が使...")

リレーションを取得ということは…
JOIN句が使...")

© 2024 Cloudbase Inc.
--...")
WHERE LIMIT . . , . . , . . . . . = ? ? OFFSET ?; `main` `User` `id` `main` `User` `email` `main` `User` `name` `main` `User` `main` `User` `name` -- 2. 取得したUserが持つPost取得 SELECT FROM WHERE IN LIMIT . . , . . , . . , . . , . . . . . (?, ?, ?) ? OFFSET ?; `main` `Post` `id` `main` `Post` `title` `main` `Post` `content` `main` `Post` `published` `main` `Post` `authorId` `main` `Post` `main` `Post` `authorId`

© 2024 Cloudbase Inc.
--...")
WHERE LIMIT . . , . . , . . . . . = ? ? OFFSET ?; `main` `User` `id` `main` `User` `email` `main` `User` `name` `main` `User` `main` `User` `name` -- 2. 取得したUserが持つPost取得 SELECT FROM WHERE IN LIMIT . . , . . , . . , . . , . . . . . (?, ?, ?) ? OFFSET ?; `main` `Post` `id` `main` `Post` `title` `main` `Post` `content` `main` `Post` `published` `main` `Post` `authorId` `main` `Post` `main` `Post` `authorId` 最初にuserを取得

© 2024 Cloudbase Inc.
--...")
WHERE LIMIT . . , . . , . . . . . = ? ? OFFSET ?; `main` `User` `id` `main` `User` `email` `main` `User` `name` `main` `User` `main` `User` `name` -- 2. 取得したUserが持つPost取得 SELECT FROM WHERE IN LIMIT . . , . . , . . , . . , . . . . . (?, ?, ?) ? OFFSET ?; `main` `Post` `id` `main` `Post` `title` `main` `Post` `content` `main` `Post` `published` `main` `Post` `authorId` `main` `Post` `main` `Post` `authorId` 取得したuser_idで Postを取得

© 2024 Cloudbase Inc.
--...")
WHERE LIMIT . . , . . , . . . . . = ? ? OFFSET ?; `main` `User` `id` `main` `User` `email` `main` `User` `name` `main` `User` `main` `User` `name` -- 2. 取得したUserが持つPost取得 SELECT FROM WHERE IN LIMIT . . , . . , . . , . . , . . . . . (?, ?, ?) ? OFFSET ?; `main` `Post` `id` `main` `Post` `title` `main` `Post` `content` `main` `Post` `published` `main` `Post` `authorId` `main` `Post` `main` `Post` `authorId` 取得したuser_idで Postを取得 WHERE IN

・WHERE INが大量になる
・通信のオーバー...")

Cloudbaseでは
肥大化した一覧取得系AP...")

他にも
create, update, dele...")

どう対応しているか?
")

Cloudbaseではど


ているか
f 原...")
deleteManyを使用すa f xxxManyなら余分なクエリが走らない UPDATE DELETE // ️ 原則使わない // 余分なクエリが走らないmanyを使う await await . . ({ { }, { }, }); . . ({ { }, { }, }); prisma user where: email: data: name: prisma user where: name: data: name: update updateMany '[email protected]' 'tockn' 'sato' 'tockn' // 原則使わない // ️ 余分なクエリが走らないmanyを使う await await . . ({ { }, }); . . ({ { }, }); prisma user where: email: prisma user where: name: delete deleteMany '[email protected]' 'tockn'

「PrismaのSQLは効率悪いのか〜」
")

ちょっと待って!
")

PrismaはSQL最適化に力を入れている
")

5.x.xからupdate, deleteでSEL...")

relational filterで発行されるクエ...")

previewFeature: relationJ...")

© 2024 Cloudbase Inc.
aw...")
{ }, { }, }); prisma user relationLoadStrategy: include: posts: where: name: findMany 'join' 'tockn' true SELECT AS FROM AS LEFT JOIN SELECT AS FROM SELECT FROM SELECT AS FROM SELECT FROM AS WHERE AS AS AS AS ON WHERE . , . , . . LATERAL ( (JSONB_AGG( ), ) ( . ( JSONB_BUILD_OBJECT ( , . , , . , , . ) ( .* . . = . ) ) ) ) TRUE . = $ ; "t1" "id" "t1" "name" "User_posts" "__prisma_data__" "posts" "public" "User" "t1" "__prisma_data__" '[]' "__prisma_data__" "t4" "__prisma_data__" 'id' "t3" "id" 'content' "t3" "content" 'authorId' "t3" "authorId" "__prisma_data__" "t2" "public" "Post" "t2" "t1" "id" "t2" "authorId" "t3" "t4" "t5" "User_posts" "t1" "name" COALESCE /* root select */ /* inner select */ /* middle select */ /* outer select */ 1 発行されるSQL

© 2024 Cloudbase Inc.
aw...")
{ }, { }, }); prisma user relationLoadStrategy: include: posts: where: name: findMany 'join' 'tockn' true SELECT AS FROM AS LEFT JOIN SELECT AS FROM SELECT FROM SELECT AS FROM SELECT FROM AS WHERE AS AS AS AS ON WHERE . , . , . . LATERAL ( (JSONB_AGG( ), ) ( . ( JSONB_BUILD_OBJECT ( , . , , . , , . ) ( .* . . = . ) ) ) ) TRUE . = $ ; "t1" "id" "t1" "name" "User_posts" "__prisma_data__" "posts" "public" "User" "t1" "__prisma_data__" '[]' "__prisma_data__" "t4" "__prisma_data__" 'id' "t3" "id" 'content' "t3" "content" 'authorId' "t3" "authorId" "__prisma_data__" "t2" "public" "Post" "t2" "t1" "id" "t2" "authorId" "t3" "t4" "t5" "User_posts" "t1" "name" COALESCE /* root select */ /* inner select */ /* middle select */ /* outer select */ 1 発行されるSQL JOINが使われている (少し複雑なSQLだが...)

ちゃんと頑張ってくれている
というのを伝えたかった
")

クエリのパフォーマンスについて
見たので
")

次は
スケーラビリティ的観点の工夫
")

Cloudbaseは
Read Heavyな側面...")

お客様のクラウド環境の
リソース情報
リスク情...")

Readのスケールをしたい
")

Read Replicaを使おう
")

PrismaでRead Replicaを扱いたい!
")

PrismaでRead Replicaを扱いたい!...")

そこで
")

Cloudbaseでは
PrismaClient...")

その名も
PrismaClientIssuer
")

const new
async =>
async...")
. (..., ( ) { . . ({ ... }) }) . (..., ( ) { . . ({ ... }) }) issuer PrismaClientIssuser primary create readReplica findMany args issuer tx tx user issuer tx tx user // primaryへのアクセス // read replicaへのアクセス await await await return © 2024 Cloudbase Inc.

const new
async =>
async...")
. (..., ( ) { . . ({ ... }) }) . (..., ( ) { . . ({ ... }) }) issuer PrismaClientIssuser primary create readReplica findMany args issuer tx tx user issuer tx tx user // primaryへのアクセス // read replicaへのアクセス await await await return © 2024 Cloudbase Inc. primaryを指定

const new
async =>
async...")
. (..., ( ) { . . ({ ... }) }) . (..., ( ) { . . ({ ... }) }) issuer PrismaClientIssuser primary create readReplica findMany args issuer tx tx user issuer tx tx user // primaryへのアクセス // read replicaへのアクセス await await await return © 2024 Cloudbase Inc. callbackの引数に primaryへ張ったTranasctionClientが来る

const new
async =>
async...")
. (..., ( ) { . . ({ ... }) }) . (..., ( ) { . . ({ ... }) }) issuer PrismaClientIssuser primary create readReplica findMany args issuer tx tx user issuer tx tx user // primaryへのアクセス // read replicaへのアクセス await await await return © 2024 Cloudbase Inc. primaryへクエリを発行

const new
async =>
async...")
. (..., ( ) { . . ({ ... }) }) . (..., ( ) { . . ({ ... }) }) issuer PrismaClientIssuser primary create readReplica findMany args issuer tx tx user issuer tx tx user // primaryへのアクセス // read replicaへのアクセス await await await return © 2024 Cloudbase Inc. read replicaも同じ

Read Replicaを扱えるようになった
")

extension-read-replicasを
...")

PrismaClientをwrapするメリットは
...")

PrimaryとRead Replicaを
使い...")

Read Replicaに対して
Write系ク...")

TransactionClientを
引数に持つ...")

開発体験悪い
型レベルで保証できたら嬉しい
")

そこで
")

2つの独自Transaction型を用意
Pri...")

CBWritableTransaction

...")

const async
=>
const async =>...")
, : ) { . . ({ }); }; = ( : ) { . . (); }; registerUser create listUsers findMany tx user tx user data: user tx tx user CBWritableTransaction User CBReadableTransaction await return © 2024 Cloudbase Inc. Write系クエリがある場合は CBWritableTranasction

const async
=>
const async =>...")
, : ) { . . ({ }); }; = ( : ) { . . (); }; registerUser create listUsers findMany tx user tx user data: user tx tx user CBWritableTransaction User CBReadableTransaction await return © 2024 Cloudbase Inc. Read系のみの場合は CBReadableTransaction

これを
PrismaClientIssuerと合...")

というわけで
PrismaClientはそのまま...")

© 2024 Cloudbase Inc.
")

Cloudbaseは
プールモデル
マルチテナ...")

同じDB、テーブルに
異なるお客様のデータが入る...")

あるデータを取得する場合
アクセス元ユーザが権限...")

あるデータを取得する場合
アクセス元ユーザが権限...")

「Whereを忘れずに付けよう!」
")

")

実装漏れ、レビュー漏れ、テスト漏れ…
Where...")

特にPrismaでは
Where対象のkeyにu...")

一発アウトの情報漏洩に
なりかねない
")

そこで
")

Row Level Security
")

© 2024 Cloudbase Inc.
")

© 2024 Cloudbase Inc.
トランザクション開始
(BE...")

© 2024 Cloudbase Inc.
OK
")

© 2024 Cloudbase Inc.
message全件ちょうだい
...")

Prismaで
RLSを扱いたい!
")

PrismaClientIssuerが再活躍
(...")

このObjectは
どこで取得するのか?
")

これで
マルチテナントのセキュリティが
担保さ...")

まだ安心できません
")

行の次は
")


")

TypeScriptは
構造的型付け
")

そしてPrismaは
純粋なObjectを返す
")

この組み合わせが
引き起こすのは
")

意図しないカラムの
露出
")

これをナイーブに実装すると?
")

このAPIのレスポンスは
")

こうなる
")

[
{
: ,
: ,
: ,
:
}
]
"id"
"name"
"a...")
} ] "id" "name" "address" "password" 1 "tockn" "〒108-0073 東京都港区三田3-2-8" "sugoi-secure-password"

[
{
: ,
: ,
: ,
:
}
]
"id"
"name"
"a...")
} ] "id" "name" "address" "password" 1 "tockn" "〒108-0073 東京都港区三田3-2-8" "sugoi-secure-password" 個人情報が丸見えに

そこで
")

zod
")

Response型の定義に
zodを使う
")

このAPIのレスポンスは
こうなる
")

middleware等で
Responseへの
...")

これで
列レベルのセキュリティも
担保された
")

© 2024 Cloudbase Inc.
8 テスタビリテ...")

© 2024 Cloudbase Inc.
")

Prismaを用いた実装では
実際のDBを使用し...")

開発生産性の
課題がある
")

シードレコードの
用意が大変問題
")

テスト対象実行後
DBのレコード検証ロジックを
...")

テスト実行後
レコードのクリーンアップが
大変...")

これらの実装で
テストコードの見通しが
悪くなる
")

そこで
")

内製Test Runnerを開発
「また内製かよ....")

it
run
method
_TEST_ONLY_prima...")
, { { [{ , }], [{ , , }], }, () ( ( ) ( , )), { }, { [{ , , }] ... '指定したgroupId配下にuserが作成される' 'テナント1' 'グループ1' 'tockn' db recordSet: tenant: id: name: group: id: name: tenantId: : tx tx returns: id: mutates: user: id: name: groupId: 1 1 1 1 1 1 1 => =>

it
run
method
_TEST_ONLY_prima...")
, { { [{ , }], [{ , , }], }, () ( ( ) ( , )), { }, { [{ , , }] ... '指定したgroupId配下にuserが作成される' 'テナント1' 'グループ1' 'tockn' db recordSet: tenant: id: name: group: id: name: tenantId: : tx tx returns: id: mutates: user: id: name: groupId: 1 1 1 1 1 1 1 => => シードレコードを 宣言的に定義

it
run
method
_TEST_ONLY_prima...")
, { { [{ , }], [{ , , }], }, () ( ( ) ( , )), { }, { [{ , , }] ... '指定したgroupId配下にuserが作成される' 'テナント1' 'グループ1' 'tockn' db recordSet: tenant: id: name: group: id: name: tenantId: : tx tx returns: id: mutates: user: id: name: groupId: 1 1 1 1 1 1 1 => => シードレコードを 宣言的に定義 DELETEしてから INSERT

it
run
method
_TEST_ONLY_prima...")
, { { [{ , }], [{ , , }], }, () ( ( ) ( , )), { }, { [{ , , }] ... '指定したgroupId配下にuserが作成される' 'テナント1' 'グループ1' 'tockn' db recordSet: tenant: id: name: group: id: name: tenantId: : tx tx returns: id: mutates: user: id: name: groupId: 1 1 1 1 1 1 1 => => シードレコードを 宣言的に定義 外部キー制約も考慮して INSERT DELETEしてから INSERT

it
run
method
_TEST_ONLY_prima...")
, { { [{ , }], [{ , , }], }, () ( ( ) ( , )), { }, { [{ , , }] ... '指定したgroupId配下にuserが作成される' 'テナント1' 'グループ1' 'tockn' db recordSet: tenant: id: name: group: id: name: tenantId: : tx tx returns: id: mutates: user: id: name: groupId: 1 1 1 1 1 1 1 => => テスト対象の メソッドを書く

it
run
method
_TEST_ONLY_prima...")
, { { [{ , }], [{ , , }], }, () ( ( ) ( , )), { }, { [{ , , }] ... '指定したgroupId配下にuserが作成される' 'テナント1' 'グループ1' 'tockn' db recordSet: tenant: id: name: group: id: name: tenantId: : tx tx returns: id: mutates: user: id: name: groupId: 1 1 1 1 1 1 1 => => テスト対象メソッドの 戻り値を定義

it
run
method
_TEST_ONLY_prima...")
, { { [{ , }], [{ , , }], }, () ( ( ) ( , )), { }, { [{ , , }] ... '指定したgroupId配下にuserが作成される' 'テナント1' 'グループ1' 'tockn' db recordSet: tenant: id: name: group: id: name: tenantId: : tx tx returns: id: mutates: user: id: name: groupId: 1 1 1 1 1 1 1 => => テスト対象メソッド実行後の レコードの状態を定義

これだけだと
自慢話で終わってしまう
")

というわけで...
")

OSS化しました
")

prisma-generator-integrat...")

© 2024 Cloudbase Inc.
A オブザーバビ...")

© 2024 Cloudbase Inc.
")

Prismaの裏で行われている処理
どのくらい把...")

DBとのコネクション確立
")

PrismaClientから
PrismaEng...")

SQLの発行
")

DBからの結果を
PrismaClientの結果...")

もっと言えば
1つのHTTPリクエストから
レ...")

そこで
")

OpenTelemetry
")

OpenTelemetry
・オブザーバビリティの...")

Prismaで
OpenTelemetryの計装...")

そこで
")

OpenTelemetry tracing
(p...")

Cloudbaseでは
可視化ツールとしてDat...")

ローカルでもJeagerを使用
日々の開発にも役...")

Prismaは裏で色々頑張ってくれている
だから...")

© 2024 Cloudbase Inc.
・結局、SQLは大事
・Prisma...")

© 2024 Cloudbase Inc.
")

Cloudbaseの
アプリケーションレイヤは
...")

Prismaも使いこなす
プロダクトエンジニアと...")

Engineer Entrance Book
")

TSKaigiのビールスポンサーです!
Cloudba...")

オレンジの服着てる人は多分Cloudbas...")