Реалізація масового імпорту товарів
Масовий імпорт — це одноразова завантаження тисяч або сотень тисяч позицій. На відміну від інкрементального оновлення, завдання інше: за розумний час обробити весь обсяг, не положивши базу даних та не зайнявши всі воркери черги на добу.
Розмір має значення: стратегії за обсягом
| Обсяг | Метод | Час обробки |
|---|---|---|
| До 1 000 позицій | Синхронно в запиті | Секунди |
| 1 000–50 000 | Одна очередна задача з чанками | Хвилини |
| 50 000–500 000 | Fan-out: N паралельних Jobs | 10–60 хвилин |
| Понад 500 000 | Batch insert + окремий конвеєр | Години |
Принцип chunk + queue
Файл з 100 000 рядків не обробляється в один Job. Файл розбивається на чанки, кожен чанк — окремий Job:
class BulkImportDispatcher
{
private const CHUNK_SIZE = 500;
public function dispatch(ImportFile $file): void
{
$import = ImportRun::create([
'file_id' => $file->id,
'status' => 'dispatching',
'total' => 0,
]);
$chunkIndex = 0;
foreach ($file->parser()->chunks(self::CHUNK_SIZE) as $chunk) {
ProcessImportChunkJob::dispatch($import->id, $chunkIndex, $chunk)
->onQueue('bulk-import');
$chunkIndex++;
}
$import->update([
'status' => 'processing',
'total_chunks' => $chunkIndex,
]);
}
}
Bulk Upsert замість поштучних операцій
Головний інструмент продуктивності — INSERT ... ON CONFLICT DO UPDATE (upsert). Laravel підтримує через Model::upsert():
class ProcessImportChunkJob implements ShouldQueue
{
public int $timeout = 120;
public function handle(): void
{
$rows = [];
foreach ($this->chunk as $item) {
$rows[] = [
'sku' => $item['sku'],
'name' => $item['name'],
'price' => $item['price'],
'updated_at' => now(),
];
}
// Один SQL-запит замість 500 окремих
Product::upsert(
$rows,
uniqueBy: ['sku'],
update: ['name', 'price', 'updated_at']
);
DB::table('import_runs')
->where('id', $this->importId)
->increment('processed_chunks');
}
}
Одна операція upsert для 500 рядків у PostgreSQL займає ~50–200 мс — проти 500 × 5 мс = 2500 мс для поштучних запитів.
Передзавантаження справочників у пам'ять
Найдорожча операція при імпорті — запити до БД для розв'язання залежностей (категорія за назвою, постачальник за ID, тег за slug). Рішення — завантажити всі справочники в пам'ять перед обробкою:
class ImportContext
{
private array $categoryMap; // ['name' => id]
private array $supplierMap; // ['code' => id]
private array $existingSkus; // ['sku' => product_id]
public function preload(int $sourceId): void
{
$this->categoryMap = Category::pluck('id', 'name')->all();
$this->supplierMap = Supplier::pluck('id', 'code')->all();
$this->existingSkus = Product::where('source_id', $sourceId)
->pluck('id', 'sku')->all();
}
public function resolveCategoryId(string $name): ?int
{
return $this->categoryMap[$name] ?? null;
}
}
Для 200 000 SKU цей словник займає ~10–20 МБ пам'яті — допустимо для воркера.
Обмеження навантаження
Під час масового імпорту важливо не деградувати сайт:
- Виділити окрему чергу
bulk-importз обмеженим числом воркерів (2–4) - Основну чергу
defaultне трогати - Запускати тяжелий імпорт в ночі через Laravel Scheduler
- Використовувати транзакції по чанку, не по всьому файлу
Тривалість реалізації
- Chunk-диспетчер, bulk upsert, передзавантаження справочників — 2 дні
- Bus::batch з фінальним коллбеком, post-import конвеєр — +1 день
- Прогрес в admin UI, обмеження навантаження, ночний планувальник — +1 день







