Slide 1

Slide 1 text

Prisma ORMを2年運用して培った ノウハウを共有する Cloudbase株式会社
 tockn (Takuto Sato)

Slide 2

Slide 2 text

© 2024 Cloudbase Inc. セッションの目的

Slide 3

Slide 3 text

Prismaは地位を確立しつつある © 2024 Cloudbase Inc.

Slide 4

Slide 4 text

情報も増えつつある © 2024 Cloudbase Inc. A 入門記3 A 採用事例記3 A エラー対処記3 A 解説記事 (いつもお世話になっています!)

Slide 5

Slide 5 text

© 2024 Cloudbase Inc. しかし

Slide 6

Slide 6 text

© 2024 Cloudbase Inc. リアルな運用事例が 足りてない!

Slide 7

Slide 7 text

Prisma ORMを2年運用して 培ったノウハウを共有する © 2024 Cloudbase Inc.

Slide 8

Slide 8 text

アジェンダ É 自己紹介 É Prisma ORMの基本 CÉ ノウハウ紹介 ~ilitiesを添えて~ HÉ まとめ © 2024 Cloudbase Inc.

Slide 9

Slide 9 text

アジェンダ É 自己紹介 É Prisma ORMの基本 CÉ ノウハウ紹介 ~ilitiesを添えて~ HÉ まとめ © 2024 Cloudbase Inc.

Slide 10

Slide 10 text

© 2024 Cloudbase Inc. tockn (Takuto Sato) Cloudbase株式会社 ソフトウェアエンジニア バックエンド〜Webフロントエンドまで で開発 @tockn_s @tockn

Slide 11

Slide 11 text

Cloudbaseの技術スタック Prismaは地位を確立しつつある Cloudbaseについて

Slide 12

Slide 12 text

Cloudbaseの技術スタック © 2024 Cloudbase Inc. RDB API Server GraphDB Data Loader Storage スキャナー お客様のクラウド環境 Web Frontend お客様

Slide 13

Slide 13 text

Cloudbaseの技術スタック © 2024 Cloudbase Inc. RDB API Server GraphDB Data Loader Storage スキャナー お客様のクラウド環境 Web Frontend お客様

Slide 14

Slide 14 text

アジェンダ É 自己紹介 É Prisma ORMの基本 CÉ ノウハウ紹介 ~ilitiesを添えて~ HÉ まとめ © 2024 Cloudbase Inc.

Slide 15

Slide 15 text

Prisma ORMとは E TypeScript向けORMライブラ3 E RDBだけでなくドキュメント指向DBも サポー E PostgreSQL, MySQL, MongoDB, etc... © 2024 Cloudbase Inc.

Slide 16

Slide 16 text

