Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Features
Speaker Deck
PRO
Sign in
Sign up for free
Search
Search
Prisma ORMを2年運用して培ったノウハウを共有する
Search
tockn
May 10, 2024
Technology
28
9.2k
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
LINEヤフーのフロントエンド組織・体制の紹介【24年12月】
lycorp_recruit_jp
0
530
Snykで始めるセキュリティ担当者とSREと開発者が楽になる脆弱性対応 / Getting started with Snyk Vulnerability Response
yamaguchitk333
2
180
プロダクト開発を加速させるためのQA文化の築き方 / How to build QA culture to accelerate product development
mii3king
1
260
Opcodeを読んでいたら何故かphp-srcを読んでいた話
murashotaro
0
200
kargoの魅力について伝える
magisystem0408
0
210
AI時代のデータセンターネットワーク
lycorptech_jp
PRO
1
280
ずっと昔に Star をつけたはずの思い出せない GitHub リポジトリを見つけたい!
rokuosan
0
150
Qiita埋め込み用スライド
naoki_0531
0
4.8k
1等無人航空機操縦士一発試験 合格までの道のり ドローンミートアップ@大阪 2024/12/18
excdinc
0
160
サイバー攻撃を想定したセキュリティガイドライン 策定とASM及びCNAPPの活用方法
syoshie
3
1.2k
PHP ユーザのための OpenTelemetry 入門 / phpcon2024-opentelemetry
shin1x1
1
200
Storage Browser for Amazon S3
miu_crescent
1
140
Featured
See All Featured
Java REST API Framework Comparison - PWX 2021
mraible
PRO
28
8.3k
Rebuilding a faster, lazier Slack
samanthasiow
79
8.7k
A Modern Web Designer's Workflow
chriscoyier
693
190k
For a Future-Friendly Web
brad_frost
175
9.4k
RailsConf & Balkan Ruby 2019: The Past, Present, and Future of Rails at GitHub
eileencodes
132
33k
Docker and Python
trallard
42
3.1k
Measuring & Analyzing Core Web Vitals
bluesmoon
4
170
How to Create Impact in a Changing Tech Landscape [PerfNow 2023]
tammyeverts
48
2.2k
A Philosophy of Restraint
colly
203
16k
Writing Fast Ruby
sferik
628
61k
Building Flexible Design Systems
yeseniaperezcruz
327
38k
GraphQLの誤解/rethinking-graphql
sonatard
67
10k
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 一緒にビール飲みましょう! 一緒にビール飲みましょう!