החזרת ייבוא מוצרים עם שמירת תמונת מצב
ייבוא עם שגיאת מיפוי יכול לשכתב מחירים על אלפי מוצרים עם נתונים שגויים. דמיינו טעינת קובץ CSV עם עמודות מחיר וכמות מוחלפות — לאחר הייבוא, 5000 מוצרים מקבלים מחירים שהם למעשה כמויות מלאי. ללא מנגנון החזרה, האפשרות היחידה היא שחזור מגיבוי מסד נתונים, שלוקח שעות ומפיל את כל האתר. בנינו מנגנון החזרה עמיד לתקלות שמשחזר את הקטלוג למצבו לפני הייבוא תוך דקות. הפתרון שלנו מתבסס על תמונות מצב של נתונים שנלקחו לפני הייבוא ועל שחזור טרנזקציוני.
יישמנו החזרה עבור קטלוגים עד 500,000 מוצרים. במקום שחזור גיבוי מלא (שיכול לקחת 2-3 שעות), החזרה מבוססת תמונת מצב מסתיימת תוך 2-5 דקות עבור 10,000 שורות. זהו חיסכון של 99% בזמן — חוסך משאבים ללא אובדן נתונים. אנו מבטיחים שלמות: ההחזרה מתבצעת בטרנזקציה אחת — או שהיא מסתיימת לחלוטין או שלא מתבצעת כלל.
אילו בעיות פותר מנגנון ההחזרה?
בעיות מרכזיות במהלך ייבוא:
- שאילתות N+1 בעת עדכון ישויות קשורות — מובילות להאטה.
- ייבוא חלקי בעת כשל — הקטלוג נשאר במצב לא שלם.
- נעילות טבלה ארוכות במהלך פעולות המוניות.
הגישה שלנו ממתנת את אלה: אצווה מפחיתה עומס, טרנזקציות מבטיחות אטומיות, ותמונות מצב מאפשרות החזרה ללא גיבוי.
כיצד פועל מנגנון ההחזרה?
האסטרטגיה פשוטה: לפני הייבוא, אנו שומרים תמונת מצב של כל שורה שתשתנה. תמונת המצב היא עותק JSON של שדות המוצר (מחיר, כמות, תיאור, קטגוריה). בעת שגיאה, תהליך הפוך רץ: מוצרים חדשים שנוצרו נמחקים, מוצרים שעודכנו משוחזרים מתמונת המצב. הפרטים בהמשך.
תמונת מצב לפני ייבוא
לפני הייבוא, אנו מאחסנים תמונות מצב של השורות המושפעות:
CREATE TABLE import_product_snapshots (
id bigserial PRIMARY KEY,
import_id int REFERENCES import_runs(id) ON DELETE CASCADE,
product_id int,
operation varchar(10), -- create | update (delete отдельно)
data_before jsonb, -- состояние ДО импорта (NULL для create)
created_at timestamptz DEFAULT now()
); שירות לכידת תמונות מצב
class ImportSnapshotService {
public function captureBeforeImport(int $importId, array $skus, int $sourceId): void {
// Берём существующие данные товаров, которые будем изменять
$products = Product::whereIn('sku', $skus)
->where('source_id', $sourceId)
->get(['id', 'sku', 'name', 'price', 'qty', 'description', 'category_id', 'deleted_at', 'updated_at']);
$snapshots = $products->map(fn($p) => [
'import_id' => $importId,
'product_id' => $p->id,
'operation' => 'update',
'data_before' => json_encode($p->toArray()),
'created_at' => now()->toDateTimeString(),
])->all();
// Батч-вставка
foreach (array_chunk($snapshots, 1000) as $chunk) {
ImportProductSnapshot::insert($chunk);
}
}
public function captureNewProduct(int $importId, int $productId): void {
ImportProductSnapshot::create([
'import_id' => $importId,
'product_id' => $productId,
'operation' => 'create',
'data_before' => null,
]);
}
} מנגנון החזרה
class ImportRollbackService {
public function rollback(ImportRun $import): RollbackResult {
if (!in_array($import->status, ['success', 'partial', 'failed'])) {
throw new \RuntimeException('Import is not in a rollbackable state');
}
if ($import->rolled_back_at) {
throw new \RuntimeException('Import already rolled back');
}
$restored = $deleted = 0;
DB::transaction(function () use ($import, &$restored, &$deleted) {
$snapshots = ImportProductSnapshot::where('import_id', $import->id)
->orderByDesc('id') // обратный порядок для зависимостей
->get();
foreach ($snapshots as $snapshot) {
if ($snapshot->operation === 'create') {
// Созданные товары — удаляем (soft)
Product::find($snapshot->product_id)?->delete();
$deleted++;
} else {
// Обновлённые товары — восстанавливаем предыдущее состояние
$before = json_decode($snapshot->data_before, true);
Product::where('id', $snapshot->product_id)->update($before);
$restored++;
}
}
$import->update([
'rolled_back_at' => now(),
'rolled_back_by' => auth()->id(),
'rollback_result' => compact('restored', 'deleted'),
]);
});
return new RollbackResult($restored, $deleted);
}
}הכל רץ בטרנזקציה אחת — או שההחזרה מסתיימת לחלוטין או ששום דבר לא משתנה. עבור ייבואים גדולים (מעל 50,000 שורות), אנו משתמשים באצווה של 1000 רשומות כל אחת כדי למנוע נעילות ארוכות. התקדמות ההחזרה מנוטרת בזמן אמת דרך ממשק אינטרנט.
מדריך יישום שלב אחר שלב
- צור את טבלת תמונות המצב (SQL למעלה).
- יישם את
CREATE TABLE import_product_snapshots ( id bigserial PRIMARY KEY, import_id int REFERENCES import_runs(id) ON DELETE CASCADE, product_id int, operation varchar(10), -- create | update (delete отдельно) data_before jsonb, -- состояние ДО импорта (NULL для create) created_at timestamptz DEFAULT now() );— קרא לו לפני הייבוא עבור כל קבוצת שורות. - יישם את
class ImportSnapshotService { public function captureBeforeImport(int $importId, array $skus, int $sourceId): void { // Берём существующие данные товаров, которые будем изменять $products = Product::whereIn('sku', $skus) ->where('source_id', $sourceId) ->get(['id', 'sku', 'name', 'price', 'qty', 'description', 'category_id', 'deleted_at', 'updated_at']); $snapshots = $products->map(fn($p) => [ 'import_id' => $importId, 'product_id' => $p->id, 'operation' => 'update', 'data_before' => json_encode($p->toArray()), 'created_at' => now()->toDateTimeString(), ])->all(); // Батч-вставка foreach (array_chunk($snapshots, 1000) as $chunk) { ImportProductSnapshot::insert($chunk); } } public function captureNewProduct(int $importId, int $productId): void { ImportProductSnapshot::create([ 'import_id' => $importId, 'product_id' => $productId, 'operation' => 'create', 'data_before' => null, ]); } }— הפעל אותו בעת שגיאה או דרך כפתור. - בדוק התאמה (תמונת מצב נשמרה, בתוך 7 ימים, הייבוא לא הוחזר כבר).
- שלב עם ממשק הניהול: כפתור החזרה ופס התקדמות.
תנאי התאמה להחזרה
לא כל ייבוא ניתן להחזרה. אנו בודקים:
| תנאי | החזרה אפשרית? |
|---|---|
| תמונת מצב נשמרה במלואה | כן |
| פחות מ-7 ימים | כן (מדיניות שמירה) |
| הייבוא כבר הוחזר | לא |
| ייבוא אחר רץ מעליו | חלקית (רק שורות לא מושפעות) |
| מוצרים שנמחקו פיזית (לא מחיקה רכה) | לא |
public function canRollback(ImportRun $import): bool
{
return !$import->rolled_back_at && $import->created_at->isAfter(now()->subDays(7)) && ImportProductSnapshot::where('import_id', $import->id)->exists();
} למה החזרה מצטברת חשובה?
החזרת 100,000 שורות בטרנזקציה אחת נועלת את מסד הנתונים לעשרות שניות. הגישה המצטברת מפצלת את הפעולה לאצוות, ומפחיתה את עומס השרת. אנו משתמשים בשיטת class ImportRollbackService { public function rollback(ImportRun $import): RollbackResult { if (!in_array($import->status, ['success', 'partial', 'failed'])) { throw new \RuntimeException('Import is not in a rollbackable state'); } if ($import->rolled_back_at) { throw new \RuntimeException('Import already rolled back'); } $restored = $deleted = 0; DB::transaction(function () use ($import, &$restored, &$deleted) { $snapshots = ImportProductSnapshot::where('import_id', $import->id) ->orderByDesc('id') // обратный порядок для зависимостей ->get(); foreach ($snapshots as $snapshot) { if ($snapshot->operation === 'create') { // Созданные товары — удаляем (soft) Product::find($snapshot->product_id)?->delete(); $deleted++; } else { // Обновлённые товары — восстанавливаем предыдущее состояние $before = json_decode($snapshot->data_before, true); Product::where('id', $snapshot->product_id)->update($before); $restored++; } } $import->update([ 'rolled_back_at' => now(), 'rolled_back_by' => auth()->id(), 'rollback_result' => compact('restored', 'deleted'), ]); }); return new RollbackResult($restored, $deleted); } } שמחילה תמונות מצב בנתחים ומעדכנת התקדמות. זה מאפשר החזרה ללא הפרעה לפעולות החנות.
החזרה מדורגת של נתונים קשורים
הייבוא משפיע לא רק על טבלת ImportSnapshotService. במהלך ההחזרה, אנו מוחקים אוטומטית תמונות קשורות, מפרטים ומסננים. בשיטת ImportRollbackService, מתבצע ניקוי מדורג. תמונות מצב נשמרות למשך 7 ימים לאחר ייבוא מוצלח, ואז נמחקות על ידי מתזמן (public function canRollback(ImportRun $import): bool { return !$import->rolled_back_at && $import->created_at->isAfter(now()->subDays(7)) && ImportProductSnapshot::where('import_id', $import->id)->exists(); } ).
השוואת גישות להחזרה
| גישה | מהירות | אמינות | מורכבות |
|---|---|---|---|
| תמונת מצב | מהירה (2-5 דקות לכל 10k) | גבוהה (טרנזקציונית) | בינונית |
| מחיקה רכה | מיידית | נמוכה (סמנים שאריים) | נמוכה |
| Event Sourcing | תלוי באירועים | גבוהה מאוד | גבוהה |
הבחירה שלנו היא שילוב של תמונות מצב ואצווה. זה אופטימלי עבור רוב חנויות המקוונות.
מה כלול בפיתוח
- שירות תמונות מצב (PHP/Laravel)
- מנגנון החזרה עם אצווה
- ממשק ניהול (כפתור החזרה, פס התקדמות)
- בדיקות התאמה ומחיקה מדורגת
- ניקוי TTL של תמונות מצב
- תיעוד API והדרכת צוות
לוח זמנים ליישום
- פונקציונליות בסיסית (תמונת מצב + החזרה בטרנזקציה) — יומיים
- אצווה, מדורג, בדיקות — +יום אחד
- ממשק ניהול ובדיקות — +יום אחד
לוח זמנים סופי: 4 עד 6 ימים תלוי במורכבות הקטלוג.
בקשו ייעוץ — נעריך את הפרויקט שלכם ביום אחד. אנו מבטיחים שההחזרה לא תאבד אף מוצר. השלמנו 50+ יישומי ייבוא מוצלחים לקטלוגים מ-1,000 עד 500,000 מוצרים. צרו קשר לדיון בפרטים.
לפי תיעוד PostgreSQL על טרנזקציות, בידוד תמונת מצב מבטיח עקביות נתונים. אנו מיישמים עיקרון זה בפתרון שלנו.







