2015-02-15

מיקרו-שירותים: API Facade

בפוסט זה אני רוצה לדבר על דפוס עיצוב נפוץ למדי בעולם ה micro-services - ה API Gateway.
בדפוס העיצוב הזה נתקלתי מכמה מקורות, כאשר כולם מפנים בסוף לבלוג של נטפליקס - משם נראה שדפוס העיצוב התפרסם.


מקור: microservices.io


לפני שנתחיל - אני רוצה להצביע על אלמנט מבלבל במינוח של תבנית העיצוב:

Facade הוא הוא אובייקט המספק תמונה (View) פשוטה של רכיבים פנימיים מורכבים (״complex internals״ - מתוך ההגדרה של GoF) - למשל כמו ב Layered Architecture.
Gateway הוא אובייקט שמספק הכמסה לגישה למערכות או משאבים חיצוניים - למשל כמו רכיב הרשת Proxy שנקרא לפעמים גם Gateway.
Proxy הוא אובייקט שמספק אותו ממשק כמו אובייקט אחר, אך מספק ערך מוסף בגישה לאובייקט (למשל Laziness או Caching).

יש משהו לא מדויק ומבלבל בשם ״API Gateway״. כפי שנראה, דפוס העיצוב הוא בעצם שילוב של שלושת הדפוסים הנ״ל.

מכל השמות, דווקא ״API Facade״ נראה לי המדויק ביותר, וזה שמכווין בצורה הטובה ביותר לאופן בו כדאי לממש את דפוס העיצוב - ולכן אצמד אליו. נחזור לדיון על השם במהלך הפוסט.

להזכיר: דפוס עיצוב, ככלל, הוא מקור השראה יותר משהוא "אמת מוחלטת" שכדאי להיצמד אליה בלהבלהבלה.....


דפוס העיצוב


API Facade החל בשל צורך של נטפליקס לארגון ה APIs שלהם: המערכת המונוליטית הפכה לעוד ועוד שירותים - עוד שירותים שעל ה clients היה להכיר. הצורך בהיכרות עם כל השירותים יצר dependency ב deployment שהפך את משימת ה deployment לקשה יותר. למשל: פיצול שירות לשני שירותים חדשים דרש עדכון של כל ה clients ו deploy של גרסה חדשה - גרסה שמודעת לכך ש API X עכשיו שייך לשירות B ולא לשירות A (מה אכפת ל Clients בכלל מהחלוקה הפנימית לשירותים?!).

ה API Facade הוא רכיב שיושב בין ה Client לשירותים, ומסתיר את פרטיהם הלא מעניינים. הוא מציג ל Client סדרת API פשוטים, כאשר מאחורי כל אחד מהם יש flow = סדרת קריאות ל APIs של שירותים שונים.

באופן זה, שינויים ב APIs של השירותים (פיצול, איחוד, או שינויים קלים בחתימה) לא משפיעים על ה Client ולא דורשים את עדכונו.
תפקיד זה הוא Facade קלאסי.


עוד בעיה שצצה היא עניין ה Latency:
כאשר ה Client ביצע יותר ויותר קריאות (כי יש יותר ויותר שירותים) עלויות ה latency של הרשת גברו:

מקור: הבלוג של נטפליקס
Latency לאמזון יכול בקלות להגיע ל 100-200ms. במקום לשלם על latency של קריאה אחת - שילמו על latency של הרבה קריאות. חלק מהקריאות הן טוריות (4 בתרשים לעיל) - ואז ה Latency האפקטיבי הוא פי 4 מ latency של קריאה יחידה.

בעזרת הצבת API Facade שמקבל קריאה יחידה, ואז מבצע סדרה של קריאות בתוך הרשת הפנימית (שהן זולות בהרבה: פחות מ 10ms באמזון, ופחות מ 1ms ב Data Center רגיל) - ניתן לקצר באמת את ה Latency של הרשת שעל ה Client "לשלם":

מקור: הבלוג של נטפליקס

חשוב לממש את ה API Facade כך, שהוא לא יגרע מהמקביליות שהייתה קיימת קודם לכן ב Client - אחרת הוא יכול לגרום ליותר נזק מתועלת. לצורך כך ממשים את ה API Facade בעזרת שפה/כלים שקל לבצע בהם מקביליות וסנכרון שיהיו גם קלים לתחזוקה, וגם יעילים מבחינת ביצועים. נטפליקס בחרה למשימה ב Closure (תכנות פונקציונלי) או Groovy עם מודל ראקטיבי (RX observables). אחרים בחרו בשפת Go או סתם #C או ג'אווה.

תפקיד זה, של ניהול קישוריות בצורה יעילה, היא תפקיד קלאסי של Gateway.
וריאציה אחרת של תפקיד Gateway היא כאשר ה API Facade יודע לתרגם את פרוטוקול ה Client האחיד (נאמר HTTP) לפרוטוקולים שונים של השירותים השונים (נאמר SOAP, ODATA, ו RPC) - וכך לסייע בקישוריות.


