ToCode
رفتن به کانال در Telegram
1 419
مشترکین
اطلاعاتی وجود ندارد24 ساعت
اطلاعاتی وجود ندارد7 روز
-230 روز
آرشیو پست ها
1 419
# למידה טבעית
אם אנחנו מסתכלים רגע מחוץ לעולם של כיתות לימוד וכל מערכת ההשכלה, קל לראות שלמידה קורית כל הזמן: גם בלי מורים, בלי תלמידים ובלי לוח. תינוקות לומדים ללכת ולדבר, ילדים גדולים יותר לומדים את החוקים החברתיים של העולם שסביבם ומבוגרים לומדים מיומנויות חדשות לבד, מתוך ניסוי וטעיה. התבוננות בלמידה טבעית יכולה ללמד אותנו כמה דברים חדשים שאולי יעזרו לנו ללמוד טוב יותר גם בצורה מכוונת.
בואו נתחיל עם זה - "איך באמת אנחנו לומדים ביום יום?" . אפשר לקחת דוגמה פשוטה מהחיים למשל של לימוד מילה חדשה בשפה:
1. שלב ראשון הוא חשיפה: אולי נתקלתי במילה חדשה בספר, בסרט, בשיחה עם חברים. איפשהו שמעתי מילה שלא הכרתי.
2. הרבה פעמים אני אזרוק את זה, אני אחליק את זה שאני לא מכיר מילה ואמשיך הלאה בחיים. אבל לפעמים אני בוחר להתחיל תהליך לימוד - השלב השני בתהליך הוא איסוף מידע: אני יכול לשאול את החברים מה משמעות המילה או לחפש במילון. אולי אני מחפש בכמה מקורות או בגוגל תמונות כדי לראות איך זה נראה. לאט לאט אני אוסף עוד מידע על הדבר החדש שלמדתי.
3. אחרי שאני יודע מספיק אני ממשיך לשלב השלישי: שימוש ושיפור. אני יכול לפגוש חבר ופתאום לזרוק את המילה החדשה הזאת בשיחה. לפעמים זה יצליח והחבר יבין את הכוונה, לפעמים החבר יתקן את ההגיה שלי ולפעמים החבר יסתכל עליי במבט עקום כי השתמשתי במילה במקום לגמרי לא הגיוני. ככל שאני משתמש יותר במילה, ומקבל יותר פידבק איכותי, כך אני לומד עוד על המשמעות שלה ומטמיע אותה ביכולת הלשונית שלי.
שווה לשים לב ששלושת שלבים אלה משחקים תפקיד בכל תהליך לימוד שנבצע. ככל שנשים לב אליהם ונדע באיזה שלב אנחנו בלימוד יהיה לנו יותר קל להתמיד ולמצוא לעצמנו אמצעי לימוד שמתאימים לאותו השלב.
1 419
# תהליך של למידה
לכל מתכנת ומתכנתת שאני מכיר יש רשימה מכובדת של טכנולוגיות חדשות שאנחנו רוצים ללמוד (או ללמוד יותר לעומק). זה יכול להיות פיתוח אפליקציות, פיתוח משחקים, למידת מכונה, Kernel, רשתות, האקינג. דבר אחד משותף לכולנו: לכל אחד ואחת יש איזו רשימה בלב. ובגלל אופי העולם ובגלל שכל טכנולוגיה חדשה שלומדים רק עושה חשק ללמוד עוד משהו, הרשימה לעולם לא נגמרת.
חלק מהמתכנתים מסתכלים על הרשימה וחושבים - אני לעולם לא אצליח ללמוד את הכל, וממילא יש לי כל כך הרבה מה לעשות עם הדברים שאני כבר יודע, אז אפשר להשאיר אותה בהקפאה. יום אחד כשיהיה זמן (או כשהבוס ייתן לי זמן לימוד) אני אבחר איזה נושא שם ואלמד אותו יותר לעומק.
ואחרים מסתכלים על הרשימה באופטימיות, עם מחשבה של "איזה כיף שתמיד יש עוד משהו ללמוד", ככה אני יכול להקדיש שעה ביום ללמידה ותמיד יהיה לי משהו מעניין שימלא אותה.
אל תתייאשו ממשימות אינסופיות, כל מה שצריך זה לשנות את נקודת המבט ולהמשיך קדימה.
1 419
# באג ריפורט: בעיית קונפיגורציה ב haproxy
אני רוצה לפתוח פה מדור חדש בבלוג שנקרא "באג ריפורט" - לא, אני לא הולך לדווח לאף אחד על באגים ומקווה שגם אתם לא תדווחו לי על באגים. הכוונה היא לקחת באג שנתקעתי עליו (או שאתם נתקעתם עליו ושיתפתם אותי) ולפרק אותו כדי שלא נצטרך לעשות שטויות כאלה שוב. והיום בבאג ריפורט - בעיית קונפיגורציה ב haproxy שלקח לי הרבה יותר מדי זמן למצוא.
## מה המשתמשים ראו
משתמשים שנכנסו למערכת בה היתה הבעיה קיבלו את המערכת תקינה בחלק מהזמן, ובחלק אחר מהזמן קיבלו הודעות שגיאה מוזרות עם הקוד 503. לא נראה שיש חוקיות מתי מקבלים תשובה תקינה ומתי שגיאת 503, עבור אותם URL-ים ממש.
## מה אני ראיתי
הדבר הראשון שעושים כדי לפענח בעיות כאלה הוא להבין מאיזה מערכת ושרת בכלל מגיעה הבעיה. רוב אתרי הווב היום מוחקים את ה HTTP Headers כדי לא להסגיר שום מידע, ולכן הדבר הראשון שאנחנו עושים כשצריך לדבג בעיות מסוג זה הוא לעדכן את הקונפיגורציה של שרתי הווב ודווקא כן לשתול שם בחזרה את ה Headers עם מזהי השרת.
אחרי הפעלה מחדש וצפיה ב curl אפשר היה לראות שמי ששולח את הודעת ה 503 היה haproxy. קצת חיטוט בתיעוד הראה שהאפרוקסי ישלח הודעות 503 אם הוא לא מוצא שרת מתאים להעביר אליו את הבקשה. אבל, אם הוא לא היה מוצא שרת מתאים להעביר אליו את הבקשה היינו צריכים לראות את השגיאה בכל פניה - למה חלק מהפניות כן הצליחו וחלק לא?
## איפה היתה הבעיה
אקצר לכם את החפירות בקבצי הלוג, השינויים המיותרים בקונפיגורציה והיותר-מדי-שעות של קריאת אותן שורות בקובץ ההגדרה ונקפוץ ישר לבלוק הבעייתי:
backend nginx
option forwardfor header X-Real-IP
reqadd X-Scheme:\ https if { ssl_fc }
server nginx 127.0.0.1:80
server nginx /var/run/nginx.sock
הבלוג מגדיר את nginx בתור Backend ל Haproxy, אבל מגדיר שתי דרכי גישה ל nginx: גם באמצעות הכתובת 127.0.0.1:80 וגם באמצעות Unix Socket. מאחר ו nginx הקשיב רק על פורט 80 ולא דרך ה socket, כל הבקשות ש Haproxy ניסה לשלוח ל server השני נכשלו.
## איך היא הגיעה לשם
התחביר של Haproxy שמאפשר להגדיר מספר שרתים בתוך Backend יחיד נועד בשביל שנחלק את העומס בין כמה שרתים. אין שום הגיון לכתוב פעמיים פקודת server שתגיע לאותו שרת רק מנתיב אחר. אפילו אם ה nginx כן היה מקשיב על ה Socket זה היה מיותר - צריך היה למצוא את דרך התקשורת המועדפת ולהישאר איתה.
השערה שלי שפעם זה עבד עם Unix Socket, מישהו החליף את זה ל TCP ואז שם את שורת ה Socket בהערה, ומתישהו (לא ברור מתי) מישהו הוציא מהערה (כנראה בטעות) את שורת ההאזנה ל socket.
## איך אפשר לשמור שלא נעשה שטויות כאלה שוב
למרות שהבאג נגרם משורה אחת לא נכונה בקובץ קונפיגורציה, הגורמים המערכתיים האמיתיים לתקלה היו רבים:
1. קבצי הקונפיגורציה לא נשמרו בתוך Source Control כמו הקוד, ולכן קשה היה לדעת מי שינה מה ומתי בקובץ.
2. מתכנתים השאירו קוד שלא צריכים בהערה, מה שגרם להשארת קוד שלא עובד (אפילו שהיה בהערה) בתוך הקובץ ובמקום זמין.
3. האפרוקסי לא התלונן בשום מקום (שהצלחתי למצוא) על זה שהוא זורק בקשה כי הוא לא מצליח להגיע ל Unix Socket. מדיניות לוגים טובה יותר אולי היתה מאפשרת לנו לראות את התקלה בזמן אמת ולחסוך זמן חיפוש.1 419
# מידע אמין
בשביל לקבל החלטות חכמות צריך להתבסס על נתונים אמינים. הבעיה היא שבכל תחומי החיים וגם במערכות תוכנה נתונים טובים זה הדבר שהכי קשה למצוא. נניח שהעליתם גירסה חדשה ועכשיו לקוחות מתקשרים להתלונן שכלום לא עובד. אנחנו רוצים לדעת:
1. מה בדיוק לא עובד להם?
2. החל מאיזו גירסה של הקוד הדבר שהם מנסים לא עבד?
3. האם יש הודעת שגיאה באחד הלוגים?
4. האם משהו (חוץ מהקוד) השתנה על השרת?
5. מה הלקוח רואה אצלו על המכונה?
6. האם יש הודעות שגיאה ב Browser Console אצל הלקוח?
יש היום אינסוף כלים שאוספים את המידע ומאפשרים גישה מהירה אליו כשצריך אותו. אם אתם לא עכשיו בתוך בעיית פרודקשן, שווה לנצל את הזמן ולהתקין את הכלים שיאפשרו לכם להתגבר על התקלה כשזו תגיע.
1 419
# המרחק בין שיפוצניק למהנדסת
בתור מתכנתים אנחנו בטוחים שיש לנו כח-על לפתור בעיות בלי ללמוד עד הסוף את כל הטכנולוגיות שמעורבות בסיפור. אני חושב שגדלנו לתוך זה כי באמת היום העולם הטכנולוגי הוא כל כך מורכב ויש תמיד עוד דברים יותר בסיסיים להכיר, שממילא מה שלא תעשה אתה הולך להתמודד עם בעיות בתנאי אי וודאות.
כך מתכנת Mathlab שקיבל משימה לתקן קוד קצר ב NumPy לא יעצור ללמוד פייתון מאפס לפני שניגש למשימה. המחשבה שלנו היא "אם נתנו לי את המשימה הזאת זה בטח בגלל שיש לי כוחות על שיאפשרו לי ללמוד קצת נאמפיי ולפתור אותה, בלי לשבת עכשיו חודש ללמוד פייתון".
אז אני ניגשים לפתור את הבעיה אחרי שקראתי קצת ברשת או עשיתי טוטוריאל קטן על נאמפיי ובאמת בדרך כלל גם מצליח לכתוב קצת קוד שנראה בכיוון הנכון ואז אני נתקע. התקיעה פה היא לגמרי צפויה כי זה מה שקורה כשמנסים לכתוב נאמפיי בלי לדעת פייתון, אבל בתור מתכנת עם כוחות-על אני מיד מספר לעצמי שתמיד יש מכשולים בדרך ובטח יש פיתרון ב Stack Overflow או באיזה קבוצת פייסבוק. וככה מתחיל המסע הדי מתסכל במטרה לפתור את הבעיה בלי לעצור ללמוד לעומק את הנושא - כל מכשול נוסף שולח אותי לחיפוש אחרי פיתרון נוסף ברשת בלי לדעת כמה מכשולים עוד יש עד שהקוד יעבוד.
אם המסלול הזה בסוף היה מוביל לדרך ללא מוצא לא הייתי צריך לכתוב עליו פוסט. אני חושב שהבעיה במסלול היא שזה מסלול שעובד: שיש מספיק קבוצות פייסבוק ושאלות ב Stack Overflow כדי שמתכנתים ממוצעים יצליחו לפתור את רוב הבעיות ובאמת לכתוב קוד שעובד. והקסם כאן זה שיש גם משהו ממכר במסלול - כל פעם פותרים עוד אתגר קטן עד שהקוד עובד ואז ממשיכים למשימה הבאה. אפשר להעביר ככה קריירה.
אבל אני כותב את הפוסט כדי להפנות את המבט לדבר שאנחנו מפסידים כשאנחנו הולכים שוב ושוב במסלול גיבורי העל שלנו - והדבר הזה הוא הידע המקצועי שבעזרתו מתכנתים הופכים לאנשי מקצועי טובים יותר.
כי תראו שיפוצניק לא הופך למהנדס רק בגלל שהוא מתקן עוד נזילה. המרחק בין שיפוצניק למהנדס עובר בהיכרות עם עולם תוכן תיאורטי עשיר, היכרות שלא מקבלים מהיום יום בפיתרון בעיות. דווקא בגלל שיש לכם כוחות על, שווה לנצל אותם כדי לעשות את ההתקדמות המקצועית שאתם כל כך צריכים. כי בסוף המתכנתים שאנחנו צריכים בתעשיה הם לא אלה שפותרים כמה שיותר מהר טאסקים בג'ירה, אלא אלה שרואים את הבעיות לעומק ומבינים את היתרונות והחסרונות של כל פיתרון כדי לבחור את הפיתרון הטוב ביותר.
1 419
# שלבים בהתמודדות עם בעיות
כשאנחנו נתקלים בבעיה בתוכנה צפויים לנו ארבעה שלבים בהתמודדות:
1. לשים לב שיש בעיה.
2. למצוא מלא פיתרונות שלא עובדים.
3. למצוא פיתרון שעובד.
4. לחפור בבעיה, להבין אותה לעומק ולמצוא עוד פיתרונות שעובדים (ובדרך להבין למה הפיתרונות שלא עבדו באמת לא עובדים).
5. להסביר את מה שגיליתם לאדם אחר (או לכתוב לעצמכם).
בשביל שלבים 1-3 אתם לא צריכים אותי. לא משנה איפה אתם עובדים אם אתם כותבים קוד כנראה היו לכם טעויות, כנראה שניסיתם פיתרונות שלא עבדו ואני מקווה שבסוף גם מצאתם איזשהו פיתרון שעובד.
אלה השלבים הרביעי והחמישי המהווים את המפתח להתפתחות המקצועית שלנו. וכן, שלבים אלה הם בדיוק הסיבה שאנחנו לא אוהבים להעתיק פיתרונות מ Stack Overflow. הבנה אמיתית של הבעיה היא הדרך היחידה להבטיח שאותה בעיה לא תחזור על עצמה בצורות חדשות בעתיד.
1 419
בניגוד לירושה, שיתוף קוד באמצעות Decorators באופן זה מעודד אותנו להדביק לתוך המחלקה התנהגויות מסוגים שונים, מה שקצת מזכיר תבנית של Mixins. בצד החסרונות מדובר במימוש מסובך יותר ולכן קשה יותר לתחזוקה.
1 419
# שיתוף קוד בין מחלקות Python באמצעות Decorators
כולם היום יודעים לדקלם איך כשיש לנו מחלקות עם קוד משותף אפשר להשתמש בירושה כדי לכתוב את הקוד הכפול רק פעם אחת, אבל מה קורה כשהדבר שאנחנו רוצים לשתף הוא קצת יותר מסובך מפונקציות ומידע? מה קורה אם כל מחלקה משתפת את הקוד הזה בצורה קצת אחרת? בפוסט זה נראה דוגמה בה ירושה לא עובדת כל כך טוב, ובמקומה נראה פיתרון אחר לשיתוף קוד בין מחלקות באמצעות Decorators.
## מה אנחנו בונים
נניח שאנחנו רוצים לבנות משחק שיש בו כדור קופץ ומלבן שצריך לתפוס את הכדור הזה. אפשר לדמיין שכל אחת מהצורות במערכת תיוצג באמצעות מחלקה ולכל צורה יש מיקום ומהירות. בקוד פייתון (עם pyxel בשביל הציורים) הקוד נראה כך:
@dataclass
class Ball:
x: int = 0
vx: int = 2
y: int = 20
color: int = 2
radius: int = 5
def draw(self):
pyxel.circ(self.x, self.y, self.radius, self.color)
def update(self):
self.x += self.vx
@dataclass
class Rect:
x: int = 0
vx: int = 2
y: int = 0
color: int = 5
def draw(self):
pyxel.rect(self.x, self.y, 60, 20, self.color)
def update(self):
self.x += self.vx
בתוכנית עצמה אפשר ליצור כדורים ומקלות ואפילו להזיז אותם (כרגע פשוט כולם זזים ימינה) עם קוד שנראה כך:
shapes = [Ball(x=x, y=10) for x in range(0, 100, 30)] + [Rect(x=0, y=100)]
def draw():
pyxel.cls(0)
for shape in shapes:
shape.draw()
def update():
for shape in shapes:
shape.update()
pyxel.init(160, 120)
pyxel.run(update, draw)
הקוד עובד אבל קל לראות שיש לנו כפל קוד בין שתי המחלקות:
1. בשתי המחלקות יש משתנים לייצוג המיקום על ציר x והמהירות על ציר x
2. בשתי המחלקות יש פונקציית update שמעדכנת את המיקום על ציר x לפי המהירות
אפשר גם לדמיין שבעתיד נצטרך להזיז את הכדורים גם על ציר y, ואולי נרצה משתנה נפרד עבור המהירות בציר y. אולי אפשר לדמיין משחק תלת מימדי עם מיקום ומהירות על ציר z. וזה מביא אותנו לאתגר - איך אפשר לשתף בצורה קלה את הקוד שמטפל במיקום ובמהירות על ציר מסוים, כאשר כל צורה תוכל להגדיר לעצמה על איזה צירים היא יכולה לזוז?
## הפיתרון: Decorators
כיוון אחד לפיתרון יהיה להגדיר את כל הצירים האפשריים במחלקת-אב בשם Shape. אני משאיר לכם את כתיבת הפיתרון הזה בתור תרגיל, אבל אגיד רק שאני לא חושב שזה פיתרון מספיק טוב במקרה הכללי. כי לא כל הצורות צריכות את המשתנים לכל הצירים.
כיוון יותר מעניין הוא שימוש ב Decorators: אם אני יכול להוסיף בצורה דינמית את המשתנים המתאימים לציר מסוים, ולעדכן את הפונקציה update ולהוסיף לה את התנועה באותו כיוון, אוכל לייצר Decorator שיוסיף את המשתנים ויעדכן את update על הצירים שאני צריך.
קוד? ברור. שימו לב לפונקציה הבאה:
def add_axis(axis, initial_value=0, initial_speed=2):
def class_wrapper(cls):
cls.__annotations__[axis] = int
cls.__annotations__['v' + axis] = int
setattr(cls, axis, initial_value)
setattr(cls, 'v' + axis, initial_speed)
original_update = getattr(cls, 'update')
def update(self):
original_update(self)
setattr(self, axis, getattr(self, axis) + getattr(self, 'v' + axis))
setattr(cls, 'update', update)
return cls
return class_wrapper
הפונקציה מקבלת מחלקה אחרת ומעדכנת את שדה __annotations__ שלה עם המשתנים המתאימים לציר החדש שאני יוצר, וגם יוצרת ערכי ברירת מחדל למשתנים בציר זה. אם axis יהיה x נקבל את המשתנים x ו-vx. אם axis יהיה y נקבל את המשתנים y ו vy, וכך לכל ציר שאפשר לדמיין.
בשביל להפעיל את הפונקציה אני משתמש בכתיב ה Decorators על המחלקה, ועל הדרך גם מוסיף לכדורים תנועה על ציר y:
@dataclass
@add_axis('x', 0, 2)
@add_axis('y', 20, 1)
class Ball:
color: int = 2
radius: int = 5
def draw(self):
pyxel.circ(self.x, self.y, self.radius, self.color)
def update(self):
pass
@dataclass
@add_axis('x', 0, 2)
@add_axis('y', 20, 0)
class Rect:
color: int = 5
def draw(self):
pyxel.rect(self.x, self.y, 60, 20, self.color)
def update(self):
pass1 419
לדוגמה נניח שיש לי מערכת פירסום בה משתמשים יכולים ליצור פירסומות שיוצגו בכל מיני אתרים. אני רוצה לוודא שיש לי פרטי כרטיס אשראי של המשתמש לפני שאני נותן לו להעלות פירסומת לאוויר. אפשר לדמיין וולידציה כללית על "פירסומת" שמוודאת שיש לי במערכת את פרטי האשראי של המשתמש לפני שאני נותן לו לשמור אוביקט פירסומת, אבל זאת תהיה חוויה די גרועה למשתמש. הרבה יותר הגיוני לתת למשתמש לבנות ולשמור פירסומות, וליצור מסך ייעודי עבור "העלאה לאוויר" של הפירסומות, ובאותו מסך לדאוג לקבלת אמצעי תשלום אם הוא לא שמור אצלנו.
בגדול כשאנחנו רואים את הפקודה:
errors.add(:base, '...')
בתור קוד של Active Record זה כמעט תמיד נקודת פתיחה טובה ל Refactoring.
יש לכם טיפים נוספים על Active Record Validations? אשמח לשמוע, מוזמנים לשתף בתגובות.1 419
=> falseהדרך להתמודד עם מספר מקורות עדכון בריילס היא לייצר מספר Validation Contexts. כל Validation Context מייצג את הטופס או המסך בו אנחנו רוצים לעדכן את ההודעה בצורה מסוימת, והוולידציות שבו יתאימו לאפשרויות באותו המסך. בחזרה להודעה אפשר לעדכן את הקוד בצורה הבאה:
class Message < ApplicationRecord
validates :from, presence: true, on: :message_form
validates :to, presence: true, on: :message_form
validates :title, length: { minimum: 5 }, on: :message_form
validates :body, presence: true, on: :message_form
end
בעת שמירת הודעה אנחנו נעביר עכשיו את הקונטקסט המתאים כדי שריילס ידע איזה וולידציות להפעיל:
m = Message.first
m.important = true
m.save(context: :message_index)
=> true
כרגע פונקציית update של ריילס לא יודעת לעבוד עם Custom Validation Context ולכן כדי לעבוד איתם בדרך כלל נרצה להוסיף פונקציה משלנו לכל ה Active Records ביישום:
class ApplicationRecord < ActiveRecord::Base
self.abstract_class = true
def update_attibutes_with_context(attributes, *contexts)
with_transaction_returning_status do
assign_attributes(attributes)
save(context: contexts)
end
end
end
ולסיום ב Controller נצטרך להוסיף את ה Context המתאים בכל פעולה:
class MessagesController < ApplicationController
before_action :set_message, only: [:show, :edit, :update, :destroy, :toggle_important]
# GET /messages
# GET /messages.json
def index
@messages = Message.all
end
# GET /messages/1
# GET /messages/1.json
def show
end
# GET /messages/new
def new
@message = Message.new
end
# GET /messages/1/edit
def edit
end
def toggle_important
@message.important = !@message.important
@message.save(context: :messages_index)
end
# POST /messages
# POST /messages.json
def create
@message = Message.new(message_params)
respond_to do |format|
if @message.save(context: :message_form)
format.html { redirect_to @message, notice: 'Message was successfully created.' }
format.json { render :show, status: :created, location: @message }
else
format.html { render :new }
format.json { render json: @message.errors, status: :unprocessable_entity }
end
end
end
# PATCH/PUT /messages/1
# PATCH/PUT /messages/1.json
def update
respond_to do |format|
if @message.update_attributes_with_context(message_params, :message_form)
format.html { redirect_to @message, notice: 'Message was successfully updated.' }
format.json { render :show, status: :ok, location: @message }
else
format.html { render :edit }
format.json { render json: @message.errors, status: :unprocessable_entity }
end
end
end
# DELETE /messages/1
# DELETE /messages/1.json
def destroy
@message.destroy
respond_to do |format|
format.html { redirect_to messages_url, notice: 'Message was successfully destroyed.' }
format.json { head :no_content }
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_message
@message = Message.find(params[:id])
end
# Only allow a list of trusted parameters through.
def message_params
params.require(:message).permit(:from, :to, :title, :body)
end
end
## אפשר ליצור וולידציה כללית (למרות שזה מבלבל את המשתמשים)
ריילס מאפשר לנו ליצור וולידציה "כללית" על אוביקט שהתוצאה שלה תוצג ברשימת הודעות השגיאה של הטופס אבל לא עבור שדה מסוים. כמעט תמיד כשאתם באים לכתוב וולידציה כללית כזו שווה להשקיע עוד כמה דקות של מחשבה ולמצוא דרך טובה יותר לביצוע המשימה.1 419
ריילס הוא אומנם מהיר אבל גם שליחת הודעה לשרת המהיר בעולם תיקח יותר זמן מוולידציה שרצה אצל הלקוח בלי לדבר עם השרת. כמעט תמיד בכל מקום שיש וולידציה ב Active Record כדאי להוסיף גם וולידציה מקבילה בטופס (אני אומר כמעט כי זה לא תמיד אפשרי - לדוגמה וידוא שערך מסוים הוא ייחודי במערכת זה משהו שאפשר לעשות רק בצד שרת).
בדוגמה שלנו אני יכול לעדכן את הטופס עם הקוד הבא וכך להרוויח גם וולידציה לפני שליחת ההודעה לשרת (כדי לקבל חווית משתמש טובה יותר), וגם וולידציה בצד השרת (בשביל אבטחת מידע ובשביל פרטים שהלקוח לא יודע עדיין):
<%= form_with(model: message, local: true) do |form| %>
<% if message.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(message.errors.count, "error") %> prohibited this message from being saved:</h2>
<ul>
<% message.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<%= form.label :from %>
<%= form.text_field :from, required: true %>
</div>
<div class="field">
<%= form.label :to %>
<%= form.text_field :to, required: true %>
</div>
<div class="field">
<%= form.label :title %>
<%= form.text_field :title, required: true, minlength: 6 %>
</div>
<div class="field">
<%= form.label :body %>
<%= form.text_area :body, required: true %>
</div>
<div class="actions">
<%= form.submit %>
</div>
<% end %>
ספריה בשם client_side_validations מתרגמת בצורה אוטומטית את ה Active Record Validations לוולידציות בצד לקוח באמצעות JavaScript ולפעמים כדאי להשתמש בה.
## וולידציה אינה אינדקס
רעיון שני שחשוב לזכור הוא שהמטרה של וולידציה היא להציג הודעת שגיאה יפה למשתמש כשאותו משתמש מנסה להקליד נתונים לא נכונים בטופס. וולידציה ב Active Record לא תמנע מהמערכת שלכם לשמור מידע שגוי או שלא עובר וולידציה ויש המון דרכים לשמור מידע ולדלג על הוולידציה.
אם חשוב לכם שמידע "לא נכון" לא ייכנס לבסיס הנתונים הדרך היחידה להבטיח את זה היא ליצור אינדקס או Constraint בבסיס הנתונים עצמו.
נשים לב שבסיסי נתונים לא יודעים לעשות כל מה שהשרת שלנו יודע לעשות, וברובם אין דרך קלה להגדיר לדוגמה אורך מינימלי למחרוזת בעמודה. זה אומר שבהחלט יהיו מצבים בהם תהיה לכם וולידציה אבל חלק מהמידע בבסיס הנתונים לא יעבור אותה. במצב כזה משתמש יצטרך לעדכן ערכים בטופס גם אם הוא לא שינה אותם רק בשביל לשמור את המידע.
בדוגמה שלנו, אני יכול ליצור מתוך Rails Console אוביקט הודעה תוך דילוג על הוולידציה:
m=Message.new(to: 'ynon')
m.save(validate: false)
באפליקציה עצמה אני אראה את ההודעה הזו ברשימת ההודעות ואוכל להיכנס לטופס עריכה כדי לעדכן אותה, אבל בתוך אותו טופס אני אהיה חייב לעדכן את השדות from, title ו body כדי שאוכל לשמור.
## היו זהירים כשאתם מוסיפים וולידציה חדשה למודל
אין בעיה להוסיף וולידציה למודל שיש בו כבר נתונים (למשל מגירסה קודמת של האפליקציה) אבל עליכם להיות זהירים ואף פעם לא להניח שכל הוולידציות עוברות על נתונים קיימים. כל הוספה של וולידציה צריכה להתחיל ממחשבה על טופס ועל הודעה שמשתמש יראה כשהוא ממלא את הטופס. אם מוסיפים וולידציה על נתונים קיימים יש לוודא שמשתמש יכול לפתוח את האוביקט במצב עריכה ולהוסיף את הערכים החסרים כדי לעבור וולידציה (אחרת אי אפשר יהיה לשמור או לעדכן אפילו שדות אחרים).
## אפשר להגביל וולידציה ל Context מסוים
המצב הזה של וולידציות חדשות הופך מבלבל במיוחד כשיש מספר דרכים לעדכן אוביקט במערכת. לדוגמה במערכת ההודעות שלנו אפשר לדמיין שלכל הודעה יהיה כפתור עם אייקון של כוכב במסך רשימת ההודעות שיסמן אותה בתור הודעה "חשובה".
עכשיו יש לנו 2 דרכים לעדכן כל הודעה:
1. טופס העדכון הרגיל של הודעה
2. לחיצה על כפתור "כוכב" שמעדכנת רק את שדה "important" בהודעה, שדה חדש שיצרנו בשביל הפיצ'ר.
אם באמת נלך לבנות את פיצ'ר הכוכבים נגלה שאנחנו לא יכולים לסמן בתור "חשוב" את ההודעה הלא וולידית שיצרנו בסעיף הקודם. ריילס ייכשל בטענה שהוא לא יכול לעדכן את האוביקט כי האוביקט נכשל בוולידציה, למרות שאנחנו עובדים מתוך ממשק אחר בו אי אפשר לשנות את השדות שלא עברו וולידציה.
אפשר לראות את זה אם נריץ את הקוד הבא מתוך הקונסול:
m = Message.first
m.important = true
m.save1 419
# טיפים לשימוש טוב יותר ב Active Record Validations
אחד הרעיונות היפים בריילס, ואחד הרעיונות שלקח לי הכי הרבה זמן להבין, הוא ה Active Record Validations. הסיפור בקצרה הוא שלפעמים יש לנו באפליקציה טפסים שמטרתם ליצור או לעדכן מידע בבסיס הנתונים. המידע בבסיס הנתונים בריילס מיוצג על ידי קלאס שנקרא Active Record, ולכן כשצריך לעשות וולידציה על המידע בטפסים אותם Active Records יהיו המקום האידאלי. בפוסט זה אראה בקצרה איך להשתמש ב Active Record Validations ביישום ולאחר מכן אמשיך למספר טיפים לשימוש נכון בהם.
## איך זה עובד
ב Rails כל טבלה בבסיס הנתונים מיוצגת על ידי מחלקה שיורשת מ Active Record, וכל אוביקט ממחלקה זו מייצג שורה בטבלה. בהגדרת המחלקה אנחנו לא צריכים להגדיר את העמודות (ריילס מזהה אותן אוטומטית). הקוד הבא עשוי להיות קוד שמגדיר את החיבור בין אפליקציית Rails לטבלת ההודעות:
class Message < ApplicationRecord
end
בואו נניח שכל הודעה כוללת את השדות הבאים:
1. שם השולח
2. שם הנמען
3. נושא ההודעה
4. גוף ההודעה
5. תאריך יצירת ההודעה
6. תאריך עדכון אחרון של ההודעה
קוד ריילס שיוצר את טבלת ההודעות נראה כך:
create_table "messages", force: :cascade do |t|
t.string "from"
t.string "to"
t.string "title"
t.text "body"
t.datetime "created_at", precision: 6, null: false
t.datetime "updated_at", precision: 6, null: false
end
כמו שהקוד כתוב אין וולידציות כלל על השדות בהודעה. אם נבנה טופס ליצירת הודעה חדשה, משתמש יוכל להזין כל ערך בטופס וגם להשאיר שדות ריקים. ה Active Record Validation נועד לתת את התשתית שמחברת בין וולידציה של השדות לבין הטופס שמשתמש ממלא, וזה עובד כך:
1. משתמש מזין מידע בטופס והמידע מגיע לשרת
2. השרת יוצר אוביקט Message וממלא אותו במידע שהמשתמש הקליד בטופס
3. השרת מפעיל Validation על האוביקט שנוצר. אם היו שגיאות, התוצאה של הוולידציה היא הוספת שדות מידע לאוביקט בצד השרת עם הודעות השגיאה המתאימות לפי השדות של האוביקט.
4. השרת יוצר HTML או JSON מאוביקט ה Message שנוצר (זה עם השגיאות) והודעות השגיאה המתאימות מוצגות בטופס או בתוך אוביקט ה JSON.
לריילס יש גם תשתית די טובה שמחברת בין טופס לבין השגיאות כך שבקוד ה ERB אנחנו צריכים בסך הכל לכתוב את השדות בטופס באופן הבא:
<%= form_with(model: message, local: true) do |form| %>
<% if message.errors.any? %>
<div id="error_explanation">
<h2><%= pluralize(message.errors.count, "error") %> prohibited this message from being saved:</h2>
<ul>
<% message.errors.full_messages.each do |message| %>
<li><%= message %></li>
<% end %>
</ul>
</div>
<% end %>
<div class="field">
<%= form.label :from %>
<%= form.text_field :from %>
</div>
<div class="field">
<%= form.label :to %>
<%= form.text_field :to %>
</div>
<div class="field">
<%= form.label :title %>
<%= form.text_field :title %>
</div>
<div class="field">
<%= form.label :body %>
<%= form.text_area :body %>
</div>
<div class="actions">
<%= form.submit %>
</div>
<% end %>
ובאופן אוטומטי החלק העליון יתמלא בהודעות השגיאה המתאימות שנלקחו ישירות מתוך ה Validation.
בשביל הדוגמה אני מוסיף בקוד של message.rb את התוכן הבא:
class Message < ApplicationRecord
validates :from, presence: true
validates :to, presence: true
validates :title, length: { minimum: 5 }
validates :body, presence: true
end
המשמעות של הוולידציה היא שבעת הגשת הטופס תתווסף Server Side Validation על המידע וההודעה תישמר רק אם כל השדות עומדים בתנאים שהגדרתי, כלומר:
1. קיים ערך לשדה from
2. קיים ערך לשדה to
3. קיים ערך לשדה title באורך מינימלי של 5 תווים
4. קיים ערך לשדה body
כל אחד מהשדות שלא מתאים לוולידציה יגרום להצגת הודעת שגיאה בראש הטופס ובנוסף ליצירת div חדש בתוך ה div עם הקלאס field שלו הקלאס field_with_errors, מה שיאפשר לצבוע את המקום הרלוונטי בטופס באדום.
ועכשיו שאנחנו מבינים את הסיפור הזה הנה כמה דברים שחשוב לזכור לגבי וולידציות:
## הוולידציות נבדקות בצד שרת ואינן מחליפות Client Side Validations