דלגו לתוכן

או התחילו מנקודה בטוחה: אינדקס הכלים המלא · מפת הטבלאות המרכזיות

מערכת חדשה — 14 יום חינם

דוגמה — סנכרון יוצא עם עימוד

עודכן 30.08.2026
GET /parse/classes/TABLE?where=…&keys=…&order=createdAt&limit=1000

משיכה מלאה היא שתי קריאות כל עוד הטבלה קטנה מ‑1,000 שורות ואף אחד לא כותב אליה תוך כדי. שני התנאים כמעט לא מתקיימים, ולכן: לולאת עימוד עם סמן המשך, בחירת שדות מצומצמת, נסיגה על שגיאות, ונקודת המשך שנשמרת בין ריצות.

Bash
curl -sS -G "https://api.mbapps.co.il/parse/classes/Accounts" \
-H "X-Parse-Application-Id: $MB_APP_ID" \
-H "X-Parse-API-Key: $MB_API_KEY" \
--data-urlencode 'where={"IsAccount":true}' \
--data-urlencode 'limit=0' \
--data-urlencode 'count=1'
JavaScript
const params = new URLSearchParams({
where: '{"IsAccount":true}',
limit: '0',
count: '1',
});
const res = await fetch(`https://api.mbapps.co.il/parse/classes/Accounts?${params}`, {
headers: {
'X-Parse-Application-Id': process.env.MB_APP_ID,
'X-Parse-API-Key': process.env.MB_API_KEY,
},
});
const data = await res.json(); // { results: [], count: 1842 }
Python
import os
import requests
res = requests.get(
"https://api.mbapps.co.il/parse/classes/Accounts",
headers={
"X-Parse-Application-Id": os.environ["MB_APP_ID"],
"X-Parse-API-Key": os.environ["MB_API_KEY"],
},
params={
"where": '{"IsAccount":true}',
"limit": 0,
"count": 1,
},
)
data = res.json() # { "results": [], "count": 1842 }
PHP
<?php
$query = http_build_query([
'where' => '{"IsAccount":true}',
'limit' => 0,
'count' => 1,
]);
$ch = curl_init("https://api.mbapps.co.il/parse/classes/Accounts?$query");
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'X-Parse-Application-Id: ' . getenv('MB_APP_ID'),
'X-Parse-API-Key: ' . getenv('MB_API_KEY'),
],
]);
$response = curl_exec($ch);
curl_close($ch);
$data = json_decode($response, true); // { "results": [], "count": 1842 }
JSON
{ "results": [], "count": 1842 }

limit=0&count=1 מחזיר את הספירה בלי לשלוף שורה אחת. 1,842 רשומות = שתי קריאות של 1,000. 184,000 = 184 קריאות, וכדאי לפרוס אותן על זמן.

הפרמטרים שהדוגמה משתמשת בהם

Section titled “הפרמטרים שהדוגמה משתמשת בהם”
פרמטר הערך בדוגמה למה
where סמן ההמשך: createdAt > cursor מה שהופך את הריצה השנייה לדלתא ולא למשיכה חוזרת
keys רשימה מפורשת של שדות בטבלה עם 130 שדות זה ההבדל בין מגה‑בייטים לקילו‑בייטים
order createdAt (עולה) מיון יציב — השדה הזה לא משתנה אחרי היצירה
limit 1000 גודל העמוד המומלץ. השרת אינו אוכף תקרה (limit=2000 מחזיר את כל השורות, ללא חיתוך), אבל עמודים גדולים יותר רק מאריכים תשובה וזיכרון. תמיד מפורש
include — לא בשימוש כאן. ראו מתי כן
skip — לא בשימוש בכוונה. ראו למה לא skip

הפירוט המלא של כל פרמטר ואופרטור: שאילתות.

limit=1000&skip=0, אחריו skip=1000, וכן הלאה. עובד על טבלה סטטית, נשבר בשקט על טבלה חיה:

מה קורה בזמן המשיכה מה זה עושה ל‑skip
איש מכירות יוצר רשומה חדשה כל הרשומות אחרי נקודת ההכנסה מוסטות באחת — רשומה אחת תיקרא פעמיים או תדולג
מישהו מעדכן רשומה, והמיון הוא לפי -updatedAt הרשומה קופצת לתחילת התוצאות — ואתם מדלגים על אחרת
הטבלה גדולה skip=150000 יקר גם בביצועים: השרת עדיין עובר על מה שדילג עליו

אין שגיאה. הייצוא מסתיים בהצלחה עם פחות רשומות ממה שיש.

הדפוס העמיד — סמן לפי createdAt

Section titled “הדפוס העמיד — סמן לפי createdAt”

createdAt נקבע ביצירה ולא משתנה, ולכן הוא סמן יציב:

עמוד 1: where {} order createdAt limit 1000
→ 1000 שורות, האחרונה נוצרה ב-2026-03-04T09:12:44.081Z
עמוד 2: where { createdAt: { $gt: 2026-03-04T09:12:44.081Z } } ← אותו מיון, אותו limit
→ 842 שורות ⇒ פחות מ-limit ⇒ זה היה העמוד האחרון

הכנסה חדשה תמיד נוחתת אחרי הסמן, כי createdAt שלה גדול יותר, ורשומה שהתעדכנה לא זזה במיון. בטבלה של 1,100 שורות עם limit=500 הלולאה מושכת 500 + 500 + 100, וריצה שנייה מנקודת ההמשך מחזירה 0 שורות; עם PAGE_SIZE = 1000 שבקוד למטה, 1000 + 100.

הסמן נשמר אחרי כל עמוד; שגיאות מטופלות בנסיגה אקספוננציאלית.

PHP
<?php
declare(strict_types=1);
const API = 'https://api.mbapps.co.il/parse';
const PAGE_SIZE = 1000; // המקסימום של שכבת ה-REST
const CHECKPOINT = __DIR__ . '/.sync-cursor'; // נקודת ההמשך בין ריצות
function headers(): array
{
return [
'X-Parse-Application-Id: ' . getenv('MB_APP_ID'),
'X-Parse-API-Key: ' . getenv('MB_API_KEY'),
];
}
/** קריאת GET עם נסיגה אקספוננציאלית על 429 ו-5xx. */
function getWithBackoff(string $url, int $attempts = 5): array
{
$delay = 0.5;
for ($i = 0; $i < $attempts; $i++) {
$ch = curl_init($url);
curl_setopt_array($ch, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTPHEADER => headers(),
]);
$body = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($body !== false && $status === 200) {
return json_decode($body, true) ?: [];
}
if ($body !== false && $status < 500 && $status !== 429) {
throw new RuntimeException("REST $status: $body"); // 400/401 — לא מנסים שוב
}
usleep((int) (($delay + mt_rand(0, 300) / 1000) * 1_000_000));
$delay *= 2;
}
throw new RuntimeException('REST failed after retries');
}
/** משיכת כל הלידים שנוצרו אחרי הסמן. מחזירה את הסמן החדש. */
function syncLeads(?string $cursor): ?string
{
while (true) {
$where = $cursor
? ['createdAt' => ['$gt' => ['__type' => 'Date', 'iso' => $cursor]]]
: new stdClass();
$qs = http_build_query([
'where' => json_encode($where, JSON_UNESCAPED_UNICODE),
'keys' => 'Name,PhoneNumber,Email,City,LeadStatusId,createdAt',
'order' => 'createdAt', // מיון יציב — לא -createdAt
'limit' => PAGE_SIZE, // תמיד מפורש
]);
$rows = getWithBackoff(API . "/classes/Accounts?$qs")['results'] ?? [];
if (!$rows) return $cursor;
foreach ($rows as $row) {
upsertIntoWarehouse($row); // הכתיבה שלכם, לפי objectId
}
$cursor = $rows[count($rows) - 1]['createdAt']; // מחרוזת ISO שטוחה
file_put_contents(CHECKPOINT, $cursor); // שמירה אחרי כל עמוד
if (count($rows) < PAGE_SIZE) return $cursor; // העמוד האחרון
}
}
$cursor = is_file(CHECKPOINT) ? trim((string) file_get_contents(CHECKPOINT)) : null;
syncLeads($cursor !== '' ? $cursor : null);

מה קורה כשהטבלה משתנה תוך כדי המשיכה

Section titled “מה קורה כשהטבלה משתנה תוך כדי המשיכה”
מה קורה במערכת מה קורה למשיכה שלכם האם צריך לעשות משהו
נוצרות רשומות חדשות הן נוחתות אחרי הסמן ונקראות בעמוד הבא, או בריצה הבאה לא — זו התנהגות רצויה
רשומה קיימת מתעדכנת היא לא תיקרא שוב — הסמן הוא createdAt כן, אם אתם צריכים עדכונים. ראו סנכרון עדכונים
רשומה נמחקת המשיכה לעולם לא תדע על כך כן. ראו מה משיכה לא תראה
שתי רשומות נוצרות באותה מילישנייה $gt על הסמן ידלג על השנייה כן, בטבלאות עמוסות. ראו למטה
ההרשאות של הטבלה משתנות באמצע חלק מהשורות פשוט לא יחזרו — בלי שגיאה כן — השוו את הספירה בסוף
Python
# וריאנט עמיד: $gte + סינון כפילויות לפי objectId
where = {"createdAt": {"$gte": {"__type": "Date", "iso": cursor}}} if cursor else {}
rows = get_with_backoff(f"{API}/classes/{table}", params)["results"]
fresh = [r for r in rows if r["objectId"] not in seen_ids_of_previous_page]
seen_ids_of_previous_page = {r["objectId"] for r in rows if r["createdAt"] == rows[-1]["createdAt"]}