עניין אחרון שבו ה API Facade מסייע הוא עניין של העשרת התקשורת. למשל: אתם רוצים שכל ה clients יבצעו SSO (קיצור של Single Sign-On, דוגמת SAML 2.0) או Audit על הגישה לשירותים שלכם. מה יותר נוח מלעשות זאת במקום אחד מרכזי (מאשר בכל אחד מהשירותים, ואז להתמודד עם פערי-גרסאות)?

כנקודת גישה מרכזית, ה API Facade יכול לרכז פעולות אלו ואחרות. זהו תפקיד קלאסי של Proxy.



סכנה!


בנקודה זו כדאי לעצור ולציין מכשלה שהולכת ומסתמנת בדפוס העיצוב של ה API Facade והיא הפיתוי להפוך את ה API Facade ל"כלבוייניק" של כל מה שקשור לקישוריות, או אבטחה, או סנכרון בין השירותים.

עצה חוזרת ונשנה היא להשאיר את ה API Facade רזה ומינימלי ככל האפשר - כדי שלא יסתבך.
מדוע?
משום שה API Facade הוא ריכוז של תלויות:
  • הוא תלוי בכל ה Services אותם הוא מסתיר
  • הוא תלוי בכל ה Clients אותם הוא משרת
את כל התלויות מהן ניסינו להיפטר - העמסנו עליו, מה שאומר שהוא הופך להיות ל "Single Point of Deployment". הרבה שינויים במערכת (שינוי API של שירות, או שינוי API של Client) - יחייבו לעשות גם לו Deploy.
אם עושים למישהו Deploy תכוף מאוד, כדאי מאוד שהוא יהיה פשוט, אמין, ולא "ייתפס באמצע פיתוח" שיעכב את ה deploy. מצד כזה בדיוק יכול להיות לרכיב שמטפל גם בתרגום פרוטוקולים (Gateway), וגם בסיפוק שירותי אבטחה (Proxy).

