Upgrade to PRO for Only $50/Year—Limited-Time Offer! 🔥
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
Prisma ORMを2年運用して培ったノウハウを共有する
Search
tockn
May 10, 2024
Technology
28
9k
Prisma ORMを2年運用して培ったノウハウを共有する
TSKaigi 2024
ref:
https://tskaigi.org/talks/tockn
tockn
May 10, 2024
Tweet
Share
More Decks by tockn
See All by tockn
進化する事業とデータ構造 ~Cloudbaseの場合~
tockn
5
510
Other Decks in Technology
See All in Technology
多様なロール経験が導いたエンジニアキャリアのナビゲーション
coconala_engineer
1
170
コーポレートデータマスター構築への道
kworkdev
PRO
0
130
re:Invent2024のIaC周りのアップデート&セッションの共有/around-re-invent-2024-iac-updates
tomoki10
0
710
Replit Agent
kawaguti
PRO
2
210
2024/12/05 AITuber本著者によるAIキャラクター入門 - AITuberの基礎からソフトウェア設計、失敗談まで
sr2mg4
2
590
Will Positron accelerate us?
lycorptech_jp
PRO
1
130
Reliability Engineering at Studist
katsuhisa91
PRO
0
120
問題を認識して解決できる人は何でもできる
i999rri
0
110
お悩みハンドブック紹介資料
grafferhandbook
0
1.6k
スパイクアクセス対策としての pitchfork 導入
riseshia
0
200
ソフトウェアエンジニアとしてキャリアの螺旋を駆け上がる方法 - 経験と出会いが人生を変える / Career-Anchor-Drive
soudai
13
2.9k
ファインディの4年にわたる技術的負債の返済 / Repaying 4 Years of Technical Debt at Findy
ma3tk
7
3.8k
Featured
See All Featured
How To Stay Up To Date on Web Technology
chriscoyier
789
250k
Embracing the Ebb and Flow
colly
84
4.5k
Designing for Performance
lara
604
68k
A Philosophy of Restraint
colly
203
16k
Building a Scalable Design System with Sketch
lauravandoore
459
33k
Six Lessons from altMBA
skipperchong
27
3.5k
Making the Leap to Tech Lead
cromwellryan
133
9k
Improving Core Web Vitals using Speculation Rules API
sergeychernyshev
0
57
Bash Introduction
62gerente
608
210k
Speed Design
sergeychernyshev
25
650
RailsConf 2023
tenderlove
29
920
実際に使うSQLの書き方 徹底解説 / pgcon21j-tutorial
soudai
169
50k
Transcript
Prisma ORMを2年運用して培った ノウハウを共有する Cloudbase株式会社 tockn (Takuto Sato)
© 2024 Cloudbase Inc. セッションの目的
Prismaは地位を確立しつつある © 2024 Cloudbase Inc.
情報も増えつつある © 2024 Cloudbase Inc. A 入門記3 A 採用事例記3 A
エラー対処記3 A 解説記事 (いつもお世話になっています!)
© 2024 Cloudbase Inc. しかし
© 2024 Cloudbase Inc. リアルな運用事例が 足りてない!
Prisma ORMを2年運用して 培ったノウハウを共有する © 2024 Cloudbase Inc.
アジェンダ É 自己紹介 É Prisma ORMの基本 CÉ ノウハウ紹介 ~ilitiesを添えて~ HÉ
まとめ © 2024 Cloudbase Inc.
アジェンダ É 自己紹介 É Prisma ORMの基本 CÉ ノウハウ紹介 ~ilitiesを添えて~ HÉ
まとめ © 2024 Cloudbase Inc.
© 2024 Cloudbase Inc. tockn (Takuto Sato) Cloudbase株式会社 ソフトウェアエンジニア バックエンド〜Webフロントエンドまで
で開発 @tockn_s @tockn
Cloudbaseの技術スタック Prismaは地位を確立しつつある Cloudbaseについて
Cloudbaseの技術スタック © 2024 Cloudbase Inc. RDB API Server GraphDB Data
Loader Storage スキャナー お客様のクラウド環境 Web Frontend お客様
Cloudbaseの技術スタック © 2024 Cloudbase Inc. RDB API Server GraphDB Data
Loader Storage スキャナー お客様のクラウド環境 Web Frontend お客様
アジェンダ É 自己紹介 É Prisma ORMの基本 CÉ ノウハウ紹介 ~ilitiesを添えて~ HÉ
まとめ © 2024 Cloudbase Inc.
Prisma ORMとは E TypeScript向けORMライブラ3 E RDBだけでなくドキュメント指向DBも サポー E PostgreSQL, MySQL,
MongoDB, etc... © 2024 Cloudbase Inc.
Prisma ORMの思想 $ アプリケーション開発者がDBを操作す る際の生産性を向上させるこ( $ 直感的なAPb $ 強力な型定! $
純粋なObjectを返す © 2024 Cloudbase Inc.
操作例: SELECT const = . . ({ { } })
users await prisma user where: name: findMany "tockn" /* [ { id: 2, email: '
[email protected]
', name: 'tockn' }, { id: 3, email: '
[email protected]
', name: 'tockn' } ] */ © 2024 Cloudbase Inc.
操作例: SELECT const = . . ({ { } })
users await prisma user where: name: findMany "tockn" /* [ { id: 2, email: '
[email protected]
', name: 'tockn' }, { id: 3, email: '
[email protected]
', name: 'tockn' } ] */ © 2024 Cloudbase Inc. 純粋なObjectを返す
操作例: INSERT , UPDATE, DELETE... // INSERT // UPDATE //
DELETE // UPSERT await await await await await await . . ({ { , } }) . . ({ { }, { } }) . . ({ { }, { } }) . . ({ { } }) . . ({ { } }) . . ({ { }, { , }, { } }) prisma user data: name: email: prisma user where: email: data: name: prisma user where: name: data: name: prisma user where: email: prisma user where: name: prisma user where: email: create: name: email: update: name: create update updateMany delete deleteMany upsert "tockn" "
[email protected]
" "
[email protected]
" "tockn" "sato" "tockn" "
[email protected]
" "tockn" "
[email protected]
" "tockn" "
[email protected]
" "sato" © 2024 Cloudbase Inc.
© 2024 Cloudbase Inc. 前提知識を整理したところで 本題です
アジェンダ É 自己紹介 É Prisma ORMの基本 CÉ ノウハウ紹介 ~ilitiesを添えて~ HÉ
まとめ © 2024 Cloudbase Inc.
ilitiesに沿ってお話します © 2024 Cloudbase Inc. F パフォーマンス、スケーラビリティ F セキュリティ F
テスタビリティ F オブザーバビリティ
パフォーマンス、スケーラビリティ © 2024 Cloudbase Inc.
© 2024 Cloudbase Inc. Prismaは 「直感的なAPIを提供しSQLを 強く意識せずに使えるようにする」 ライブラリ
© 2024 Cloudbase Inc. 雰囲気で使えちゃうので嬉しい
© 2024 Cloudbase Inc. でも結局、SQLは大事
© 2024 Cloudbase Inc. 運用する中で注意すべきと感じた SQLを見ていく
find系のincludeについて await . . ({ { , }, }); prisma
user include: post: findFirst true © 2024 Cloudbase Inc.
find系のincludeについて await . . ({ { , }, }); prisma
user include: post: findFirst true © 2024 Cloudbase Inc. リレーションを持つテーブルを指定
includeして得られる結果 { , , , [ { , , ,
, , }, ], }; 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.
includeして得られる結果 { , , , [ { , , ,
, , }, ], }; 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. リレーション先のデータも取得
© 2024 Cloudbase Inc. リレーションを取得ということは… JOIN句が使われている?
© 2024 Cloudbase Inc. リレーションを取得ということは… JOIN句が使われている? NO (デフォルトでは)
find系のincludeで発行されるSQL © 2024 Cloudbase Inc. -- 1. Userの取得 SELECT FROM
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`
find系のincludeで発行されるSQL © 2024 Cloudbase Inc. -- 1. Userの取得 SELECT FROM
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を取得
find系のincludeで発行されるSQL © 2024 Cloudbase Inc. -- 1. Userの取得 SELECT FROM
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を取得
find系のincludeで発行されるSQL © 2024 Cloudbase Inc. -- 1. Userの取得 SELECT FROM
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
© 2024 Cloudbase Inc. ・WHERE INが大量になる ・通信のオーバーヘッド ・DBのオプティマイザの力を活かせない
© 2024 Cloudbase Inc. Cloudbaseでは 肥大化した一覧取得系APIの パフォーマンスが問題に 他にもRelation Filterの非効率なSQLもあった
© 2024 Cloudbase Inc. 他にも create, update, deleteで SELECT文が走ったり… (4.x.x)
© 2024 Cloudbase Inc. どう対応しているか?
© 2024 Cloudbase Inc. Cloudbaseではど う し ているか g 対象データ量が多い場合はqueryRawを使C
g 少ない場合はincludeを使う await . <{ : ; : }> ; prisma id name $queryRaw number string ` SELECT u.id, u.name FROM user as u INNER JOIN post as p ON u.id = p."authorId" WHERE u.name = 'tockn'`
© 2024 Cloudbase Inc. Cloudbaseではど う し ているか f 原則updateMany,
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'
© 2024 Cloudbase Inc. 「PrismaのSQLは効率悪いのか〜」
© 2024 Cloudbase Inc. ちょっと待って!
© 2024 Cloudbase Inc. PrismaはSQL最適化に力を入れている
© 2024 Cloudbase Inc. 5.x.xからupdate, deleteでSELECT文無しに https://github.com/prisma/prisma-engines/pull/4595
© 2024 Cloudbase Inc. relational filterで発行されるクエリ改善 https://github.com/prisma/prisma-engines/pull/4235
© 2024 Cloudbase Inc. previewFeature: relationJoins https://github.com/prisma/prisma/releases/tag/5.8.0
previewFeature: relationJoins await . . ({ { }, { },
}); prisma user include: posts: where: name: findMany true 'tockn' © 2024 Cloudbase Inc.
previewFeature: relationJoins await . . ({ , { }, {
}, }); prisma user relationLoadStrategy: include: posts: where: name: findMany 'join' 'tockn' true © 2024 Cloudbase Inc. joinかqueryか選べるように!
find系のincludeで発行されるSQL © 2024 Cloudbase Inc. await . . ({ ,
{ }, { }, }); 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
find系のincludeで発行されるSQL © 2024 Cloudbase Inc. await . . ({ ,
{ }, { }, }); 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だが...)
© 2024 Cloudbase Inc. ちゃんと頑張ってくれている というのを伝えたかった
© 2024 Cloudbase Inc. クエリのパフォーマンスについて 見たので
© 2024 Cloudbase Inc. 次は スケーラビリティ的観点の工夫
© 2024 Cloudbase Inc. Cloudbaseは Read Heavyな側面もある
© 2024 Cloudbase Inc. お客様のクラウド環境の リソース情報 リスク情報 どれも非常に大量…
© 2024 Cloudbase Inc. Readのスケールをしたい
© 2024 Cloudbase Inc. Read Replicaを使おう
© 2024 Cloudbase Inc. Read Replica V Read専用のノーD V Primaryの内容をほぼリアルタイムにコピー
Primary レプリケーション Read Replica
© 2024 Cloudbase Inc. PrismaでRead Replicaを扱いたい!
© 2024 Cloudbase Inc. PrismaでRead Replicaを扱いたい! ↓ クエリの投げ先を切り替えたい!
© 2024 Cloudbase Inc. そこで
© 2024 Cloudbase Inc. Cloudbaseでは PrismaClientをwrapする 独自クラスを作っています
© 2024 Cloudbase Inc. その名も PrismaClientIssuer
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.
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. primaryを指定
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. callbackの引数に primaryへ張ったTranasctionClientが来る
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. primaryへクエリを発行
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. read replicaも同じ
© 2024 Cloudbase Inc. Read Replicaを扱えるようになった
© 2024 Cloudbase Inc. extension-read-replicasを 何故直接使っていないか Prisma公式の拡張機能
© 2024 Cloudbase Inc. PrismaClientをwrapするメリットは 他にもある
© 2024 Cloudbase Inc. PrimaryとRead Replicaを 使い分けるうえでの課題
© 2024 Cloudbase Inc. Read Replicaに対して Write系クエリは投げられないが… エラーになる
© 2024 Cloudbase Inc. TransactionClientを 引数に持つメソッドを呼ぶ時 Primaryを渡すべきか? Read Replicaで良いのか? わからない問題
PrismaClientIssuerについて const async => = ( : . , :
) { ... }; handleUser tx user Prisma TransactionClient User © 2024 Cloudbase Inc.
PrismaClientIssuerについて const async => = ( : . , :
) { ... }; handleUser tx user Prisma TransactionClient User © 2024 Cloudbase Inc. primaryを渡すべき? readReplicaを渡すべき?
PrismaClientIssuerについて const async => = ( : . , :
) { . . ({ }); }; handleUser create tx user tx user data: user Prisma TransactionClient User await © 2024 Cloudbase Inc. 実装を見ないとわからない (この例はcreateしてるのでprimaryが必要)
© 2024 Cloudbase Inc. 開発体験悪い 型レベルで保証できたら嬉しい
© 2024 Cloudbase Inc. そこで
© 2024 Cloudbase Inc. 2つの独自Transaction型を用意 PrismaClientIssuerと合わせて使う
© 2024 Cloudbase Inc. CBWritableTransaction と CBReadableTransaction
© 2024 Cloudbase Inc. 2つのTransaction型 ・PrismaのTransactionClientの交差型 ・ ・issuerのprimaryメソッドで渡る型
・ に対しても ・ ・issuerのreadReplicaメソッドで渡る型 ・ に対して CBWritableTransaction CBWritableTransaction CBReadableTransaction CBReadableTransaction 渡せる 渡せない
独自Transaction型を使う 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.
独自Transaction型を使う 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
独自Transaction型を使う 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
© 2024 Cloudbase Inc. これを PrismaClientIssuerと合わせて使う
PrismaClientIssuerと独自Transaction型(OKパターン) // primary // CBWritableTransaction // CBReadableTransaction // read replica
// CBReadableTransaction await await return await return . (..., ( ) { ( ) ( ) }) . (..., ( ) { ( ) }) issuer tx tx tx issuer tx tx primary registerUser listUsers readReplica listUsers async => async => © 2024 Cloudbase Inc.
PrismaClientIssuerと独自Transaction型(OKパターン) // primary // CBWritableTransaction // CBReadableTransaction // read replica
// CBReadableTransaction await await return await return . (..., ( ) { ( ) ( ) }) . (..., ( ) { ( ) }) issuer tx tx tx issuer tx tx primary registerUser listUsers readReplica listUsers async => async => © 2024 Cloudbase Inc. Writable, Readableどちらも呼べる primaryでは CBWritableTransactionが貰える
PrismaClientIssuerと独自Transaction型(OKパターン) // primary // CBWritableTransaction // CBReadableTransaction // read replica
// CBReadableTransaction await await return await return . (..., ( ) { ( ) ( ) }) . (..., ( ) { ( ) }) issuer tx tx tx issuer tx tx primary registerUser listUsers readReplica listUsers async => async => © 2024 Cloudbase Inc. readReplicaでは CBReadableTransactionが貰える Readableのみ呼べる
PrismaClientIssuerと独自Transaction型(NGパターン) // NG // 型エラー // NG // 型エラー await
return return . (..., ( ) { ( ) }) = ( : ) { ( ) . . () } issuer tx tx tx tx tx user readReplica registerUser listUsers registerUser findMany async => const async => CBReadableTransaction © 2024 Cloudbase Inc.
PrismaClientIssuerと独自Transaction型(NGパターン) // NG // 型エラー // NG // 型エラー await
return return . (..., ( ) { ( ) }) = ( : ) { ( ) . . () } issuer tx tx tx tx tx user readReplica registerUser listUsers registerUser findMany async => const async => CBReadableTransaction © 2024 Cloudbase Inc. CBWritableTransactionが必要なメソッドを readReplicaで呼ぶと型エラー
PrismaClientIssuerと独自Transaction型(NGパターン) // NG // 型エラー // NG // 型エラー await
return return . (..., ( ) { ( ) }) = ( : ) { ( ) . . () } issuer tx tx tx tx tx user readReplica registerUser listUsers registerUser findMany async => const async => CBReadableTransaction © 2024 Cloudbase Inc. メソッド内で渡してしまうことも防止
© 2024 Cloudbase Inc. というわけで PrismaClientはそのまま使わずに wrapすると色々仕込めて便利
© 2024 Cloudbase Inc. パフ ォーマンス、 スケーラビリテ ィ ノ ウハウまとめ
・発行されるクエリに注意 ・結局queryRawが必要になることも ・とはいえ、Prismaはクエリ最適化を頑張ってる ・Read Replicaによるスケールアウト ・PrismaClientをwrapするクラスを作ると便利 ・独自のTransaction型を定義すると便利
ilitiesに沿ってお話します © 2024 Cloudbase Inc. 4 セキュリティ 4 テスタビリティ 4
オブザーバビリティ
セキュリティ © 2024 Cloudbase Inc.
© 2024 Cloudbase Inc. Cloudbaseは プールモデル マルチテナントシステム
© 2024 Cloudbase Inc. 同じDB、テーブルに 異なるお客様のデータが入る (プロダクト仕様実現のため)
© 2024 Cloudbase Inc. あるデータを取得する場合 アクセス元ユーザが権限を持っているもの だけを返す必要がある
© 2024 Cloudbase Inc. あるデータを取得する場合 アクセス元ユーザが権限を持っているもの だけを返す必要がある どう実現する?
© 2024 Cloudbase Inc. 「Whereを忘れずに付けよう!」
© 2024 Cloudbase Inc.
© 2024 Cloudbase Inc. 実装漏れ、レビュー漏れ、テスト漏れ… Whereを忘れる可能性はゼロではない
© 2024 Cloudbase Inc. 特にPrismaでは Where対象のkeyにundefinedを渡すと 「条件なし」を意味する
whereとundefined const = (); . . ({ { } });
tenantId await await getMyTenantId findMany prisma message where: tenantId © 2024 Cloudbase Inc.
whereとundefined const = (); . . ({ { } });
tenantId await await getMyTenantId findMany prisma message where: tenantId © 2024 Cloudbase Inc. これがundefinedを返した場合
whereとundefined const = (); . . ({ { } });
tenantId await await getMyTenantId findMany prisma message where: tenantId © 2024 Cloudbase Inc. tenant関係なく 全件取得
© 2024 Cloudbase Inc. 一発アウトの情報漏洩に なりかねない
© 2024 Cloudbase Inc. そこで
© 2024 Cloudbase Inc. Row Level Security
© 2024 Cloudbase Inc. Row Level Security ・通称RLS ・PostgreSQLにある機能 ・DBユーザ、セッション、トランザクションにおいて
アクセス可能なレコードを行レベルで制御する仕組み
RLSざっくり解説 © 2024 Cloudbase Inc.
RLSざっくり解説 © 2024 Cloudbase Inc. トランザクション開始 (BEGIN)
RLSざっくり解説 © 2024 Cloudbase Inc. このトランザクションでは tenantId = 1 のデータだけ返してね
(set_config)
RLSざっくり解説 © 2024 Cloudbase Inc. OK
RLSざっくり解説 © 2024 Cloudbase Inc. message全件ちょうだい (SELECT * FROM messages)
RLSざっくり解説 © 2024 Cloudbase Inc. どうぞ (SELECT * FROM messagesの結果)
id 1 こんにちは〜 どうも〜 さようなら〜 1 1 1 6 29 body tenant_id
RLSざっくり解説 © 2024 Cloudbase Inc. どうぞ (SELECT * FROM messagesの結果)
id 1 こんにちは〜 どうも〜 さようなら〜 1 1 1 6 29 body tenant_id Whereが無くても tenantIdで絞られる
© 2024 Cloudbase Inc. Prismaで RLSを扱いたい!
© 2024 Cloudbase Inc. PrismaClientIssuerが再活躍 (独自クラス)
PrismaClientIssuerとRLS await . ( , ( ) { ... })
issuer accessToken tx primary async => © 2024 Cloudbase Inc.
PrismaClientIssuerとRLS await . ( , ( ) { ... })
issuer accessToken tx primary async => © 2024 Cloudbase Inc. 認可情報を渡す
PrismaClientIssuerとRLS await . ( , ( ) { ... })
issuer accessToken tx primary async => © 2024 Cloudbase Inc. RLSが適用された Transaction
© 2024 Cloudbase Inc. このaccessTokenって何者? await . ( , (
) { ... }) issuer accessToken tx primary async =>
© 2024 Cloudbase Inc. そのユーザがアクセス可能な テナントIDが入ったObject const = { ;
} accessToken tenantId: 1 (厳密にはもっと工夫がある)
© 2024 Cloudbase Inc. このObjectは どこで取得するのか?
© 2024 Cloudbase Inc. express middlewareが発行する 権限を持つtenantId取得 Request Objectとして渡す express
application auth middleware handler
handlerの実装 async => async => ( , ) { .
( . , ( ) { ... }) } req: res: issuer req accessToken tx express.Request express.Response await readReplica © 2024 Cloudbase Inc. reqから取得して 渡すだけ
© 2024 Cloudbase Inc. どうやってRLSを適用した Transactionを生成している? await . ( ,
( ) { ... }) issuer accessToken tx primary async =>
PrismaClientIssuerの実装 async accessToken callback prisma tx tx accessToken tenantId tx
( , ) { ... . ( ( ) { . . ; ( ); }) } readReplica $transaction $executeRaw callback return await return async => ${ } ` SELECT set_config( 'app.rls_config.tenant', , TRUE )` © 2024 Cloudbase Inc.
PrismaClientIssuerの実装 async accessToken callback prisma tx tx accessToken tenantId tx
( , ) { ... . ( ( ) { . . ; ( ); }) } readReplica $transaction $executeRaw callback return await return async => ${ } ` SELECT set_config( 'app.rls_config.tenant', , TRUE )` © 2024 Cloudbase Inc. set_configを呼んで callbackに渡してるだけ
© 2024 Cloudbase Inc. これで マルチテナントのセキュリティが 担保される
© 2024 Cloudbase Inc. まだ安心できません
© 2024 Cloudbase Inc. 行の次は
© 2024 Cloudbase Inc. 列
© 2024 Cloudbase Inc. TypeScriptは 構造的型付け
© 2024 Cloudbase Inc. そしてPrismaは 純粋なObjectを返す
© 2024 Cloudbase Inc. この組み合わせが 引き起こすのは
© 2024 Cloudbase Inc. 意図しないカラムの 露出
© 2024 Cloudbase Inc. 例えば 以下のテーブルがあるとする model User id Int
name String address String password String { }
© 2024 Cloudbase Inc. 例えば 以下のテーブルがあるとする model User id Int
name String address String password String { } 個人情報
© 2024 Cloudbase Inc. このテーブルを使った ユーザ一覧取得APIを考える model User id Int
name String address String password String { }
© 2024 Cloudbase Inc. Responseの型を以下とする type = { : ;
: ; }[]; ListUsersResponse number string id name
© 2024 Cloudbase Inc. これをナイーブに実装すると?
こうなる const async => const = ( , ) {
: = . . (); . ( ); }; listUsers findMany json req res prisma user res resp resp ListUsersResponse await
こうなる const async => const = ( , ) {
: = . . (); . ( ); }; listUsers findMany json req res prisma user res resp resp ListUsersResponse await 型的には問題ないが…?
© 2024 Cloudbase Inc. このAPIのレスポンスは
© 2024 Cloudbase Inc. こうなる
こうなる [ { : , : , : , :
} ] "id" "name" "address" "password" 1 "tockn" "〒108-0073 東京都港区三田3-2-8" "sugoi-secure-password"
こうなる [ { : , : , : , :
} ] "id" "name" "address" "password" 1 "tockn" "〒108-0073 東京都港区三田3-2-8" "sugoi-secure-password" 個人情報が丸見えに
© 2024 Cloudbase Inc. そこで
© 2024 Cloudbase Inc. zod
© 2024 Cloudbase Inc. zod ・スキーマ定義とバリデーションを行うライブラリ ・z.object() 等を使用してスキーマを定義 ・Schema.parse(obj) で渡したobjのバリデーション
const type typeof = . ({ . (), . (), }); = . < >; . ( ); User z id: z name: z User User obj object number string parse User z infer // バリデーション
© 2024 Cloudbase Inc. Response型の定義に zodを使う
Response型をzodで書き換える const type typeof = . ( . ({ .
(), . (), }), ); = . < >; ListUsersResponse z z id: z name: z ListUsersResponse array object number string ListUsersResponse z infer
zodのparseを行う const async => const = ( , ) {
= . ( . . () ); . ( ); }; listUsers parse findMany json req res ListUsersResponse prisma user res resp resp await
zodのparseを行う const async => const = ( , ) {
= . ( . . () ); . ( ); }; listUsers parse findMany json req res ListUsersResponse prisma user res resp resp await parseメソッドを実行
© 2024 Cloudbase Inc. このAPIのレスポンスは こうなる
こうなる [ { : , : } ] "id" "name"
1 "tockn"
こうなる [ { : , : } ] "id" "name"
1 "tockn" 個人情報が切り落とされた! parseは未定義フィールドを切り落とす
© 2024 Cloudbase Inc. middleware等で Responseへの zodの使用を強制する
© 2024 Cloudbase Inc. これで 列レベルのセキュリティも 担保された
© 2024 Cloudbase Inc. セキ ュ リテ ィ ノ ウハウまとめ
・マルチテナントにおける行レベルのセキュリティ ・PrismaでRLSを使う ・またしてもPrismaClientをwrapするクラスが便利 ・列レベルのセキュリティも忘れてはいけない ・PrismaはPOJO返し、TypeScriptは構造的型付け ・ナイーブな実装は情報漏洩になることも ・zodを使うことで回避
ilitiesに沿ってお話します © 2024 Cloudbase Inc. 8 テスタビリティ 8 オブザーバビリティ
テスタビリティ © 2024 Cloudbase Inc.
© 2024 Cloudbase Inc. Prismaを用いた実装では 実際のDBを使用した インテグレーションテストを 書きたくなる
© 2024 Cloudbase Inc. 開発生産性の 課題がある
© 2024 Cloudbase Inc. シードレコードの 用意が大変問題
© 2024 Cloudbase Inc. テスト対象実行後 DBのレコード検証ロジックを 毎度書くのが面倒問題
© 2024 Cloudbase Inc. テスト実行後 レコードのクリーンアップが 大変問題
© 2024 Cloudbase Inc. これらの実装で テストコードの見通しが 悪くなる
© 2024 Cloudbase Inc. そこで
© 2024 Cloudbase Inc. 内製Test Runnerを開発 「また内製かよ...」 と思った方、少々お待ち下さい
内製Test Runner全体像 it run method _TEST_ONLY_primaryDBWithRLS registerUserInGroup ( , (
, { { [{ , }], [{ , , }], }, () ( ( ) ( , )), { }, { [{ , , }] ... '指定した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 => =>
内製Test Runner全体像 it run method _TEST_ONLY_primaryDBWithRLS registerUserInGroup ( , (
, { { [{ , }], [{ , , }], }, () ( ( ) ( , )), { }, { [{ , , }] ... '指定した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 => => シードレコードを 宣言的に定義
内製Test Runner全体像 it run method _TEST_ONLY_primaryDBWithRLS registerUserInGroup ( , (
, { { [{ , }], [{ , , }], }, () ( ( ) ( , )), { }, { [{ , , }] ... '指定した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
内製Test Runner全体像 it run method _TEST_ONLY_primaryDBWithRLS registerUserInGroup ( , (
, { { [{ , }], [{ , , }], }, () ( ( ) ( , )), { }, { [{ , , }] ... '指定した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
内製Test Runner全体像 it run method _TEST_ONLY_primaryDBWithRLS registerUserInGroup ( , (
, { { [{ , }], [{ , , }], }, () ( ( ) ( , )), { }, { [{ , , }] ... '指定した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 => => テスト対象の メソッドを書く
内製Test Runner全体像 it run method _TEST_ONLY_primaryDBWithRLS registerUserInGroup ( , (
, { { [{ , }], [{ , , }], }, () ( ( ) ( , )), { }, { [{ , , }] ... '指定した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 => => テスト対象メソッドの 戻り値を定義
内製Test Runner全体像 it run method _TEST_ONLY_primaryDBWithRLS registerUserInGroup ( , (
, { { [{ , }], [{ , , }], }, () ( ( ) ( , )), { }, { [{ , , }] ... '指定した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 => => テスト対象メソッド実行後の レコードの状態を定義
© 2024 Cloudbase Inc. 内製Test Runner ・レコードの状態、戻り値を宣言的に定義 ・良い感じにINSERTし、良い感じに検証 ・非常に開発者体験が良い ・他にも便利なオプションがある
・snapshot ・エラー検証
© 2024 Cloudbase Inc. これだけだと 自慢話で終わってしまう
© 2024 Cloudbase Inc. というわけで...
© 2024 Cloudbase Inc. OSS化しました
© 2024 Cloudbase Inc. prisma-generator-integration-test-runner https://github.com/Levetty/prisma-generator-integration-test-runner
© 2024 Cloudbase Inc. 今から使えます! schema.prisma shell npm @cloudbase-inc/ install
prisma-generator-integration-test-runner -D generator integration test runner provider - - { = } "prisma-generator-integration-test-runner"
© 2024 Cloudbase Inc. テスタビリテ ィ まとめ ・開発生産性に課題 ・テストレコードの用意、検証など ・内製Test
Runnerが便利 ・宣言的な状態定義 ・OSSとして公開しました ・ぜひ使ってみてください
ilitiesに沿ってお話します © 2024 Cloudbase Inc. A オブザーバビリティ
オブザーバビリティ © 2024 Cloudbase Inc.
© 2024 Cloudbase Inc. Prismaの裏で行われている処理 どのくらい把握できていますか?
© 2024 Cloudbase Inc. DBとのコネクション確立
© 2024 Cloudbase Inc. PrismaClientから PrismaEngine向けクエリの変換
© 2024 Cloudbase Inc. SQLの発行
© 2024 Cloudbase Inc. DBからの結果を PrismaClientの結果として変換
© 2024 Cloudbase Inc. もっと言えば 1つのHTTPリクエストから レスポンスまでに起きてることを どのくらい把握できていますか?
© 2024 Cloudbase Inc. そこで
© 2024 Cloudbase Inc. OpenTelemetry
© 2024 Cloudbase Inc. OpenTelemetry ・オブザーバビリティのためのフレームワーク ・テレメトリデータの作成、管理 ・トレース、メトリクス、ログ ・環境に依らず計装できる ・保存と可視化は責務外
© 2024 Cloudbase Inc. Prismaで OpenTelemetryの計装をしたい
© 2024 Cloudbase Inc. そこで
© 2024 Cloudbase Inc. OpenTelemetry tracing (preview)
© 2024 Cloudbase Inc. OpenTelemetry tracing ・PrismaにあるPreview Feature ・OpenTelemetryの計装 ・PrismaClientの様々な処理をトレースできる
・環境構築は公式ドキュメントを参照 ・expressなどの計装と合わせて見れる
トレースをJaegerで可視化 const = (); . . ({ { } });
tenantId await await getMyTenantId findMany prisma message where: tenantId © 2024 Cloudbase Inc.
トレースをJaegerで可視化 const = (); . . ({ { } });
tenantId await await getMyTenantId findMany prisma message where: tenantId © 2024 Cloudbase Inc. どのAPIで
トレースをJaegerで可視化 const = (); . . ({ { } });
tenantId await await getMyTenantId findMany prisma message where: tenantId © 2024 Cloudbase Inc. どのような処理が どのくらい実行されたか
トレースをJaegerで可視化 const = (); . . ({ { } });
tenantId await await getMyTenantId findMany prisma message where: tenantId © 2024 Cloudbase Inc. どのようなSQLが発行されたか
© 2024 Cloudbase Inc. Cloudbaseでは 可視化ツールとしてDatadogを使用
© 2024 Cloudbase Inc. ローカルでもJeagerを使用 日々の開発にも役立てている
© 2024 Cloudbase Inc. Prismaは裏で色々頑張ってくれている だからこそ オブザーバビリティが大事
© 2024 Cloudbase Inc. オブザーバビリテ ィ ノ ウハウまとめ ・Prismaの裏でも様々な処理が走る ・OpenTelemetry
tracingを使って可視化
アジェンダ É 自己紹介 É Prisma ORMの基本 CÉ ノウハウ紹介 ~ilitiesを添えて~ HÉ
まとめ © 2024 Cloudbase Inc.
全体まとめ © 2024 Cloudbase Inc. ・結局、SQLは大事 ・PrismaClientをwrapすると共通処理を仕込めて便利 ・インテグレーションテスト基盤をOSS化しました ・OpenTelemetry tracing使おう
宣伝 © 2024 Cloudbase Inc.
© 2024 Cloudbase Inc. Cloudbaseの アプリケーションレイヤは フルTypeScript
© 2024 Cloudbase Inc. Prismaも使いこなす プロダクトエンジニアとして 一緒に働きませんか?
We Are Hiring! Engineer Entrance Book
TSKaigiのビールスポンサーです! TSKaigiのビールスポンサーです! Cloudbase は Cloudbase は
オレンジの服着てる人は多分Cloudbaseですw オレンジの服着てる人は多分Cloudbaseですw 一緒にビール飲みましょう! 一緒にビール飲みましょう!