היום למדתי: strict loading ב Rails
הנה פיצ'ר שנכנס בריילס 6.1, פספסתי אותו לגמרי כשהוא יצא ובסוף כשגיליתי עליו (בזכות ה AI) הבנתי למה העדפתי לחיות בלעדיו. לפני שנצלול לריילס יש פה לקח חשוב - כש AI מוסיף למערכת מנגנון שלא הכרתם, כדאי לקרוא לעומק על המנגנון ולחשוב אם הוא באמת רצוי או שמדובר במנגנון ישן שבאותו זמן שנוצר היתה התלהבות גדולה ממנו אבל אחרי כמה שנים גילינו שהוא לא היה כזה רעיון טוב.
1. הבעיה עם טעינה עצלה
בריילס מאוד אוהבים לטעון דברים רק כשצריך אותם לכן הקוד הזה:
Book.all
עדיין לא יטען את כל הספרים מבסיס הנתונים אלא הוא מייצג רק התחלה של שאילתה. אני יכול להמשיך אותו עם:
Book.all.first
ולקבל רק ספר אחד מבסיס הנתונים בלי שטענתי את כל הספרים לזכרון. זה רעיון מצוין עד שהוא פוגש מפתחים. כי הבעיה עם מפתחים היא שאנחנו יצירתיים וכותבים לולאות כאלה:
Book.all.each do |book|
puts book.author.name
end
כיף, נוח, קריא ומאוד לא יעיל. הלולאה תריץ עבור כל ספר שאילתה לבסיס הנתונים כדי להבין את פרטי המחבר ותדפיס את שם המחבר. אם יש לי 10 ספרים זה אומר 11 שאילתות ובגלל זה לולאות כאלה נקראות N+1 Queries.
שתי הדרכים המרכזיות לא לכתוב לולאות כאלה הן טעינה מראש או JOIN. עם טעינה מראש היינו כותבים:
Book.includes(:author).each do |book|
puts book.author.name
end
ואז ריילס היה מריץ שתי שאילתות, אחת מושכת את כל נתוני הספרים והשניה מושכת את כל נתוני המחברים במכה אחת.
דרך שניה היא JOIN והיא נראית כך:
Book.joins(:author).pluck("authors.name").each do |author_name|
puts author_name
end
ופה יש לנו כבר שאילתה אחת שמחזירה רק את שמות המחברים.
2. איך לזהות את התבנית השבורה
הדרך הכי קלה לזהות את התבנית השבורה היא בזמן ריצה להסתכל על השאילתות. יש gem בריילס בשם bullet שעושה את זה ואפשר לקרוא עליו כאן:
https://github.com/flyerhzm/bullet
בעבודה עם AI הסוכן הציע להגדיר strict loading כדי לצמצם את הסכנה בתבנית זו:
Mitigation: the `Current`-scoped `localized_translation` association everywhere + `strict_loading` on `phrase_translations` / `token_translations` in development so a forgotten preload raises instead of silently N+1-ing.
אני מודה שלא הכרתי את המנגנון אז הלכתי לחפש, התחלתי עם המאמר הזה בנושא:
https://thoughtbot.com/blog/strict-loading-in-rails-8-a-railsy-way-to-avoid-n-1-queries
המשכתי לקצת Issues בגיטהאב ולשיחה עם ChatGPT.
למדתי ש strict loading בגרסתו הראשונית נועד למנוע טעינה עצלה על יחסים מסוימים בריילס. לדוגמה ניתן להגדיר:
class Movie < ApplicationRecord
has_many :reviews, strict_loading: true
end
ואז הקוד הזה יזרוק שגיאה במצב פיתוח:
puts Movie.first.reviews.count
והדרכים להגיע לביקורות יהיו להשתמש ב includes או join:
movie = Movie.first.includes(:reviews)
puts movie.reviews.count
הרעיון הזה באמת מונע את כל שגיאות ה N+1 אבל הוא מונע גם הרבה שימושים ממש תקינים במנגנון הטעינה העצלה של ריילס ולכן לא נרצה להשתמש בו.
גרסה 7 של ריילס הוסיפה למנגנון מצב פעולה שנקרא n_plus_one_only שאמור לעזור לנו לזהות תבניות N+1 בלי לשבור המון קוד שאנחנו אוהבים. הבעיה שמנגנון זה לא הצליח לזהות בצורה אמינה את כל התבניות הבעייתיות וכאן יש דוגמה למקרה קלאסי שלא נתפס ברשת:
https://github.com/rails/rails/issues/42576
בסוף אין ספק שטעינה עצלה מגיעה עם אתגר אמיתי בזיהוי N+1. אבל האתגר הזה היום הרבה פחות חמור ממה שהיה בעבר בגלל שסוכני קידוד ממש טובים בזיהוי תבניות ו N+1 כמעט תמיד מזוהה כתבנית בקוד המקור. מספיק להוסיף ל AGENTS.md ולפרומפט של ה Code Review שלנו הוראות מפורשות לזיהוי ותיקון שגיאות N+1 כדי לצמצם אותן משמעותית.