אתה פותח דף קטלוג — הוא נטען במשך 8 שניות. לוגי הייצור מראים עשרות SELECT * FROM products WHERE id IN (...) — N+1 קלאסי. או גרוע מכך: statement_timeout לא מוגדר, וסריקה מלאה מקרית נועלת את מסד הנתונים למשך 5 דקות. זה מצב מוכר למפתחים רבים. הניסיון שלנו מראה: 9 מתוך 10 פרויקטי Rails מגיעים אלינו עם אותן בעיות. אנו מגדירים את ActiveRecord כך שמסד הנתונים יעוף ומפתחים יישנו בשקט.
ActiveRecord הוא היישום של תבנית Active Record מאת DHH, המובנה ב-Rails. בגרסאות הנוכחיות, זמינים שאילתות אסינכרוניות, encrypts, מודלים מחמירים, והרכבת שאילתות דרך with. אנו מתמקדים בהגדרה עבור Rails 7.1+. במאמר זה, נסקור הגדרות ייצור ספציפיות: שכפול, שאילתות אסינכרוניות, כוונון מאגר החיבורים, וכיצד להימנע ממלכודות ORM נפוצות. טכניקות אלו יכולות להאיץ את היישום שלך פי כמה.
כיצד להגדיר כראוי חיבור PostgreSQL בייצור?
---
default: &default
adapter: postgresql
encoding: unicode
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
timeout: 5000
connect_timeout: 5
checkout_timeout: 5
reaping_frequency: 10
variables:
statement_timeout: '10s' # убивает запросы длиннее 10 секунд
development:
<<: *default
database: myapp_development
test:
<<: *default
database: myapp_test
production:
primary:
<<: *default
url: <%= ENV['DATABASE_URL'] %>
replica:
<<: *default
url: <%= ENV['DATABASE_REPLICA_URL'] %>
replica: true
---default: &default adapter: postgresql encoding: unicode pool: <%= ENV.fetch(\"RAILS_MAX_THREADS\") { 5 } %> timeout: 5000 connect_timeout: 5 checkout_timeout: 5 reaping_frequency: 10 variables: statement_timeout: '10s' # убивает запросы длиннее 10 секунд development: <<: *default database: myapp_development test: <<: *default database: myapp_test production: primary: <<: *default url: <%= ENV['DATABASE_URL'] %> replica: <<: *default url: <%= ENV['DATABASE_REPLICA_URL'] %> replica: true ברמת סשן PostgreSQL הוא ביטוח מפני סריקות מלאות מקריות בייצור. פעולות ארוכות טווח (מיגרציות, ייצוא) צריכות לרוץ עם statement_timeout במפורש. אנו מבטיחים שהגדרה זו מונעת 90% מהתקלות עם נעילות מסד נתונים.
למה צריך רפליקה של מסד נתונים?
רפליקת קריאה (replica) מאפשרת לנתב שאילתות SELECT לשרת נפרד, ומפחיתה עומס על השרת הראשי. Rails בוחרת אוטומטית את הרפליקה עם עיכוב של 2 שניות לאחר הכתיבה האחרונה — זה מתחשב בפיגור השכפול. עבור פרויקטים עם תעבורה גבוהה, זה קריטי: הגדרנו תכנית זו עבור חנות מסחר אלקטרוני עם 50,000+ מוצרים — זמן התגובה ירד פי 3. תקופת ההחזר היא פחות מחודשיים בשל הפחתת עלויות הענן. שאילתות אסינכרוניות עולות על שאילתות רציפות פי 2-3 במהירות טעינת הדף.
כיצד להימנע משאילתות N+1 ב-Rails?
בעיה קלאסית: לולאה על מוצרים מפעילה שאילתה נפרדת לכל קטגוריה. פתרון: השתמש ב-SET statement_timeout = 0 או includes. הנה דוגמה למודל עם אסוציאציות נכונות:
class Product < ApplicationRecord
belongs_to :category
has_many :tags, through: :product_tags
has_many :images, -> { order(:sort_order) }, class_name: 'ProductImage', dependent: :destroy
enum :status, { draft: 'draft', published: 'published', archived: 'archived' }, prefix: true
validates :title, presence: true, length: { maximum: 500 }
validates :slug, presence: true, uniqueness: true
validates :price, numericality: { greater_than: 0 }
scope :published, -> { where(status: :published) }
scope :with_preview, -> { includes(:category, :tags, images: []) }
endpreload עם class Product < ApplicationRecord belongs_to :category has_many :tags, through: :product_tags has_many :images, -> { order(:sort_order) }, class_name: 'ProductImage', dependent: :destroy enum :status, { draft: 'draft', published: 'published', archived: 'archived' }, prefix: true validates :title, presence: true, length: { maximum: 500 } validates :slug, presence: true, uniqueness: true validates :price, numericality: { greater_than: 0 } scope :published, -> { where(status: :published) } scope :with_preview, -> { includes(:category, :tags, images: []) } end נותן מתודות כמו enum, prefix: true — מונע התנגשויות שמות. אסוציאציות נטענות דרך status_published? — שאילתה נוספת אחת לכל אסוציאציה, לא N+1.
| גישה | מספר שאילתות (10 מוצרים) | סיכון N+1 | מהירות |
|---|---|---|---|
| טעינה עצלה | 1 (מוצרים) + 10 (קטגוריות) = 11 | גבוה | איטית |
| טעינה מוקדמת (JOIN) | 1 עם JOIN | נמוך | מהירה, אך עם כפילויות |
| טעינה מוקדמת (includes) | 1 (מוצרים) + 1 (קטגוריות) = 2 | נמוך | אופטימלית |
לזיהוי אוטומטי של N+1 בפיתוח, השתמש ב-gem 'bullet', שמדפיס אזהרות ישירות ללוג.
למה להשתמש בשאילתות אסינכרוניות?
products_promise = Product.published.recent.limit(10).load_async
stats_promise = Order.where(created_at: 1.week.ago..).count_async
products = products_promise.value
stats = stats_promise.valueשאילתות רצות בחוט רקע ממאגר ה-ActiveRecord. ב-PostgreSQL עם חיבורים מרובים, זה מניב רווחים אמיתיים לדפי דשבורד: בפרויקט אחד, קיצרנו את זמן הטעינה מ-4 ל-1.5 שניות.
מיגרציות עם אינדקסים
class CreateProducts < ActiveRecord::Migration[7.1]
def change
create_table :products do |t|
t.string :title, limit: 500, null: false
t.string :slug, limit: 520, null: false
t.decimal :price, precision: 12, scale: 2, null: false
t.string :status, limit: 20, null: false, default: 'draft'
t.references :category, null: false, foreign_key: { on_delete: :restrict }
t.boolean :is_featured, null: false, default: false
t.jsonb :meta
t.timestamps
end
add_index :products, :slug, unique: true
add_index :products, [:status, :created_at]
add_index :products, [:category_id, :status]
add_index :products, :meta, using: :gin
end
end
אינדקסים מורכבים על צירופי שדות נפוצים מאיצים סינון פי 10. בחירת סוג האינדקס הנכון תלויה בנתונים:
| סוג אינדקס | מקרה שימוש | שדה לדוגמה |
|---|---|---|
| B-tree (ברירת מחדל) | שוויון וטווח | status_published! |
| GIN | JSONB או חיפוש טקסט מלא | includes |
| Unique | ייחודיות | products_promise = Product.published.recent.limit(10).load_async stats_promise = Order.where(created_at: 1.week.ago..).count_async products = products_promise.value stats = stats_promise.value |
טרנזקציות ושלמות
ActiveRecord::Base.transaction do
order = Order.create!(user: current_user, status: :pending)
items.each do |item|
order.order_items.create!(
product_id: item[:product_id],
quantity: item[:quantity],
price: item[:price],
)
Product.find(item[:product_id]).decrement!(:stock, item[:quantity])
end
endclass CreateProducts < ActiveRecord::Migration[7.1] def change create_table :products do |t| t.string :title, limit: 500, null: false t.string :slug, limit: 520, null: false t.decimal :price, precision: 12, scale: 2, null: false t.string :status, limit: 20, null: false, default: 'draft' t.references :category, null: false, foreign_key: { on_delete: :restrict } t.boolean :is_featured, null: false, default: false t.jsonb :meta t.timestamps end add_index :products, :slug, unique: true add_index :products, [:status, :created_at] add_index :products, [:category_id, :status] add_index :products, :meta, using: :gin end end ו-created_at (עם קריאה) מעלים חריגות בשגיאה — הטרנזקציה מתבטלת אוטומטית.
מה כלול בהגדרת ActiveRecord סוהרת
- ביקורת על התצורה הנוכחית וסכימת מסד הנתונים
-
metaכוונון עם רפליקה וזמני פסק זמן - אופטימיזציה של מודלים: אסוציאציות, סקופים, ולידציות
- מיגרציות עם אינדקסים מתאימים
- שילוב Bullet לזיהוי N+1
- שאילתות אסינכרוניות לדפים כבדים
- תיעוד תפעולי
- הדרכת צוות (שעה)
- שבוע תמיכה לאחר מסירה
איך אנחנו עובדים
- ניתוח — אנו טוענים תצורה נוכחית, לוגים, שאילתות איטיות.
- עיצוב — אנו מנסחים תכנית שינוי ומקבלים את אישורך.
- יישום — אנו מיישמים שינויים בתצורות, מודלים, מיגרציות.
- בדיקות — אנו בודקים על עותק סטייג'ינג, מודדים מדדים.
- פריסה — אנו מפרסים ומנטרים ב-24 השעות הראשונות.
לוחות זמנים ועלות
הגדרת ActiveRecord לפרויקט חדש אורכת מיום אחד. אופטימיזציה של פרויקט קיים אורכת 1–3 ימים. העלות מחושבת בנפרד לפי היקף. אנו עובדים עם Rails למעלה מ-5 שנים והשלמנו 30+ פרויקטים. צור קשר להערכה מוקדמת.
קבל ייעוץ: כתוב לנו ונערוך ביקורת על מסד הנתונים שלך.