לכן, ה Best Practice הוא לבצע extraction של כל לוגיקה אפשרית מה API Facade לשירותי-עזר שיספקו שירותים שכאלה (תרגום, SSO, וכו'), בעוד ה API Facade הוא רק נקודת החיבור של שירותי העזר האלו ל API של ה Client.
התוצאה: ה API Facade נשאר "easily deployable" - הוא מתמחה רק בהחזקת תלויות רבות ובקריאה לרצפים (flows) של APIs של שירותים עסקיים, בעוד שירותי העזר שלו הם בעלי הלוגיקה, ולהם ניתן לבצע עדכון / deploy - רק ע"פ הצורך ובקצב שנוח לעשות כן.

כך בערך זה נראה:


איך תקשרו את זה לצוות שלכם? את העובדה שאתם רוצים שה API Facade יהיה רזה ומינימליסטי, ולא יכלול שום דבר אחר? שאתם לא רוצים "ליפול בפח" של יצירת רכיב מורכב שתלוי כמעט-בכולם, ובאופן זה הוא מערער את יציבות המערכת?

נכון! ע"י כך שתקראו לו "API Facade", ולא "API Gateway" או "API Proxy".
מכיוון ש "Facade", בהגדרה, הוא "שלד ועצמות", רזה ומינימליסטי, בעוד "Gateway" או "Proxy" - הם לא.


הנה עוד המלצה לאופן שיצמצם את התלויות שה-API Facade נושא עליו:
לבצע "partitioning" של ה API Facade לכך שיהיו כמה API Facades במערכת - אחד לכל Client.
נטפליקס גילו שהקמת צוות ייעודי לתחזוקת ה API Facade יצרה צוואר-בקבוק וחוסר תקשורת מול צוותי ה client - הלקוח העיקרי של התוצר. הם העבירו את האחריות לקוד ה API Facade לצוות ה Client - כאשר יש API Facade לכל Client שקיים. התוצאות היו חיוביות - וגם חברות אחרות אימצו גישה זו.
מאוחר יותר, נטפליקס איחדו את התשתית של כל ה API Facades לתשתית אחידה - עבור יעילות ואחידות גבוהות יותר. עדיין כל צוות אחראי להגדרת ה flows שלו (flow = קריאה נכנסת מה Client, שמתתרגמת לסדרת קריאות מול השירותים השונים).



סיכום


בתבנית ה API Gateway Facade נתקלתי כבר מספר פעמים. בקרוב, אנחנו ננסה גם ליישם אותה אצלנו ב GetTaxi.
התבנית נראית כמו חלק טבעי במחזור החיים של ארכיטקטורה: ארכיטקטורת Micro-Services הופכת לפופולרית, ואיתה צצות כמה בעיות חדשות. מה עושים? מתארים Best Practices, או דפוסים - שמציעים פתרונות לבעיות הללו, ומתקשרים אותם כ"דפוסי עיצוב".
עם הזמן, הפתרונות המוצלחים שביניהם יהפכו לחלק מהגדרת הארכיטקטורה של MSA, ויפנו את מקומם ל Best Practices חדשים...

API Facade הוא דפוס שעוזר להתמודד עם בעיה ספציפית שנוצרת בעקבות ריבוי השירותים של Micro-Services Architecture, והצורך של ה Client לתקשר עם כל "הסבך הזה" (יש כאלו שהחלו לצייר את השירותים כ"ענן שירותים"). API-Facade מקל על הבעיה, אך גם יוצר פיתוי מסוים ליצור "גוש מונוליטי של פונקציונליות, מלא תלויות" ממנו כדאי להיזהר...


שיהיה בהצלחה!


----


מקורות:





2015-02-07

ריילס: Active Record (סקירה כללית)

Active Record, או בקיצור ARֿ, היא שכבת ה ORM של ריילס.
סיפקתי הקדמה על AR בפוסט על ריילס (כולל למשל, חוקי הקשר בין שמות מחלקות לשמות טבלאות בבסיס הנתונים) ולא אחזור על מידע זה.

AR הוא מימוש של דפוס עיצוב בשם זהה, שהופיע לראשונה בספר של מרטין פאוולר.
כל מחלקת מודל של AR מתארת טבלה בבסיס הנתונים, כאשר מופע של המחלקה מתאר מצביע לשורה מסוימת בטבלה (Row).

אובייקט של מודל AR הוא לא מבנה נתונים פשוט - זוהי מחלקה עם מתודות לכל דבר. המתודות יכולות להיות helpers לעבודה עם נתונים (למשל: calculated fields או find_matching_x), או business logic של ממש - כל עוד הלוגיקה נוגעת לרשומה הבודדת בבסיס הנתונים או מה שהיא מייצגת. לוגיקה הנוגעת בכמה אובייקטים במערכת - תשב במקומות אחרים במערכת (ב Controller או ב Lib).

אין אכיפה ממשית שכללים אלו באמת נשמרים ע״י המפתחים, וזה עניין של אחריות אישית והיכרות עם הכללים.
דפוס ה AR משנה במעט את כללי ה MVC ה״קלאסי״ (כי המודל כולל עכשיו גם לוגיקה), והוא זוכה על כך גם לביקורת:

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

בניגוד לחלק מכלי ה ORM (קיצור של Object-Relational Mapping) האחרים, AR לא מנסה ״להחביא לגמרי״ את ה SQL. הגישה היא יותר גישה של ״בואו נהפוך את השימוש ב SQL ליותר DRY".
למשל, AR, תחסוך לנו את העבודה המעצבנת ב SQL של לציין שוב ושוב את שמות השדות הרלוונטיים (חשבו למשל על Join מורכב). מצד שני היא כן רואה בשימוש במידה מסוימת של SQL כהכרחי, ולכן מספקת מתודות כגון find_by_sql בהן מזינים שאילתת SQL.

לאחר שנתקלתי שוב ושוב בצורך ״לעקוף את ה ORM״ בעבר - הגישה הזו נראית לי מאוזנת ובריאה. בעוד ששימוש ב SQL הוא לא דבר מומלץ ב Hibernate (כי אז ה caches של Hibernate לא יהיו מעודכנים, למשל), בריילס הנחת העבודה היא ש SQL יהיה בשימוש מפעם לפעם.
מה המחיר ש AR שילמה על זה? כנראה בביצועים, ובמגוון היכולות שיש לכלי ORM ואינם מפותחים כ"כ ב AR.
מחיר סביר, לטעמי.

AR, כמו ריילס, הוא כלי שנכתב בחשיבה ראשונית על אפליקציות CRUD (״מילוי טפסים והפקת דוחות״) ופחות לאפליקציות שמטפלות בלוגיקה מורכבת, או זקוקות לביצועים גבוהים.
כמו כל כלי ORM אחר - הביצועים ש AR תשיג הם אולי טובים יותר בממוצע מהביצועים של קוד שנכתב ללא חשיבה כלל על ביצועים, אך הם לא יוכלו להתחרות במפתח שמכוון לביצועים גבוהים.







סכמה


מודל (Model, של MVC) של AR היא מחלקה שיורשת ממחלקת הבסיס ActiveRecord::Base.
AR יצפה ששם המחלקה יופיע בצורת יחיד, ושם טבלה המתאימה - בצורת רבים (ע״פ החוקים שתיארתי בפוסט הקודם).

אין צורך להגדיר במחלקת המודל את העמודות השונות של הטבלה: בזמן ריצה AR יתשאל את בסיס הנתונים, ימצא את העמודות, ותוסיף שדות ו getters/setters לגישה אליהם על מחלקת המודל.
מדוע? בכדי למנוע מצבים של חוסר עקביות בין בסיס-הנתונים לקוד, ולחסוך תחזוקה כפולה של השדות הקיימים על כל טבלה (עקרון ה DRY).

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

בתיקיית ה db בפרויקט ריילס נמצא כמה קבצים הקשורים לעניין:
  • קובץ ה schema.db הכולל תיאור של הסכמה השלמה.
  • תיקיית migrate - בה יש ״סקריפטים״ שמעדכנים את הסכמה בבסיס הנתונים.
  • קובץ ה seeds.rb.
באופן מעט לא-אינטואטיבי, אין לשנות את קובץ ה schema.rb. זהו קובץ שבעצמו הוא generated מתוך בסיס הנתונים, ונועד ליצירת סכמה מהירה ואמינה עבור התקנה חדשה של האפליקציה שלכם בבסיס נתונים חדש. הוא יכול להיות רפרנס טוב בכדי להבין את הסכמה בקלות.

ה״סקריפטים״ (מחלקות רובי) שבתיקיית ה migrate הן מה שמעדכן את הסכמה, והם מסודרים כך שיוכלו להעביר בסיס נתונים מ״מצב 5״ ל״מצב 6״ (שנוצר בעקבות שינויים במודל של האפליקציה), או לאחור מ ״מצב 6״ בחזרה ל״מצב 5״ (ככלי rollback).
את הסקריפטים האלו מריצים בעזרת פקודת rake db:migrate מה command line, בעוד שאת בניית הסכמה מחדש (על בסיס schema.rb) מפעילים בעזרת פקודת rake db:schema:load.

פקודות אלו נוגעות רק לסכמה. עבור הכנסת נתונים ראשוניים לבסיס הנתונים קיים הקובץ seeds.rb, שמופעל בפקודה rake db:seed.

פקודת rake db:setup, שנהוג להפעיל בהתקנת האפליקציה על מערכת חדשה, מפעילה בפועל גם db:create, גם db:schema:load ולבסוף את db:seed. ניתן לבחון את הסריפטים עצמם בגיטהאב.


בכדי לייצר migration חדש בד״כ משתמשים ב generator הבא:

$ bin/rails g migration <migration human-friendly name>

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

20150108083136_create_price_records.rb

כאשר ה״קישקוש״ בתחילתו הוא ה timestamp המתאר את זמן יצירת הקובץ.
הקובץ עצמו עשוי להראות כך:

class CreateProducts < ActiveRecord::Migration
  def change
    create_table :products do |t|
      t.string :name
      t.text :description
      t.decimal :price

      t.timestamps null: false
    end
  end
end

change היא מתודת העבודה העיקרית. בתוכה נוכל לבצע שינויים בבסיס הנתונים כגון יצירת טבלה (כמו במקרה שלנו), הוספת או הרדת עמודה, או אינדקס, ועוד מספר פעולות. AR יידע להשתמש בתוכן המתודה בכדי לבצע את השינוי, וגם להחזיר אותו בחזרה למצב הקודם במקרה ונרצה לעשות מתישהו rollback (תקלות ב production, מישהו?).
יש מצבים בהם לא ניתן להסיק אוטומטית את הדרך חזרה, למשל: שינוי טיפוס של עמודה, או החלטה שמעתה לא יתקבלו nulls - בגלל שאין תשובה אחת לשאלה "מה לעשות עם הערכים הנוכחיים?". במקרים כאלה יש להשתמש בשתי מתודות אחרות: up (השינוי הרצוי) ו down (פעולת ה rollback) - והטיפול בשאלות הלא-ברורות.

AR מגדיר סט של כמה טיפוסי-ביניים בהן ניתן להשתמש, למשל: string, text, float, decimal, time, וכו׳
על כל עמודה ניתן להגדיר עוד properties, למשל null:false - עבור עמודה שנרצה שבסיס הנתונים יגן שלא תקבל ערך null.
בעזרת Adapter מתאים לכל בסיס נתונים, AR מחליט לאילו טיפוסים של בסיס הנתונים למפות את טיפוסי-הביניים שלו בפועל. למשל, האם text ישמר כ varchar, nvarchar, או CLOB. זהו סוג של שירות שאמור לחסוך למפתח את ההבנה העמוקה בין הטיפוסים השונים של בסיס הנתונים, ועבור רוב התסריטים - הבחירות שלו הן בהחלט טובות.

עבור בסיסי נתונים מסוימים, ה Adapter גם יתמוך בטיפוסים נוספים. למשל, אם אתם עובדים עם PostgreSQL, תוכלו להשתמש בטיפוסי AR בשם json או hstore או properties מסוימים - שיתמפו ליכולות של PostgreSQL.

AR ייצר getters ו setter על המודל עבור על עמודה, דרכם נוכל לגשת לנתונים.
  • אם מדובר בשדה מסוג timestamp ה getter יחזיר אובייקט רובי מסוג Time.
  • אם מדובר במספר עשרוני אז ה getter יחזיר אובייקט רובי מסוג BigDecimal (בכדי לא לאבד נתונים - בסיס הנתונים יכול לשמור דיוק גבוה מהיכולת של רובי). אם מדובר במספר עשרוני ללא ספרות עשרוניות לאחר הנקודה - יוחזר אובייקט רובי מסוג Fixnum (כלומר: "פרמיטיב" ה integer).
  • אם מדובר בשדה בולאני, יווצר getter שסימן שאלה בסוף שמו - ע"פ הקונבנציה המקובלת ברובי.
  • וכו'....


עמודות עם טיפול מיוחד

אם לא הגדרתם בעצמכם את ה primary key (מה שמומלץ רק במצב של טבלה שכבר קיימת לפני המערכת שלכם), AR ייצר עבורכם עמודת id מסוג auto-increment (המימוש המדויק תלוי בבסיס הנתונים).

גם אם הגדרתם את ה primary key, (לדוגמה: עמודה בשם isbn) הגישה אליו בקוד הרובי תהייה דרך השם "id" ולא דרך השם המקורי של העמודה.

אם תפעילו את הפקודה t.timestamps (כמו בדוגמה למעלה), AR ייצר עבורכם 2 עמודות: created_at ו updated_at וינהל אותם עבור כל רשומה, כלומר: בכל עדכון של ערך של רשומה - יתעדכן גם ערך ה updated_at.




המודל


המודלים של AR הן מחלקות היורשות ממחלקת הבסיס ActiveRecord::Base, לדוגמה:

class City < ActiveRecord::Base
  belongs_to :country
  has_many :streets_to_cities
  has_many :streets, :through=> :streets_to_cities

  def self.search_for_country(text)
    City.search(
      # Some complex search logic
    )
  end

end

כפי שכבר הזכרנו - לא מציינים את השדות של המודל במחלקת המודל עצמה. בזמן עליה AR יתשאל את הסכמה של בסיס הנתונים ואז יוסיף את הפרמטרים ו getters/setters (כלומר: accessors) המתאימים בזמן ריצה. בעת הוספת עמודה חדשה בבסיס הנתונים - אין צורך בשינוי קוד במודל, ועדיין אפשר להשתמש בשדה החדש הזה - מה-Controller או ה View, למשל.


אז מה בכל מוגדר על המודל?

קשרים בין הטבלאות (associations)

ע"י פקודות הקשר:
  • belongs_to
  • has_one :through ,has_one
  • has_many :through ,has_many
  • has_and_belongs_to_many
השימוש הנכון בפקודות הקשר דורשות מעט אימון. קיים תיעוד מקיף (עם תרשימים), אך הייתי רוצה להסביר כמה נקודות מבלבלות במיוחד:

ניתן לומר שספר has_one "תוכן עניינים", או להיהפך: ש"תוכן עניינים" belongs_to ספר. לכאורה זה סימטרי.

בפועל השאלה היא היכן נכון שינוהל ה foreign key בין הטבלאות?
בשימוש ב belongs_to הוא ינוהל על המודל שהגדיר את הקשר ("המקומי"), ובשימוש ב has_one הוא יוגדר על המודל האחר ("הרחוק").
פעמים רבות רוצים ניווטיות לשני הכיוונים, ואז מגדירים גם את belongs_to וגם את has_one, בשני המודלים במקביל.

כאשר אנו רוצים להגדיר קשר one-to-many, אזי אנו מגדירים אותו בשני הצדדים: נאמר גם שספר has_many דפים וגם שדף belongs_to ספר. שימו לב ששומרים על צורת היחיד והרבים כך שיתאמו לשפה הטבעית (pages:, אבל book:) :

class Book < ActiveRecord::Base
  has_many :pages
end

class Page < ActiveRecord::Base
  belongs_to :book
end

בקשר של many-to_many מגדירים בשני הצדדים "<has_and_belongs_to_many :<other table".

הערה: AR מנהל את ה associations בעצמו. אם אתם רוצים שבסיס הנתונים יאכוף את תקינות ה foreign keys בנוסף ל AR, תוכלו להגדיר foreign keys על ה migration.


ניתן למצוא מידע על עוד אפשרויות בתיעוד של Associations


validation

ע"י שימוש בשורה של פקודות ואלידציה כמו validate_presence_of או validates_numericality_of


Overriding Accessors

למשל: אתם רוצים לעשות formatting לערך השדה המגיע מבסיס הנתונים (getter) הוא להוסיף validation לא שגרתי או המרה של נתונים (setter).


Transient Model Members

לפעמים אתם רוצים שיהיו במודל שדות זמניים שלא יישמרו לבסיס הנתונים.
מגדירים אותם על המודל כמחלקת רובי רגילה (לרוב - בעזרת attr_accessors).


Scopes

שאילתות ביניים אותן ניתן להרחיב, רעיון דומה ל Views בבסיס הנתונים. פרטים בהמשך.


מתודות עזר שונות

שעוזרות לגשת לנתונים (כמו search_for_country בדוגמה למעלה) או כאלה שמבצעות business logic הקשור ל scope של המודל (אך לא יותר מכך!)


שמירה

עדכון המודל לא גורם לשמירה אוטומטית של הנתונים בבסיס הנתונים. יש לקרוא במפורש למתודת save.
מתודה יוצאת דופן היא create (יצירת row חדש) - שתבצע את השמירה בעצמה.

יש שתי גרסאות של המתודות הנ"ל:
  • save - שתחזיר nil אם הייתה בעיה בעדכון הנתונים.
  • !save -שתזרוק Error אם הייתה בעיה בעדכון הנתונים. כנ"ל לגבי !create.


ניווט וחיפוש


הדרך הפשוטה והבסיסית לשליפת רשומה ב AR היא בעזרת ה id (קרי: primary key):

a_book = Book.find(63114)

דרך פופולרית נוספת היא לבצע דרך פונקציית ה where, שמקבלת תנאי WHERE של SQL:

a_book = Book.where("name = 'The White Horse' and book_type = 'Fiction' ").first

ע"י צמצום השאילתה לתנאי ה WHERE בלבד, AR מספקת כוח רב תוך כדי "דילוג" על חלקים טכניים בשאילתת SQL (כמו בחירת הטבלה והעמודות שיחזרו, תוך כדי הימנעות מדו-משמעות).

בכדי להתגונן בפני חולשות של SQL Injection וגם מטעויות SQL, קיימת צורת שימוש מוגנת של הפונקציה where:

a_book = Book.where(name: 'The White Horse', book_type: 'Fiction')

AR ידאג לבצע escaping מתאים על הפרמטרים בכדי להגן בפני התקפת SQL Injection.
בקצה השני יש את מתודת find_by_sql שדורשת כפרמטר שאליתת SQL שלמה - ומחזירה תוצאה דומה. אין לה הגנות בפני SQL Injection, כמובן.


התוצאה של פעולת ה where היא אובייקט בשם ActiveRecord::Relation המכיל את כל הרשומות התואמות.

מלבד פעולות שליפת שורות כגון: first, all או to_a, מאפשר אובייקט ה Relation מאפשר לשרשר עוד תנאים (נקראים finder methods) לשאילתה. למשל:

a_book = Book.where('name LIKE The%').order('publish_date DESC').limit(10)

עבור limit יש finder method אחות שעוזרת לעשות paging קל, בשם offset:

a_book = Book.where('name LIKE The%').order('publish_date DESC').limit(10).offset(100)

גרסה זו תחזיר את הספרים מספר 100-109 ששמם מתחיל ב "The", ממיונים ע"פ תאריך הפרסום בסדר יורד.

עוד finder method נפוצה היא joins המאפשרת להכתיב לשאילה לכלול טבלאות נוספות. בד"כ משתמשים בה כאשר כותבים לבד את ה SELECT:

an_order = Order.select("orders.customer_id, count(*) as total")
.group(:driver_id)
.joins(:customer)
.where(:'customer.type' => 'premium')


בניסיון להפוך את בניית השאילתה לרצף קריאות על אובייקטים - יש משהו מכשיל.

הנה שימוש בעייתי ב joins:
companies = Company.joins(:persons).where(:persons => { active: true } ).all

# ...

companies.each do |company|
  company.person.name
end
הסבר קצר:
נתונה טבלת חברות, עם association לטבלת עובדים. בטבלת העובדים יש שדה שמציין אם העובד עדיין נמצא בחברה (active).

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

מה קרה? השאילתה לא הביאה את שם העובדים (אין הרחבה של ה SELECT לכלול את employee.name).
AR מספק לנו "מציאות מדומיינת" של אובייקטים בזיכרון - ולכן אינו מונע מאיתנו לגשת ל person.name. הוא פשוט עושה SELECT נוסף בכל גישה ל name בכדי להביא את הנתון החסר. אבל... כאשר ניגשים ל person.name בלולאה - עשויות להיות הרבה מאוד שאילתות כאלה --> מה שכנראה יגרום לבעיית ביצועים משמעותית.

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

אפשר לעשות זאת בצורה מרכזית ע"י החלפת הפקודה joins בפקודה includes. פקודת includes פועלת כמו joins, אבל מוסיפה ל SELECT את כל ערכי שדות הטבלה שאילתה עשינו join לתוצאה - כך שיהיו זמינים לגישה ללא קריאה נוספת לבסיס הנתונים.

כפי שאתם מבינים - includes, בשימוש לא נכון, יכולה ליצור בעיית ביצועים בעצמה: אין טעם להשתמש בה כאשר עושים join לצורך פילטור הנתונים ואין כוונה להשתמש בשדות הטבלה האחרת. includes גם יכולה לגרום לבעיה אם הטבלה (או טבלאות) איתה אנו עושים join כוללת המון עמודות - הרבה נתונים שיעברו מבסיס הנתונים לזיכרון ולא יעשה בהם שימוש.


Scopes


תנאים המשורשרים של finder methods נוטים להתארך ולחזור על עצמם. AR מספק מנגנון של שימוש חוזר בהם בשם scopes.
class Order < ActiveRecord::Base
  scope :created_today, -> { where('created_at >= ?', Time.zone.now.beginning_of_day) }
end

todays_biggest_invoices = Order.created_today.order('amount DESC').limit(25)
invoices_to_handle = Order.created_today.where(status: open)
invoices_to_handle = Order.where(status: open).created_today # also valid



מחזור-החיים של מודל 


AR מספק שורה של events לאורך מחזור החיים בהם אנו יכולים להתערב: לבצע validations מורכבים, למנוע ברגע האחרון עדכון נתונים לבסיס הנתונים, או לבצע שינויים במודל on-the-fly:


בנוסף יש עוד 2 callback:
  • after_initialize - מיד לאחר יצירה של כל מופע של המודל
  • after_find - מיד לאחר כל פעולת find

ל callback אפשר להרשם בצורה דקלרטיבית, או בעזרת בלוק:
class CreditCard < ActiveRecord::Base
  belongs_to :person
  after_validation :do_after_validation
  
  before_validation(on: :create) do
    self.number = number.gsub(/[^0-9]/, '') if attribute_present?('number')
  end

  protected
  def do_after_validation 
    log.write('something')
  end
end
הפרמטר on: :create מאפשר לי לבצע את הקוד רק לפני validation של רשומה חדשה (פעולות create ולא update).
כמובן שצורת הבלוק מאפשרת לנו להוציא לוגיקה למקום משותף ולקרוא לה מכמה מודלים / מכמה שלבים שונים ב lifecycle.

כדי לעצור את ה Lifecycle ניתן לזרוק מתוך ה callback שגיאה מסוג ActiveRecord::Rollback. כל שגיאה אחרת תחלחל לשאר המערכת.
באירועי before_xxx, ניתן להחזיר את הערך false - בכדי להשיג תוצאה זהה.



Best practices


הגבלת הניווט בין מודלים ע"פ חוק דמטר (Law of Demeter)

החוק של דמטר הוא החוק של "the least knowledge": התוכנה תהיה ניתנת יותר לתחזוקה אם כל מחלקה תדע כמה שפחות על מחלקות אחרות. הכלל הפרקטי ב AR הוא כלל "הנקודה האחת": אל תנווט בין מודלים מעבר לנקודה אחת.

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

<%= @driver.fleet.contract.overtime_rate ... %>
...
foo(driver.fleet.contract.cancellation_fee, ....)

התוצאה תהיה שאנו רוצים לעשות שינוי באובייקט ה contract ופתאום מגלים עשרות מקומות בקוד שדורשים שינוי, מקומות שבכלל לא דמיינו שיהיו מודעים ל contract
שינוי עשרות שורות קוד היא בעיה לא-גדולה, אך הסיכוי לביצוע טעות באחד מעשרות שינויים - היא בעיה גדולה יותר. בעיה מספיק גדולה שתגרום לנו, פעמים רבות, לבצע שינוי פחות נכון - רק בכדי להימנע מהסיכון לרגרסיה בקוד. מכירים את המקרה?

ע"פ חוק הנקודה האחת, הקוד שלנו היה אמור להראות כך:

<%= @driver.fleet_contract_overtime_rate ... %>
...
foo(driver.fleet_cancellation_fee, ....)

כאשר למודל fleet ולמודל driver יש "חסמי תלות" - מן getters שמחזירים לנו תשובה ללא הצורך להמשיך לנווט ולהיות תלויים בתלויות הפנימיות שלהם עצמם.

כתיבת ה getters הללו היא כמובן לא "DRY", ולשם כך ריילס מספקת פקודה בשם delegate שתקצר לנו את העבודה:
class Driver < ActiveRecord::Base
  belongs_to :fleet
  delegate :overtime_rate,
           :cancellation_fee,
           :to => :Fleet,
           :prefix => true
end

class Fleet < ActiveRecord::Base
  has_many :drivers
  has_one  :contract
  delegate :overtime_rate,
           :cancellation_fee,
           :to => :Contract
end

class Contract < ActiveRecord::Base
  belongs_to :fleet
end
משמעות השימוש ב delegate בדומה לעיל היא כזו:
  • אם מבקשים מ driver את דמי הביטול - העבר את הבקשה ל Fleet.
  • אם מבקשים מה Fleet את דמי הביטול - העבר את הבקשה ל Contract.
כאשר ברמת ה Driver אנו מוסיפים את ה prefix עם שם ה delegate בגישה לשדות - עבור הקריאות / בגלל הקונבנציה.

בעתיד, כאשר נרצה לעשות שינוי בקוד ולהביא את דמי הביטול לא מתוך חוזה, אלא מתוך חישוב אחד - נוכל לעשות שינוי רק במחלקה Fleet ע"י כתיבת getter בשם מתאים - והסרת ה delegete.
אם דמי הביטול יהיו דינאמיים לכל נהג - נוכל פשוט להוסיף getter בשם fleet_cancellation_fee על מודל הנהג - ולהסיר את ה delegation. שינוי במקום אחד וללא פחדים.

העיקרון הוא אותו עיקרון מאחורי השימוש ב getter ו setter - אבל הוא אפילו יותר שימושי: שינויים בקשרים במודל הם יותר נפוצים (ויותר כואבים לשינוי?) מאשר התסריט של הפיכת member במחלקה לערך מחושב - התסריט עליו getters/setter מגנים.

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

כאשר נאלצים לעשות שינויים "עקומים" בקוד אנו מאשימים את אנשי ה product - שלא מסכימים "לשאת במחיר השינוי הנכון": שבוע עבודה וסכנה מסוימת לרגרסיות. "למה הם לא מוכנים להשקיע באיכות???" - אנחנו שואלים.

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



מקוצר זמן, אתאר את שני ה best-practices הבאים בקצרה בלבד:


ביצוע כל פעולות ה find מתוך המודל (ולא חס-ושלום מתוך ה View)

אפשר, ואולי נוח לבצע פעולות find על המודל מתוך ה View, כמו ב PHP או ASP של שנות ה-90!   ;-)
יש בכך 2 בעיות:
  • סבירות גבוהה לשכפול קוד - כי כמה Views זקוקים לאותו חיפוש.
  • קשה להרכיב תמונה ברורה של הגישות לבסיס הנתונים. כאשר רוצים לעשות שינויים במודל - לא ברור אילו השפעות יהיו לו, כי הקוד הרלוונטי "מפוזר" ברחבי המערכת וקשה למצוא את כולו. 