Prisma ORMの思想 $ アプリケーション開発者がDBを操作す る際の生産性を向上させるこ( $ 直感的なAPb $ 強力な型定! $ 純粋なObjectを返す © 2024 Cloudbase Inc.

Slide 17

Slide 17 text

操作例: 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.

Slide 18

Slide 18 text

操作例: 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を返す

Slide 19

Slide 19 text

操作例: 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.

Slide 20

Slide 20 text

© 2024 Cloudbase Inc. 前提知識を整理したところで 本題です

Slide 21

Slide 21 text

アジェンダ É 自己紹介 É Prisma ORMの基本 CÉ ノウハウ紹介 ~ilitiesを添えて~ HÉ まとめ © 2024 Cloudbase Inc.

Slide 22

Slide 22 text

ilitiesに沿ってお話します © 2024 Cloudbase Inc. F パフォーマンス、スケーラビリティ F セキュリティ F テスタビリティ F オブザーバビリティ

Slide 23

Slide 23 text

パフォーマンス、スケーラビリティ © 2024 Cloudbase Inc.

Slide 24

Slide 24 text

© 2024 Cloudbase Inc. Prismaは 「直感的なAPIを提供しSQLを 強く意識せずに使えるようにする」 ライブラリ

Slide 25

Slide 25 text

© 2024 Cloudbase Inc. 雰囲気で使えちゃうので嬉しい

Slide 26

Slide 26 text

© 2024 Cloudbase Inc. でも結局、SQLは大事

Slide 27

Slide 27 text

© 2024 Cloudbase Inc. 運用する中で注意すべきと感じた SQLを見ていく

Slide 28

Slide 28 text

find系のincludeについて await . . ({ { , }, }); prisma user include: post: findFirst true © 2024 Cloudbase Inc.

Slide 29

Slide 29 text

find系のincludeについて await . . ({ { , }, }); prisma user include: post: findFirst true © 2024 Cloudbase Inc. リレーションを持つテーブルを指定

Slide 30

Slide 30 text

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.

Slide 31

Slide 31 text

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. リレーション先のデータも取得

Slide 32

Slide 32 text

© 2024 Cloudbase Inc. リレーションを取得ということは… JOIN句が使われている?

Slide 33

Slide 33 text

© 2024 Cloudbase Inc. リレーションを取得ということは… JOIN句が使われている? NO (デフォルトでは)

Slide 34

Slide 34 text

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`

Slide 35

Slide 35 text

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を取得

Slide 36

Slide 36 text

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を取得

Slide 37

Slide 37 text

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

Slide 38

Slide 38 text

© 2024 Cloudbase Inc. ・WHERE INが大量になる ・通信のオーバーヘッド ・DBのオプティマイザの力を活かせない

Slide 39

Slide 39 text

© 2024 Cloudbase Inc. Cloudbaseでは 肥大化した一覧取得系APIの パフォーマンスが問題に 他にもRelation Filterの非効率なSQLもあった

Slide 40

Slide 40 text

© 2024 Cloudbase Inc. 他にも create, update, deleteで SELECT文が走ったり… (4.x.x)

Slide 41

Slide 41 text

© 2024 Cloudbase Inc. どう対応しているか?

Slide 42

Slide 42 text

© 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'`

Slide 43

Slide 43 text

© 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'

Slide 44

Slide 44 text

© 2024 Cloudbase Inc. 「PrismaのSQLは効率悪いのか〜」

Slide 45

Slide 45 text

© 2024 Cloudbase Inc. ちょっと待って!

Slide 46

Slide 46 text

© 2024 Cloudbase Inc. PrismaはSQL最適化に力を入れている

Slide 47

Slide 47 text

© 2024 Cloudbase Inc. 5.x.xからupdate, deleteでSELECT文無しに https://github.com/prisma/prisma-engines/pull/4595

Slide 48

Slide 48 text

© 2024 Cloudbase Inc. relational filterで発行されるクエリ改善 https://github.com/prisma/prisma-engines/pull/4235

Slide 49

Slide 49 text

© 2024 Cloudbase Inc. previewFeature: relationJoins https://github.com/prisma/prisma/releases/tag/5.8.0

Slide 50

Slide 50 text

previewFeature: relationJoins await . . ({ { }, { }, }); prisma user include: posts: where: name: findMany true 'tockn' © 2024 Cloudbase Inc.

Slide 51

Slide 51 text

previewFeature: relationJoins await . . ({ , { }, { }, }); prisma user relationLoadStrategy: include: posts: where: name: findMany 'join' 'tockn' true © 2024 Cloudbase Inc. joinかqueryか選べるように!

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

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だが...)

Slide 54

Slide 54 text

© 2024 Cloudbase Inc. ちゃんと頑張ってくれている というのを伝えたかった

Slide 55

Slide 55 text

© 2024 Cloudbase Inc. クエリのパフォーマンスについて 見たので

Slide 56

Slide 56 text

© 2024 Cloudbase Inc. 次は スケーラビリティ的観点の工夫

Slide 57

Slide 57 text

© 2024 Cloudbase Inc. Cloudbaseは Read Heavyな側面もある

Slide 58

