Массовые операции и upsert
Проблема
Создание 10 000 записей через модель:
foreach ($data as $row) {
Post::create($row); // 10 000 INSERT-запросов
}
Проблемы:
- 10 000 запросов к БД — медленно.
- 10 000 объектов в памяти.
- События модели × 10 000.
- Timestamps × 10 000.
Решение: массовые операции.
insert
Массовая вставка без событий:
Post::insert([
['title' => 'A', 'body' => 'Text A', 'created_at' => now(), 'updated_at' => now()],
['title' => 'B', 'body' => 'Text B', 'created_at' => now(), 'updated_at' => now()],
['title' => 'C', 'body' => 'Text C', 'created_at' => now(), 'updated_at' => now()],
]);
Что НЕ работает:
- События (
creating,created) — не запускаются. - Timestamps — не проставляются автоматически. Добавляй вручную.
- Casts — не применяются.
- Возвращаемое значение —
bool, не модели. - ID — не возвращаются.
Когда использовать: логи, импорт, аналитика — где события не нужны.
insertOrIgnore
Вставить, игнорируя дубликаты:
Post::insertOrIgnore([
['slug' => 'a', 'title' => 'A'],
['slug' => 'b', 'title' => 'B'],
['slug' => 'a', 'title' => 'C'], // проигнорируется (уникальный slug)
]);
Не падает при нарушении уникальности. Полезно при импорте.
insertGetId
Вставить одну запись и получить ID:
$id = Post::insertGetId([
'title' => 'X',
'body' => 'Text',
'created_at' => now(),
'updated_at' => now(),
]);
Только одна запись. Возвращает ID.
upsert
INSERT ... ON CONFLICT UPDATE — создать или обновить в одном запросе.
Post::upsert([
['id' => 1, 'title' => 'A', 'slug' => 'a'],
['id' => 2, 'title' => 'B', 'slug' => 'b'],
['id' => 3, 'title' => 'C', 'slug' => 'c'],
], ['id'], ['title', 'slug']);
Аргументы:
- Данные — массив массивов.
- Уникальные колонки — по ним определяется конфликт.
- Обновляемые колонки — что менять при совпадении.
Один запрос вместо N. Работает быстро.
Поддерживается: MySQL 5.7+, PostgreSQL 9.5+, SQLite 3.24+.
Пример — синхронизация данных:
$posts = [
['id' => 1, 'title' => 'Обновлён', 'slug' => 'post-1'],
['id' => 2, 'title' => 'Новый', 'slug' => 'post-2'],
];
Post::upsert($posts, ['id'], ['title', 'slug']);
Что происходит:
- ID 1 есть — обновит
titleиslug. - ID 2 нет — создаст.
updateOrCreate в цикле
Для небольших данных:
foreach ($data as $row) {
Post::updateOrCreate(
['slug' => $row['slug']],
['title' => $row['title'], 'body' => $row['body']],
);
}
Плюсы:
- События работают.
- Casts применяются.
- Timestamps автоматически.
Минусы:
- 2 запроса на строку (SELECT + INSERT/UPDATE).
- Медленно для больших объёмов.
Chunk для больших массивов
Разбить на части:
$rows = [...]; // 100 000 записей
foreach (array_chunk($rows, 1000) as $chunk) {
Post::insert($chunk);
}
100 запросов вместо 100 000.
Для очень больших — LazyCollection:
LazyCollection::make($rows)
->chunk(1000)
->each(fn ($chunk) => Post::insert($chunk->all()));
Не грузит всё в память.
Обновление массово
Без событий:
Post::where('status', 'draft')
->update(['status' => 'archived', 'updated_at' => now()]);
С событиями — по одной:
Post::where('status', 'draft')->chunk(500, function ($posts) {
foreach ($posts as $post) {
$post->update(['status' => 'archived']);
}
});
Правило: массовые операции — без событий, по одной — с событиями.
increment массово
Post::where('user_id', 1)->increment('views');
Один UPDATE. Быстро.
С дополнительными полями:
Post::where('user_id', 1)->increment('views', 1, [
'last_viewed_at' => now(),
]);
Транзакции
Массовые операции оборачивай в транзакцию:
DB::transaction(function () use ($rows) {
foreach (array_chunk($rows, 1000) as $chunk) {
Post::insert($chunk);
}
});
Если что-то упадёт — всё откатится.
Практика: импорт CSV
use Illuminate\Support\Facades\DB;
function importCsv(string $path): int
{
$handle = fopen($path, 'r');
$header = fgetcsv($handle);
$count = 0;
DB::transaction(function () use ($handle, $header, &$count) {
$batch = [];
while (($row = fgetcsv($handle)) !== false) {
$batch[] = array_combine($header, $row);
if (count($batch) >= 1000) {
Post::insert($batch);
$count += count($batch);
$batch = [];
}
}
if (!empty($batch)) {
Post::insert($batch);
$count += count($batch);
}
});
fclose($handle);
return $count;
}
Частые ошибки
1. insert без timestamps:
Post::insert(['title' => 'X']); // created_at = NULL
2. upsert без уникальных полей в данных:
Post::upsert([['title' => 'X']], ['id'], ['title']);
// Warning: нет id — не сможет определить конфликт
3. insert в цикле без chunk:
foreach ($rows as $row) {
Post::insert($row); // медленно
}
4. Забыть про транзакцию:
Post::insert($batch1);
Post::insert($batch2); // упадёт — batch1 уже сохранён
Итог
insert— быстро, но без событий и timestamps.insertOrIgnore— не падает на дублях.upsert— создать или обновить одним запросом.updateOrCreate— для небольших данных.chunk— разбить на части.increment— атомарное обновление.- Транзакция — обязательна для батчей.
Дальше — Soft Deletes.