Free

לתת ל-Claude למדל schema רב-לשוני ב-Rails עם jsonb

Schema רב-לשוני ב-Rails: jsonb מנצח 19 טבלאות/עמודות, אילוצים מדורגים, שאילתות ->>, תזמון אינדוקס.


how2claude מפרסם כל מאמר ב-19 שפות (zh / en / ja / ko / ar / ...), ואיך לאחסן את זה בשכבת הנתונים צריך להחליט מראש. תבקש מ-Claude לעצב את הטבלה וסביר שתקבל 19 עמודות string, או גרוע מכך, 19 טבלאות — "טבלת articles לכל שפה". שניהם מלכודות.

התשובה הנכונה היא טבלה אחת + 3 עמודות jsonb, כל jsonb עם locale כמפתח. המאמר מסביר למה הגישה הזו מנצחת, איך לכתוב את ה-schema, איך לשאול, מתי לאנדקס, ו-4 הכיוונים שבהם Claude טועה כברירת מחדל.


שלוש אפשרויות על השולחן

פרוש את כל המועמדים:

אפשרות A: 19 טבלאות (articles_zh, articles_en, ...)
- להביא מאמר אחד דורש 19 join / 19 שאילתות
- הוספת שפה 20 = CREATE TABLE
- מחיקת מאמר → קסקייד על 19 טבלאות
- יתרון: שאילתות טבלה-יחידה מהירות
- מציאות: אלא אם לוגיקת התצוגה באמת שונה לפי שפה (כמעט אף פעם), זו חלוקה מופרזת

אפשרות B: 19 עמודות string (title_zh, title_en, ..., summary_zh, summary_en, ...)
- הטבלה הופכת ענקית ברוחב (3 תוכנים × 19 שפות = 57 עמודות)
- רוב התאים NULL (תרגום לתאית לרוב מגיע אחרון או לעולם לא)
- הוספת שפה 20 = ALTER TABLE ADD COLUMN × 3
- פריסת שורה דלילה, יעילות IO גרועה

אפשרות C: 3 עמודות jsonb (title jsonb, summary jsonb, content jsonb)
- שורה אחת לכל מאמר, locale הוא מפתח בתוך ה-jsonb
- הוספת שפה לא נוגעת ב-schema (פשוט מכניסים מפתח)
- אזרח ממעלה ראשונה ב-Postgres — שאילתות אטומיות, אינדקסים, שליפות חלקיות
- תחביר שאילתה קצת רועש יותר (title->>'zh') אבל אפשר לעטוף

בחר C. שאר המאמר הוא איך לעשות C נכון.

Schema: 3 עמודות jsonb + טרייד-אוף אילוצים

טבלת articles האמיתית של how2claude (קטע):

create_table :articles do |t|
  t.jsonb :title,   default: {}, null: false
  t.jsonb :summary, default: {}
  t.jsonb :content, default: {}

  t.string :slug, null: false
  t.boolean :free, default: false, null: false
  # ... bigint אחרים, timestamp וכו'
end

למה האילוצים שונים:

  • title: null: false + default: {}. הכותרת חובה — מאמר חייב כותרת בשפה אחת לפחות. אפילו רק en, לפחות זה. כל ה-jsonb לא יכול להיות NULL.
  • summary / content: רק default: {}, NULL מותר. אלה יכולים להיות ריקים (מיד אחרי יצירה, לפני כתיבת התוכן), אז מרפים.

default: {} חובה. השמט אותו, ורשומת Article.new טרייה מקבלת title = nil, ומיד אחר כך article.title["en"] מתפוצץ: NoMethodError: undefined method '[]' for nil:NilClass. ברירת מחדל של hash ריק הופכת כל גישה לבטוחה.