Slide 58 text

© 2024 Cloudbase Inc. お客様のクラウド環境の リソース情報 リスク情報 どれも非常に大量…

Slide 59

Slide 59 text

© 2024 Cloudbase Inc. Readのスケールをしたい

Slide 60

Slide 60 text

© 2024 Cloudbase Inc. Read Replicaを使おう

Slide 61

Slide 61 text

© 2024 Cloudbase Inc. Read Replica V Read専用のノーD V Primaryの内容をほぼリアルタイムにコピー Primary レプリケーション Read Replica

Slide 62

Slide 62 text

© 2024 Cloudbase Inc. PrismaでRead Replicaを扱いたい!

Slide 63

Slide 63 text

© 2024 Cloudbase Inc. PrismaでRead Replicaを扱いたい! ↓ クエリの投げ先を切り替えたい!

Slide 64

Slide 64 text

© 2024 Cloudbase Inc. そこで

Slide 65

Slide 65 text

© 2024 Cloudbase Inc. Cloudbaseでは PrismaClientをwrapする 独自クラスを作っています

Slide 66

Slide 66 text

© 2024 Cloudbase Inc. その名も PrismaClientIssuer

Slide 67

Slide 67 text

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.

Slide 68

Slide 68 text

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を指定

Slide 69

Slide 69 text

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が来る

Slide 70

Slide 70 text

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へクエリを発行

Slide 71

Slide 71 text

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も同じ

Slide 72

Slide 72 text

© 2024 Cloudbase Inc. Read Replicaを扱えるようになった

Slide 73

Slide 73 text

© 2024 Cloudbase Inc. extension-read-replicasを 何故直接使っていないか Prisma公式の拡張機能

Slide 74

Slide 74 text

© 2024 Cloudbase Inc. PrismaClientをwrapするメリットは 他にもある

Slide 75

Slide 75 text

© 2024 Cloudbase Inc. PrimaryとRead Replicaを 使い分けるうえでの課題

Slide 76

Slide 76 text

© 2024 Cloudbase Inc. Read Replicaに対して Write系クエリは投げられないが… エラーになる

Slide 77

Slide 77 text

© 2024 Cloudbase Inc. TransactionClientを 引数に持つメソッドを呼ぶ時 Primaryを渡すべきか? Read Replicaで良いのか? わからない問題

Slide 78

Slide 78 text

PrismaClientIssuerについて const async => = ( : . , : ) { ... }; handleUser tx user Prisma TransactionClient User © 2024 Cloudbase Inc.

Slide 79

Slide 79 text

PrismaClientIssuerについて const async => = ( : . , : ) { ... }; handleUser tx user Prisma TransactionClient User © 2024 Cloudbase Inc. primaryを渡すべき? readReplicaを渡すべき?

Slide 80

Slide 80 text

PrismaClientIssuerについて const async => = ( : . , : ) { . . ({ }); }; handleUser create tx user tx user data: user Prisma TransactionClient User await © 2024 Cloudbase Inc. 実装を見ないとわからない (この例はcreateしてるのでprimaryが必要)

Slide 81

Slide 81 text

© 2024 Cloudbase Inc. 開発体験悪い 型レベルで保証できたら嬉しい

Slide 82

Slide 82 text

© 2024 Cloudbase Inc. そこで

Slide 83

Slide 83 text

© 2024 Cloudbase Inc. 2つの独自Transaction型を用意 PrismaClientIssuerと合わせて使う

Slide 84

Slide 84 text

© 2024 Cloudbase Inc. CBWritableTransaction と CBReadableTransaction

Slide 85

Slide 85 text

© 2024 Cloudbase Inc. 2つのTransaction型 ・PrismaのTransactionClientの交差型 ・   ・issuerのprimaryメソッドで渡る型   ・ に対しても ・   ・issuerのreadReplicaメソッドで渡る型   ・ に対して CBWritableTransaction CBWritableTransaction CBReadableTransaction CBReadableTransaction 渡せる 渡せない

