אילוץ CHECK הוא כלל שכל שורה חייבת לקיים
אילוץ CHECK הוא ביטוי בוליאני שמצמידים לטבלה. SQLite בודקת אותו בכל INSERT ו-UPDATE, ואם הביטוי יוצא שקרי, הפעולה נכשלת. זו דרך לאפות כלל עסקי, "המחיר לא יכול להיות שלילי", "הסטטוס חייב להיות אחד משלושת הערכים האלה", ישירות לתוך הסכמה.
שתי השורות הראשונות נכנסות. השלישית זורקת CHECK constraint failed ונדחית: הטבלה אף פעם לא רואה אותה. האילוץ אוכף את הכלל על כל מי שכותב, בין אם זו האפליקציה שלכם, סקריפט מיגרציה או מישהו שמשחק ב-CLI.
ברמת העמודה מול ברמת הטבלה
אפשר לכתוב CHECK בשני מקומות: אחרי הגדרת עמודה (ברמת העמודה) או אחרי כל העמודות (ברמת הטבלה). הם מתנהגים אותו דבר; ההבדל הוא מה נקרא באופן טבעי.
ההזמנה הראשונה נכנסת. השנייה נכשלת: הסוף לפני ההתחלה. כללים על עמודה אחת נקראים טוב יותר ברמת העמודה; כל דבר שמשווה שתי עמודות או יותר נקרא טוב יותר ברמת הטבלה.
הגבלת ערכים לרשימה
שימוש נפוץ הוא לחייב עמודה לקבל אחד מתוך קבוצה קבועה של ערכים. ל-SQLite אין טיפוס enum מובנה, ולכן CHECK ... IN (...) הוא הדרך המקובלת:
השורה השלישית נכשלת: 'pending' לא ברשימת הערכים המותרים. אם אי פעם תצטרכו להוסיף סטטוס חדש, תצטרכו לבנות את הטבלה מחדש (עוד על זה בהמשך), אז חשבו קצת לפני שאתם נועלים את הרשימה. אבל לאוצר מילים קבוע באמת, כמו שמות תפקידים או מצבי הזמנה, זה בדיוק האילוץ שאתם רוצים.
מתן שמות לאילוצים
כברירת מחדל, אילוץ הוא אנונימי. הודעת השגיאה אומרת רק "CHECK constraint failed" עם הביטוי, וזה בסדר כשיש CHECK אחד בטבלה, ומבלבל כשיש חמישה. הוסיפו שם עם CONSTRAINT:
עכשיו הודעת הכישלון כוללת את שם האילוץ, כך שאתם יודעים מיד איזה כלל הופר. מתן שם עולה כמה תווים נוספים ומחזיר את עצמו בפעם הראשונה שמשהו נשבר בפרודקשן.
CHECK ו-NULL: המלכודת
CHECK עובר כשהביטוי אמת או NULL. הוא נכשל רק על שקר מפורש. זה נשמע מוזר עד שזוכרים שכמעט כל השוואה עם NULL מחזירה NULL, לא אמת ולא שקר.
השורה עם NULL נכנסת בלי בעיה: NULL >= 0 הוא NULL, לא שקר, ולכן ה-CHECK לא נכשל. אם אתם באמת רוצים לאסור גם מספרים שליליים וגם ערכים חסרים, שלבו NOT NULL עם ה-CHECK:
עכשיו ההכנסה נכשלת על אילוץ ה-NOT NULL עוד לפני שה-CHECK רץ. שני האילוצים עובדים יחד: NOT NULL מכסה היעדר ערך, CHECK מכסה את הצורה שלו.
פונקציות מובנות שימושיות בתוך CHECK
הביטוי יכול להשתמש ברוב הפונקציות המובנות של SQLite. כמה שעולות הרבה:
שלושה כישלונות: כתובת אימייל במבנה שגוי, שם משתמש קצר מדי וקוד מדינה באותיות קטנות. LIKE מטפל בתבניות פשוטות; length(), upper(), lower() וחשבון מותרים כולם. רק הקפידו שהביטוי יהיה דטרמיניסטי: שימוש במשהו כמו random() או current_timestamp בתוך CHECK יוצר כללים שיכולים להתהפך משורה לשורה, וזה כמעט אף פעם לא מה שאתם רוצים.
CHECK מול Trigger
גם CHECK וגם triggers יכולים לדחות נתונים גרועים, ומתחילים תוהים לא פעם במה לבחור. כלל האצבע:
- CHECK כשהכלל תלוי רק בשורה שנכתבת. "העמודה הזו בהשוואה לעמודה ההיא", "הערך הזה בטווח", "המחרוזת הזו מתאימה לתבנית".
- Trigger (ספציפית trigger מסוג
BEFORE INSERT/UPDATEשקורא ל-RAISE) כשהכלל תלוי ב_שורות אחרות_, ב_טבלאות אחרות_, או צריך לעשות משהו מסובך יותר מביטוי בוליאני יחיד.
CHECK מהיר יותר, פשוט יותר וגלוי בסכמה: כל מי שקורא את CREATE TABLE רואה את הכלל. פנו ל-trigger רק כש-CHECK לא יכול לבטא את מה שאתם צריכים.
אי אפשר למחוק CHECK עם ALTER
זו הפינה המחוספסת היחידה. ל-SQLite אין ALTER TABLE ... DROP CONSTRAINT. כדי להסיר או לשנות CHECK, בונים את הטבלה מחדש:
BEGIN;
CREATE TABLE products_new (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
price REAL NOT NULL CHECK (price >= 0 AND price <= 1000000)
);
INSERT INTO products_new SELECT * FROM products;
DROP TABLE products;
ALTER TABLE products_new RENAME TO products;
COMMIT;
עטפו את כל התהליך בטרנזקציה, כך שכישלון באמצע ישאיר את מסד הנתונים כמו שהוא. אם לטבלאות אחרות יש מפתחות זרים שמצביעים על הטבלה שאתם בונים מחדש, הריקוד מתארך: מכבים את foreign_keys, בונים מחדש, מפעילים שוב, בודקים שוב. נכסה את זה במדריך על מיגרציות בהמשך תוכנית הלימודים.
הבא בתור: אילוצי UNIQUE
CHECK מאמת את הצורה של ערכים בתוך שורה. האילוץ הבא, UNIQUE, מאמת קשרים בין שורות: הוא מבטיח ששתי שורות לא יחלקו את אותו ערך בעמודה או בקבוצת עמודות. זה מה שמחכה בהמשך.
שאלות נפוצות
מהו אילוץ CHECK ב-SQLite?
אילוץ CHECK הוא ביטוי בוליאני שמוצמד לטבלה וכל שורה חייבת לקיים אותו. SQLite בודקת אותו בכל INSERT או UPDATE, ודוחה את השינוי אם הביטוי שקרי. זו הדרך הפשוטה ביותר לאכוף כלל כמו 'המחיר חייב להיות חיובי' בלי לכתוב קוד באפליקציה.
אילוץ CHECK ב-SQLite יכול להתייחס לכמה עמודות?
כן: כתבו אותו כאילוץ ברמת הטבלה במקום להצמיד אותו לעמודה אחת. למשל, CHECK (start_date <= end_date) שמוגדר אחרי רשימת העמודות יכול להתייחס לשתי העמודות. מבחינה טכנית גם בדיקות ברמת העמודה יכולות להתייחס לעמודות אחרות, אבל ברמת הטבלה זה קריא יותר כשמעורבת יותר מעמודה אחת.
למה אילוץ ה-CHECK שלי ב-SQLite לא נכשל על NULL?
CHECK עובר כשהביטוי אמת או NULL: הוא נכשל רק כשהביטוי שקרי במפורש. לכן CHECK (age >= 0) מקבל גיל NULL, כי NULL >= 0 הוא NULL, לא שקר. אם אתם רוצים לאסור גם NULL, הוסיפו אילוץ NOT NULL לצד ה-CHECK.
אפשר למחוק או לשנות אילוץ CHECK ב-SQLite?
לא ישירות. SQLite לא תומכת ב-ALTER TABLE ... DROP CONSTRAINT. כדי לשנות CHECK, אפשר לערוך את sqlite_schema עם PRAGMA writable_schema (מתקדם ומסוכן) או לבנות את הטבלה מחדש: ליצור טבלה חדשה עם האילוצים הרצויים, להעתיק אליה את הנתונים, למחוק את הטבלה הישנה ולשנות את השם. מתן שמות לאילוצים הופך את סקריפט הבנייה מחדש לקריא יותר.