כש-Claude כותב את ה-migration הזה, הוא נוטה:
- לדלג על default: {} → קריסת nil לעיל
- להוסיף null: false לכל jsonb → summary לא יכול להיות NULL ברשומות חדשות שעדיין נמצאות בהשלמה, שובר זרימה של "כותרת קודם, תוכן אחר כך"
- לכתוב default: "{}" (literal string) → Postgres שומר את המחרוזת הליטרלית "{}" במקום json ריק, חוסר התאמת טיפוס בשאילתה

חוק: ברירת המחדל של עמודת jsonb חייבת להיות {} — literal hash של Ruby, לא מחרוזת. null: false רק על ה-jsonb החובה.

שכבת המודל: לעטוף את ה-fallback

Schema לבד לא מספיק — המודל זקוק ל-accessors עם fallback:

class Article < ApplicationRecord
  validates :title, presence: true

  def title_for(locale = I18n.locale)
    title[locale.to_s] || title["en"] || title.values.first
  end

  def summary_for(locale = I18n.locale)
    summary[locale.to_s] || summary["en"]
  end

  def content_for(locale = I18n.locale)
    content[locale.to_s] || content["en"]
  end
end

Fallback בשלושה שלבים: locale נוכחי → en → מה שיש. קורא סיני נופל על מאמר ביפנית, אם אין יפנית נופל לאנגלית; אם אין גם אנגלית, נופל ל"מה שיש", לעולם לא nil.

validates :title, presence: true מטפל ב-{} אוטומטית — ActiveRecord מתייחס ל-hash ריק כ-blank, אז title ריק לא עובר וולידציה. זה התמורה של קומבו null: false + presence.

תבניות שאילתה: ->> לעומת ->

שני אופרטורי jsonb יומיומיים של Postgres:

  • title->>'zh'מחזיר text (מחרוזת)
  • title->'zh'מחזיר jsonb (שומר על טיפוס json)

99% מהזמן אתה רוצה ->> — מוציא מחרוזת ומשתמש. השתמש ב--> רק כשאתה מקונן הלאה (למשל content->'metadata'->>'author').

מאמרים עם תרגום ספציפי

Article.where("title ? :loc", loc: "zh")
# SELECT * FROM articles WHERE title ? 'zh'

? הוא אופרטור "המפתח קיים" של jsonb. מחזיר מאמרים שבהם hash ה-title מכיל את המפתח zh.

מיון לפי כותרת בשפה ספציפית

Article.order("title->>'en' ASC")

סינון + מיון לפי locale

Article.where("title ? :loc", loc: "ja")
       .order("title->>'ja' ASC")

שאילתות ש-Claude כותב שגוי כברירת מחדל

# שגוי 1: השוואת כל ה-jsonb כמחרוזת
Article.where(title: '{"zh": "..."}')

# שגוי 2: סינון ב-Ruby
Article.all.select { |a| a.title["zh"].present? }  # מושך את כל הטבלה לזיכרון

# שגוי 3: = בלי האופרטור
Article.where("title->>'zh' = ?", "איזו כותרת")  # זה נכון, אבל Claude מערבב עם שגוי 1

חוק: כל סינון / מיון / בדיקת קיום קורה ב-SQL דרך ->> או ?; אתה פונה ל-Ruby article.title["zh"] רק כשאתה מעבד עבור המשתמש.

אינדקסים: מה להוסיף, מתי

jsonb לא מאנדקס אוטומטית. שלושה צרכים נפוצים → שלושה אינדקסים:

1. שאילתות קיום / @> הכלה → אינדקס GIN

add_index :articles, :title, using: :gin

מאיץ title ? 'zh', title @> '{"zh": ...}' וכו'. GIN כבד בכתיבה ותופס מקום, אך חובה לשאילתות קיום מפתח/ערך ב-jsonb.

2. מיון / שוויון על locale ספציפי → אינדקס ביטוי

add_index :articles, "(title->>'en')", name: "idx_articles_title_en"

מאיץ רק title->>'en' = ? או ORDER BY title->>'en'. אם אתה ממיין רק לפי כותרת אנגלית (למשל רשימה אלפביתית באתר אנגלי), אינדקס ביטוי אחד באנגלית מספיק.

