Slide 1

Slide 1 text

Copyright© M&Aクラウド 【Laravel】 サクッとN + 1問題を見つけて倒しチャオ! PHPerKaigi 2022 M&Aクラウド つざき

Slide 2

Slide 2 text

Copyright© M&Aクラウド 2 自己紹介 つざき M&Aクラウド所属 Laravel(PHP) / Nuxt.js (TypeScript) キャンプ/筋トレ/サウナ 820zacky

Slide 3

Slide 3 text

Copyright© M&Aクラウド N+1問題? 3

Slide 4

Slide 4 text

Copyright© M&Aクラウド DBアクセスが合計 N+1 回実行される問題です。 4

Slide 5

Slide 5 text

Copyright© M&Aクラウド DBアクセスは時間がかかるので データが何件でもDBアクセスはできるだけ少ない方が早 い N件ごとにN回DBアクセスがあると 扱うデータが増えると処理時間が線形に伸びてしまう 5

Slide 6

Slide 6 text

Copyright© M&Aクラウド 本来はデータがN件でも1回のクエリで済むところ データに付随する別のデータ取得がN回実行されて 1 + N回のクエリになってしまった!! というのが「N+1問題」です! 7

Slide 7

Slide 7 text

Copyright© M&Aクラウド どんな時にN+1問題が起こる? - EloquentのリレーションによるN+1問題 - Eloquent以外のN+1問題 8

Slide 8

Slide 8 text

Copyright© M&Aクラウド EloquentのリレーションによるN+1問題 9

Slide 9

Slide 9 text

Copyright© M&Aクラウド 10 EloquentのリレーションによるN+1問題 Eager(積極的)ロードでN+1問題を直します すべて「本」に紐づく「著者」をとってきたい場合 $books = Book::all(); foreach ($books as $book) { echo $book->author->name ; } 何も工夫せず書くと すべての本取得に1クエリ実行 1つの本に紐づく著者の取得に1クエリ実行。本が25個ある場合、25+1回クエリが実行される。 with句を使えば2クエリで済むようになる。 $books = Book::with('author')->get(); foreach ($books as $book) { echo $book->author->name; }

Slide 10

Slide 10 text

Copyright© M&Aクラウド 11 EloquentのリレーションによるN+1問題 ここで別の問題が……

Slide 11

Slide 11 text

Copyright© M&Aクラウド 12 EloquentのリレーションによるN+1問題 with忘れがち🥺

Slide 12

Slide 12 text

Copyright© M&Aクラウド 13 EloquentのリレーションによるN+1問題 そんなとき便利なのが preventLazyLoading

Slide 13

Slide 13 text

Copyright© M&Aクラウド 14 EloquentのリレーションによるN+1問題 preventLazyLoadingはLaravel 8から追加された機能です。 AppServiceProviderのboot()で呼び出しを追加します。 use Illuminate¥Database¥Eloquent¥Model; public function boot() { Model::preventLazyLoading(! $this->app->isProduction()); } このように設定することで実行時に例外を投げるようになるので、開発中にwithのつけ忘れに気づくことができます。

Slide 14

Slide 14 text

Copyright© M&Aクラウド 15 EloquentのリレーションによるN+1問題 アプリケーションに途中から追加する場合、いちいち例外で停止されると面倒です。 そんなときはwith漏れをログ出力させて後で一気に直しましょう! Model::handleLazyLoadingViolationUsing(function ($model, $relation) { $class = get_class($model); info("Attempted to lazy load [{$relation}] on model [{$class}]."); });

Slide 15

Slide 15 text

Copyright© M&Aクラウド Eloquent外でのN+1問題 16

Slide 16

Slide 16 text

Copyright© M&Aクラウド 17 Eloquent外でのN+1問題 - そもそもEloquent使ってないケース - 直接PHPでSQL書いてる - クエリビルダーを使っている - など - ドメイン層など別のレイヤーのループ処理でDBアクセスを行っているケース - など

Slide 17

Slide 17 text

Copyright© M&Aクラウド 18 Eloquent外でのN+1問題 どうしたらよいか?

Slide 18

Slide 18 text

Copyright© M&Aクラウド 19 Eloquent外でのN+1問題 たくさんSQLを実行している Featureテストを見つけチャオ! たくさんクエリ実行があるテストはN+1問題が存在する可能性があります。 (テストで扱うデータ数が少ないと検知できないかもしれない)

Slide 19

Slide 19 text

Copyright© M&Aクラウド 20 Eloquent外でのN+1問題 DB::listen()でクエリの実行をハンドルできるので、実行回数をカウントします。 Featureテストの親クラスを作ってテスト前後に処理をはさむことで、カウント+ログ出力ができます。 DB::listen(function () { $this->queryCount++; });

Slide 20

Slide 20 text

Copyright© M&Aクラウド 21 Eloquent外でのN+1問題 サンプルコード abstract class MyTestCase extends TestCase { protected int $queryCount = 0; protected function setUp(): void { parent::setUp(); DB::listen(function () { $this->queryCount++; }); } protected function tearDown(): void { if ($this->queryCount > 50) { Log::warning("[WARNING] TOO MANY QUERY! query count: " . $this->queryCount . " " . get_class($this) . "#" . $this- >getName()); } parent::tearDown(); } }

Slide 21

Slide 21 text

Copyright© M&Aクラウド まとめ preventLazyLoading と テスト時のSQL実行数カウント を使って N+1問題をサクッと倒しチャオ! 22

Slide 22

Slide 22 text

Copyright© M&Aクラウド おわり 23