Free

Claude'a jsonb ile Rails çok dilli schema tasarlatmak

Çok dilli Rails schema: jsonb 19 tablo/kolonu yener, kademeli kısıtlar, ->> sorgular, indeks zamanlaması.


how2claude her makaleyi 19 dilde yayınlıyor (zh / en / ja / ko / ar / ...), ve bunu veri katmanında nasıl saklayacağın en başta belirlenmeli. Claude'a bu tabloyu tasarlatırsan muhtemelen sana 19 string kolon verir, ya da beteri, 19 tablo — "dil başına bir articles tablosu". İkisi de tuzak.

Doğru cevap 1 tablo + 3 jsonb kolon, her jsonb locale'lerin anahtar olduğu şekilde. Bu yazı neden bu yaklaşımın kazandığını, schema'yı nasıl yazacağını, nasıl sorgulayacağını, ne zaman indeksleyeceğini ve Claude'un varsayılan olarak saptığı 4 yönü anlatıyor.


Masadaki üç seçenek

Aday yaklaşımları ortaya dök:

Seçenek A: 19 tablo (articles_zh, articles_en, ...)
- Tek bir makaleyi çekmek 19 join / 19 sorgu ister
- 20. dili eklemek = CREATE TABLE
- Bir makaleyi silmek 19 tabloya cascade olur
- Artı: tek tablo sorguları hızlı
- Gerçeklik: sunum mantığı gerçekten dile göre farklı değilse (neredeyse hiç), bu aşırı bölümleme

Seçenek B: 19 string kolon (title_zh, title_en, ..., summary_zh, summary_en, ...)
- Tablo devasa genişler (3 içerik × 19 dil = 57 kolon)
- Çoğu hücre NULL (Tayca çevirisi çoğunlukla en son gelir ya da hiç gelmez)
- 20. dili eklemek = ALTER TABLE ADD COLUMN × 3
- Satır içi alanlar seyrek, IO verimliliği kötü

Seçenek C: 3 jsonb kolon (title jsonb, summary jsonb, content jsonb)
- Makale başına bir satır, locale jsonb'nin içindeki anahtar
- Dil eklemek schema'ya dokunmaz (yeni anahtar eklemek yeter)
- Postgres'te birinci sınıf vatandaş — atomik sorgular, indeksler, kısmi fetch
- Sorgu sözdizimi biraz daha gürültülü (title->>'zh') ama sarmalanabilir

C'yi seç. Bu yazının geri kalanı C'yi doğru yapmakla ilgili.

Schema: 3 jsonb kolon + kısıt ödünleşimi

how2claude'un gerçek articles tablosu (kısmen):

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
  # ... diğer bigint, timestamp vs.
end

Kısıtların neden farklı olduğu:

  • title: null: false + default: {}. Başlık zorunlu — bir makalenin en az bir dilde başlığı olmalı. Sadece en bile olsa, en az o. jsonb bütün olarak NULL olamaz.
  • summary / content: sadece default: {}, NULL serbest. Bunlar boş olabilir (oluşturulduktan hemen sonra, içerik yazılmadan önce), o yüzden gevşet.

default: {} zorunlu. Atla, yeni Article.new'lanmış bir kayıtta title = nil olur ve hemen article.title["en"] patlar: NoMethodError: undefined method '[]' for nil:NilClass. Varsayılanı boş hash yapmak tüm erişimleri güvenli kılar.

Claude bu migration'ı yazarken sık sık:
- default: {}'i atlar → yukarıdaki nil çöker
- Her jsonb'ye null: false ekler → summary yeni kayıtlarda hâlâ doldurulurken NULL olamaz, "önce başlık sonra içerik" akışı kırılır
- default: "{}" (string literali) yazar → Postgres literal string "{}"'i saklar, boş json değil, sorgu anında tip uyuşmazlığı

Kural: jsonb kolonunun varsayılanı {} Ruby hash literali olmalı, string değil. null: false sadece zorunlu jsonb'ye.

Model katmanı: fallback'i sar

Schema tek başına yetmez — model'in fallback destekli erişimcilerine ihtiyacı var:

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

Üç kademeli fallback: mevcut locale → en → ne varsa o. Japonca bir makaleye giren Çinli okuyucu, Japonca yoksa İngilizce'ye düşer; İngilizce de yoksa "orada her ne varsa"ya düşer, asla nil olmaz.

validates :title, presence: true {}'i otomatik işler — ActiveRecord boş hash'i blank sayar, bu yüzden boş title doğrulamadan geçemez. null: false + presence kombosunun getirisi bu.

Sorgu desenleri: ->> vs ->

Postgres jsonb'sinin günlük iki operatörü:

  • title->>'zh'text döner (string)
  • title->'zh'jsonb döner (json tipini korur)

Zamanın %99'unda ->> istersin — string çekip kullanırsın. ->'u sadece daha derine inerken kullan (örn. content->'metadata'->>'author').

Belirli bir çevirisi olan makaleler

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

? jsonb'nin "anahtar var" operatörüdür. Başlık hash'inde zh anahtarı olan makaleleri döner.

Belirli bir dildeki başlığa göre sırala

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

Locale'e göre filtrele + sırala

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