3. אין שאילתה בתדירות גבוהה → לא מוסיפים

אינדקסי jsonb הם אובייקטים יקרים. אם כמות המאמרים קטנה (אלפים) וכל השאילתות לפי id/slug, אל תאנדקס מראש את ה-jsonb. חכה עד שיעלה צורך שאילתה ברור.

ל-how2claude כרגע אין אינדקס jsonb על title/summary/content — היקף נמוך, שאילתות לפי slug, אינדקס יהיה בזבוז מקום גרידא. יתווסף כשיצמח לנקודה מסוימת (נאמר, 10 אלף מאמרים + שאילתות בכמות תכופות).

4 הכיוונים שבהם Claude טועה כברירת מחדל

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

1. משתמש ב-jsonb כדלת יציאה "אני לא יודע את ה-schema"

"אני לא בטוח מה השדה הזה יחזיק, אעשה jsonb..."

הפנה מחדש: jsonb מתאים למצבים עם מבנה יציב אך מערך מפתחות פתוח או דליל-רחב — רב-לשוניות, feature flags, העדפות משתמש. לא תירוץ לדלג על עיצוב schema. אם יש לך מערך מפתחות ברור (נאמר 5 שדות קבועים), השתמש בעמודות רגילות.

2. default: "{}" כמחרוזת

הפנה מחדש: Rails ישמור את זה כמחרוזת ליטרלית "{}" ב-Postgres, לא כ-json ריק. אחרי ה-migration, article.title["zh"] מתפוצץ (מחרוזות לא נגישות כך). חייב להיות default: {} (literal hash של Ruby).

3. כל עמודת jsonb מקבלת null: false

הפנה מחדש: שאל קודם מה באמת חובה. Title חובה (מאמר בלי כותרת חסר משמעות) → null: false. summary ו-content יכולים להיות ריקים (שלב הטיוטה) → רק default: {}, בלי null: false. דרג אילוצים; אל תבזבז פצצות שטיח.

4. סינון jsonb בשכבת היישום

"אני עושה Article.all ואז .select { |a| a.title["zh"] }..."

הפנה מחדש: כל המהות של jsonb היא תמיכה מקורית ב-SQL. כל סינון / קיום / מיון חי ב-Postgres דרך ->> / ? / @>. תגרור חזרה ל-Ruby, וברגע שהטבלה מגיעה לאלף שורות, אתה מת.

רשימת בדיקה

לתת ל-Claude לעצב schema רב-לשוני של Rails ב-jsonb — 6 כללים:

  1. פסול 19 טבלאות / 19 עמודות קודם. אלא אם לוגיקת התצוגה שונה מהותית לפי locale, jsonb הוא התשובה היחידה.
  2. 3 עמודות jsonb + אילוצים מדורגים: title null: false, default: {}; summary / content רק default: {}.
  3. default: {} הוא literal hash, לא מחרוזת. מחרוזת "{}" גורמת לחוסר התאמת טיפוס.
  4. המודל מספק accessors עם fallback: current → en → .values.first. תצוגות לעולם לא רואות nil.
  5. שאילתות ב-SQL: title ? 'zh' (קיום), title->>'zh' (ערך/מיון). אל תגרור ל-Ruby לסינון.
  6. חכה לצורך שאילתה אמיתי לפני אינדוקס. הוספת GIN מונעת היא הנדסת יתר; כמויות קטנות עם lookups לפי slug/id לא צריכות שום אינדקס jsonb.

ההחלטה האמיתית אינה "jsonb או לא" — עם 19 שפות התשובה ברורה. ההחלטות האמיתיות הן אילו שדות חובה לעומת אופציונליים, איך שרשרת ה-fallback של המודל עובדת, מתי לעבור מ"עדיין בלי אינדקס" ל"הוסף אינדקס ביטוי". אלה החלטות מוצר ונפח. Claude נותן לך קוד אך לא מקבל את ההחלטות האלה במקומך.