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

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

      ריילס, רובי on ריילס

      ריילס, או בשמה המלא Ruby On Rails (קיצור מקובל: ROR) היא מסגרת פיתוח (Framework) פופולרית לבניית אפליקציות ווב.

      ספציפית: אפליקציות עם ממשק וובי מצד אחד, ובסיס נתונים (בד\"כ רלציוני, אבל לא רק) – בצד השני. 
      מדוע ריילס?
      ריילס הגיעה כתגובה ל JEE (בימיו הפחות יפים – תחילת דרכו) וכאלטרנטיבה פשוטה ומהירה לכתיבת אפליקציות ווב. באותם הימים, מרכז הכובד של JEE היה ה Enterprise JavaBeans (בקיצור EJB) שהיו כבדים ומסובכים. למשל, ה Guideline היה לכתוב לכל רכיב שמייצג נתונים 4 Interfaces נפרדים (רגיל, Home, Remote, ו LocalHome – אם אני זוכר נכון). ל EJB היו ה-מ-ו-ן קובצי קונפיגורציה XML לא-ידידותיים, ולמרות הכל – זה היה טרנד חם ורבים עברו לעבוד בו.
      JEE הביא יתרונות רבים (סביבת multi-platform סטנדרטית, המתאימה ל Enterprise), אך במחיר גבוה. הוא כנראה היה טוב יותר מ Microsoft DNA או CORBA (כנראה…), אבל היו אנשים שחשבו שאפשר להשיג את הערך בעלות נמוכה יותר.
      אחד מהם הוא Rod Johnson שיצר את Spring Framework, ה Framework שהפך הכל להרבה יותר פשוט (בעיקר הציג אלטרנטיבה פשוטה ל EJBs) – ומה שאח\"כ הפך להיות הסטנדרט של JEE, בצורת EJB גרסה 3.0 (שהעתיקה הכל, כמעט, מ Spring + Hibernate).
      חלוץ אחר הוא David Heinemeier Hansson (ידוע גם בקיצור DHH), בחור דני צעיר יחסית (יליד 79) שעבד בחברה בשם 37singals (היום נקראת basecamp) שפיתחה לעצמה Framework יעיל מאוד לפיתוח מערכות ווב בשפת רובי. ה Framework הזה יצא כ Open Source ונקרא Ruby on Rails. נראה שריילס, אגב, השפיעה באזורים מסוימים גם על Spring בגרסאותיה המאוחרות (אני לא מדבר רק על Spring Boot) – ומשם היא השפיעה גם על JEE. בכלל, ריילס הוא Framework רק השפעה, שניתן למצוא השפעות שלו במקומות רבים בעולם התוכנה.

      מקור השם Rails הוא כזה: \"במקום להתקדם באיטיות, כל אחד במסלול שלו, הצטרפו אלי: אני קובע את הדרך – אבל תוכלו לנוע במהירות\". ריילס הוא יישום של אוסף של best practices (ש DHH האמין שהן מוצלחות, לא אוסף גנרי) – שמתאימים זה לזה, ומסביבם נכתב Framework שהופך את השימוש בהם למאוד קל ומאוד מהיר. ריילס הוא לא סיפור על פלורליזם – זהו סיפור על דעה מוצקה כיצד נכון לפתח אפליקציות ווב. אם אתם רוצים להיות יעילים בריילס, עליכם לקבל עליכם את \"The Rails Way\", או לפחות את רובה. אם תקבלו – תגלו את אחד ה Framework הפרודקטייבים הקיימים לפיתוח אפליקציות ווב. ריילס פרחה ביוחד בסביבות של סאטראט-אפים, שהדבר שהכי חשוב להם הוא לספק Value מהיר לשוק שאותו הם עוד לומדים.

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

      התכונות העיקריות של ריילס

      ההחלטה התכנונית המרכזית של ריילס, אם כן, היא \"פריון (productivity) על פני גמישות\".

      שני עקרונות מרכזיים שנוהגים להזכיר שוב ושוב, בהקשר של ריילס, הם:
      • Convention over Configuration – במקום לומר היכן נמצא משהו (קובץ, משתנה, וכו\') ומה שמו – בואו נגדיר כלל מוסכם (convention) היכן הוא אמור להימצא / איך הוא ייקרא – ונחסוך לנו את כל התקשורת הזו. הרעיון של CoC הוא לא חדש, ניתן למצוא אותו במייבן, למשל, שקצת קדמה לריילס. בכל זאת, ריילס היא זו שעשתה PR משמעותי מאוד לעקרון התכנוני הזה (ובמיוחד כ contra ל JEE) – והשפיעה בכך רבות על התעשייה.
      • Don\'t Repeat Yourself (בקיצור DRY) – גם זה רעיון שכבר מקובל עשרות שנים, אבל ריילס חרטה אותו על דגלה. לא לכתוב בקוד שום דבר פעמיים.
      בעזרת עקרונות אלו (ועוד כמה אחרים) הצליחה ריילס ליצור סביבה סופר-פרודקטיבית לכתיבת אפליקציות ווב. כלל אצבע אחד אומר שבריילס יהיה עליכם לכתוב חמישית משורות הקוד בג\'אווה בכדי להשיג את אותה התוצאה.
      ריילס צמחה על גבי שפת רובי, קצת רחוק מהקהילה הטבעית של ג\'אווה (ה Enterprises), ולכן ההשפעה שלה הייתה קצת יותר רחוקה. אל דאגה! JEE ננגחה שוב ושוב על היתירות של הקוד שהיא יצרה (בתרגיל הנדסי כ\"כ מרשים): גם ע\"י NET. (מי שזוכר את קרבות ה PetShop) וגם ע\"י Spring – בניצוחו של רוד ג\'ונסון (Rod Johnson), שגילה מעט מאוד עדינות כלפי החבר\'ה של JEE. היום מצבה של JEE טוב בהרבה מאשר היה באותם הימים, אבל עדיין – פיתוח בריילס נחשב מהיר בהרבה.
      עוד אלמנטים מרכזיים בריילס שכדאי לציין הם:
      הרחבה לשפת רובי:
      רובי, כתכונה של השפה מאפשרת (ומעודדת) שימוש ב metaprogramming בכדי להרחיב את השפה עצמה וליצור DSLs חדשים. יכולות אלו, הן אולי הסיבה המרכזית שריילס הצליחה להיות יותר אפקטיבית [א] מספריות ווב אחריות (ASP.NET MVC, Lavarel או !Play) – שהיו משוחררות גם הן מ\"כבלי ארכיטקטורת ה Enterprise של JEE\". פקודות שימושיות בכתיבה של אפליקציות ריילס (למשל הגדרות שונות על המודל) הן בעצם הרחבות של ריילס לשפת רובי. מתכנת ריילס ללא רקע משמעותי ברובי עשוי לא להיות מסוגל להבחין אלו תכונות בהן הוא משתמש שייכות לשפת רובי – ואלו שייכות לריילס (ואולי גם לא ממש אכפת לו).

      אינטגרציה:

      ריילס כוללת stack של טכנולוגיות שמחוברות טוב מאוד זו לזו, ומספקות חווית פיתוח אחידה end-to-end. החל מבסיס הנתונים (Active Records – כלי ה ORM של ריילס), ועד צד הלקוח (SASS, Haml, ו CoffeeScript [ה]) – הכל מרגיש מתאים, ודומה למדי. החוויה דומה, אולי, לזו של פיתוח בפלטפורמת NET. של מייקרוסופט – לה שליטה רחבה על כל ה Stack הטכנולוגי.

      עד גרסה 2, ריילס הייתה gem [ב] אחד, שלא גמיש להחלפות. החל מגרסה 2 חלק מהרכיבים (Active Records, למשל) הופיעו כרכיבים הניתנים להחלפה, אם כי ההחלפה בפועל הייתה מאתגרת למדי. ובמשך הזמן ממש התפתחו plug-in APIs המאפשרים לעשות החלפות אלו בצורה פשוטה יותר ויותר.
      שלא תבינו לא נכון: אם אתם הולכים לכתוב אפליקציה ראשונה בריילס, ורוצים כבר בשלב זה להחליף רכיבים כראות עיניכם – זה לא ממש כדאי. החלפת רכיבים היא עדיין פרקטיקה מתקדמת שמומלצת רק למי שמבין היטב את דרכי העבודה של ריילס. באפליקציות הראשונות – כדאי להיצמד לסטדרטי ולמקובל.

      הגישה שרואה בריילס \"אוסף של רכיבים\" (מה שטכנית באמת נכון) – מהם ניתן להרכיב תצורה מותאמת-אישית, נקראת (גם) Hexagonal Rails (ע\"ש Hexagonal Architecture, שדוגלת בהפרדת ה domain model משאר הרכיבים, ורואה בהם סוג של plugins שמתחברים מסביב אליו) – והיא פחות נפוצה, ואולי אף שנויה במחלוקת: האם ההשקעה, הלא פשוטה, \"לאלף את ריילס\" מתירה בסופו של דבר תוצר שמצדיק את ההשקעה הזו?

      בדיקתיות:

      תכונה אחרונה של ריילס שארצה לציין היא \"בדיקתיות\". יוצרי ריילס (ממש כמו יוצרי AngularJS, למשל) היו מודעים היטב לצורך בבדיקות – והם בנו תשתית שמאפשרת לבדוק את הקוד שרץ עליה בקלות יחסית. בדיקות יחידה ואינטגרציה הן חלק מובנה מריילס, ו flows הבדיקה מאופשרים היטב בכל הרמות , אם כי קצת יותר לכיוון ה integration tests וקצת פחות לכיוון ה isolated unit tests. כמות ספריות הבדיקה הזמינה לשפת רובי היא מרשימה (RSpec, MiniTest, Test::Unit, Cucumber, Factory_girl), ולכמה מהספריות הללו יש הרחבות מיוחדות לריילס (למשל Rspec_rails או Factory_girl_rails). בדיקות הוא לא רעיון זר בקהילת הרובי או ה RoR…

      מעניין לציין שדווקא DHH הוא זה בשנה האחרונה העלה כמה דברי ביקורת על כתיבת בדיקות. בגלל ש DHH קידם מאוד את תרבות כתיבת הבדיקות (וגם בגלל שהוא הגורו הבלתי-מערוער בעולם של ריילס?) – דבריו זכו לתהודה רבה. הוא כתיב פוסט בשם Test-induced design damage, ואח\"כ העביר סשן בשם \"?Is TDD Dead\". בסוף הפוסט תוכלו למצוא לינק לסדרת הסשנים המעניינת בה הפגישו אותו עם יוצר ה TDD (קנט בק) ומרטין פאוולר (ששימש כמגשר?). סדרה של 3 שעות – אך בהחלט מעניינת.

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

      ריילס בחזית הטכנולוגיה

      בעולם של Single Page Applications, בעולם בו חווית משתמש שהפכה מקובלת, שדורשת שקוד משמעותי ירוץ בדפדפן, ובעולם של noSQL – ריילס כבר זוהרת פחות. אפשר לעשות את כל הדברים האלו עם ריילס, אבל היא כבר לא יעילה כמו שהייתה בעבר, ומפתחי ריילס צריכים לפעמים להיות יבשושים ולכתוב קוד משעמם / קצת פחות יעיל.
      אם אתם מחפשים את החלופה \"העכשווית\" לריילס שתוכננה במקור לקוד צד-לקוח משמעותי, ו MongoDB – אתם כנראה מחפשים את MeteorJS (עם כל היתרונות, וגם החסרונות שלה).

      אכן נראה שחלק ממפתחי הריילס (כלומר: רובי + ריילס) עוזבים את ריילס ומחפשים Frameworks חדשים לעבוד איתם. הטיעון ששמעתי כבר כמה פעמים הוא \"אני לא רואה הגיון לעשות MVC גם בשרת וגם בצד הלקוח\" (ברור שלא! מוזר לשמוע בכלל ניסו כזה דבר). באופן טבעי, חלקם מחפשים את המוכר, ולא מפתיע למוצא frameworks שמנסים לספק את הצורך הזה. קשה לי שלא לציין את Sails – של node.js ו grails של שפת גרובי (groovy). האחרון הוא לא חדש – אבל קשה להתעלם מהצליל המוכר שהוא בחר לעצמו 🙂

      הרכיבים העיקריים בריילס

      ריילס בבסיסה היא ספריית MVC שמושפעת ישירות מה MVC (המקורי) של שפת smalltalk:

      המודל – שומר את ה state של המערכת (בעזרת בסיס הנתונים, אם כי לפעמים רק בזכרון), אוכף \"חוקים עסקיים\" על נכונות ה data (למשל: סטודנט חייב להיות רשום לתוכנית לימודים כלשהי). הוא לא בהכרח המאמת היחידי של הנתונים, אלא ה gatekeeper לפני שהמידע נשמר / משותף עם חלקים אחרים במערכת. בניגוד ל MVC של Smalltalk הוא גם כולל חלק מהלוגיקה – ראו בהמשך את ההסבר על Active Records.
      המודל, בגדול – הוא זמין לכלל האפליקציה, אין בו partitioning (או bounded context).

      ה View – הוא הקוד שמייצר דפי HTML למשתמש, על בסיס מידע במודל. ריילס היא תשתית לכתיבת MVC בצד-השרת (כלומר: השרת מייצר HTML, ומוסיף JavaScript לאינטרקטיביות). בדומה למודל ה MVC הקלאסי ה View לא מקבל input מהמשתמש, ולא מנהל איתו שום סוג של אינטרקציה (לפחות לא בצד-השרת).

      ה controller – אחראי לקבלת קלט מהמשתמש, טיפול בו, ותפעול (ניתן לומר: orchestration) של המערכת עד שהמשתמש מקבל בחזרה את התשובה שלו. דרך העבודה שלו היא לעדכן את המודל, ואז להזניק את ה View המתאים (הוא לא מעביר מידע ל view ישירות).

      בריילס יש גם routers – שהם ממפים URLs, ומצבים – ל Views. לדוגמה: אם משתמש שהוא admin ניגש לדף מסוים – הוא יקבל View אחר ממשתמש רגיל. ה router עושה מה שבמערכות אחרות עושה סוג של \"super controller\". הוא דקלרטיבי, בעזרת סט של ״פקודות״ מיוחדות לעניין (מה שהופך אותו לפשוט יותר לכתיבה ותחזוקה) – וסה\"כ הוא מהווה הרחבה מבורכת על מודל ה MVC ה\"קלאסי\".

      רכיבים עיקריים של ריילס הם:

      Active Records (בקיצור: AR)

      כלי ה lightweight ORM של רובי, המנהל בפועל את הגישה לבסיס הנתונים של המודל. רובי מספקת גם כלים להגדרת טבלאות ברובי דקלרטיבי (ולא SQL, על הווריאנטים תלויי בסיס הנתונים הספציפי שלו) וכלים מובנים לביצוע migrations בין גרסאות שונות לסכמת בסיס הנתונים (מה שמקובל בג׳אווה לעשות עם flyway).

      השם \"Active Records\" הוא בעצם של שם של דפוס-עיצוב (מופיע בספר PoEAA) אותו המודול מממש.

      דפוס העיצוב \"Active Record\" מגדיר מחלקה בשפה (במקרה שלנו: רובי) שמתארת נתונים של שורה (להלן Record) בבסיס הנתונים, אבל מוסיפה עליהם גם את התנהגות ה domain הרלוונטית לאותם נתונים (להלן Active). למשל: validation, חישובים, derived fields, וכו\'. הבהרה: יש מחלקה אחת לכל טבלה, ומייצרים מופעים שלה ע\"פ הרשומות להם נזקקים באותו הרגע.

      דפוס עיצוב זה מרשה לערבב בין מיפוי הנתונים בין בסיס הנתונים לזיכרון, עם לוגיקה עסקית – כל עוד הלוגיקה העסקית היא רק ברמה של הרשומה הבודדת (ולא \"שייכת\" ל scope רחב יותר). ערבוב זה, נמצא בוויכוח תמידי על הלגיטימיות שלו, מכיוון שהוא סותר כלל מקובל בארכיטקטורה הנפוצה ביותר למערכות מבוססות-נתונים (הרי היא Layered Architecture) – הפרדה בין Persistence ל Business Logic, או בצורת דפוס העיצוב \"Data Mapper\" אותה מממשים רוב כלי ה ORM המוכרים – הפרדה מוחלטת בין לוגיקה עסקית למיפוי הנתונים בין בסיס הנתונים לזיכרון [ו].

      בריילס, יידרש מאמץ מיוחד בכדי לא-לעבוד עם Active Records, אז כדאי שתתנו לדפוס זה הזדמנות – גם אם כל חייכם \"גדלתם\" על Data Mappers ;-).

      Active Records מאפשר לנו לכתוב אובייקט \"פשוט\" ברובי, לרשת מ ActiveRecord::Base ולקבל את פעולות ה CRUD הבסיסיות ע\"י סיפוק הגדרות בסיסיות של מה שאנו רוצים (declarative programming).
      לא! זה לא \"בחינם\", אין ארוחות-חינם בהנדסת תוכנה. אנו מוותרים על גמישות וקצת על ביצועים – בכדי לקבל את הנוחות הזו.

      Active Records מכתיב כללים מוסכמים לשמות –  בכדי לצמצם את הצורך בהגדרות דומות, בכל פעם מחדש.
      למשל: שם הטבלה בבסיס הנתונים צריך להתאים לשם המחלקה (בוריאציה מסוימת), וכן שמות ה primary key וה foreign_keys, ועוד. פרטים על כללי השמות – אספק בהמשך הפוסט. אפשר שתהיה חוסר התאמה בשמות – אבל אז צריך לכתוב קונפיגורציה לכך.

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

      • Action Pack – זהו הרכיב שמתפעל את ה MVC בריילס. כל פעולת משתמש בריילס נקראת \"Action\". הוא מורכב מ -3 תתי-מודולים:
        • Action Dispatch – אחראי ל routing.
        • Action Controller – מספק את מחלקת ה ActionController::Base ממנה יורשים כל ה controllers במערכת. 
        • Action View – אחראי לרינדור ה Views, בעזרת מנגנון ה ERB (קיצור של Embedded Ruby) – מנגנון שדומה מאוד ל JSP בג\'אווה או ל PHP: סוג של template (בד\"כ HTML, יכול להיות אחר) שבו מושתל קוד רובי המתאר את ההתנהגות הדינאמית.
      • Active Model – המשמש כ Facade של AR מול ה Action Pack.
      • Active Resource – המספק יכולות צריכה של שירותי REST חיצוניים. סוג של שירות אינטגרציה בין מערכות.
      • Active support – סט של utilities כלליים (שימושיים) שמגיע עם ריילס. המקבילה של Apache Commons – של מג\'אווה.
      • Action Mailer – אשר תופס בתיעוד, משום מה, נתח מכובד. הוא בסה״כ שולח מיילים.
      • Active Job – לניהול Queues של jobs. הוא בעצם שכבת הפשטה למימושים קיימים (כגון Resque) ולא מימוש בפני עצמו. הוא חדש בריילס 4.2 .
      • Railties (\"קשרי מסילה\") – זהו ה gluing logic של כל רכיבי ה Active / Action למיניהם. ה Strategy של תפעול אפליקציות הריילס.
      • Rails – רכיב קטן למדי, שאחראי על אתחול המערכת – והכנסת המודולים האחרים לפעולה.

      כל עבודת ה low-level ברמת ה HTTP הוא לא חלק מריילס. חלק זה יטופל ברמת ה Application Server – עליו נדבר בהמשך.

      מבנה אפליקציית ריילס

      כחלק מה Convention over Configuration, לאפליקציית ריילס יש מבנה תיקיות מוסכם-מראש. בכדי ליצור מבנה זה יש להקליד בשורת הפקודה:

      $ rails new [optional: -d ]

      d- הוא פרמטר שמגדיר באיזה בסיס נתונים אתם הולכים להשתמש – והוא יחסוך לכם יצירה של כמה קונפיגוריות בצורה ידנית מאוחר יותר.

      מבנה אפליקציית ריילס חדשה

      התיקיות המעניינות ביותר באפליקציית הריילס הן תיקיות ה app, config, ו lib. אם יש בדיקות טובות – הוסיפו לכך (במקום גבוה) את תיקיית ה tests (לפעמים תראו אותה בשם spec).

      תיקיית ה bin כוללת את הסקריפטים לפעולות הבסיסיות של האפליקציה. אלו בעצם wrapper scripts (נקראים ברובי גם binstubs) שרק מכינים את סביבת הריצה ואז קוראים ל executable הנכון. לפני ריילס גרסה 4 קראו לתיקיה זו בשם \"scripts\".

      קובץ ה Gemfile (מתחיל באות גדולה) הוא קובץ של כלי רובי בשם bundler, כלי שעוזר להגדיר, ואז להתקין – תלויות (gems [ב]) על סביבות חדשות בהן תרוץ האפליקציה.
      ברגע שמריצים את הפקודה \"bundle install\", אז bundler יקרא את קובץ ה Gemfile, יבדוק מהן הספריות הנדרשות וגרסאותיהן, ויתקין את מה שלא נמצא – בגרסאות המתאימות. אם אתם מכירים את קובץ ה package.json של node.js – זה ממש אותו הדבר.

      קובץ ה Gemfile שנוצר בעקבות הפקודה \"rails new\" כולל כבר רשימה של ספריות שימושיות: דרייבר לבסיס הנתונים שהגדרתם, כמה ספריות צד-לקוח, ספרייה ל generation של תיעוד, ספריה לעבודה עם JSON, וכו\'.

      Gemfile.lock הוא קובץ בו bundler מנהל לעצמו את הגרסאות שמותקנות בפועל. זהו קובץ שתרצו להכניס ל git repository שלכם – אך לא לשנות ידנית (אין קשר לקבצי lock של MS Office, שמטפלים ב concurrency).


      קובץ ה rakefile הוא של כלי ה build של רובי שנקרא rake. ה build ברובי כולל בעיקר בדיקות, פענוח הסכמה, Linting, וטיפול בקצבי HTML ו CSS (למשל: minification).

      קובץ ה rakefile של ריילס יהיה לרוב מינימלי, והדבר המרכזי שהוא יעשה הוא לטעון ולהריץ רשימה של rake tasks (שאתם תכתבו). קבצי ה task יושבים בתיקיה lib/tasks.

      בריילס, עובדים לא מעט עם command line עבור scaffolding [ג]: ניהול בסיס הנתונים, ביצוע buid, הרצת האפליקציה, ועוד.

      לפעמים הפקודות הללו לא עובדות כראוי, ו\"טריק\" מקובל הוא לקרוא ל  <bundle exec rake <rake params – מה שהופך את כל ה gems שמצוינים ב Gemfile לזמינים, אפילו אם הם לא ב path של מערכת ההפעלה.

      התיקייה lib, ע\"פ הגדרה אחת, כוללת קוד שהוא לא מודל, controller, router, או View.
      התיקייה lib, ע\"פ הגדרה נוספת, כוללת קוד שניתן לעשות שימוש חוזר בו בין הרכיבים השונים (view, model, controller).
      מלבד כמה תתי ספריות נדרשות (למשל tasks) – יש לכם חופש כיצד לארגן אותה.

      בגרסאות ישנות של ריילס, כל הקבצים שישבו ב lib נטענו כזמינים לכל הרכיבים במערכת. כלל זה שונה והיום עליכם להדיר במפורש באיזה lib רכיב ה app שלכם רצה להשתמש – בכדי להשתמש בו. ( או פשוט להוסיף ל config/application.rb את הפקודה config.autoload_paths += %W(#{Rails.root}/lib שתעשה מה שריילס עשתה בגרסאות ישנות).

      נוהגים לומר שהקהילה של ריילס \"התבגרה\" בשנים האחרונות. ריילס \"ניסתה לדלג\" על כל מיני כללים מקובלים של הנדסת תוכנה בכדי להאיץ את קצב הפיתוח. חלק מהדילוגים הללו הוכחו כבעייתיים – ואז ריילס חזרה בה (נראה לי שהמקרה הנ״ל הוא אחד כזה). דילוגים אחרים – אולי הוכחו כלא בעייתיים, בהקשרים בהם ריילס עובדת. אני זוכר ששמעתי דיונים על כך בקהילת הג\'אווה לפני מספר שנים: \"הנה ריילס מתבגרת, והחברה שם עושים היום יותר ויותר הנדסת תוכנה כמו ג\'אווה\". המסקנה שהוסקה (שגם לי נראתה הגיונית, בזמנו) הייתה ש\"ג\'אווה צדקה\" (שהחילה על עצמה כל כלל אפשרי של הנדסת תוכנה) – וריילס \"בדרך להיות סוג של ג\'אווה\".
      נו טוב, סתם רכילות cross-קהילתית.

      שני קבצים חשובים שנטענים בעליה של ריילס הם config/environement.rb ו config/application.rb.
      בנוסף תמצאו בתיקיה config/environemnts קובץ קונפיגורציה לכל סוג סביבה: development, production ו test – זה הבסיס למערכת ה staging של ריילס.
      כשמפעילים את אפליקציית הריילס, ניתן לציין את הסביבה כפרמטר, למשל:

      $ rails server – e development

      בכדי לטעון קונפיגורציה לסביבת ה staging המתאימה.

      זהו.

      ייתכן ותתקלו בקבצים ריקים בשם ״keep.״. מטרתם היא למנוע מכלים מסוימים להתעלם מהתיקיות שנוצרו ע\"י \"rails new\" – וניתן (ומומלץ) למחוק אותם כאשר התיקיה מתמלאה בתוכן. חבל לראות פרויקט ריילס בן כמה שנים – שעדיין שמר את הקבצים הללו.

      החידושים שהגיעו עם ריילס 4, ששוחררה באמצע 2013. גרסה מג\'ורית של ריילס משוחררת אחת לשלוש שנים, בערך. מקור.

      ה Application Server

      הפעלת האפליקציה נעשית ע\"י פקודת \"rails server\", או בקיצור \"rails s\".
      כל אפליקציית ריילס רצה על \"שרת ווב משלה\", בתוך תהליך של מערכת ההפעלה. ניהול של כמה אפליקציות על אותו שרת פיסי, מתרחשת ע\"י הפעלה של כמה \"תהליכי שרת\", אולי מסוגים שונים ו/או גרסאות שונות.

      ריילס מגיע כברירת מחדל עם שרת \"רזה\" בשם WEBrick. זהו שרת פשוט למדי – שמשמש בעיקר לצורכי פיתוח (ולא ל production). ב Production מקובל להשתמש ב Puma, Raptor או ב Unicorn.

      Rack הוא שמה של הספציפיקציה של שרת ווב ברובי, וגם השם של ה Reference Implementation שלה.
      Rack דומה בתפקידו ל Servlet API של ג\'אווה, שבמקרה יש לו גם מימוש באותו השם (דמיינו ש Tomcat נקרא \"Servlet\"). החלק החשוב יותר של Rack הוא הספסיפיקציה. כנראה שהחבר\'ה של רובי ראו מה קרה בעולם של פייטון, שבו לא היה תקן שכזה, וכל שרת ווב עושה את אותו הדבר – אבל מגדיר זאת אחרת.

      סה\"כ ה API ש Rack מציע הוא מאוד בסיסי: הוא עובד ברמת הפשטה מאוד נמוכה – ה HTTP Request-Response Lifecycle (בדומה ל CGI – אם אתם ותיקים מספיק בכדי לדעת מה זה).
      מצד מימושי השרת, המימושים הנפוצים ביותר הם Rack (כמובן), Unicorn, או Mongrel.
      ספסיפקציית ה Rack משתמש לא רק את RoR, אלא גם Frameworks אחרים כגון Sinatra, Ramaze ו Merb (שמוזג לתוך RoR בגרסה 3.0).

      כמה מלים על Rack כ reference implementation:
      מקור השם Rack הוא הכוננית בחדר השרתים שעליה מתקינים את השרתים / ציוד הרשת. יוצריו בחרו בשם זה בגלל מנגנון ה plugins שלו, שמאפשר למודולים צד שלישי (הנקראים middleware, דומים ל Servlet Filters בג\'אווה) להרחיב אותו בקלות. הנה רשימה של מודולים זמינים.
      מעניין להזכיר את Rack::MockRequest שמייצר mocks לאובייקטי request ו response (נחמד שזה מגיע מ\"הספק\"), ו Rack::Lint שעושה Linting על קוד המשתמש ב API של Rack ומוצא כמה common pitfalls.

      קובץ הקונפיגורציה של Rack, הנקרא config.ru מכיל את ההגדרה של ה middlewares שאנו משתמשים בהם, ו metals.
      Rails Metal Apps (בקיצור: metal, מלשון \"bare-metal\" – אני מניח) הן חתיכות קוד באפליקציית הריילס שלנו, שירוצו ישירות מעל ה API של Rack, ללא תוספות.

      מדוע לעשות זאת? בגלל ביצועים, למשל: כדי לספק REST API שקוראים לו בקצב מהיר.

      ה stack של ריילס שמריץ את ה controllers (ה Action Controller Stack) מציב תקורה משמעותית לכל request שמטופל. אם אנו רוצים לספק תשובה פשוטה, ולעשות זאת מהר – כדאי לעקוף אותו.

      הערה: ברור שאם אנו זקוקים לביצועים ממש גבוהים – גם metal עדיין מציב מגבלה על מה שאפשר להשיג מהחומרה. שפת רובי היא לא concurrent (השתפרה, אבל עדיין לא ממש) ואין לה parallelism. כדי להגיע ל throughput או tps גבוה במיוחד – כדאי לשקול עבודה נקודתית עם node.js (עבור קוד \"רעב\" ל I/O) או עבודה ב Go (עבור קוד \"רעב\" ל CPU) [ד].

      אחרונה חביבה, היא ספרייה בשם spring שעובדת עם rake (שהיא כבר חלק מובנה מריילס 4.1 ומעלה) שעושה preloading לשינויים באפליקציית הריילס מבלי לעשות restart לשרת (אלא על בסיס של fork של ה process – לא עובד ב Windows או ב JRuby). יכולת דומה קיימת באופן מובנה בשרת ה Unicorn.

      ה Conventions של ריילס

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

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

      שמות של טבלאות בבסיס הנתונים יהיו ב snake_case – כמו משתנים. טבלאות תמיד יקראו ע\"פ צורת הרבים של האובייקט (plural): למשל People או Invoices ולא Person או Invoice.
      למה? כדי שהקוד יהיה קרוב יותר לשפה הטבעית: \"Select a Product from products\". אני מניח שהבדל זה יכול גם להקל להבחין מהר בין מחלקה לטבלה בבסיס הנתונים.

      שמות של קבצים (model, view, controller, וכו\') יהיו גם הם ב snake_case – כפי שמקובל ברובי גם מחוץ לריילס.

      נניח שאנו רוצים ליצור מודל בשם LineItem (שם מחלקה ברובי הוא CamelCase). כדי שהכל יעבוד, ללא קונפיגורציה נוספת, על שם הקובץ המכיל את המחלקה להיות line_item.rb (והיא תהיה בתיקייה app/models), ועל שם הטבלה בבסיס הנתונים להיות line_items.

      \"אני מבין איך עושים זאת עבור line_items, אבל איך *לעזאזל* ריילס תקשר עבורי בין people ל person?\". שאלה טובה. הציצו ב utility של ActiveSupport שעושה זאת: inflections.rb. הוא מכיר יוצאי-דופן מוכרים כמו person-people או octopus-octupi, ואתם גם יכולים ללמד אותו כללים חדשים, למשל: selfie-selfieez.

      ל controllers יש כללים נוספים:

      אם יש לנו Controller ששם המחלקה שלו הוא InvoiceController, אז עליו להיות בקובץ בשם invoice_controller.rb שנמצא בתיקייה app/controllers. עד כאן – זה כמו המודלים.

      בנוסף, ריילס תצפה שיהיה קובץ נוסף בשם invoice_helper.rb בתיקייה app/helpers שיכיל מחלקה בשם InvoiceHelper.

      ריילס גם תצפה שה view templates של ה controller הזה יהיו בתיקיה בשם apps/views/invoice. כהתנהגות ברירת מחדל, ריילס תשתמש ב output של ה templates הללו ותכיל אותם בתוך layout template (לא דיברנו עדיין ממש על הפרטים של ה view…) בשם invoice.html.erb (או invoice.xml.erb – אם מישהו משתמש עדיין ב XML/XHTML) שנמצא בתיקייה app/views/layout.

      בגלל שריילס מודעת לקשרים בין הקבצים, היא תוסיף עבורנו, מאחורי הקלעים, את ה requires הנדרשים – כך שאין צורך לכתוב אותם.

      אם יש לנו כמה controllers הקשורים זה לזה (למשל Admin screens), אנו יכולים להגדיר אותם בהיררכיה בתוך התיקיה app/controllers. אם הדפדפן ביקש URL בשל admin/user אז ריילס תחפש את ה controller בקובץ בשם user_controller.rb בתיקיה app/controllers/admin (כלומר: תת-התיקיה admin מקבצת את כל ה controllers בקבוצה).

      בכדי למנוע התנגשות בין שני controllers בעלי אותו השם (נאמר user_conroller.rb בתיקיה app/controller/report), שם המחלקה יהיה namespaced ע\"י מודול בשם של תת-התיקיה. במקרה שלנו: Admin::UserController.

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



      קישורים נוספים:

      The Rails Doctrine – הרעיונות התכנוניים / פילוסופיים מאחורי ריילס.

      מה ההבדל בין RDoc ל Markdown

      ?Is TDD Dead

      A Conversation with Badri Janakiraman about Hexagonal Rails

      פוסט על הארכיטקטורה של ריילס



      [א] זו לא אמת חד-משמעית, אך כך מקובל להאמין.

      [ב] Gems, הן ספריות קוד של רובי אותן ניתן להתקין בקלות (<gem install <gem name $) מ repository משותף-לכל. דומה ל npm packages של node.s, ל eggs של פייטון, או Pears (אגסים) של PHP.

      [ג] פירוש המילה: פיגומים. הוספת templates קטנים (controller חדש, מודל חדש) תוך כדי עבודה ע\"י הפעלה ב command line של \"… rails generate\", או בקיצור: \"… rails g\". זו המקבילה הרזה ל Create New Wizard ב Eclipse, למשל. פקודת rails generate דומה מאוד ל Yo – למי שמכיר, ובעצם הייתה עבורה מקור ההשראה.
      [ד] יש כמובן את Scala, Elixier, Rust, ועוד. הצגתי את הבחירות הנפוצות של אנשי רובי.

      [ה] טכנולוגיות אלו הן לא \"של ריילס\", אבל ריילס עושה בהן שימוש מקיף, ואלי גם השפיעה עליהן במידה:

      • SASS – שפת מטא ל CSS
      • Haml – תחביר מקוצר ל HTML 
      • CoffeeScript – שפת מטא לכתיבת ג\'אווהסקריפט, בתחביר שדומה לשפת רובי.

      כתבתי על שפות המטא של ג\'אווהסקיפט ו CCS בעבר.

      [ו] Data Mapper, כמו Hibernate – מבודד את ה Domain מבסיס הנתונים, בכדי להפוך את ה Domain Logic לבלתי תלוי, קל לבדיקה (Unit Testing), ולאפשר שינויים בבסיס הנתונים במנותק מה Domain וליהפך. אמנם במקרים רבים הצוות משתמש ב Data Mapper כי זה \"Best Practice\" למרות שהכוונה הברורה היא שבסיס הנתונים יהיו מיפוי מדויק של המודל – או ליהפך (הכוונה: תמיד אובייקט = טבלה, בלי שום רמת הפשטה). כמובן שרמת ההפשטה האפשרית בפועל מעל בסיס הנתונים היא מסוימת – ולא \"אינסופית\". כדאי להזכיר ש Data Mappers נולדו בסביבת ה Enterprise, בה לעתים רבות לאפליקציה לא הייתה בלעדיות על ה Database schema.