Slide 86

Slide 86 text

独自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.

Slide 87

Slide 87 text

独自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

Slide 88

Slide 88 text

独自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

Slide 89

Slide 89 text

© 2024 Cloudbase Inc. これを PrismaClientIssuerと合わせて使う

Slide 90

Slide 90 text

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.

Slide 91

Slide 91 text

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が貰える

Slide 92

Slide 92 text

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のみ呼べる

Slide 93

Slide 93 text

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.

Slide 94

Slide 94 text

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で呼ぶと型エラー

Slide 95

Slide 95 text

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. メソッド内で渡してしまうことも防止

Slide 96

Slide 96 text

© 2024 Cloudbase Inc. というわけで PrismaClientはそのまま使わずに wrapすると色々仕込めて便利

Slide 97

Slide 97 text

© 2024 Cloudbase Inc. パフ ォーマンス、 スケーラビリテ ィ ノ ウハウまとめ ・発行されるクエリに注意  ・結局queryRawが必要になることも  ・とはいえ、Prismaはクエリ最適化を頑張ってる ・Read Replicaによるスケールアウト  ・PrismaClientをwrapするクラスを作ると便利  ・独自のTransaction型を定義すると便利

Slide 98

Slide 98 text

ilitiesに沿ってお話します © 2024 Cloudbase Inc. 4 セキュリティ 4 テスタビリティ 4 オブザーバビリティ

Slide 99

Slide 99 text

セキュリティ © 2024 Cloudbase Inc.

Slide 100

Slide 100 text

© 2024 Cloudbase Inc. Cloudbaseは プールモデル マルチテナントシステム

Slide 101

Slide 101 text

© 2024 Cloudbase Inc. 同じDB、テーブルに 異なるお客様のデータが入る (プロダクト仕様実現のため)

Slide 102

Slide 102 text

© 2024 Cloudbase Inc. あるデータを取得する場合 アクセス元ユーザが権限を持っているもの だけを返す必要がある

Slide 103

Slide 103 text

© 2024 Cloudbase Inc. あるデータを取得する場合 アクセス元ユーザが権限を持っているもの だけを返す必要がある どう実現する?

Slide 104

Slide 104 text

© 2024 Cloudbase Inc. 「Whereを忘れずに付けよう!」

Slide 105

Slide 105 text

© 2024 Cloudbase Inc.

Slide 106

Slide 106 text

© 2024 Cloudbase Inc. 実装漏れ、レビュー漏れ、テスト漏れ… Whereを忘れる可能性はゼロではない

Slide 107

Slide 107 text

© 2024 Cloudbase Inc. 特にPrismaでは Where対象のkeyにundefinedを渡すと 「条件なし」を意味する

Slide 108

Slide 108 text

whereとundefined const = (); . . ({ { } }); tenantId await await getMyTenantId findMany prisma message where: tenantId © 2024 Cloudbase Inc.

Slide 109

Slide 109 text

whereとundefined const = (); . . ({ { } }); tenantId await await getMyTenantId findMany prisma message where: tenantId © 2024 Cloudbase Inc. これがundefinedを返した場合

Slide 110

Slide 110 text

whereとundefined const = (); . . ({ { } }); tenantId await await getMyTenantId findMany prisma message where: tenantId © 2024 Cloudbase Inc. tenant関係なく 全件取得

Slide 111

Slide 111 text

© 2024 Cloudbase Inc. 一発アウトの情報漏洩に なりかねない

Slide 112

Slide 112 text

© 2024 Cloudbase Inc. そこで

Slide 113

Slide 113 text

© 2024 Cloudbase Inc. Row Level Security

Slide 114

Slide 114 text

© 2024 Cloudbase Inc. Row Level Security ・通称RLS ・PostgreSQLにある機能 ・DBユーザ、セッション、トランザクションにおいて  アクセス可能なレコードを行レベルで制御する仕組み