סנכרון עדכונים — updatedAt במקום createdAt

Section titled “סנכרון עדכונים — updatedAt במקום createdAt”

צריכים גם שינויים ולא רק רשומות חדשות: החליפו את שדה הסמן.

הסמן תופס לא תופס המחיר
createdAt רשומות חדשות עדכונים, מחיקות סמן יציב לחלוטין
updatedAt חדשות וגם מעודכנות מחיקות אותה רשומה תחזור בכל ריצה שבה עודכנה — חתכו כפילויות לפי objectId בצד שלכם, ובצעו upsert ולא insert

עם updatedAt המיון כבר אינו יציב: רשומה שמתעדכנת בזמן המשיכה זזה קדימה. הסמן מגן מדילוג, לא מקריאה כפולה, ולכן הכתיבה בצד שלכם חייבת להיות אידמפוטנטית, לפי objectId.

שדות שההרשאות מסתירות לא יחזרו, בלי שגיאה, ורשומה שאינה נראית להרשאה שלכם נראית כרשומה שאינה קיימת (code 101). עם API Key זה לא רלוונטי, הוא עוקף הכול: קריאה ל‑_User עם API Key מחזירה 200. עם Session Token כן. ראו אימות.

הגנה חלקית ממחיקות: במערכת חדשה מופעל checkDependenciesBeforeDelete, ומחיקה של רשומה שיש אליה Pointer נחסמת ב‑400 code 119 Pointer to Object … exists in table Tasks in key AccountId. Delete is not allowed. רשומות שאין אליהן הפניות עדיין נעלמות בשקט.

include — מתי שווה לשלם עליו

Section titled “include — מתי שווה לשלם עליו”

הדוגמה שולפת את שדה ה‑Pointer (LeadStatusId) כמצביע, ומתרגמת אותו במחסן מול טבלת הסטטוסים שנמשכה פעם אחת.

המצב מה עדיף
טבלת lookup קטנה וקבועה (סטטוסים, מקורות, ערים) משכו אותה פעם אחת לזיכרון ותרגמו אצלכם. אל תשלמו include על כל שורה
Pointer לטבלה גדולה שאתם צריכים ממנה 2–3 שדות include — הוא חוסך קריאת N+1 לכל שורה
Pointer שאתם רק מעבירים הלאה כמזהה בלי include — המזהה כבר בתשובה
המגבלה הערך איך הדוגמה מתמודדת
limit ברירת מחדל ב‑REST 100 מציינת limit במפורש, תמיד
limit מרבי ב‑REST אין תקרה נאכפת — limit=2000 מחזיר את כל השורות שנמצאו; 1000 הוא המלצה PAGE_SIZE = 1000 — גודל עמוד, לא מגבלת שרת
where הוא JSON שעובר URL‑encoding — קידוד דרך הספרייה, לא שרשור מחרוזות
$or תקף רק ברמה העליונה (או בתוך $and) — $or בתוך שדה → 400 code 107 bad constraint: $or; בתוך $and עובד. לא בשימוש כאן; ראו שאילתות
אגרגציה — /parse/aggregate/… (כולל distinct) אינו קיים בפריסה — 404 גם עם Master Key. הנתיב הפנימי /parse/classes-aggregate/… עונה גם ל‑API Key. הדוגמה מושכת שורות; לסכומים — אגרגציות
מגבלות קצב אין מגבלה מתועדת, ולא נצפתה — 150 קריאות במקביל (1.2 שניות) ועוד 100 עוקבות (2.2 שניות) חזרו כולן 200, בלי 429 נסיגה אקספוננציאלית + jitter, והשהיה מרצון בטעינות גדולות

«לא נצפה» אינו «לא קיים»: אל תבנו סנכרון שמניח תעבורה בלתי מוגבלת. מגבלות ומכסות.

צ’קליסט לפני שמריצים על ייצור

Section titled “צ’קליסט לפני שמריצים על ייצור”
  • ספרתם קודם (limit=0&count=1) ויודעים כמה קריאות זה
  • ה‑where מקודד על ידי הספרייה — JSON שבור ב‑where מחזיר 500 ונראה כמו תקלת שרת שראוי לנסות שוב
  • limit מפורש בכל קריאה
  • המיון הוא createdAt עולה — לא -createdAt ולא שדה שמשתנה
  • keys מכיל רק את השדות שאתם באמת צריכים
  • הסמן נשמר במקום קבוע ששורד הפעלה מחדש, ונכתב אחרי כל עמוד
  • הכתיבה אצלכם היא upsert לפי objectId, לא insert
  • יש נסיגה אקספוננציאלית על 429 ו‑5xx — ואין ניסיון חוזר על 4xx אחרות
  • יש timeout על כל קריאה
  • הוחלט מה עושים עם מחיקות (ארכיון בצד המערכת, או השוואת ספירות)
  • המפתח מגיע ממשתנה סביבה או ממנהל סודות — לא מהקוד