🧩

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専用だ。

キーポイント

1

マイグレーションでjsonbカラム追加 — t.jsonb :settings, default: {}, null: false

2

モデルに store_accessor :settings, :theme, :language を宣言

3

型が必要なら attribute :theme, :string でキャスト追加

4

検索が必要ならGINインデックス — add_index :users, :settings, using: :gin

5

内部キーで照会 — settings->>theme 演算子、または包含検索 settings @> {...} 演算子を使う

メリット

  • カラム追加なしで流動的データを管理
  • GINインデックスでJSON内部の高速検索(PostgreSQL)
  • store_accessorで通常の属性のようにアクセス

デメリット

  • jsonbはPostgreSQL専用(SQLiteはjsonのみ)
  • 中核の検索データを詰め込むとクエリが汚くなる
  • jsonb内部に制約/外部キーを張れない

ユースケース

ユーザー設定/環境設定の保存 外部APIレスポンスのキャッシュ スキーマが流動的なメタデータ