Slide 115

Slide 115 text

RLSざっくり解説 © 2024 Cloudbase Inc.

Slide 116

Slide 116 text

RLSざっくり解説 © 2024 Cloudbase Inc. トランザクション開始 (BEGIN)

Slide 117

Slide 117 text

RLSざっくり解説 © 2024 Cloudbase Inc. このトランザクションでは tenantId = 1 のデータだけ返してね (set_config)

Slide 118

Slide 118 text

RLSざっくり解説 © 2024 Cloudbase Inc. OK

Slide 119

Slide 119 text

RLSざっくり解説 © 2024 Cloudbase Inc. message全件ちょうだい (SELECT * FROM messages)

Slide 120

Slide 120 text

RLSざっくり解説 © 2024 Cloudbase Inc. どうぞ (SELECT * FROM messagesの結果) id 1 こんにちは〜 どうも〜 さようなら〜 1 1 1 6 29 body tenant_id

Slide 121

Slide 121 text

RLSざっくり解説 © 2024 Cloudbase Inc. どうぞ (SELECT * FROM messagesの結果) id 1 こんにちは〜 どうも〜 さようなら〜 1 1 1 6 29 body tenant_id Whereが無くても tenantIdで絞られる

Slide 122

Slide 122 text

© 2024 Cloudbase Inc. Prismaで RLSを扱いたい!

Slide 123

Slide 123 text

© 2024 Cloudbase Inc. PrismaClientIssuerが再活躍 (独自クラス)

Slide 124

Slide 124 text

PrismaClientIssuerとRLS await . ( , ( ) { ... }) issuer accessToken tx primary async => © 2024 Cloudbase Inc.

Slide 125

Slide 125 text

PrismaClientIssuerとRLS await . ( , ( ) { ... }) issuer accessToken tx primary async => © 2024 Cloudbase Inc. 認可情報を渡す

Slide 126

Slide 126 text

PrismaClientIssuerとRLS await . ( , ( ) { ... }) issuer accessToken tx primary async => © 2024 Cloudbase Inc. RLSが適用された Transaction

Slide 127

Slide 127 text

© 2024 Cloudbase Inc. このaccessTokenって何者? await . ( , ( ) { ... }) issuer accessToken tx primary async =>

Slide 128

Slide 128 text

© 2024 Cloudbase Inc. そのユーザがアクセス可能な テナントIDが入ったObject const = { ; } accessToken tenantId: 1 (厳密にはもっと工夫がある)

Slide 129

Slide 129 text

© 2024 Cloudbase Inc. このObjectは どこで取得するのか?

Slide 130

Slide 130 text

© 2024 Cloudbase Inc. express middlewareが発行する 権限を持つtenantId取得 Request Objectとして渡す express application auth middleware handler

Slide 131

Slide 131 text

handlerの実装 async => async => ( , ) { . ( . , ( ) { ... }) } req: res: issuer req accessToken tx express.Request express.Response await readReplica © 2024 Cloudbase Inc. reqから取得して 渡すだけ

Slide 132

Slide 132 text

© 2024 Cloudbase Inc. どうやってRLSを適用した Transactionを生成している? await . ( , ( ) { ... }) issuer accessToken tx primary async =>

Slide 133

Slide 133 text

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.

Slide 134

Slide 134 text

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に渡してるだけ

Slide 135

Slide 135 text

© 2024 Cloudbase Inc. これで マルチテナントのセキュリティが 担保される

Slide 136

Slide 136 text

© 2024 Cloudbase Inc. まだ安心できません

Slide 137

Slide 137 text

© 2024 Cloudbase Inc. 行の次は

Slide 138

Slide 138 text

© 2024 Cloudbase Inc. 列

Slide 139

Slide 139 text

© 2024 Cloudbase Inc. TypeScriptは 構造的型付け

Slide 140

Slide 140 text

© 2024 Cloudbase Inc. そしてPrismaは 純粋なObjectを返す