התוצאה - נטיה להעדיף שינויים פחות מסוכנים, אך גם פחות טובים. "אולי נכתוב trigger בבסיס הנתונים - וגמרנו?"

נכון, אפשר להעביר את פעולות ה find ל controller ולקבל 90% מהתמורה. לוגיקת הגישה לבסיס הנתונים עכשיו מפוזרת על 2 מחלקות בלבד. אבל למה לא לחסוך את הצעד הקטן הנוסף?? פשוט שימו את כל לוגיקת הגישה לבסיס הנתונים במודל - וחסכו בלבולים וחוסרי הבנות.



שחרור לוגיקה שאיננה חלק מהמודל למחלקות משנה

ככל שמחלקת המודל תהיה גדולה יותר - יהיה קשה יותר לעקוב אחרי מה שהיא עושה. לא נדיר למצוא פעולות parsing בתוך המודל, למשל: to_json  ו parse_json => סוג של מומחיות משנה של המודל.

למען הסדר הטוב, הוציאו קוד שאינו קשור ישירות למודל למחלקות-משנה (או module-משנה) של המודל. למשל:
  • לוגיקת parsing / serialization. יתרון נוסף - תוכלו לבדוק (בדיקות-יחידה) את הקוד הזה בקלות.
  • finders - אם יש הרבה.
הכל במידה, כמובן. אם יש מתודת parsing אחת באורך 4 שורות - זה יהיה כנראה מיותר להוציא אותה החוצה.



סיכום


טוב, סקרנו את ActiveRecord - בצורה שתאפשר לקבל תמונה טובה במה מדובר.
בפוסט הצלחתי לכסות רק חלקים מסך היכולות של AR: לא דיברנו על transactions, לא נכנסו לנבכי אפשרויות ה validation, ודילגנו על הרבה אפשרויות ותכונות הקיימות ב AR. AR הוא ותיק, ועשיר באפשרויות.

הערות יתקבלו בברכה.



שיהיה בהצלחה!




-----

לינקים מעניינים:

http://blog.rubybestpractices.com/posts/gregory/055-issue-23-solid-design.html

Bypassing ActiveRecord for better performance

דרכים לעדכון מודל של AR