ריילס: routing

נדבך חשוב בריילס הוא ה routing, המיפוי איזה Action (=מתודה) של איזה Controller תופעל ל URL נתון.
הרכיב שעושה את ה routing נקרא ActionDispatcher.

ה routing מתבצע באפליקציה בקובץ בשם config/routes.rb בעזרת סט פקודות מיוחדות (בעצם: DSL) שמגדיר את ה routes. סט הפקודות הזמין הוא עשיר ומגוון למדי, ולרוע המזל – אינו מתועד בצורה נוחה ללמידה. לא כ\"כ באנגלית – ובוודאי שלא בעברית.

התחביר של ה routes הוא סוג של תחביר שקל לנחש את משמעותו, פחות או יותר, בעת קריאה – אך לא-קל לדייק בו בכתיבה או לתפוס את כל הפינות האפשריות. לא מחזה נדיר בריילס.

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

כפי שנראה ה routing של ריילס מבוסס עמוקות על עקרונות ה REST – ניתן לקרוא בקצרה על עקרונות ה REST בפוסט הזה על REST או בפוסט על HTTP.

    לפני ריילס 3, הגדרת ה routes הייתה מסורבלת למדי. אני מתעלם מתחביר זה לחלוטין, ומתמקד במה שזמין בריילס 3 ו 4 (או ליתר דיוק: מתייחס למצב בריילס 4.2).

    האנטומיה של route

    הנה דוגמה ל route טיפוסי:

    1. HTTP verb / method
    2. pattern של URL יחסי, המכיל בתוכו:
    3. segment key (אחד או יותר) – המוגדר כ \"symbol\" ב path.
      segment key ממפה ארגומנטים שמועברים ב URL (או כ Query String[א]).
    4. יעד המיפוי, בפורמט: \"controller#action\". שם ה controller מופיע ללא המילה Controller ובאותיות קטנות (בכדי לקצר בכתיבה). Action הוא שם המתודה ב controller שמטפלת באירוע.
    5. רשימת אופציות ל routing. במקרה שלנו יש אופציה אחת בשם \"as:\" עם ערך של \"purchase\" (הסבר על אופציה זו – בהמשך).

    routes יכולים להיות מוגדרים בצורות שונות, ואף מורכבות יותר – על מבנים אלו נדבר בהמשך.

    בריילס 3, היה מקובל להגדיר routes פשוטים בעזרת פקודת match, למשל:

    match \'products/:id\' => \'products#show\', via: :get

    via הוא פרמטר שמגביל את ההתאמה ל HTTP verb/method מסוים (אפשר גם לשלוח רשימה – במערך), והוא היה אופציונלי עד גרסה 4 של ריילס. בגרסה 4 זו הפכה לחובה (RuntimeError ייזרק אם לא הוגדרה http method, אפשר להשתמש גם ב any:, אם כי לא מומלץ).

    הדרישה להגבלת ה http method נולדה משיקולי אבטחה ואמינות של המוצר. מומלץ תמיד להגדיר HTTP verb יחיד ל route, ולצורך כך נוספו הקיצורים get, post, put וכו\' – שהם כתיבה מקוצרת ל <match… via:<method.
    מעתה והלאה נעבוד רק איתם, אך כדאי לזכור שבמקור הם קיצור תחבירי ל match ושאת הפרטים על האופציות השונות הזמינות ל route – יש עדיין לחפש בתיעוד של match.

    הצורות הבאות, הן צורות מקבילות לחלוטין ל route שהגדרנו למעלה, שקצת פחות מקובלות היום (ישנות, ויותר ארוכות):

    match \'products/:id\', to: \'products#show\', via: :get
    match \'products/:id\', controller: \'products\', action: \'show\', via: :get

    אני מזכיר אותן, כי ייתכן שתתקלו בפרמטרים של to: ו controller: ב routes – ושתבינו את משמעותם / מקורם.

    בקיצור, היום נכתוב את ה route הזה באופן הבא:

    get \'products/:id\' => \'products#show\'

    משמעות ההגדרה הזו היא:
    אם יש קריאת GET עם URL המתאים לדפוס \"<products/<x", קרא ל controller בשם ProductsController (שנמצא בתיקיה app/controllers/) ולמתודה בשם show, ושלח כארגומנט את המחרוזת x כערך של הפרמטר \"id:\".
    את הערך ניתן לקרוא בתוך ה controller בעזרת המתודה params, למשל:

    params[:id] # string \"x\"

    כיצד זה עובד?

    הנה דוגמת קוד מינימלית לשימוש ב routing, controller, ו view:

    • בקובץ ה routes.rb, הגדרנו route בסיסי – שאתם אמורים כבר להבין
    • ה Controller (הקטן ביותר האפשרי, בערך) מכיל מתודה (= action) בשם show שאליה ה route שהגדרנו יפנה. היא מחפשת במודל את המוצר ושומרת אותו כ product@.
    • כשה Controller יוצר את ה View, ריילס באופן \"פלאי\" (הסבר) מעתיק את ה instance variables, על ערכיהם, מה Controller (כדוגמת product@) ל View. משם, ניתן לגשת לשדות השונים בתוך ה product.
    • שימו לב לפקודה בשם link_to אותה תראו הרבה ב views של ריילס. היא מייצרת עבורנו link עם הכותרת שהגדרנו (\"Show Details\") לפעולה מסוימת של ה Controller. כיצד זה עובד? – נסביר בהמשך.

    אופציות מתקדמות יותר להגדרת routes

    Segment Keys אופציונליים
    ממש כמו optional parameters בפונקציה, יש גם Segment Keys אופציונליים ב route. למשל:

    get \'products/:id(/:facet)\' => \'products#show\'

    יתאים ל 2 צורות של url, למשל:

      במקרה הראשון יהיה ערך רק לפרמטר id: (הערך = \'441\'), ו facet: יהיה nil.

      במקרה השני facet: יהיה שווה \'specifications\'

      שימו לב לצורה הנפוצה הבאה:

      get \'products/:id(.:format)\' => \'products#show\'

      המשמעות שלה היא matching ל url מהנוסח הבא:

      כאשר הפרמטר format: מקבל את הערך \'json\'.
      הנקודה שליד שם הפרמטר מתארת את חלק מה Path.

      ספציפית לגבי format:, זהו פרמטר עם התנהגות מיוחדת: פקודת respond_to שבשימוש בתוך ה controllers בודקת את הערך שלו, ולפיו מחליטה כיצד לפעול (האם להחזיר HTML או json – בד\"כ).

      Redirect
      ניתן להגדיר route שיבצע redirect ל URL אחר ברשת. למשל:

      get \'products/:id\', to: redirect(\'v2/products/:id\')

      המילה to: מגיעה מתוך התחביר הישן של match שהזכרנו למעלה.
      הערך של to: הוא Rack Endpoint שיכול להיות קוד inline (בשימוש בפונקציות lambda או proc) או שם של אפליקציית Rack אחרת (שיושבת בתיקייה app/metal/), או סתם URL אחר (כמו במקרה שלנו).

      Constraints
      אופציה זו מאפשרת לנו להציג תנאים נוספים (בדמות RegEx) על ה matching של ה route. למשל:

      get \'products/:id\' => \'products#show\', constraints: {:id => /\\d+/}

      תתאים רק כאשר ה id: הוא מספר (ולא כולל אותיות, למשל).
      בגלל ש constraint הוא כ\"כ נפוץ על שדה שנקרא id:, נוצר לכך קיצור מיוחד בריילס. את הדוגמה הנ\"ל ניתן לכתוב גם כך:

      get \'products/:id\' => \'products#show\', id: /\\d+/

      ניתן להשתמש ב constraints על מנת לעשות בדיקת קלט למשתמש – אך זה לא מומלץ!
      הכלי הוא יחסית מוגבל, וקובץ ה routing הוא לא בהכרח המקום הנכון לעשות זאת.
      הכלל המנחה הוא להוסיף constraint על route, אם יש לכם route אחר שיתאים במקום. למשל:

      get \'products/:id\' => \'v2/products#show\', id: /\\d+/
      get \'products/:id\' => \'v1/products#show\'

      כלומר: בעבר קיבלתי מוצרים עם id: מכל סוג, ומתישהו (v2) הגבלתי אותם רק למספרים.
      אם מופיע בקשה עם id: שאינו מספר – שלח אותה לטיפול ב controller הישן.

      ל constraints יש גמישות נוספת מעבר לבדיקת ערכי segment keys, ניתן לקרוא עוד בנושא בפוסט הבא.

      Wildcard Segment
      אם מבנה ה URL מכיל מידע, ויכול להופיע בווריאציות שונות, ניתן להשתמש ב wildcard segment. כלומר, ה route:

      get \'products/*other\' => \'products#show\'

      יתאים ל URL כמו:

      וישלח ל controller משתנה בשם other: שערכו הוא \'my/id/366/type/special\'.

      ניתן להרכיב Wildcard Segments, עם Segment Keys רגילים. אם אתם זקוקים לעוד מידע בנושא חפשו את המונחים Wildcard Segment או Route Globbing.



      רשימת יכולות שכיסינו כאן כנראה מכסה את רוב השימושים הנפוצים.
      ניתן לקרוא בתיעוד הרשמי, Rails Routing from the Outside In, על אפשרויות נוספות.



      Named Routes

      מנגנון ה Named Routes בא לפשט (אפילו יותר) את העבודה עם routes.

      כאשר נותנים ל route את השם \"abc\", ייווצרו שתי מתודות הזמינות לשימוש ב Controllers וה Views: אחת בשם abc_url, והשנייה abc_path.

      • המתודה abc_url היא תייצר url מלא שיתמפה ל route שהגדרנו.
      • המתודה abc_path תייצר את את חלק ה path של ה url, בלי protocol/host/port.

      מנגנון ה Routing של ריילס בעצם משמש לשני כיוונים: גם לפענוח URL והתאמתו ל route (ומשם ל controller#action), וגם לצורך generation של URL (או path) שיוביל ל route שהוגדר.

      הנה דוגמה:

      get \'products/:id\' => \'products#show\', as: :show_product

      תיצור את 2 המתודות show_product_url ו show_product_path על אובייקט ה app שזמין ל controllers וה views.

      עכשיו אפשר ליצור קיצור ל path הזה מתוך אחד ה views בעזרת פקודת link_to. הפקודה, בגרסתה הארוכה, מקבלת hash כפרמטר:

      link_to \"הצג מוצר\",
        controller: \"products\",
        action: \"show\",
        id: 12
      אך בעקבות השימוש ב as:, יש לנו דרך מקוצרת להפעיל אותה:

      link_to \"הצג מוצר\", show_product_url(id: 12)

      יש לנו את מתודת העזר show_product_url שחוסכת מאתנו להגדיר Controller ו Action בנפרד. זה גם יותר קצר, וגם קוד שקל יותר לתחזוקה. (למשל: במידה ומשנים את שם ה Controller)

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

      אם הערך שאתם רוצים לספק כפרמטר לפונקציית ה abc_url הוא id: – אז אתם יכולים לשלוח את הערך וזהו, בלי hash. למשל:

      link_to \"הצג מוצר\", show_product_url(12)

      בד\"כ זה לא יהיה ערך עצמו אלא משתנה. למשל product.id, כאשר product הוא המודל שהכנתם ב controller. במקרה כזה ניתן לקצר אפילו יותר ולשלוח פשוט אובייקט:
      link_to \"הצג מוצר\", show_product_url(product)
      וריילס יידע לחפש אם יש עליו שדה בשם id: ולהשתמש בו.
      בעצם, אתם יכולים להשתמש בגישה זו לא רק לשדה בשם id, אלא לכל שם של שדה או רצף של שדות.
      עבור debugging, ניתן להריץ את הפונקציות xxx_url/xxx_path אובייקט ה app (מבלי להריץ את ה view). למשל:
      app.show_product_path(12) # \'/products/12\'

      שאלה של סגנון
      מה ההבדל בין הפונקציה show_product_url לפונקציה show_product_path? מתי יש להשתמש בכל אחת מהן?

      זה בעיקר עניין של סגנון:

      • show_product_url מייצרת Fully Qualified URL (כלומר: URL מלא ועצמאי), כפי שנדרש בתקן ה HTTP בפעולות redirect.
      • show_product_path, מייצרת URL רלטיבי שהדפדפן ידע להפוך אותו למלא בעת הצורך. ה URL קצר יותר ולכן נוח יותר לעבוד עם קבצי ה html שנוצרו. 
      אז מה אתם מעדיפים? להיות דקדקנים (url, לעבוד ע\"פ התקן) או לא לכתוב קצר (path, הדרך של ריילס)? עניין שלכם.
      אני בד\"כ מעדיף להיות דקדקן, אבל גם לפני ריילס עבדתי עם URLs יחסיים – וכך נראה לי שאמשיך.

      בכל מקרה, כדאי להכיר של helper functions יש עלות של ביצועים. בזמן ריצה הן מחפשות בטבלת ה routing מה שלוקח זמן. קריאה ל link_to עם controller ו action – היא אפילו יותר יקרה.

      Scoping

      scope הוא מנגנון שעוזר לארגן את ה routes בקובץ ה routes.rb, ולחסוך כמה שורות קוד על הדרך. יש לו הרבה מאוד וריאציות של קיצור – אציג כמה מהעיקריות שבהן.

        # original
      get \'drivers/new\' => \'drivers#new\', as: :driver_new
      get \'drivers/edit/:id\' => \'drivers#edit\', as: :driver_edit
      post \'drivers/reassign/:id\' => \'drivers#reassign\', as: :driver_reassign

      # scope - DRY a bit with controller (1)
      # alternatives: \"controller :drivers do ...\", \"scope :driver do\"
      scope controller: :drivers do
      get \'drivers/new\' => :new, as: :driver_new
      get \'drivers/edit/:id\' => :edit, as: :driver_edit
      post \'drivers/reassign/:id\' => :reassign, as: :driver_reassign
      end

      # scope - DRY a bit more with path (2)
      scope path: \'/drivers\', controller: :drivers do
      get \'new\' => :new, as: :driver_new
      get \'edit/:id\' => :edit, as: :driver_edit
      post \'reassign/:id\' => :reassign, as: :driver_reassign
      end

      # scope - DRY a bit further with default params (3)
      scope \'/drivers\', controller: :drivers, as: \'driver\' do
      get \'new\' => :new, as: \'new\'
      get \'edit/:id\' => :edit, as: \'edit\'
      post \'reassign/:id\' => :reassign, as: \'reassign\'
      end

      במקור היו לנו 3 routes שהיינו צריכים להקליד לכל אחד כמה וכמה תווים…

      1. הגדרנו, בעזרת הפקודה scope, לכולם controller אחיד, וכך קיצרנו את הגדרות ה controller#action לשם ה action בלבד. יש עוד 2 דרכים להגיע לתחביר המקוצר הזה (בהערה, השני יכול לבלבל).
      2. הגדרנו path, שמקצר את חלק ה relative URL ב route.
      3. הגדרנו as, שמהווה prefix ל as: של ה routes הספציפיים, והצבנו את ה path כארגומנט הראשון לפונקציה scope, מה שמאפשר לנו לוותר את המפתח path:.
      4. אפשר לוותר על כל המופעים של אותיות a ו e – וריילס ישלים אותם בעצמו על סמך מילון מוכן מראש. סתאאםם.
      אם אתם רואים את התוצאה הסופית וחושבים שניתן היה לקצר אותה יותר – אתם צודקים.
      עוד פרמטר פופולרי של scope הוא module: :abc שמניח ש:
      1. ה Controller שייך ל Module (שפת רובי) בשם Abc. כלומר Abc::SomeController.
      2. => ה Controller נמצא בתת תיקיה בשם abc/
      ישנה פונקציית קיצור בשם namespace שפועלת כך:
        namespace :drivers do
      # routes
      end

      # syntactic sugar of / equivalent to writing:
      scope path: \'/drivers\', module: :drivers, as: \'driver\' do
      # routes
      end

      הקיצור האולטימטיבי ליצירת routes

      אם אתם מכירים את ריילס, קשה לדמיין שכאן יגמרו הקיצורים. הקיצור \"האולטימטיבי\" להגדרת routes היא פקודה בשם resources שדחיתי את העיסוק בה עד רגע זה.

      כחלק מההתאמה של ריילס למודל ה REST, הוגדר ל Controller סט ה Actions הגנרי הבא:

      • index – הצגת רשימה של אובייקטים
      • create – יצירת אובייקט חדש (מתוך פעולת POST)
      • new – החזרת template ליצירת אובייקט חדש (ללא יצירת האובייקט בפועל). בד\"כ מדובר בהחזרת טופס וובי, \"יצירת אובייקט חדש\", למשתמש.
      • show – הצגת הפרטים של אובייקט מסוים
      • update – עדכון אובייקט מסוים
      • edit – החזרת template לעדכון אובייקט חדש. בד\"כ מדובר בהחזרת טופס וובי \"עדכון\" אובייקט שבעקבותיו תגיע פעולת update.
      • destroy – מחיקת אובייקט מסוים

      הפקודה הבאה, תמפה סדרה של routes עבור שבע הפעולות:

      resources :products

      # syntactic sugar of / equivalent to writing:
      get \'products(.:format)\' => \'products#index\', as: :products # e.g. products_url()
      post \'products(.:format)\' => \'products#create\',
      get \'products/new(.:format)\' => \'products#new\', as: :new_product # e.g. new_product_url(12)
      get \'products/:id/edit(.:format)\' => \'products#edit\', as: :edit_product # e.g. edit_product_url(12)
      get \'products/:id(.:format)\' => \'products#show\', as: :product # e.g. product_url(12)
      patch \'products/:id(.:format)\' => \'products#update\',
      delete \'products/:id(.:format)\' => \'products#delete\'

      למיטב הבנתי, ה named routes נוצר רק לפעולות ה get, מכיוון שרק לפעולות אלו ניתן לעשות re-direct בפדפדפן.

      שימו לב שאם יש לנו route כמו הבא, שמופיע אחרי שורת ה resources:

      get \'products/poll\' => \'products#poll\'

      לעולם לא יגיעו אליו. ריילס תתאים אותו ל products/:id שנוצר בעקבות פקודת ה resources. הדרך היחידה שיוכלו להגיע אליו הוא אם נציב את ה route הזה לפני פקודת ה resources. ההתאמה ל routes נעשית בסדר שבו הם מופיעים בקובץ.

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

      resources :products, only: [:index, :show, :edit]

      או השתמשו בפקודה ההופכית: except:.

      קינון routes

      קינון של routes מתבצע לרוב בין שני אובייקטים (REST resources) שיש בניהם קשר של שייכות. דוגמה שחוקה: כתבה, והערות לכתבה. מהות הקשר שמומלץ לנהל עבורו routes מקוננים הוא קשר \"חזק\" של הרכבה (composition), כאשר לאובייקט התלוי (למשל הערה) אין משמעות ללא אובייקט האב. למשל: כאשר מוחקים כתבה, טבעי שמוחקים את ההערות שלה. לא טבעי יהיה, בד\"כ, למחוק את אובייקט המחבר – וזו אינדיקציה טובה שלא כדאי לקנן את המחבר תחת כתבה, אפילו שיש ביניהם קשר.

      הנה דוגמה לקינון:

      resources :articles do
      resources :comments
      end

      # generates 7 routes for articles + routes such as:
      get \'articles/:article_id/comments\' => \'comments#show\'
      get \'articles/:article_id/comments/new\' => \'comments#new\'
      put \'articles/:article_id/comments/:id\' => \'comments#update\'

      … את הרשימה המלאה של ה routes הנוספים שנוצרים ניתן להסיק או לבדוק את הרשימה בתיעוד הרשמי.
      כמובן שה routes של article יפנו ל ArticlesController וה routes המקוננים יפנו ל CommentsController. הקינון, חוץ מזה שהוא נוח מבחינת מידול ה REST, מאפשר ל CommentsController לקבל גם את ה article_id:.

      עוד כלל הוא לעולם לא לעשות nesting עמוק (כלומר: 2

      הנה עוד 2 כלים נפוצים, member ו collection, המאפשרים להוסיף פעולות חדשות:

      resources :articles do
      member do
      get :preview
      end
      collection do
      get :search
      end
      end

      member מוסיף תחת ה resource עוד פעולה, אולי כזו שלא מוגדרת ב default, למשל preview. הפעולה היא כזו שמוגדרת על אובייקט יחיד – ולכן יש צורך במזהה. למשל, במקרה הנ\"ל ייווצר ה route הבא:

      /articles/:id/:preview

      collection הוא דומה מאוד, אבל מגדיר פעולה על סט ה resources, כזו שלא צריכה מזהה. בדוגמה הנ\"ל:

      /articles/search

      בדיקת ה routes בפועל

      כדי לבדוק שה routes נכונים, יש בריילס 2 כלים שימושיים:

      • מ command line, בעזרת הפקודה rake routes. ניתן לבדוק Controller ממוקד ע\"י שימוש בפרמטר, למשל: rake routes CONTROLLER=articles.
      • כאשר השרת רץ (rails s), ניתן לראות את רשימת ה routes תחת ה path הבא: rails/info/routes/

      סיכום

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

      כפי שאתם רואים אני עוד שקוע ריילס – ויש עוד כברת דרך….

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

      —–

      לינקים רלוונטיים

      פיצול ה routes לקבצים שונים

      —–

      [א] ה route:

      get \'some_path/:a/:b/:c\' => …
      יקבל ערכים זהים עבור 2 ה urls הבאים:

      /some_path/xx/yy/zz
      /some_path/xx?a=yy&b=zz

      מיקרו-שירותים: 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 מקל על הבעיה, אך גם יוצר פיתוי מסוים ליצור "גוש מונוליטי של פונקציונליות, מלא תלויות" ממנו כדאי להיזהר…

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

      —-

      מקורות:

      ריילס: 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

      ניתן לייצר את הקובץ ידנית, אך חשוב לשמור על שם קובץ עם 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 הוא כלל \"הנקודה האחת\": אל תנווט בין מודלים מעבר לנקודה אחת.

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

      foo(driver.fleet.contract.cancellation_fee, ….)

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

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

      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

      רובי: RVM

      RVM הוא כלי בסיסי ונפוץ מאוד ברובי המאפשר לנהל כמה גרסאות רובי שונות על אותה המערכת.

      אם תנסו לפתח רובי על מערכת ההפעלה \"חלונות\" – RVM עשוי להיות האכזבה הראשונה שלכם. כ\"כ הרבה מדריכים מניחים שאתם משתמשים ב RVM – אבל RVM לא רץ על חלונות (רק יוניקס / לינוקס / מק). החלופות הקיימות לחלונות (למשל pik) – הן לא ממש טובות.

      RVM הוא קיצור של \"Ruby enVironment Manager\" (כפי שמוסבר באתר), אם כי באותו האתר, בכותרת בגדול, מצוין דווקא הקיצור הישן (\"Ruby Version Manager\"). צחוקים.

      RVM מאפשר:

      • להתקין גרסאות רובי שונות (נקראות Rubies) על אותה מערכת ההפעלה, ללא התנגשות (למשל 1.9.3, 2.1.0, ו 2.1.5).
      • להחליף בכל רגע את גרסת הרובי \"הנוכחית\" בפקודת shell פשוטה.
      • להצמיד גרסת רובי מסוימת לתיקיה מסוימת במערכת הקבצים (כלומר: לפרויקט רובי שנמצא בתיקיה הזו).
      • לנהל רשימה נפרדת של Gems (נקרא gemset) לכל גרסת רובי או לכל פרויקט.
      מה מיוחד ברובי, שכלי כמו RVM הופך ל\"הכרחי\"? מדוע בג\'אווה, למשל, אין כלי דומה?

      • RVM מפשט את ההתקנה של רובי. בג\'אווה זה היה פשוט, ואין קושי להתקין כמה גרסאות זו לצד זו.
      • אי-היכולת לנהל רשימה נפרדת של Gems לכל פרויקט היא סוג של \"חור\" במנגנון ה Gem המקורי. RVM מציג פתרון לבעיה זו אם כי גם Bundler, כלי נפוץ מאוד אחר – גם הוא מציע פתרון לאותה הבעיה.
      • התאימות לאחור של המפרשנים של רובי (יהיו אלו Rubinius, YARV, או JRuby) היא פחות טובה מהמקובל בג\'אווה. אם פיתחתם אפליקציה על רובי 2.0.3, כנראה שלא תרצו להריץ אותה על מפרשן של גרסה 2.0.5, ובטח לא על מפרשן של 2.2.0.
        יש פה גם עניין תרבותי, אני חושב: הסבירות שמפתחי ג\'אווה יבחרו לעבוד על JDK 1.7 עם patch level מסוים – כי פרויקט אחר שלהם על אותה הגרסה (למרות שיש כבר ג\'אווה 8), היא גבוהה יותר מהסיכוי שמפתחי רובי יעשו כן. אנשי רובי נמשכים חזק יותר ל\"חדש\". לעבוד עם רובי 2.1.3, כשיש רובי 2.2.0 (בטא) כבר בחוץ? חחחחחחח!

      פוסט זה שייך לסדרה: רובי (Ruby) למפתחי ג\'אווה ותיקים

      הדוד בוב (Robert C. Martin), בתחפושת \"נער הרובי\" שלו…

      אמונות מקובלות לגבי RVM – נכון, או לא נכון?

      \"RVM מתאים בעיקר לפיתוח, ב Production הוא עושה בעיות וכדאי להימנע ממנו\".

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


      RVM עובד רק עם Bash

      ובכן, RVM מבצע שינויים בקובץ ה bashrc. בזמן ההתקנה.
      הוא עצמו כתוב ב shell script, והוא מניח שמספר כלי shell זמינים לו ב path (למשל awk, sed, tar, git וכו\'), וכמו כן הוא מניח על זמינות של כמה פקודות shell (מערכים וכו\').

      אני עובד עם zsh – והוא עובד לי מצוין. אם אתם עובדים עם shell אחר, כנראה שיהיה עליכם לעשות כמה צעדים ידניים (קונפיגורציות + וידוא זמינות הכלים הנחוצים ל RVM). חפשו בגוגל.

      זו הזדמנות טובה לספר שיוצר RVM הצליח לגייס לפני כשנה תקציב שיעזור לו לשכתב את RVM מחדש לשפת רובי. באופן זה הוא אמור להיות יציב יותר, להתפתח מהר יותר, ולהיות זמין בקלות על כל ה Shells ואפילו על חלונות (!).

      אסור להפעיל RVM עם Sudo (וללא רשות מרגע ודודלי!)

      האיסור רלוונטי להתקנה של RVM עצמו ופעולות gem install – שכאשר משתמשים ב RVM אין לבצע מתוך חשבון עם הרשאות root. לא רק שהפקודה עלולה להיכשל, היא יכול לגרום לנזק להתקנה של ה Gem או של RVM – שיהיה לא-קל להתאושש ממנה.

      מקור המגבלה הוא השפה בה RVM כתוב, שהיא shell script. כשעובדים ב shell scripts, כמה משתני סביבה ו paths הם שונים בתור משתמש root – מה שמשבש את ההנהגות של RVM, וכנראה ללא דרך מוצא פשוטה.

      \"את RVM לא ניתן להסיר! פרמטו את המחשב – בכדי להסיר אותו!!\"

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

      האמונה הזו כנראה נובעת מאנשים שמצאו תשובות שגויות / חלקיות בגוגל, מה שסיבך להם אח\"כ את תהליך ההסרה. או אולי, חס וחלילה, מישהו שהתקין את RVM עם sudo, ואז בלי sudo – ואז ניסה להסיר….

      RVM הוא העבר, יחי rbenv! … או chruby! … ?!

      RVM הוא כנראה הכלי הנפוץ בתחומו, מה שעשוי אוטומטית (במיוחד בקהילה כמו זו של רובי) לגרום לכלים האחרים להראות אטרקטיביים מעט יותר. שתי האלטרנטיבות הנפוצות הן rbenv ו chruby.

      אם תבחנו את ההבדלים בין RVM ל rbenv, למשל – תראו שהכלים דיי דומים. RVM מכסה קצת יותר קרקע, והיא ממומשת בצורה אחרת: עטיפה פקודת ה cd של ה shell (פקודה מאוד נפוצה) ובמחיר של כמה עשרות ms נוספים לכל הפעלה של הפקודה, בעוד rbenv עוטפת בעזרת \"shims\" את gem את רובי ועוד כמה קבצים אחרים – מה שמסוכן לא פחות.

      To paraphrase Jonathan Jackson, RVM is to Rails as rbenv is to Sinatra. Sinatra is a lightweight framework whereas Rails is much more robust. Sometimes Sinatra just fits, and other times you\'d be a fool to not go with Rails.

      לא יודע… עבורי הרעיון של ללכת עם האופציה הבסיסית והפשוטה (יהיה זה Sinatra או Lotus) – דווקא קוסם.

      אז איך נראית העבודה ב RVM (בקצרה)?

      אחרי שהתקנתם את RVM, הנה כמה פעולות שימושיות:

      $ rvm install 2.0.0

      יתקין את רובי גרסה 2.0.1, וייצור לה gemset בשם default. ניתן לציין גם patch level מדויק.

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

      $ rvm use 2.0.0

      האם אנו רוצים לשנות ידנית את גרסת הרובי בכל מעבר בין פרויקטים? ברור שלא!

      ניתן ליצור קובץ rvmrc. שיעשה זאת עבורנו. נכנס לתיקיית הפרויקט ונקליד:

      $ echo \'rvm use 2.0.0\' > .rvmrc

      כשנצא מהתיקייה ונחזור אליה – RVM ישאל אותנו אם אנו יצרנו את הקובץ, ואם כן, הוא \"יאשר\" אותו ויחליף את גרסת הרובי הנוכחית אוטומטית בכל כניסה לתיקיה. את קובץ ה rvmrc. כדאי להפקיד ב git repository שלנו.

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

      $ rm .rvmrc
      $ echo \'2.0.0\' > .ruby-version

      יש יתרון בסטנדרטיות לא רק במחשבה על מעבר מהיר (הפעלת שורה אחת פחות?!) לכלי ניהול גרסאות אחר – אלא בעיקר כי מספר כלי צד-שלישי מכבדים את הפורמט המשותף, אך לא מכבדים את קובץ ה rvmrc.
      היתרון של קובץ rvmrc. הוא שמדובר בקובץ סקריפט לכל דבר שמופעל בעת כניסה לתיקיה. השורה שהכנסנו למעלה rvm use היא פקודת shell לכל דבר, וניתן להוסיף פקודות נוספות.

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

      $ echo \'ruby-2.0.0\' > .ruby-version

      יופי!

      עכשיו בואו נרחיב את השימוש ל gemsets. ניצור gemset חדש (בשם \"project_name\") תחת גרסת הרובי הנוכחית (2.0.1), ונהפוך גם את ה gemset שיצרנו להיות הנוכחי הנוכחי:

      $ rvm gemset create project_name
      $ rvm use 2.0.0@project_name

      הקונבנציה המקובלת היא לקרוא ל gemset כשם ה root folder של הפרויקט. RVM משתמש בפורמט של ruby_version@gemset_name בכדי לתאר צמד. נזכיר ש gemset משוייך לגרסת רובי מסוימת.

      מכיוון שאנו רוצים שגם המעבר ל gemset יהיה אוטומטי בכניסה לתיקיה, נגדיר קובץ קונפיגורציה:

      $ echo \'project_name\' > .ruby-gemset

      ייתכן ותתקלו בהמלצה לוותר על קובץ ה ruby-gemset. ולכתוב בקובץ ה ruby-version. את הפורמט של RVM, קרי \"ruby-2.0.0@project_name\". זה יעבוד לכם עם RVM, אך עלול לא לעבוד עם כלים אחרים (זה לא חלק מהסטנדרט).

      אם נקרא ל gem list – נראה רק את ה default gems שמגיעים עם רובי גרסה 2.0.0 (יש כ 14 כאלה).
      נוודא, ליתר בטחון, שאנו משתמשים בגרסת הרובי וה gemset שאליו התכוונו, ע״י פקודת

      $ rvm info | grep HOME


      (הדרך הכי קצרה שאני מכיר)
      נתקין gem לדוגמה, ונראה שהוא נוסף ל gemset:

      אתם יכולים לקרוא ל rvm use 2.0.0 ואז ל gem list בכדי לראות שה gem שהתקנו לא נמצא ב default gemset.

      את ה gem עצמו תוכלו למצוא בתיקיית ה gemset המנוהלת תחת התיקייה rvm/gems./~.
      זה, פחות או יותר, הבסיס.

      RVM מספק עוד כל מיני יכולות. הנה פקודה נחמדה:

      $ rvm all do ruby test.rb
      המריצה את קובץ הרובי (יש עוד אפשרויות) בכל הגרסאות המותקנות של רובי במערכת, ומסייעת לבדוק הבדלי תאימות / ביצועים / וואט-אבר, בין גרסאות הרובי השונות.

      אלטרנטיבה לשימוש ב gemsets

      יש כאלו שמעדיפים להימנע משימוש ב gemset, ולהשאיר את ניהול ה gems ל bundler.
      למה? אולי זה עושה יותר סדר בראש, אולי בגלל יכולות של bundler כמו פקודת bundle update שתעדכן את כל ה gems לגרסאות עדכניות – כולל התלויות שלהן. לכו תעשו את זה ידנית בעזרת gem update[א]
      אם אתם משתמשים ב bundler, אז:
      • קובץ ה Gemfile מנהל עבורכם את הגרסאות השונות של ה gems.
      • את התקנת ה gems אתם עושים, כנראה, ע\"י עדכון של ה Gemfile והרצת הפקודה bundle install.

      RVM נוצר בכדי לפתור \"חורים\" ב RubyGems, אבל הנה bundler עושה את אותו הדבר בעצמו.

        אם אתם בוחרים בגישה זו חשוב לשים לב ש RubyGems מריץ תמיד את הגרסה האחרונה המתוקנת במערכת של ה gem שציינתם.
        מצוין לכם ב Gemfile את rake גרסה 10.0 בעוד פרויקט אחר דרש את גרסה 10.1? – שתי הגרסאות מותקנות ובהפעלת הפקודה rake תרוץ הגרסה המאוחרת (10.1) על אף שהגרסה המצוינת ב Gemfile היא 10.0. יותר גרוע: התלויות של rake גם הן תבחרנה בגרסה המאוחרת ביותר שצוינה כתואמת. \"סמוך על סחבק\" – מה שנקרא.
        מה עושים?
        פשוט זוכרים להפעיל את rake בצורה:
        $ bundle exec rake

        שזו בעצם הדרך לעבוד עם bundler, כהגדרה. (אנו פשוט נוטים לשכוח)
        לא אוהבים להקליד bundle exec כל הזמן? הפקודה:
        $ bundle install –binstubs
        תיצור עבורכם תיקיית bin שבה יהיו wrappers לכל הפקודות הרלוונטיות (נקראים binstubs, ריילס משתמשת בהם כברירת מחדל), שרצים ב context של bundle exec.

        סיכום

        האם זה הגיוני שניהול gems שונים לכל פרויקט דורש צד-שלישי שאינו חלק מהפלטפורמה של השפה?
        האם ייתכן שכלי כ\"כ נפוץ כתוב בעצם ב shell script (כלומר: 20 אלף שורות של shell script), עם כל המגרעות והבעיות שבדבר? שכל כלי בתחום מציב כמה ״חוסרי נוחות״ שיש להתמודד איתם?

        ברוכים הבאים לעולם הרובי! אני בעצמי עוד מתרגל 🙂

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

        בסופו של דבר, בעת כתיבת הקוד, שזה (בשאיפה) היכן שרוב זמן המתכנת הולך – רובי מציעה הרבה נוחיות שמתכנתי ג׳אווה יכולים רק לחלום עליהן. אז נכון… ה tooling הם פחות מלוטשים.

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

        [א] נראה ש RVM מקנאה ביכולת הזו, והיום היא מציעה יכולת דומה בעזרת הפקודה do:

        $ rvm all do gem update -V
        אני לא יודע לומר אם פקודה זו גם מעדכנת את ה gems שהם תלויות, בצורה רקורסיבית.

        מקורות / לינקים רלוונטיים:

        אות חיים

        כפי שאתם אולי יודעים, התחלתי לפני כשלושה שבועות לעבוד ב GetTaxi כ Chief Architect.

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

        התחלתי לכתוב פוסט על סקראם (\"האם סקראם נכשל?\") – אבל הרגשתי שהוא לא מחדש הרבה (כבר כתבתי משהו דומה), אז מחקתי.
        התחלתי לכתוב על Github – אבל לא הצלחתי לסיים.
        ניסיתי לכתוב על Continuous Integration – אבל לא הצלחתי בכלל להתחיל.

        הראש שלי נמצא כרגע ב\"תביאמונית\" (GetTaxi), והכל הדברים החדשים לי: טכנולוגיה, סביבה, ואנשים.

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

        ההבדלים נראים בהתחלה טכניים: מק במקום PC, רובי במקום ג\'אווה, Google Apps במקום אופיס.
        אבל תוך כמה ימים מתחילים להרגיש בהבדלים יותר מהותיים: ה DNA הארגוני.

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

        עם הזמן, ועם הנחת – אני מקווה לחזור לכתוב גם פוסטים יותר כלליים: מתודולוגיה, ארכיטקטורה, וכו\'.

        נשתמע,
        ליאור