Slide 141

Slide 141 text

© 2024 Cloudbase Inc. この組み合わせが 引き起こすのは

Slide 142

Slide 142 text

© 2024 Cloudbase Inc. 意図しないカラムの 露出

Slide 143

Slide 143 text

© 2024 Cloudbase Inc. 例えば 以下のテーブルがあるとする model User id Int name String address String password String { }

Slide 144

Slide 144 text

© 2024 Cloudbase Inc. 例えば 以下のテーブルがあるとする model User id Int name String address String password String { } 個人情報

Slide 145

Slide 145 text

© 2024 Cloudbase Inc. このテーブルを使った ユーザ一覧取得APIを考える model User id Int name String address String password String { }

Slide 146

Slide 146 text

© 2024 Cloudbase Inc. Responseの型を以下とする type = { : ; : ; }[]; ListUsersResponse number string id name

Slide 147

Slide 147 text

© 2024 Cloudbase Inc. これをナイーブに実装すると?

Slide 148

Slide 148 text

こうなる const async => const = ( , ) { : = . . (); . ( ); }; listUsers findMany json req res prisma user res resp resp ListUsersResponse await

Slide 149

Slide 149 text

こうなる const async => const = ( , ) { : = . . (); . ( ); }; listUsers findMany json req res prisma user res resp resp ListUsersResponse await 型的には問題ないが…?

Slide 150

Slide 150 text

© 2024 Cloudbase Inc. このAPIのレスポンスは

Slide 151

Slide 151 text

© 2024 Cloudbase Inc. こうなる

Slide 152

Slide 152 text

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

Slide 153

Slide 153 text

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

Slide 154

Slide 154 text

© 2024 Cloudbase Inc. そこで

Slide 155

Slide 155 text

© 2024 Cloudbase Inc. zod

Slide 156

Slide 156 text

© 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 // バリデーション

Slide 157

Slide 157 text

© 2024 Cloudbase Inc. Response型の定義に zodを使う

Slide 158

Slide 158 text

Response型をzodで書き換える const type typeof = . ( . ({ . (), . (), }), ); = . < >; ListUsersResponse z z id: z name: z ListUsersResponse array object number string ListUsersResponse z infer

Slide 159

Slide 159 text

zodのparseを行う const async => const = ( , ) { = . ( . . () ); . ( ); }; listUsers parse findMany json req res ListUsersResponse prisma user res resp resp await

Slide 160

Slide 160 text

zodのparseを行う const async => const = ( , ) { = . ( . . () ); . ( ); }; listUsers parse findMany json req res ListUsersResponse prisma user res resp resp await parseメソッドを実行

Slide 161

Slide 161 text

© 2024 Cloudbase Inc. このAPIのレスポンスは こうなる

Slide 162

Slide 162 text

こうなる [ { : , : } ] "id" "name" 1 "tockn"

Slide 163

Slide 163 text

こうなる [ { : , : } ] "id" "name" 1 "tockn" 個人情報が切り落とされた! parseは未定義フィールドを切り落とす

Slide 164

Slide 164 text

© 2024 Cloudbase Inc. middleware等で Responseへの zodの使用を強制する

Slide 165

Slide 165 text

© 2024 Cloudbase Inc. これで 列レベルのセキュリティも 担保された

Slide 166

Slide 166 text

© 2024 Cloudbase Inc. セキ ュ リテ ィ ノ ウハウまとめ ・マルチテナントにおける行レベルのセキュリティ  ・PrismaでRLSを使う  ・またしてもPrismaClientをwrapするクラスが便利 ・列レベルのセキュリティも忘れてはいけない  ・PrismaはPOJO返し、TypeScriptは構造的型付け  ・ナイーブな実装は情報漏洩になることも  ・zodを使うことで回避

Slide 167

Slide 167 text

ilitiesに沿ってお話します © 2024 Cloudbase Inc. 8 テスタビリティ 8 オブザーバビリティ

Slide 168

