この内容は古いバージョンです。最新バージョンを表示するには、戻るボタンを押してください。
バージョン:3
ページ更新者:爽健
更新日時:2026-06-11 07:07:02

タイトル: モデルでinsert
SEOタイトル: Laravel Eloquent insert (create/save/insert) 完全ガイド

この記事の要点
  • 単発の挿入: $model->save() または Model::create([...])
  • $fillable / $guarded を設定しないと create() は MassAssignmentException
  • あれば取得・無ければ作成: firstOrCreate / updateOrCreate
  • 大量挿入: Model::insert([[...], [...]])(低レベル、created_at 自動付与なし)
  • リレーション経由: $user->posts()->create([...]) で外部キー自動セット

Eloquent での INSERT の選択肢

メソッド用途$fillable 必須イベント発火タイムスタンプ
$model->save()属性を個別代入後に保存不要
Model::create([...])属性配列で一発作成必要
firstOrCreate条件で検索、無ければ作成必要
updateOrCreateあれば更新、無ければ作成必要
Model::insert([...])クエリビルダレベルの直接 INSERT不要××(自分でセット)
insertGetId([...])insert + 採番 ID 取得不要××
upsert一括 upsert(MySQL ON DUPLICATE KEY)×

基本: save() と create()

use App\Models\Post;

// 方法1: new + 属性代入 + save
$post = new Post();
$post->title = 'Hello';
$post->body = '本文';
$post->user_id = 1;
$post->save();
// → created_at / updated_at 自動セット、$post->id に採番 ID

// 方法2: create で一発
$post = Post::create([
    'title' => 'Hello',
    'body' => '本文',
    'user_id' => 1,
]);

// 方法3: fill + save
$post = (new Post())->fill([
    'title' => 'Hello',
    'body' => '本文',
])->save();

$fillable と MassAssignmentException

Laravel は意図しない列の書き換えを防ぐため、$fillable / $guarded を設定しないと create() 等の一括代入が失敗します:

namespace App\Models;

use Illuminate\Database\Eloquent\Model;

class Post extends Model
{
    // 推奨: 許可リスト(whitelist)
    protected $fillable = ['title', 'body', 'user_id', 'published_at'];

    // または禁止リスト(blacklist、$fillable と排他)
    // protected $guarded = ['id', 'created_at', 'updated_at'];

    // すべて許可(非推奨)
    // protected $guarded = [];
}

// $fillable に含まれていない列は無視される
Post::create([
    'title' => 'Hello',
    'is_admin' => true,    // ★ $fillable に無いので無視される
]);

firstOrCreate / firstOrNew

// 検索条件 + 無いときの追加属性
$tag = Tag::firstOrCreate(
    ['slug' => 'laravel'],            // 検索条件
    ['name' => 'Laravel', 'order' => 1]  // 無いとき追加で入れる値
);
// → INSERT またはそのレコードを取得

// firstOrNew: DB には保存せずインスタンスだけ
$tag = Tag::firstOrNew(['slug' => 'laravel']);
$tag->name = 'Laravel';
$tag->save();   // 明示的に save が必要

// 競合に強い書き方(同時実行で重複しないように)
// PostgreSQL/MySQL の UNIQUE 制約を併用すること
DB::transaction(function () {
    return Tag::firstOrCreate(['slug' => 'laravel'], [...]);
});

updateOrCreate(upsert の基本)

// あれば更新、無ければ作成
$config = Config::updateOrCreate(
    ['key' => 'site_title'],                  // 検索条件
    ['value' => '新しいタイトル', 'updated_at' => now()]
);

// API 連携でよく使うパターン
foreach ($externalData as $item) {
    User::updateOrCreate(
        ['external_id' => $item['id']],
        [
            'name' => $item['name'],
            'email' => $item['email'],
            'last_synced_at' => now(),
        ]
    );
}

一括挿入: insert() と upsert()

// 単純な一括 INSERT(イベント・タイムスタンプ無し)
Post::insert([
    ['title' => 'A', 'body' => 'aa', 'user_id' => 1, 'created_at' => now(), 'updated_at' => now()],
    ['title' => 'B', 'body' => 'bb', 'user_id' => 1, 'created_at' => now(), 'updated_at' => now()],
    ['title' => 'C', 'body' => 'cc', 'user_id' => 2, 'created_at' => now(), 'updated_at' => now()],
]);

// insertGetId: 1 行挿入 + 採番 ID 取得
$id = Post::insertGetId([
    'title' => 'X', 'body' => 'xx', 'user_id' => 1,
    'created_at' => now(), 'updated_at' => now(),
]);

// upsert: 一括で「あれば更新、無ければ作成」(Laravel 8+)
Post::upsert(
    [
        ['external_id' => 1, 'title' => 'A', 'body' => 'aa'],
        ['external_id' => 2, 'title' => 'B', 'body' => 'bb'],
    ],
    ['external_id'],          // 検索キー
    ['title', 'body']         // 衝突時に更新する列
);
// → MySQL: INSERT ... ON DUPLICATE KEY UPDATE
// → PostgreSQL: INSERT ... ON CONFLICT ... DO UPDATE