Claude'un varsayılan olarak yanlış yazdığı sorgular

# Yanlış 1: tüm jsonb'yi string gibi karşılaştır
Article.where(title: '{"zh": "..."}')

# Yanlış 2: Ruby tarafında filtrele
Article.all.select { |a| a.title["zh"].present? }  # tüm tabloyu belleğe çeker

# Yanlış 3: operatörsüz = kullan
Article.where("title->>'zh' = ?", "bir başlık")  # bu doğru ama Claude Yanlış 1 ile karıştırır

Kural: tüm filtre / sıralama / varlık kontrolü SQL'de ->> veya ? ile olur; sadece kullanıcıya göstermek için Ruby'de article.title["zh"]'a uzan.

İndeksler: ne, ne zaman eklenmeli

jsonb otomatik indekslenmez. Üç yaygın ihtiyaç üç tür indekse karşılık gelir:

1. Varlık / @> içerim sorguları → GIN indeksi

add_index :articles, :title, using: :gin

title ? 'zh', title @> '{"zh": ...}' vs. hızlandırır. GIN yazmada ağır ve yer kaplar, ama jsonb'nin anahtar/değer varlık sorguları için zorunlu.

2. Belirli locale üzerinde sıralama / eşitlik → ifade indeksi

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

Sadece title->>'en' = ? veya ORDER BY title->>'en''i hızlandırır. Sadece İngilizce başlığa göre sıralıyorsan (örn. İngiliz site alfabetik listesi), bir İngilizce ifade indeksi yeter.

3. Sık sorgu yok → eklemiyor

jsonb indeksleri pahalı nesnelerdir. Makale sayısı düşükse (binler) ve tüm sorgular id/slug ile ise, jsonb'yi önceden indeksleme. Net bir sorgu ihtiyacı oluşana kadar bekle.

how2claude'da şu anda title/summary/content üzerinde jsonb indeksi yok — hacim düşük, sorgular slug aramaları, indeks sadece yer israfı. Toplu sorgular sıklaşmaya başladığında (diyelim 10k makale) eklenecek.

Claude'un varsayılanda saptığı 4 yön

Bu tür schema tasarımında Claude'un ilk refleksi çoğu zaman yanılır. Her birini yakala:

1. jsonb'yi "schema'yı bilmiyorum" kaçış yolu olarak kullanır

"Bu alanın ne tutacağını bilmiyorum, jsonb yapayım..."

Yönlendir: jsonb yapısı kararlı ama anahtar kümesi açık uçlu ya da seyrek-geniş durumlara uyar — çok dilli, feature flags, kullanıcı tercihleri. Schema tasarımını atlama bahanesi değil. Net bir anahtar kümen varsa (mesela 5 sabit alan), normal kolonlar kullan.

2. default: "{}" string olarak

Yönlendir: Rails bunu Postgres'te literal string "{}" olarak saklar, boş json olarak değil. Migration sonrası article.title["zh"] patlar (string'ler böyle subscript olmaz). default: {} olmalı (Ruby hash literali).

3. Her jsonb kolonuna null: false

Yönlendir: Önce hangisinin gerçekten zorunlu olduğunu sor. Title zorunlu (başlıksız makale saçma) → null: false. summary ve content boş olabilir (taslak aşaması) → sadece default: {}, null: false yok. Kısıtları kademelendir; toptan atma.

4. jsonb'yi uygulama katmanında filtrele

"Article.all yapıp .select { |a| a.title["zh"] }..."

Yönlendir: jsonb'nin tüm olayı SQL'de yerel destek. Tüm filtre / varlık / sıralama Postgres'te ->> / ? / @> ile yaşar. Ruby'ye geri sürüklersen, tablo bin satıra ulaştığı an ölürsün.

Kontrol listesi

Claude'a jsonb ile Rails çok dilli schema tasarlatma — 6 kural:

  1. Önce 19 tablo / 19 kolon'u ele. Sunum mantığı locale'e göre esasen farklı değilse, jsonb tek cevap.
  2. 3 jsonb kolon + kademeli kısıtlar: title null: false, default: {}; summary / content sadece default: {}.
  3. default: {} bir hash literalidir, string değil. String "{}" tip uyuşmazlığı yaratır.
  4. Model fallback destekli erişimciler sağlasın: current → en → .values.first. View'lar asla nil görmez.
  5. Sorgular SQL'de: title ? 'zh' (varlık), title->>'zh' (değer/sıralama). Filtrelemek için Ruby'ye çekme.
  6. Gerçek bir sorgu ihtiyacın olana dek indekslemeyi bekle. GIN'i peşinen eklemek aşırı mühendisliktir; küçük hacimde slug/id aramaları jsonb indeksine hiç ihtiyaç duymaz.

Gerçek karar "jsonb mi değil mi" değil — 19 dilde cevap zaten belli. Gerçek kararlar şunlar: hangi alanlar zorunlu vs isteğe bağlı, modelin fallback zinciri nasıl işlesin, "henüz indeks yok"tan "ifade indeksi ekle"ye ne zaman geçilsin. Bunlar ürün ve hacim kararları. Claude kodu verir ama bu kararları senin yerine vermez.