Slide 168 text

テスタビリティ © 2024 Cloudbase Inc.

Slide 169

Slide 169 text

© 2024 Cloudbase Inc. Prismaを用いた実装では 実際のDBを使用した インテグレーションテストを 書きたくなる

Slide 170

Slide 170 text

© 2024 Cloudbase Inc. 開発生産性の 課題がある

Slide 171

Slide 171 text

© 2024 Cloudbase Inc. シードレコードの 用意が大変問題

Slide 172

Slide 172 text

© 2024 Cloudbase Inc. テスト対象実行後 DBのレコード検証ロジックを 毎度書くのが面倒問題

Slide 173

Slide 173 text

© 2024 Cloudbase Inc. テスト実行後 レコードのクリーンアップが 大変問題

Slide 174

Slide 174 text

© 2024 Cloudbase Inc. これらの実装で テストコードの見通しが 悪くなる

Slide 175

Slide 175 text

© 2024 Cloudbase Inc. そこで

Slide 176

Slide 176 text

© 2024 Cloudbase Inc. 内製Test Runnerを開発 「また内製かよ...」 と思った方、少々お待ち下さい

Slide 177

Slide 177 text

内製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 => =>

Slide 178

Slide 178 text

内製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 => => シードレコードを 宣言的に定義

Slide 179

Slide 179 text

内製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

Slide 180

Slide 180 text

内製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

Slide 181

Slide 181 text

内製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 => => テスト対象の メソッドを書く

Slide 182

Slide 182 text

内製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 => => テスト対象メソッドの 戻り値を定義

Slide 183

Slide 183 text

内製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 => => テスト対象メソッド実行後の レコードの状態を定義

Slide 184

Slide 184 text

© 2024 Cloudbase Inc. 内製Test Runner ・レコードの状態、戻り値を宣言的に定義  ・良い感じにINSERTし、良い感じに検証 ・非常に開発者体験が良い ・他にも便利なオプションがある  ・snapshot  ・エラー検証

Slide 185

Slide 185 text

© 2024 Cloudbase Inc. これだけだと 自慢話で終わってしまう

Slide 186

Slide 186 text

© 2024 Cloudbase Inc. というわけで...

Slide 187

Slide 187 text

© 2024 Cloudbase Inc. OSS化しました

Slide 188

Slide 188 text

© 2024 Cloudbase Inc. prisma-generator-integration-test-runner https://github.com/Levetty/prisma-generator-integration-test-runner

Slide 189

Slide 189 text

© 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"

Slide 190

Slide 190 text

© 2024 Cloudbase Inc. テスタビリテ ィ まとめ ・開発生産性に課題  ・テストレコードの用意、検証など ・内製Test Runnerが便利  ・宣言的な状態定義 ・OSSとして公開しました  ・ぜひ使ってみてください

Slide 191

Slide 191 text

ilitiesに沿ってお話します © 2024 Cloudbase Inc. A オブザーバビリティ

Slide 192

Slide 192 text

オブザーバビリティ © 2024 Cloudbase Inc.

Slide 193

Slide 193 text

© 2024 Cloudbase Inc. Prismaの裏で行われている処理 どのくらい把握できていますか?

Slide 194

Slide 194 text

© 2024 Cloudbase Inc. DBとのコネクション確立

Slide 195

Slide 195 text

© 2024 Cloudbase Inc. PrismaClientから PrismaEngine向けクエリの変換

Slide 196

Slide 196 text

© 2024 Cloudbase Inc. SQLの発行

Slide 197

Slide 197 text

© 2024 Cloudbase Inc. DBからの結果を PrismaClientの結果として変換

Slide 198

Slide 198 text

© 2024 Cloudbase Inc. もっと言えば 1つのHTTPリクエストから レスポンスまでに起きてることを どのくらい把握できていますか?

Slide 199

Slide 199 text

© 2024 Cloudbase Inc. そこで

Slide 200

Slide 200 text

