jsonbとstore_accessor
PostgreSQLのjsonbカラムをモデル属性のように扱う
PostgreSQLのjsonbはJSONをバイナリで保存するカラム型だ。通常のjson型と違い、書き込み時に一度パースするため、読み込み時に再パースしない。
本当の違いはインデックスだ。jsonbはGINインデックスを張れるので、JSON内部のキーで検索しても速い。json型ではそれができない。
store_accessor — jsonbを属性として
1つのjsonbカラムに複数の値を入れ、モデル上では通常のカラムのように読み書きする機能だ。
class User < ApplicationRecord
store_accessor :settings, :theme, :language
end
user.theme = 'dark'
user.language = 'ko'
user.save
# settings = { "theme": "dark", "language": "ko" }
別カラムを作らなくても、settings 1つで流動的な設定値を管理する。値が増えるたびにマイグレーションを回す必要がない。
いつ使うか
スキーマが頻繁に変わる、あるいは事前に決めにくいデータに使う。ユーザー設定、外部APIレスポンスのキャッシュ、メタデータなど。
逆に、頻繁に検索・ジョインする中核データは普通にカラムに切り出す方がいい。jsonbに全部詰め込むとクエリがすぐ汚くなる。
SQLiteでは
store_accessor自体はSQLiteのjson(TEXT)カラムでも読み書きできる。ただしGINインデックスと@>、->>演算子ベースの高速検索はPostgreSQL専用だ。SQLiteでは便利機能だけ享受し、検索性能の恩恵はない。
json vs jsonb
| 項目 | json | jsonb |
|---|---|---|
| 保存方式 | テキスト原本そのまま | バイナリパース後保存 |
| 読み込み速度 | 毎回パース | パース不要 → 速い |
| GINインデックス | ❌ | ✅ |
| 内部キー検索 | 遅い | 速い |
検索演算子
User.where("settings->>'theme' = ?", 'dark')
Product.where("metadata @> ?", { category: 'book' }.to_json)
⚠ 本プロジェクトはSQLite
SQLiteではjsonbの代わりにt.jsonを使う。store_accessorの属性管理の便利さは動くが、GINインデックスと高速検索はPostgreSQL専用だ。
キーポイント
マイグレーションでjsonbカラム追加 — t.jsonb :settings, default: {}, null: false
モデルに store_accessor :settings, :theme, :language を宣言
型が必要なら attribute :theme, :string でキャスト追加
検索が必要ならGINインデックス — add_index :users, :settings, using: :gin
内部キーで照会 — settings->>theme 演算子、または包含検索 settings @> {...} 演算子を使う
メリット
- ✓ カラム追加なしで流動的データを管理
- ✓ GINインデックスでJSON内部の高速検索(PostgreSQL)
- ✓ store_accessorで通常の属性のようにアクセス
デメリット
- ✗ jsonbはPostgreSQL専用(SQLiteはjsonのみ)
- ✗ 中核の検索データを詰め込むとクエリが汚くなる
- ✗ jsonb内部に制約/外部キーを張れない