ActiveRecord и PostgreSQL JSONB: когда обычных таблиц уже недостаточно

Вы когда-нибудь чувствовали, что реляционные таблицы — это как пытаться впихнуть квадратный колышек в круглое отверстие? Когда очередной users с 50 колонками превращается в ад миграций, а serialize :preferences, JSON просто смеётся вам в лицо своей неэффективностью — пора знакомиться с PostgreSQL JSONB.


В этой статье разберём, как грамотно использовать JSONB в Rails-приложениях, чтобы сохранить и производительность, и рассудок.

🧠 Теория: JSONB vs JSON vs Hstore

PostgreSQL предлагает три способа хранить “нереляционные” данные:

Тип Индексирование Поддержка вложенности Изменяемость
JSON Нет Да Да
JSONB Да Да Да
Hstore Да Нет Да

JSONB — это “бинарный JSON” с:

  • Поддержкой индексов (GIN/GIST)
  • Быстрым поиском по вложенным полям
  • Автоматической валидацией структуры

🔧 Практика: добавляем JSONB в Rails

Допустим, у нас есть модель User с динамическими настройками:

# Миграция
class AddPreferencesToUsers < ActiveRecord::Migration[7.0]
  def change
    add_column :users, :preferences, :jsonb, default: {}
    add_index :users, :preferences, using: :gin  # Для быстрого поиска
  end
end

Теперь можно работать как с обычным хэшем:

user = User.create!(
  email: "dev@example.com",
  preferences: {
    theme: "dark",
    notifications: {
      email: true,
      push: false
    }
  }
)

user.preferences.dig(:notifications, :email) # => true

💡 Когда использовать JSONB?

  1. Динамические атрибуты (настройки, метаданные)
  2. Лёгкие NoSQL-структуры (например, кэшированные данные API)
  3. Быстрое прототипирование (когда схема ещё не устаканилась)

Но помните: если данные критичны для бизнеса и требуют сложных JOIN — возможно, это повод для отдельной таблицы.


🚀 Продвинутые фишки

1. Поиск по вложенным полям

# Найти всех с тёмной темой
User.where("preferences->>'theme' = ?", "dark")

# Те, кто отключил email-уведомления
User.where("preferences->'notifications'->>'email' = ?", "false")

2. Частичное обновление

user.update!(
  "preferences.notifications.push": true
)

3. Дефолтные значения через модель

class User < ApplicationRecord
  attribute :preferences, :jsonb, default: -> {
    {
      theme: "light",
      notifications: {
        email: true
      }
    }
  }
end

💣 Антипаттерны

1. JSONB как мусорка

# Плохо: всё подряд
{
  last_login_ip: "...",
  cached_api_response: "...",
  temp_flags: [...]
}

# Лучше: только логически связанные данные
{
  ui_settings: { ... },
  notification_rules: [ ... ]
}

2. Отсутствие схемы

Если у вас есть чёткие требования к структуре — добавьте валидацию:

validate :preferences_schema

def preferences_schema
  errors.add(:preferences, "must include theme") unless preferences.key?(:theme)
end

3. Игнорирование индексов

Без индексов поиск по JSONB превращается в O(n):

# В миграции
add_index :users, "(preferences->>'theme')", name: "index_users_on_preferences_theme"

🧪 Тестирование

describe User do
  describe "preferences" do
    let(:user) { build(:user, preferences: { theme: "dark" }) }

    it "validates presence of theme" do
      user.preferences.delete(:theme)
      expect(user).not_to be_valid
    end

    it "allows nested updates" do
      user.update!("preferences.notifications.email" => false)
      expect(user.preferences.dig(:notifications, :email)).to eq false
    end
  end
end

🎤 Что сказать на собеседовании

— Почему вы выбрали JSONB вместо отдельной таблицы для настроек?

— Мы храним только слабоструктурированные данные с низкой волатильностью. JSONB даёт нам гибкость без потерь производительности благодаря индексам GIN, при этом сохраняя возможность сложных запросов через PostgreSQL-операторы.


� Вывод

JSONB в PostgreSQL — это как швейцарский нож:

  • Режет проблемы с динамическими атрибутами
  • Открывает бутылки с производительностью
  • И даже может пригодиться в неожиданных ситуациях

Главное — не пытаться забивать им гвозди (читай: использовать для всего подряд). Когда схема стабилизируется — возможно, пришло время для “настоящих” таблиц.

P.S. Если ваш JSONB-поле стало глубже, чем Марианская впадина — это верный знак, что пора рефакторить.

🗓 Дата публикации: 18.09.2024, но это не точно...

Ruby PostgreSQL JSONB Rails базы данных миграции