© 2024 Cloudbase Inc. OpenTelemetry

Slide 201

Slide 201 text

© 2024 Cloudbase Inc. OpenTelemetry ・オブザーバビリティのためのフレームワーク  ・テレメトリデータの作成、管理   ・トレース、メトリクス、ログ ・環境に依らず計装できる ・保存と可視化は責務外

Slide 202

Slide 202 text

© 2024 Cloudbase Inc. Prismaで OpenTelemetryの計装をしたい

Slide 203

Slide 203 text

© 2024 Cloudbase Inc. そこで

Slide 204

Slide 204 text

© 2024 Cloudbase Inc. OpenTelemetry tracing (preview)

Slide 205

Slide 205 text

© 2024 Cloudbase Inc. OpenTelemetry tracing ・PrismaにあるPreview Feature ・OpenTelemetryの計装 ・PrismaClientの様々な処理をトレースできる ・環境構築は公式ドキュメントを参照 ・expressなどの計装と合わせて見れる

Slide 206

Slide 206 text

トレースをJaegerで可視化 const = (); . . ({ { } }); tenantId await await getMyTenantId findMany prisma message where: tenantId © 2024 Cloudbase Inc.

Slide 207

Slide 207 text

トレースをJaegerで可視化 const = (); . . ({ { } }); tenantId await await getMyTenantId findMany prisma message where: tenantId © 2024 Cloudbase Inc. どのAPIで

Slide 208

Slide 208 text

トレースをJaegerで可視化 const = (); . . ({ { } }); tenantId await await getMyTenantId findMany prisma message where: tenantId © 2024 Cloudbase Inc. どのような処理が どのくらい実行されたか

Slide 209

Slide 209 text

トレースをJaegerで可視化 const = (); . . ({ { } }); tenantId await await getMyTenantId findMany prisma message where: tenantId © 2024 Cloudbase Inc. どのようなSQLが発行されたか

Slide 210

Slide 210 text

© 2024 Cloudbase Inc. Cloudbaseでは 可視化ツールとしてDatadogを使用

Slide 211

Slide 211 text

© 2024 Cloudbase Inc. ローカルでもJeagerを使用 日々の開発にも役立てている

Slide 212

Slide 212 text

© 2024 Cloudbase Inc. Prismaは裏で色々頑張ってくれている だからこそ オブザーバビリティが大事

Slide 213

Slide 213 text

© 2024 Cloudbase Inc. オブザーバビリテ ィ ノ ウハウまとめ ・Prismaの裏でも様々な処理が走る ・OpenTelemetry tracingを使って可視化

Slide 214

Slide 214 text

アジェンダ É 自己紹介 É Prisma ORMの基本 CÉ ノウハウ紹介 ~ilitiesを添えて~ HÉ まとめ © 2024 Cloudbase Inc.

Slide 215

Slide 215 text

全体まとめ © 2024 Cloudbase Inc. ・結局、SQLは大事 ・PrismaClientをwrapすると共通処理を仕込めて便利 ・インテグレーションテスト基盤をOSS化しました ・OpenTelemetry tracing使おう

Slide 216

Slide 216 text

宣伝 © 2024 Cloudbase Inc.

Slide 217

Slide 217 text

© 2024 Cloudbase Inc. Cloudbaseの アプリケーションレイヤは フルTypeScript

Slide 218

Slide 218 text

© 2024 Cloudbase Inc. Prismaも使いこなす プロダクトエンジニアとして 一緒に働きませんか?

Slide 219

Slide 219 text

We Are Hiring! Engineer Entrance Book

Slide 220

Slide 220 text

TSKaigiのビールスポンサーです! TSKaigiのビールスポンサーです! Cloudbase は Cloudbase は

Slide 221

Slide 221 text

オレンジの服着てる人は多分Cloudbaseですw オレンジの服着てる人は多分Cloudbaseですw 一緒にビール飲みましょう! 一緒にビール飲みましょう!