דלגו לתוכן

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

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

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

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

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

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

ספירה היא הקריאה הזולה ביותר ב‑API, והיא ההבדל בין “שתי קריאות” ל‑“180 קריאות ותכנון”.

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 מחזיר את הספירה בלי לשלוף שורה אחת (אומת: {"results":[],"count":24} עם API Key). 1,842 רשומות = שתי קריאות של 1,000. 184,000 = 184 קריאות, וכדאי לפרוס אותן על זמן.

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

Section titled “הפרמטרים שהדוגמה משתמשת בהם”
פרמטר הערך בדוגמה למה
where סמן ההמשך: createdAt > cursor מה שהופך את הריצה השנייה לדלתא ולא למשיכה חוזרת
keys רשימה מפורשת של שדות בטבלה עם 130 שדות זה ההבדל בין מגה‑בייטים לקילו‑בייטים
order createdAt (עולה) מיון יציב — השדה הזה לא משתנה אחרי היצירה
limit 1000 גודל העמוד המומלץ. השרת אינו אוכף תקרה (אומת: limit=2000 החזיר 1,100 שורות — באג core-api-04), אבל עמודים גדולים יותר רק מאריכים תשובה וזיכרון. תמיד מפורש
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 — מתי שווה לשלם עליו”

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

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

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

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