実装を読んで理解する Laravelのsaveメソッドの 思想と仕組み 1
目次 1. saveとinsertの使い分け 2. 設定値からみるsave()の思想 3. save()の実装コードを読む 4. Eventsでsave()に対応する 2
概要 Laravelのデータ保存メソッドの代表格 save() $book = new \App\Models\Book(); $book->title = '新しい本'; $book->save(); 1. 似た役割を持つ insert() 2. save()関連の2つの設定値 ( Global Scopes , Events ) 実装コードを読みながら、この2つに着目し、 その思想と使いどころを理解していきましょう。 3
プロフィール 発表者 ma@me 所属 直近の登壇 PHP実行環境の歴史 PHP-FPMからFrankenPHPの誕生へ https://www.docswell.com/s/1313108/ZP2QQ1-2025-03-21-123847 どこまで違う?!PHP実行環境パフォーマンス対決 - mod_php vs php-fpm vs Swoole vs FrankenPHP https://www.docswell.com/s/1313108/ZGGREJ-2025-07-17-000927 4
宣伝 2025年11月開催の「PHPカンファレンス福岡2025」に登壇します 同時間帯の他のプレゼンが豪華すぎて、既に不安です 5
save とinsertの使い分け 6
insert の役割 新しいレコードをデータベースに追加するために使用されます。 例えば、次のように使用します。 DB::table('books')->insert([ ['title' => '本1', 'author' => '著者1'], ['title' => '本2', 'author' => '著者2'] ]); 7
Doc コメントから読み解く、insertの役割 は纏めて大量のデータをインサートするための バッチ対応向けのメソッド コメント抜粋 > Insert new record「s」 > すべての insert はバッチ挿入として扱われるため、~ insert() /* Insert new records into the database. */ public function insert(array $values) { // Since every insert gets treated like a batch insert, we will make sure the // bindings are structured in a way that is convenient when building these // inserts statements by verifying these elements are actually an array. (省略) 参照: framework/src/Illuminate/Database/Query/Builder.php#L2300 付近 8
ここまでの簡易まとめ は大量データの一括挿入用途に作られている save() は単一モデルの保存用途に作られている insert() 9
設定値からみるsave()の思想 10
該当の設定値
1. Global Scopes
2. Events
設定コードサンプル
#[ScopedBy([BookScope::class])]
class Book extends Model
{
protected static function boot() {
parent::boot();
static::saving(function ($book) {
$book->book_id = uuid();
});
・・・
11
Global Scopes の役割 ・Modelを介して実行するSQLに、 ScopedBy で指定されたクラスに記載された スコープ条件を付与してくれる 使いどころ ・ログインユーザーIDなど、動的な値を加えたい場合に便利 ↓ ここがグローバルスコープの指定箇所 #[ScopedBy([BookScope::class])] class Book extends Model 12
Global Scopes の動作例 一見bookテーブルの全件取得にみえる Book::all(); しかしグローバルスコープ内でログインユーザーに絞る whereを加えていた場合、生成・実行SQLは次のようになる。 生成されたSQL select * from `books` where `user_id` = 123 13
Global Scopes 実装コード に期待した動作 $book = new \App\Models\Book(); $book->title = '新しい本'; $book->save(); 新規作成insertの期待動作 insert文なので global scope(where) を外してくれる INSERT INTO book (title) VALUES ('タイトル'); 更新updateの期待動作 update文なので、where句に global scope(where) を付与してくれる UPDATE book SET title = 'タイトル' WHERE book_id = 100 AND user_id = 123; 14
Global Scopes 実装コード の「現実」 $book = new \App\Models\Book(); $book->title = '新しい本'; $book->save(); 新規作成insertの実動作 問答無用で外れる INSERT INTO book (title) VALUES ('タイトル'); 更新updateの実動作 update文はprimary_keyしか効かない UPDATE book SET title = 'タイトル' WHERE book_id = 100; 15
save() の実装コードを読む 16
save() の実装コードを読む 何故このような動作になるのか、実装コードを読んで理解していきましょう saveの実装コード抜粋 public function save(array $options = []) { $this->mergeAttributesFromCachedCasts(); $query = $this->newModelQuery(); if ($this->fireModelEvent('saving') === false) { return false; } (・・・省略・・・) 17
原因のメソッド、newModelQuery()
public function save(array $options = [])
{
↓ このメソッド
$query = $this->newModelQuery();
/* Get a new query builder that doesn't have any global scopes or eager loading. */
public function newModelQuery() {
return $this->newEloquentBuilder(
$this->newBaseQueryBuilder()
)->setModel($this);}
newModelQueryのDocコメント(意訳):
「グローバルスコープや eager loading を 一切含まない
新しいクエリビルダを返します」。
18
ここまでの簡易纏め part2 ・新規か更新かに関わらず、 newModelQuery() を呼び出し、 グローバルスコープを一切含まないクエリビルダを取得している ・Laravelはprimary_keyでの設定と更新を推奨している 参考issue https://github.com/laravel/framework/issues/11989 19
(update)時に 条件を加えたい場合はどうするか save グローバルスコープがsaveの時に付与されないのは分かった とはいえ更新時のwhere句にログインユーザーなど 動的な条件を加えたい場合はどうすればいいのか? public function save(array $options = []) { $this->mergeAttributesFromCachedCasts(); $query = $this->newModelQuery(); ↓↓↓ ここに注目 ↓↓↓ if ($this->fireModelEvent('saving') === false) { (・・・省略・・・) 20
Events でsave()に対応する 21
dispatchesEvents
の概要
save()に対応させるには次の2つの方法があります
1. dispatchesEvents
2. Observers
時間の都合で dispatchesEvents のみ紹介します
dispatchesEvents, Observersの指定例
#[ObservedBy([BookObserver::class])]
class Book extends Model
{
protected $dispatchesEvents = [
'saving' => \App\Events\BookSaving::class,
];
}
22
dispatchesEvents で対応させる dispatchesEventsにはCURD操作における CUDの部分のイベントが用意されています。 1. saving 2. saved 3. updating 4. updated 5. deleting 6. deleated etc... 23
dispatchesEvents
実装コード
の設定例
class Book extends Model
{
protected $dispatchesEvents = [
'saving' => \App\Events\BookSaving::class,
];
}
保存処理
$book = new \App\Models\Book();
$book->title = '新しい本';
$book->save();
24
dispatchesEvents 保存処理 の動作 $book = new \App\Models\Book(); $book->title = '新しい本'; $book->save(); 新規作成insert insert文なのでwhere句はつかない INSERT INTO book (title) VALUES ('タイトル'); 更新updateの期待動作 update文なので、where句に saving で与えた条件を付与してくれる UPDATE book SET title = 'タイトル' WHERE book_id = 100 AND user_id = 123; 25
save()
の実装コードにも書いてある
系のメソッド内で
が呼び出されて実行されている
perform~
fireModelEvent('~')
public function save(array $options = [])
{
$query = $this->newModelQuery();
if ($this->fireModelEvent('saving') === false) {
return false;
}
(・・・省略・・・)
$this->performUpdate($query) : true;
(・・・省略・・・)
$saved = $this->performInsert($query);
26
まとめ メソッドの使い分け insert()は大量データの一括挿入向け save()は単一モデルの保存向け Modelを介してのSQL生成・発行時に何かしら条件を加えたい場合 CURDのうち、CUDは dispatchesEvents を利用する CURDのうち、Rは Global Scope を利用する 27
まとめ(続き) ドキュメントだけでなく、実装コードを読むことで、理解が深まりました。 迷ったときには実装コードも読んで見ようと思います 今はdeepwikiもあるので、実装コードを読むハードルも下がっています https://deepwiki.com/ ご清聴ありがとうございました 28