リレーション経由の作成

// hasMany: 親の id を自動でセット
$user = User::find(1);
$post = $user->posts()->create([
    'title' => 'リレーション経由で作成',
    'body' => '本文',
]);
// → post.user_id = 1 が自動セット

// 複数同時
$user->posts()->createMany([
    ['title' => 'A', 'body' => 'aa'],
    ['title' => 'B', 'body' => 'bb'],
]);

// belongsToMany: ピボット経由
$post->tags()->attach([1, 2, 3]);                           // 既存タグを関連付け
$post->tags()->attach(1, ['priority' => 'high']);           // ピボット列付き
$post->tags()->sync([1, 2, 3]);                             // 同期(差分更新)
$post->tags()->syncWithoutDetaching([4, 5]);                // 既存削除なし
$post->tags()->detach([2]);                                 // 関連解除

// morphMany
$comment = $post->comments()->create(['body' => '...']);

Observer / イベントフック

// app/Observers/PostObserver.php
namespace App\Observers;

use App\Models\Post;

class PostObserver
{
    public function creating(Post $post): void {
        // INSERT 直前。属性を補完できる
        $post->slug ??= str()->slug($post->title);
    }

    public function created(Post $post): void {
        // INSERT 直後。通知発火など
        \Log::info("Post created: {$post->id}");
    }

    public function updating(Post $post): void { /* ... */ }
    public function updated(Post $post): void  { /* ... */ }
    public function deleting(Post $post): void { /* ... */ }
    public function deleted(Post $post): void  { /* ... */ }
}

// app/Providers/AppServiceProvider.php
public function boot(): void
{
    Post::observe(PostObserver::class);
}

// ★ insert() / upsert() は Eloquent イベントを発火しない
//   保存処理に対し普遍的にフックしたいなら save()/create() ベースに

トランザクションでまとめる

use Illuminate\Support\Facades\DB;

DB::transaction(function () {
    $user = User::create([
        'name' => 'taro',
        'email' => 'taro@example.com',
        'password' => bcrypt('secret'),
    ]);

    $user->profile()->create([
        'bio' => 'こんにちは',
    ]);

    $user->posts()->createMany([
        ['title' => 'Hello', 'body' => '...'],
        ['title' => 'World', 'body' => '...'],
    ]);
});
// → 途中でエラーが出ると全部ロールバック

// 明示的にロールバック
DB::beginTransaction();
try {
    User::create([...]);
    Post::create([...]);
    DB::commit();
} catch (\Throwable $e) {
    DB::rollBack();
    throw $e;
}

UUID / ULID 主キー

use Illuminate\Database\Eloquent\Concerns\HasUuids;
use Illuminate\Database\Eloquent\Concerns\HasUlids;

class Post extends Model
{
    use HasUuids;     // または HasUlids
}

// マイグレーション
Schema::create('posts', function (Blueprint $table) {
    $table->uuid('id')->primary();    // または $table->ulid('id')->primary();
    $table->string('title');
    $table->timestamps();
});

// 通常通り create するだけで id 自動生成
$post = Post::create(['title' => 'Hello']);
// $post->id == '01h4...' のような ULID

パフォーマンスの注意

  • ループ内で create() を呼ぶと N 回 INSERT → insert([配列]) でまとめる
  • insert() はイベント発火しない → Observer や Cast に頼っているなら save() ベースに
  • 大量データの場合は chunk() を使って 1000 件ずつ insert
  • 外部キー違反のとき MySQL は暗黙コミットせず例外 → トランザクションで包む
  • upsert は MySQL 8.0+ / PostgreSQL 9.5+ が必要
// chunked bulk insert
collect($bigData)->chunk(1000)->each(function ($chunk) {
    Post::insert($chunk->toArray());
});

// LazyCollection で省メモリ
LazyCollection::make(function () use ($file) {
    foreach (file($file) as $line) {
        yield json_decode($line, true);
    }
})->chunk(1000)->each(fn ($chunk) => Post::insert($chunk->toArray()));

FAQ

Q: create() で MassAssignmentException が出る
A: モデルクラスに protected $fillable = [...] を設定してください。または $guarded = [] ですべて許可(非推奨)。

Q: save() 後に id が取れない
A: $post->id で取れます。一括 insert() では取れません。1 行なら insertGetId() を使用。

Q: Observer の creatingcreated の違い
A: creating は INSERT クエリ実行(属性補完用)、created は(通知・ログ用)。creating で return false すれば INSERT を中断できます。

Q: 同じ条件で先に作成されてしまう競合状態を防ぎたい
A: DB の UNIQUE 制約を併用 + updateOrCreate をトランザクション内で実行。Laravel 10 から lockForUpdate() 併用も有効。