Перейти к основному содержимому

Работа с базами данных из Ruby

Разработчику Архитектору

Три уровня данных в Ruby-приложении

  1. ФайлыFile, IO, каталоги на диске.
  2. Объекты в памяти — строки, хэши, модели до записи в БД.
  3. СУБД — SQL через gem (pg, mysql2) или Active Record / Sequel (ORM).

Цепочка типична: файл/CSV → объект → SQL → объект. Ниже — файлы, затем подключение к PostgreSQL и паттерны ORM.

Смежно: Основы БД · SQL


Как Ruby работает с файлами

Работа с файлами в Ruby реализована посредством класса File, а также вспомогательных классов Dir, Pathname, Tempfile и модуля FileUtils. Все операции с файлами в Ruby происходят в рамках потоков ввода-вывода, представленных классом IO — родительским для File, Socket, StringIO и других.


Файловые дескрипторы и потоки

Каждый открытый файл в Ruby представлен экземпляром File, который инкапсулирует файловый дескриптор — целочисленный идентификатор, выданный ядром ОС. Дескрипторы управляются операционной системой, а Ruby предоставляет высокоуровневый API для их использования.

file = File.open('data.txt', 'r')
# ... работа с файлом
file.close

Разбор:

  • Фрагмент показывает конкретный сценарий, который стартует со строки file = File.open('data.txt', 'r') и задаёт контекст выполнения.
  • Ключевые элементы блока — file, File, open, data, txt, они определяют основную логику примера.
  • По шагам код выполняется так: file = File.open('data.txt', 'r') -> # ... работа с файлом -> file.close.
  • Практически важно добавить обработку ошибок и явные проверки входа, чтобы исключить скрытые падения в рантайме.
  • Типичная ошибка при развитии такого кода — смешивать бизнес-правила и инфраструктурные детали в одном месте; лучше разделять ответственность.

Рекомендуемый способ — использование блоковой формы File.open, при которой Ruby автоматически гарантирует закрытие файла по выходу из блока, даже при возникновении исключения:

File.open('data.txt', 'r') do |f|
content = f.read
# обработка
end
# файл гарантируется закрыт

Разбор:

  • Фрагмент показывает конкретный сценарий, который стартует со строки File.open('data.txt', 'r') do |f| и задаёт контекст выполнения.
  • Ключевые элементы блока — File, open, data, txt, r, они определяют основную логику примера.
  • По шагам код выполняется так: File.open('data.txt', 'r') do |f| -> content = f.read -> # обработка -> end.
  • Практически важно добавить обработку ошибок и явные проверки входа, чтобы исключить скрытые падения в рантайме.
  • Типичная ошибка при развитии такого кода — смешивать бизнес-правила и инфраструктурные детали в одном месте; лучше разделять ответственность.

Режимы доступа

Режимы открытия файлов ('r', 'w', 'a', 'r+', 'w+', 'a+') определяют, как будет использоваться файл — только для чтения, для записи с обнулением, для дописывания, совмещённый доступ и т.д. Каждый режим переводится в соответствующие флаги системного вызова open(2).

Важно: Ruby не накладывает собственных ограничений на размер обрабатываемых файлов, но при работе с большими объёмами данных необходимо учитывать потребление памяти. Методы вроде read загружают содержимое целиком, тогда как each_line, readpartial, gets позволяют читать потоково, построчно или блоками.


Кодировки и бинарные данные

Ruby с версии 1.9 обладает встроенной поддержкой многобайтовых кодировок. По умолчанию строки создаются с кодировкой Encoding.default_external, обычно UTF-8. При открытии файла можно явно указать кодировку:

File.open('data.txt', 'r:UTF-8') { |f| f.read }

Разбор:

  • Фрагмент показывает конкретный сценарий, который стартует со строки File.open('data.txt', 'r:UTF-8') { |f| f.read } и задаёт контекст выполнения.
  • Ключевые элементы блока — File, open, data, txt, r:UTF, они определяют основную логику примера.
  • По шагам код выполняется так: File.open('data.txt', 'r:UTF-8') { |f| f.read }.
  • Практически важно добавить обработку ошибок и явные проверки входа, чтобы исключить скрытые падения в рантайме.
  • Типичная ошибка при развитии такого кода — смешивать бизнес-правила и инфраструктурные детали в одном месте; лучше разделять ответственность.

Для бинарных данных используется модификатор 'b', отключающий автоматическое преобразование окончаний строк и интерпретацию байтов как символов:

File.open('image.png', 'rb') { |f| f.read }

Разбор:

  • Фрагмент показывает конкретный сценарий, который стартует со строки File.open('image.png', 'rb') { |f| f.read } и задаёт контекст выполнения.
  • Ключевые элементы блока — File, open, image, png, rb, они определяют основную логику примера.
  • По шагам код выполняется так: File.open('image.png', 'rb') { |f| f.read }.
  • Практически важно добавить обработку ошибок и явные проверки входа, чтобы исключить скрытые падения в рантайме.
  • Типичная ошибка при развитии такого кода — смешивать бизнес-правила и инфраструктурные детали в одном месте; лучше разделять ответственность.

Таким образом, работа с файлами в Ruby — это управляемая, кодировко-осознанная, потоково-ориентированная система ввода-вывода, строго привязанная к семантике операционной системы, но при этом адаптированная под удобство разработчика.


Как Ruby работает с данными в памяти

Прежде чем данные попадут в базу или будут прочитаны из неё, они проходят стадию жизни в оперативной памяти. В Ruby любые данные представлены объектами — экземплярами классов — String, Integer, Array, Hash, Time, BigDecimal, пользовательские классы и т.д.


Изменяемость и неизменяемость

Ruby следует стратегии "по умолчанию — мутабельность" — большинство встроенных структур (String, Array, Hash) позволяют изменять своё состояние in-place. Пример:

arr = [1, 2, 3]
arr << 4 # изменяет сам объект `arr`, а не создаёт новый

Разбор:

  • Фрагмент показывает конкретный сценарий, который стартует со строки arr = [1, 2, 3] и задаёт контекст выполнения.
  • Ключевые элементы блока — arr, arr, arr, они определяют основную логику примера.
  • По шагам код выполняется так — arr = [1, 2, 3] -> arr << 4 # изменяет сам объект arr, а не создаёт новый.
  • Практически важно добавить обработку ошибок и явные проверки входа, чтобы исключить скрытые падения в рантайме.
  • Типичная ошибка при развитии такого кода — смешивать бизнес-правила и инфраструктурные детали в одном месте; лучше разделять ответственность.

Для фиксации состояния используют freeze, а для безопасного копирования — методы dup и clone. В многопоточной среде и в длинных цепочках преобразований часто выбирают стиль, где методы (map, select, reduce) возвращают новые объекты и не меняют исходные.


Сериализация и десериализация

Для сохранения состояния объекта в файл или передачи по сети применяется сериализация. Ruby поддерживает несколько форматов:

  • Marshal — встроенный механизм двоичной сериализации, быстрый и компактный, но не совместим между версиями Ruby, не безопасен для десериализации из ненадёжных источников (возможен RCE), и не поддерживает кросс-языковое чтение;
  • JSON — через JSON.generate / JSON.parse; требует, чтобы объекты были совместимы с JSON-моделью (только nil, true, false, числа, строки, массивы, хэши с символьными или строковыми ключами);
  • YAML — через YAML.dump / YAML.load; выразительный, человекочитаемый, но медленнее JSON и потенциально опасен при десериализации (CVE-2013-0156 и другие уязвимости);
  • CSV, XML, protobuf, MessagePack — через внешние библиотеки (csv, nokogiri, google-protobuf, msgpack соответственно).

Сериализация — это ключевой мост между оперативным представлением данных и их внешним хранением, будь то файл, сетевой пакет или строка SQL-запроса.


Временные зоны, форматы дат и локализация

Ruby различает Time (основан на системном времени Unix, обычно UTC) и DateTime/Date (из стандартной библиотеки date). Класс Time поддерживает временную зону, но по умолчанию не хранит её метаданные — только смещение в момент конвертации. Для правильной работы с часовыми поясами рекомендуется:

  • использовать UTC внутри приложения;
  • конвертировать в локальное время только при вводе/выводе;
  • применять библиотеку tzinfo (входит в Rails) для работы с именованными зонами (например, 'Europe/Moscow'), а не только с UTC-смещениями.

Это особенно важно при сохранении временных меток в базе: хранение timestamp without time zone вместо timestamptz — частая ошибка, ведущая к рассинхронизации приложений в распределённых системах.


Как Ruby работает с базами данных и СУБД

Непосредственное взаимодействие Ruby с СУБД происходит через драйверы — библиотеки, реализующие протокол общения с конкретной системой (PostgreSQL, MySQL, SQLite и др.). Ruby не включает в стандартную поставку универсальный драйвер для СУБД, но предлагает Database Interface (DBI) — исторически первая попытка унификации, и, более современный и широко принятый стандарт — Ruby DB API, реализованный в виде интерфейса Database Connector.


Архитектура подключения — драйвер, адаптер и приложение

В типичной схеме:

  1. Драйвер (native driver) — низкоуровневая библиотека, написанная на C/Ruby, реализующая протокол СУБД (например, pg для PostgreSQL, mysql2 для MySQL, sqlite3 для SQLite). Она управляет соединением, отправкой байтовых пакетов, разбором ответов, обработкой ошибок на уровне протокола.
  2. Адаптер (adapter) — прослойка, нормализующая API разных драйверов под единый интерфейс. Часто реализуется в рамках ORM (например, ActiveRecord::ConnectionAdapters::PostgreSQLAdapter).
  3. Уровень приложения — код бизнес-логики, использующий единый API без привязки к конкретной СУБД.

Play ITЗагрузка интерактивного демо…


Пример — прямая работа через драйвер pg

Код ITЗагрузка примера кода…

Разбор:

  • Фрагмент показывает конкретный сценарий, который стартует со строки require 'pg' и задаёт контекст выполнения.
  • Ключевые элементы блока: SELECT, они определяют основную логику примера.
  • По шагам код выполняется так: require 'pg' -> conn = PG.connect( -> host — 'localhost', -> dbname: 'myapp_dev',.
  • Для производительности важны индексы по условиям выборки, проверка плана выполнения и контроль N+1 при связях.
  • Типичная ошибка при развитии такого кода — смешивать бизнес-правила и инфраструктурные детали в одном месте; лучше разделять ответственность.

Здесь:

  • PG.connect устанавливает TCP-соединение с PostgreSQL, проходит аутентификацию (например, по md5 или scram-sha-256);
  • exec отправляет подготовленный запрос с параметризацией, предотвращая SQL-инъекции на уровне драйвера (значения $1, $2 передаются отдельно от текста запроса);
  • res — объект PG::Result, реализующий перебор строк без загрузки всего результата в память;
  • res.clear освобождает память, выделенную драйвером под ответ (важно при работе с большими выборками);
  • conn.close закрывает TCP-соединение.

Обратите внимание: даже на этом уровне Ruby-разработчик не работает с сырыми сокетами. Драйвер берёт на себя:

  • управление буферами;
  • повторные попытки при обрыве соединения (в некоторых реализациях);
  • конвертацию типов PostgreSQL (int4Integer, timestamptzTime, jsonbHash);
  • обработку уведомлений LISTEN/NOTIFY;
  • поддержку COPY для массовой загрузки.

Подключение к другим СУБД

Для MySQL используется драйвер mysql2. Его API похож на pg, но детали отличаются:

client = Mysql2::Client.new(host: 'localhost', username: 'root')
results = client.query('SELECT * FROM users')
results.each { |row| ... }

Разбор:

  • Фрагмент показывает конкретный сценарий, который стартует со строки client = Mysql2::Client.new(host: 'localhost', username: 'root') и задаёт контекст выполнения.
  • Ключевые элементы блока: SELECT, они определяют основную логику примера.
  • По шагам код выполняется так: client = Mysql2::Client.new(host: 'localhost', username: 'root') -> results = client.query('SELECT * FROM users') -> results.each { |row| ... }.
  • Для производительности важны индексы по условиям выборки, проверка плана выполнения и контроль N+1 при связях.
  • Типичная ошибка при развитии такого кода — смешивать бизнес-правила и инфраструктурные детали в одном месте; лучше разделять ответственность.

Mysql2::Result тоже поддерживает потоковый перебор. Для безопасной параметризации используйте prepare + execute, а не ручную сборку SQL-строк.

SQLite3 работает через SQLite3::Database, полностью встраиваемую библиотеку — соединение устанавливается с файлом .db, а не с сетевым хостом.


Пул соединений

В веб-приложениях соединение с БД нельзя открывать/закрывать на каждый HTTP-запрос — это приведёт к исчерпанию лимитов СУБД и высокой задержке. Поэтому используется пул соединений — ограниченный набор открытых соединений, из которого приложение берёт свободное на время обработки запроса, а затем возвращает его в пул.

Драйверы вроде pg и mysql2 сами по себе не реализуют пул — он строится на уровне адаптера (например, в ActiveRecord) или с помощью отдельных библиотек (connection_pool). Пример минимального пула:

Код ITЗагрузка примера кода…

Это критически важный механизм масштабируемости: без пула даже десяток параллельных запросов могут привести к отказу СУБД.


Обработка транзакций

Транзакции в Ruby реализуются через явные вызовы BEGIN, COMMIT, ROLLBACK или через методы-обёртки драйверов/адаптеров:


Разбор:
- Фрагмент показывает конкретный сценарий, который стартует со строки `Это критически важный механизм масштабируемости: без пула даже десяток параллельных запрос` и задаёт контекст выполнения.
- Ключевые элементы блока: `Ruby`, `BEGIN`, `COMMIT`, `ROLLBACK`, они определяют основную логику примера.
- По шагам код выполняется так: `Это критически важный механизм масштабируемости: без пула даже десят` -> `---` -> `#### Обработка транзакций` -> `Транзакции в Ruby реализуются через явные вызовы `BEGIN`, `COMMIT`, `.
- Практически важно добавить обработку ошибок и явные проверки входа, чтобы исключить скрытые падения в рантайме.
- Типичная ошибка при развитии такого кода — смешивать бизнес-правила и инфраструктурные детали в одном месте; лучше разделять ответственность.

conn.transaction do
conn.exec('INSERT INTO logs VALUES ($1)', ['start'])
# при исключении автоматически происходит ROLLBACK
end

Такие блоки гарантируют атомарность: либо все изменения фиксируются, либо ни одно. Поддержка вложенных транзакций (savepoints) зависит от СУБД и драйвера.

Пример транзакции с повтором при конкурентном конфликте:

Код ITЗагрузка примера кода…

Разбор:

  • User.transaction do ... end открывает атомарный блок: изменения попадут в БД только после успешного завершения всего блока.
  • User.lock.find(42) выполняет SELECT ... FOR UPDATE и берёт строковую блокировку, чтобы избежать гонки при списании баланса.
  • По шагам: открыть транзакцию -> заблокировать пользователя -> изменить balance_cents -> save! -> COMMIT; при конфликте ловим исключение и делаем retry.
  • Пара ActiveRecord::Deadlocked и ActiveRecord::SerializationFailure покрывает типичные конкурентные конфликты в PostgreSQL/MySQL под нагрузкой.
  • Экспоненциальная/линейная пауза sleep(0.05 * retries) снижает шанс повторного столкновения и уменьшает "шум" ретраев.

ORM в Ruby — смысл, границы и компромиссы

Объектно-реляционное отображение (ORM) — архитектурный паттерн, решающий фундаментальную проблему: несоответствие между парадигмами объектно-ориентированного программирования и реляционной модели данных.

Это несоответствие проявляется в нескольких плоскостях:

ПлоскостьОбъектная модельРеляционная модель
ГранулярностьОбъекты могут содержать вложенные структуры, коллекции, ссылки на другие объектыТаблицы — плоские наборы строк; связи реализуются через внешние ключи
ИдентичностьОбъект идентифицируется по ссылке (object_id) и/или бизнес-значениюСтрока идентифицируется первичным ключом (обычно суррогатным id)
НаследованиеПоддерживается на уровне языка (классы, модули, super)Не поддерживается напрямую; требует стратегий: одна таблица на иерархию, одна таблица на класс, или класс-таблица
ПоведениеДанные + методы в одном объекте (инкапсуляция)Данные отделены от логики (логика в триггерах, процедурах, либо вынесена в приложение)
Жизненный циклОбъект создаётся, изменяется, уничтожается в памяти независимо от БДСтрока существует до явного DELETE; изменения фиксируются только внутри транзакции

ORM призван смягчить это несоответствие, но не устранить его полностью. Любая ORM — это компромисс между выразительностью, производительностью и контролем.

В Ruby этот компромисс проявляется особенно чётко — язык позволяет писать очень лаконичный, "магический" код, но цена этой магии — снижение прозрачности и предсказуемости. Поэтому выбор ORM (или отказ от неё) — это стратегическое решение, влияющее на:

  • скорость разработки прототипов;
  • сложность отладки производительности;
  • гибкость при изменении схемы БД;
  • возможность использования продвинутых SQL-фич (CTE, оконные функции, полнотекстовый поиск, гео-индексы).

Сравнение подходов — raw SQL, микрослои и ORM

1. Прямой SQL ("bare metal")

Что это — использование драйверов (pg, mysql2) напрямую, без промежуточных слоёв.

Преимущества:

  • Полный контроль над запросами, индексами, планами выполнения;
  • Возможность использовать 100 % возможностей СУБД (например, JSONB-операторы в PostgreSQL, ON CONFLICT DO UPDATE, LATERAL JOIN);
  • Минимальные накладные расходы на абстракции;
  • Прозрачная отладка: текст запроса виден явно.

Недостатки:

  • Дублирование кода при повторяющихся операциях;

Разбор:
- Фрагмент показывает конкретный сценарий, который стартует со строки `Такие блоки гарантируют атомарность: либо все изменения фиксируются, либо ни одно. Поддерж` и задаёт контекст выполнения.
- Ключевые элементы блока: `JOIN`, они определяют основную логику примера.
- По шагам код выполняется так: `Такие блоки гарантируют атомарность: либо все изменения фиксируются,` -> `---` -> `### ORM в Ruby — смысл, границы и компромиссы` -> `Объектно-реляционное отображение (ORM) — **архитектурный паттерн**, `.
- Для производительности важны индексы по условиям выборки, проверка плана выполнения и контроль N+1 при связях.
- Типичная ошибка при развитии такого кода — смешивать бизнес-правила и инфраструктурные детали в одном месте; лучше разделять ответственность.

SELECT * FROM users WHERE id = ?
  • Уязвимость к SQL-инъекциям при неправильной параметризации;
  • Сложность поддержки: изменение схемы требует ручного поиска всех запросов;
  • Отсутствие автоматической синхронизации объектов с БД. Модель сущности легко "расползается" по коду.

Когда оправдан:
в высоконагруженных сервисах, аналитических задачах, ETL-процессах и микросервисах с нетиповыми запросами.


2. Микрослои — Sequel, ROM

Что это — библиотеки, предоставляющие DSL для генерации SQL, но не навязывающие объектную модель. Пример — Sequel:


Разбор:
- Фрагмент показывает конкретный сценарий, который стартует со строки `- Уязвимость к SQL-инъекциям при неправильной параметризации;` и задаёт контекст выполнения.
- Ключевые элементы блока: `SQL`, `ETL`, `Sequel`, `ROM`, `DSL`, они определяют основную логику примера.
- По шагам код выполняется так: `- Уязвимость к SQL-инъекциям при неправильной параметризации;` -> `- Сложность поддержки: изменение схемы требует ручного поиска всех з` -> `- Отсутствие автоматической синхронизации объектов с БД. Модель сущн` -> `**Когда оправдан**:`.
- Практически важно добавить обработку ошибок и явные проверки входа, чтобы исключить скрытые падения в рантайме.
- Типичная ошибка при развитии такого кода — смешивать бизнес-правила и инфраструктурные детали в одном месте; лучше разделять ответственность.

DB[:users].where(active: true).order(:created_at).limit(10)
# → SELECT * FROM users WHERE active = true ORDER BY created_at LIMIT 10

Особенности:

  • Запросы строятся программно, через цепочки методов;
  • Результат — хэши или простые структуры (Sequel::Model опционален);
  • Поддержка сложных конструкций — CTE, UNION, подзапросы;
  • Возможность "провалиться" в raw SQL в любой момент (Sequel.lit("..."));
  • Независимость от фреймворка — работает вне Rails.

Преимущества:

  • Баланс между выразительностью и контролем;
  • Лёгкость тестирования (запросы изолированы);
  • Хорошо подходит для read-heavy приложений и API;
  • Чёткое разделение: запросдоменная сущность.

Недостатки:

  • Нет встроенной поддержки миграций, ассоциаций, валидаций (требуются доп. расширения);
  • Меньше "магии" — разработчик сам управляет загрузкой связанных данных;
  • Меньше документации и сообщества по сравнению с ActiveRecord.

3. Полноценные ORM — ActiveRecord

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

Основной тезис ActiveRecord:

"Объект, соответствующий строке в таблице, несёт в себе и данные, и поведение, и знание о том, как сохранять/загружать себя".

Это реализует принцип Active Record (Martin Fowler, Patterns of Enterprise Application Architecture), в отличие от Data Mapper (например, Hibernate, SQLAlchemy), где объекты и механизм сохранения строго разделены.


ActiveRecord — устройство и принципы

Ядро: ActiveRecord::Base

Любой класс, наследующий от ActiveRecord::Base, автоматически:

  • связывается с таблицей по соглашению (например, Userusers);
  • получает методы доступа к атрибутам (столбцам таблицы);
  • наследует CRUD-операции — create, find, save, destroy и т.д.

Пример:

Код ITЗагрузка примера кода…

Внутри происходит:

  1. При первом обращении к User ActiveRecord читает метаданные таблицы (DESCRIBE users или information_schema);
  2. Создаётся кэш столбцов — имена, типы, NULL-допуск;
  3. Для каждого столбца динамически определяются геттеры/сеттеры (name, name=);
  4. При save генерируется SQL-запрос на изменение записи.

Разбор:
- Фрагмент показывает конкретный сценарий, который стартует со строки `Внутри происходит:` и задаёт контекст выполнения.
- Ключевые элементы блока: `User`, `ActiveRecord`, `DESCRIBE`, `users`, `information_schema`, они определяют основную логику примера.
- По шагам код выполняется так: `Внутри происходит:` -> `1. При первом обращении к `User` ActiveRecord читает метаданные табл` -> `2. Создаётся кэш столбцов: имена, типы, NULL-допуск;` -> `3. Для каждого столбца динамически определяются геттеры/сеттеры (`na`.
- Практически важно добавить обработку ошибок и явные проверки входа, чтобы исключить скрытые падения в рантайме.
- Типичная ошибка при развитии такого кода — смешивать бизнес-правила и инфраструктурные детали в одном месте; лучше разделять ответственность.

UPDATE users SET name = 'Тимур' WHERE id = 42

Важно: ActiveRecord не кэширует сами данные — каждый вызов find идёт в БД (если не используется ActiveRecord::QueryCache). Это ключевое отличие от Data Mapper, где часто применяется Identity Map.


Соглашения по умолчанию

ActiveRecord строго следует принципу "convention over configuration". Основные соглашения:

ПонятиеСоглашениеПример
Таблицамножественное, нижний регистр, подчёркиванияusers, order_items
Первичный ключid, тип bigintusers.id
Внешний ключ<имя_связи>_iduser_id в orders
Метка времениcreated_at, updated_at (автообновление при save)
Полиморфная связь<имя>_id, <имя>_typecommentable_id, commentable_type
Соединительная таблицалексикографический порядок имёнposts_tags (не tags_posts)

Эти соглашения снижают количество конфигурации, но требуют строгого следования. Отклонение возможно через явные настройки (self.table_name = 'my_users'), но нарушает предсказуемость.


Ассоциации — связи между сущностями

ActiveRecord предоставляет декларативный DSL для описания отношений:

Код ITЗагрузка примера кода…

Под капотом:

  • belongs_to :user добавляет метод user, который выполняет запрос к таблице пользователей;

Разбор:
- Фрагмент показывает конкретный сценарий, который стартует со строки `Под капотом:` и задаёт контекст выполнения.
- Ключевые элементы блока: `belongs_to`, `user`, `user`, они определяют основную логику примера.
- По шагам код выполняется так: `Под капотом:` -> `- `belongs_to :user` добавляет метод `user`, который выполняет запро`.
- Практически важно добавить обработку ошибок и явные проверки входа, чтобы исключить скрытые падения в рантайме.
- Типичная ошибка при развитии такого кода — смешивать бизнес-правила и инфраструктурные детали в одном месте; лучше разделять ответственность.

SELECT * FROM users WHERE id = self.user_id
  • has_many :posts добавляет метод posts, который выполняет запрос к таблице постов;

Разбор:
- Фрагмент показывает конкретный сценарий, который стартует со строки `- `has_many :posts` добавляет метод `posts`, который выполняет запрос к таблице постов;` и задаёт контекст выполнения.
- Ключевые элементы блока: `has_many`, `posts`, `posts`, они определяют основную логику примера.
- По шагам код выполняется так: `- `has_many :posts` добавляет метод `posts`, который выполняет запро`.
- Практически важно добавить обработку ошибок и явные проверки входа, чтобы исключить скрытые падения в рантайме.
- Типичная ошибка при развитии такого кода — смешивать бизнес-правила и инфраструктурные детали в одном месте; лучше разделять ответственность.

SELECT * FROM posts WHERE user_id = self.id
  • При первом обращении к user.posts происходит ленивая загрузка (lazy loading) — запрос в БД выполняется только тогда, когда данные реально нужны.

Проблема N+1:
Если в цикле @users.each { |u| puts u.posts.count } — для каждого пользователя будет отдельный SELECT COUNT(*) FROM posts WHERE user_id = ?. Это классическая ошибка, решаемая через includes, eager_load, preload или joins.


Миграции — управление схемой как код

Миграции — это версионированный, воспроизводимый способ изменения структуры БД. Каждая миграция — Ruby-класс, наследующий от ActiveRecord::Migration:

Код ITЗагрузка примера кода…

Внутри change — идемпотентные операции. При выполнении rails db:migrate:

  1. ActiveRecord читает таблицу schema_migrations;
  2. Находит неприменённые миграции (по версии времени);
  3. Выполняет change в транзакции (если СУБД это поддерживает);
  4. Записывает версию в schema_migrations.

Миграции позволяют:

  • Синхронизировать схему между разработчиками и средами;
  • Откатывать изменения (down);
  • Генерировать schema.rb — Ruby-представление текущей схемы (для быстрой загрузки в тестах).

Ограничения:

  • Миграции не должны зависеть от данных (например, User.first.update(...) — антипаттерн);
  • Изменения в продакшене требуют осторожности: add_column с NOT NULL без default блокирует таблицу в PostgreSQL.

Коллбэки и валидации — поведение объекта

ActiveRecord интегрирует логику в жизненный цикл объекта (фрагмент ниже — Rails, не stdlib):

Код ITЗагрузка примера кода…

Коллбэки (before_save, after_create, around_destroy и др.) — это хуки, вызываемые в определённые моменты транзакции. Они удобны для:

  • нормализации данных;
  • логирования;
  • отправки уведомлений.

Но опасны при:

  • побочных эффектах вне транзакции (отправка email в after_commit — безопасно, в after_create — нет);
  • глубокой вложенности (save → коллбэк → update → другой коллбэк);
  • скрытой логике, усложняющей тестирование.

Валидации выполняются до отправки запроса в БД. Это позволяет:

  • давать человекочитаемые ошибки без round-trip к СУБД;
  • проверять сложные бизнес-правила.

Однако: валидации — не замена ограничениям БД. Уникальность email должна быть обеспечена и validates :uniqueness, и UNIQUE INDEX в БД — иначе возможны гонки при параллельных запросах.


Ленивые загрузки и выполнение запросов

ActiveRecord использует ленивые реляционные запросы (lazy relations). Выражение:


Разбор:
- Фрагмент показывает конкретный сценарий, который стартует со строки `**Коллбэки** (`before_save`, `after_create`, `around_destroy` и др.) — это хуки, вызываемы` и задаёт контекст выполнения.
- Ключевые элементы блока: `before_save`, `after_create`, `around_destroy`, `email`, `after_commit`, они определяют основную логику примера.
- По шагам код выполняется так: `**Коллбэки** (`before_save`, `after_create`, `around_destroy` и др.)` -> `- нормализации данных;` -> `- логирования;` -> `- отправки уведомлений.`.
- Практически важно добавить обработку ошибок и явные проверки входа, чтобы исключить скрытые падения в рантайме.
- Типичная ошибка при развитии такого кода — смешивать бизнес-правила и инфраструктурные детали в одном месте; лучше разделять ответственность.

users = User.where(active: true).order(:name)

не выполняет SQL. Оно возвращает объект ActiveRecord::Relation — отложенный запрос. SQL будет сгенерирован и исполнен только при:

  • переборе (each, map);
  • вызове методов, требующих данных (first, last, count, to_a);
  • явном load.

Это позволяет строить запросы по частям:


Разбор:
- Фрагмент показывает конкретный сценарий, который стартует со строки `не выполняет SQL. Оно возвращает объект `ActiveRecord::Relation` — отложенный запрос. SQL ` и задаёт контекст выполнения.
- Ключевые элементы блока: `SQL`, `ActiveRecord::Relation`, `SQL`, `each`, `map`, они определяют основную логику примера.
- По шагам код выполняется так: `не выполняет SQL. Оно возвращает объект `ActiveRecord::Relation` — о` -> `- переборе (`each`, `map`);` -> `- вызове методов, требующих данных (`first`, `last`, `count`, `to_a`` -> `- явном `load`.`.
- Практически важно добавить обработку ошибок и явные проверки входа, чтобы исключить скрытые падения в рантайме.
- Типичная ошибка при развитии такого кода — смешивать бизнес-правила и инфраструктурные детали в одном месте; лучше разделять ответственность.

scope = User.all
scope = scope.where(active: true) if params[:active]
scope = scope.limit(10) if params[:limit]
@users = scope # выполнится только при рендеринге

Но создаёт риск неожиданного запроса в "неподходящем месте" (например, внутри шаблона — это N+1 в чистом виде).


Производительность и отладка — как видеть то, что делает ActiveRecord

Логирование SQL

ActiveRecord автоматически логирует все генерируемые SQL-запросы в Rails.logger (по умолчанию — log/Разработка.log). Формат записи включает:

  • текст запроса с подставленными параметрами (в развёрнутом виде, не ?);
  • время выполнения в миллисекундах;
  • источник вызова (файл и строка, если включено config.active_record.verbose_query_logs = true).

Пример:


Разбор:
- Фрагмент показывает конкретный сценарий, который стартует со строки `Но создаёт риск неожиданного запроса в "неподходящем месте" (например, внутри шаблона — эт` и задаёт контекст выполнения.
- Ключевые элементы блока: `N`, `ActiveRecord`, `SQL`, `ActiveRecord`, `SQL`, они определяют основную логику примера.
- По шагам код выполняется так: `Но создаёт риск неожиданного запроса в "неподходящем месте" (наприме` -> `---` -> `### Производительность и отладка — как видеть то, что делает ActiveR` -> `#### Логирование SQL`.
- Практически важно добавить обработку ошибок и явные проверки входа, чтобы исключить скрытые падения в рантайме.
- Типичная ошибка при развитии такого кода — смешивать бизнес-правила и инфраструктурные детали в одном месте; лучше разделять ответственность.

User Load (0.8ms) SELECT "users".* FROM "users" WHERE "users"."id" = — [["id", 42]]

Важно: в production логирование SQL отключено по умолчанию (из соображений безопасности и производительности). При диагностике проблем его можно временно включить через:


Разбор:
- Фрагмент показывает конкретный сценарий, который стартует со строки `Важно: **в production логирование SQL отключено по умолчанию** (из соображений безопасност` и задаёт контекст выполнения.
- Ключевые элементы блока: `production`, `SQL`, они определяют основную логику примера.
- По шагам код выполняется так: `Важно: **в production логирование SQL отключено по умолчанию** (из с`.
- Практически важно добавить обработку ошибок и явные проверки входа, чтобы исключить скрытые падения в рантайме.
- Типичная ошибка при развитии такого кода — смешивать бизнес-правила и инфраструктурные детали в одном месте; лучше разделять ответственность.

ActiveRecord::Base.logger = Logger.new(STDOUT)

или на уровне запроса:


Разбор:
- Фрагмент показывает конкретный сценарий, который стартует со строки `или на уровне запроса:` и задаёт контекст выполнения.
- Ключевые действия здесь связаны с подготовкой данных, выполнением операции и фиксацией результата.
- По шагам код выполняется так: `или на уровне запроса:`.
- Практически важно добавить обработку ошибок и явные проверки входа, чтобы исключить скрытые падения в рантайме.
- Типичная ошибка при развитии такого кода — смешивать бизнес-правила и инфраструктурные детали в одном месте; лучше разделять ответственность.

User.where(active: true).explain
# → выводит EXPLAIN ANALYZE для PostgreSQL

Инструмент explain

Метод .explain вызывает EXPLAINEXPLAIN ANALYZE, если поддерживается) в СУБД и возвращает план выполнения. Для PostgreSQL:


Разбор:
- Фрагмент показывает конкретный сценарий, который стартует со строки `---` и задаёт контекст выполнения.
- Ключевые элементы блока: `explain`, они определяют основную логику примера.
- По шагам код выполняется так: `---` -> `#### Инструмент `explain`` -> `Метод `.explain` вызывает `EXPLAIN` (и `EXPLAIN ANALYZE`, если подде`.
- Практически важно добавить обработку ошибок и явные проверки входа, чтобы исключить скрытые падения в рантайме.
- Типичная ошибка при развитии такого кода — смешивать бизнес-правила и инфраструктурные детали в одном месте; лучше разделять ответственность.

User.where(email: 'timur@example.com').explain
# → Seq Scan on users (cost=0.00..22.50 rows=1 width=202)
# Filter: ((email)::text = 'timur@example.com'::text)

Если индекс по email отсутствует — будет Seq Scan. С индексом — Index Scan.
Это прямой способ проверить, используется ли индекс в конкретном контексте запроса, а не только "есть ли он в таблице".


Отладка N+1 и ленивых загрузок

Проблема N+1 возникает, когда один запрос порождает N дополнительных. ActiveRecord не предотвращает её автоматически — но предоставляет инструменты диагностики.

  1. Статический анализ: гем bullet (не для продакшена!) перехватывает запросы и предупреждает о потенциальных N+1 при загрузке страницы.

  2. Ручной аудит через логи: при включённом логировании видно множественные повторяющиеся запросы к одной таблице с разными WHERE id = ?.

  3. Принудительная строгость: в тестах или staging можно включить режим, при котором любой запрос вне транзакции или контроллера вызывает исключение:


Разбор:
- Фрагмент показывает конкретный сценарий, который стартует со строки `Если индекс по `email` отсутствует — будет `Seq Scan`. С индексом — `Index Scan`.` и задаёт контекст выполнения.
- Ключевые элементы блока: `email`, `Seq`, `Scan`, `Index`, `Scan`, они определяют основную логику примера.
- По шагам код выполняется так: `Если индекс по `email` отсутствует — будет `Seq Scan`. С индексом — ` -> `Это прямой способ проверить, используется ли индекс *в конкретном ко` -> `---` -> `#### Отладка N+1 и ленивых загрузок`.
- Практически важно добавить обработку ошибок и явные проверки входа, чтобы исключить скрытые падения в рантайме.
- Типичная ошибка при развитии такого кода — смешивать бизнес-правила и инфраструктурные детали в одном месте; лучше разделять ответственность.

# config/environments/test.rb
config.active_record.raise_in_transactional_tests = true
  1. Эвристика includes и joins:
    • includesLEFT OUTER JOIN + отдельный SELECT для ассоциаций (preload), если есть условия по связанным таблицам — переключается на eager_load (join);
    • joins → только INNER JOIN, без загрузки ассоциированных объектов (подходит для фильтрации, но не для использования полей связи);
    • preload → гарантированно два отдельных запроса: один для основной таблицы, один для связанных.

Выбор зависит от:

  • нужна ли фильтрация по полям связи (joins);
  • нужен ли доступ к полям связи (includes/preload);
  • допустима ли избыточность данных при JOIN (дублирование строк основной таблицы).

Измерение времени и профилирование

Для точного замера используется Benchmark или ActiveSupport::Notifications:


Разбор:
- Фрагмент показывает конкретный сценарий, который стартует со строки `4. **Эвристика `includes` и `joins`**:` и задаёт контекст выполнения.
- Ключевые элементы блока: `join`, `SELECT`, `JOIN`, `includes`, они определяют основную логику примера.
- По шагам код выполняется так: `4. **Эвристика `includes` и `joins`**:` -> `- `includes` → `LEFT OUTER JOIN` + отдельный `SELECT` для ассоциаций` -> `- `joins` → только `INNER JOIN`, без загрузки ассоциированных объект` -> `- `preload` → гарантированно два отдельных запроса: один для основно`.
- Для производительности важны индексы по условиям выборки, проверка плана выполнения и контроль N+1 при связях.
- Типичная ошибка при развитии такого кода — смешивать бизнес-правила и инфраструктурные детали в одном месте; лучше разделять ответственность.

ActiveSupport::Notifications.subscribe('sql.active_record') do |*args|
event = ActiveSupport::Notifications::Event.new(*args)
puts "#{event.name} – #{event.duration} ms"
end

Это позволяет строить агрегированные отчёты — сколько запросов, суммарное время, медленные операции.


Транзакции и конкурентность

Уровни изоляции

ActiveRecord позволяет задать уровень изоляции при открытии транзакции:


Разбор:
- Фрагмент показывает конкретный сценарий, который стартует со строки `Это позволяет строить агрегированные отчёты: сколько запросов, суммарное время, медленные ` и задаёт контекст выполнения.
- Ключевые элементы блока: `ActiveRecord`, они определяют основную логику примера.
- По шагам код выполняется так: `Это позволяет строить агрегированные отчёты: сколько запросов, сумма` -> `---` -> `### Транзакции и конкурентность` -> `#### Уровни изоляции`.
- Практически важно добавить обработку ошибок и явные проверки входа, чтобы исключить скрытые падения в рантайме.
- Типичная ошибка при развитии такого кода — смешивать бизнес-правила и инфраструктурные детали в одном месте; лучше разделять ответственность.

User.transaction(isolation: :serializable) do
# ...
end

Поддерживаемые уровни зависят от СУБД:

  • PostgreSQL — read_uncommitted, read_committed (по умолчанию), repeatable_read, serializable;
  • MySQL (InnoDB) — read_uncommitted, read_committed, repeatable_read (по умолчанию), serializable.

Важно: Ruby не эмулирует уровни изоляции — он просто передаёт директиву SET TRANSACTION ISOLATION LEVEL ... СУБД. Поведение определяется ядром базы данных.


Optimistic Locking

Защита от перезаписи при параллельном редактировании. Включается автоматически, если в таблице есть столбец lock_version (целое число, по умолчанию 0):

Код ITЗагрузка примера кода…

Это проверяет, не изменилась ли строка с момента чтения. Подходит для сценариев с низкой конкуренцией и частыми конфликтами.


Pessimistic Locking

Явная блокировка строки через SELECT ... FOR UPDATE:


Разбор:
- Фрагмент показывает конкретный сценарий, который стартует со строки `Это проверяет, не изменилась ли строка с момента чтения. Подходит для сценариев с низкой к` и задаёт контекст выполнения.
- Ключевые элементы блока: `SELECT`, они определяют основную логику примера.
- По шагам код выполняется так: `Это проверяет, не изменилась ли строка с момента чтения. Подходит дл` -> `---` -> `#### Pessimistic Locking` -> `Явная блокировка строки через `SELECT ... FOR UPDATE`:`.
- Для производительности важны индексы по условиям выборки, проверка плана выполнения и контроль N+1 при связях.
- Типичная ошибка при развитии такого кода — смешивать бизнес-правила и инфраструктурные детали в одном месте; лучше разделять ответственность.

article = Article.lock.find(1)
# Выполняет — SELECT * FROM articles WHERE id = 1 FOR UPDATE
# Другие транзакции, пытающиеся взять FOR UPDATE или изменить строку, будут ждать
article.update!(title: 'Locked')

Методы:

  • .lockFOR UPDATE;
  • .lock('FOR SHARE') → разделяемая блокировка (только чтение);
  • .with_lock { ... } → оборачивает блок в транзакцию + FOR UPDATE.

Осторожно:
FOR UPDATE может вызвать взаимные блокировки (deadlock);
— в PostgreSQL блокировка снимается только по COMMIT/ROLLBACK;
— в MySQL (InnoDB) — то же самое, но поведение при SELECT без индекса может привести к блокировке всей таблицы.


touch: true и кэширование

Опция touch: true в ассоциациях автоматически обновляет updated_at у родительского объекта при изменении дочернего:


Разбор:
- Фрагмент показывает конкретный сценарий, который стартует со строки `Методы:` и задаёт контекст выполнения.
- Ключевые элементы блока: `SELECT`, `lock`, они определяют основную логику примера.
- По шагам код выполняется так: `Методы:` -> `- `.lock` → `FOR UPDATE`;` -> `- `.lock('FOR SHARE')` → разделяемая блокировка (только чтение);` -> `- `.with_lock { ... }` → оборачивает блок в транзакцию + `FOR UPDATE`.
- Для производительности важны индексы по условиям выборки, проверка плана выполнения и контроль N+1 при связях.
- Типичная ошибка при развитии такого кода — смешивать бизнес-правила и инфраструктурные детали в одном месте; лучше разделять ответственность.

class Comment < ActiveRecord::Base
belongs_to :post, touch: true
end

При сохранении комментария:


Разбор:
- Фрагмент показывает конкретный сценарий, который стартует со строки `При сохранении комментария:` и задаёт контекст выполнения.
- Ключевые действия здесь связаны с подготовкой данных, выполнением операции и фиксацией результата.
- По шагам код выполняется так: `При сохранении комментария:`.
- Практически важно добавить обработку ошибок и явные проверки входа, чтобы исключить скрытые падения в рантайме.
- Типичная ошибка при развитии такого кода — смешивать бизнес-правила и инфраструктурные детали в одном месте; лучше разделять ответственность.

UPDATE posts SET updated_at = NOW() WHERE id = ?;
UPDATE comments SET ... WHERE id = ?;

Это критически важно для:

  • инвалидации кэшей (например, Rails.cache.fetch("post/#{post.id}"));
  • сортировки по времени последней активности;
  • интеграций, отслеживающих изменения через updated_at.

Без touch изменение комментария не отразится на updated_at поста — и кэш не обновится.


Расширение ActiveRecord — типы, scope, enum, сериализация

Кастомные типы

ActiveRecord позволяет регистрировать собственные преобразователи значений "столбец ↔ объект". Например, для хранения Money как целого числа (копейки):

Код ITЗагрузка примера кода…

Теперь product.price — объект Money, а в БД хранится INTEGER. Преимущества:

  • вынос логики преобразования из модели;
  • переиспользование типа в разных моделях;
  • поддержка валидаций и сериализации.

Scope

Scope — это именованные, цепляемые запросы, возвращающие Relation:

Код ITЗагрузка примера кода…

Scope всегда ленив: не выполняет запрос, возвращает Relation.
Правило — scope не должен вызывать all, to_a, first — это нарушает композируемость.


Enum

Символические перечисления, маппящиеся на целочисленные или строковые значения в БД:

Код ITЗагрузка примера кода…

Генерируемые методы:

  • status'active';
  • status= → присваивает символ или строку;
  • active! → сохраняет статус active;
  • active? → проверка.

Под капотом — преобразование между символом и значением. Хранение как INTEGER экономит место; как VARCHAR — повышает читаемость дампов.


Сериализация в JSON/Hash

Для хранения структурированных данных в одном столбце:

Код ITЗагрузка примера кода…

store_accessor создаёт геттеры/сеттеры для вложенных ключей. Важно:

Разбор:

  • Здесь ключевая конструкция — store_accessor, она создаёт методы доступа к ключам внутри JSON-поля settings как к обычным атрибутам модели.
  • В примере user.theme = 'dark' и user.language = 'ru' меняют вложенные значения, после save эти пары сериализуются в одну JSON-строку в колонке settings.
  • По шагам выполнение такое: Rails пишет значения в accessors -> кодер JSON формирует документ -> SQL INSERT сохраняет итог в таблицу.
  • Важно учитывать ограничения: фильтрация и сортировка по вложенным ключам без специальных индексов (jsonb_path_ops и выражения) заметно деградируют на больших таблицах.
  • Типичная ошибка — хранить в settings критичные поля доменной модели; такие поля лучше выносить в отдельные колонки с явными ограничениями и индексами.

Границы ActiveRecord — когда и как выходить за рамки

ActiveRecord не универсален. Его слабые места:

СценарийПроблема ActiveRecordАльтернатива
Сложные отчёты с CTE, оконными функциямиНет встроенного DSLRaw SQL через find_by_sql, execute, или Sequel
Массовые операции (bulk insert/update)save — 1 запрос на записьinsert_all, upsert_all (Rails 6+), activerecord-import
Данные вне реляционной модели (графы, документы)Нет нативной поддержкиneo4j-ruby-driver, elasticsearch-ruby, прямой API
Высокая конкурентность, fine-grained lockinglock ограничен SELECT ... FOR UPDATERedis-мьютексы, pg_advisory_lock через raw SQL
Микросервисы с разными БДЖёсткая привязка к одной схемеRepository-паттерн, ROM, или отдельные классы данных

Интеграция raw SQL в ActiveRecord

Можно безопасно встраивать SQL без отказа от ActiveRecord:

class User < ActiveRecord::Base
def self.top_contributors(limit: 10)
find_by_sql(<<~SQL, limit: limit)
SELECT users.*, COUNT(posts.id) AS post_count
FROM users
LEFT JOIN posts ON posts.user_id = users.id
GROUP BY users.id
ORDER BY post_count DESC
LIMIT :limit
SQL
end
end

Разбор:

  • Фрагмент показывает конкретный сценарий, который стартует со строки class User < ActiveRecord::Base и задаёт контекст выполнения.
  • Ключевые элементы блока — find, SELECT, JOIN, GROUP BY, они определяют основную логику примера.
  • По шагам код выполняется так: class User < ActiveRecord::Base -> def self.top_contributors(limit: 10) -> find_by_sql(<<~SQL, limit: limit) -> SELECT users.*, COUNT(posts.id) AS post_count.
  • Для производительности важны индексы по условиям выборки, проверка плана выполнения и контроль N+1 при связях.
  • Типичная ошибка при развитии такого кода — смешивать бизнес-правила и инфраструктурные детали в одном месте; лучше разделять ответственность.

find_by_sql возвращает массив объектов User, инициализированных из результата — включая виртуальные атрибуты (post_count доступен как user.post_count).

Для запросов без маппинга на модель — connection.execute:


Разбор:
- Фрагмент показывает конкретный сценарий, который стартует со строки `Разбор:` и задаёт контекст выполнения.
- Ключевые элементы блока: `find`, `SELECT`, `JOIN`, `GROUP BY`, они определяют основную логику примера.
- По шагам код выполняется так: `Разбор:` -> `- Фрагмент показывает конкретный сценарий, который стартует со строк` -> `- Ключевые элементы блока: `find`, `SELECT`, `JOIN`, `GROUP BY`, они` -> `- По шагам код выполняется так: `class User < ActiveRecord::Base` ->`.
- Для производительности важны индексы по условиям выборки, проверка плана выполнения и контроль N+1 при связях.
- Типичная ошибка при развитии такого кода — смешивать бизнес-правила и инфраструктурные детали в одном месте; лучше разделять ответственность.

ActiveRecord::Base.connection.execute(
'UPDATE counters SET value = value + 1 WHERE name = — RETURNING value',
['hits']
).values.first

Использование Sequel внутри Rails

Можно инициализировать отдельный Sequel::Database для части приложения:

Код ITЗагрузка примера кода…

Преимущества:

  • не влияет на пул соединений ActiveRecord;
  • использует тот же DATABASE_URL;
  • позволяет применять мощный DSL Sequel для аналитики, оставляя доменные модели в ActiveRecord.

Безопасность — защита от угроз на уровне данных

SQL-инъекции — как ActiveRecord предотвращает и где остаётся уязвимость

ActiveRecord защищает от инъекций только при корректном использовании параметризованных запросов. Основные безопасные паттерны:

  • where(active: true)WHERE active = $1;
  • where("name = ?", name) → параметр передаётся отдельно от строки;
  • where(name: name) — автоматическая параметризация;
  • find(id) — приведение id к целому числу и проверка типа.

Небезопасные паттерны (инъекция возможна):

  • where("name = #{name}") — интерполяция в строку;
  • order(params[:sort]) — динамическое имя столбца без вайтлиста;
  • group("CASE WHEN #{cond} THEN ... END").

Для безопасной работы с динамическими именами столбцов/таблиц используется Arel.sql только с валидацией по белому списку:


Разбор:
- Фрагмент показывает конкретный сценарий, который стартует со строки `Преимущества:` и задаёт контекст выполнения.
- Ключевые элементы блока: `where`, `find`, `Arel.sql`, они определяют основную логику примера.
- По шагам код выполняется так: `Преимущества:` -> `- не влияет на пул соединений ActiveRecord;` -> `- использует тот же `DATABASE_URL`;` -> `- позволяет применять мощный DSL Sequel для аналитики, оставляя доме`.
- Для производительности важны индексы по условиям выборки, проверка плана выполнения и контроль N+1 при связях.
- Типичная ошибка при развитии такого кода — смешивать бизнес-правила и инфраструктурные детали в одном месте; лучше разделять ответственность.

allowed_columns = %w[name email created_at]
column = params[:sort].in?(allowed_columns) ? params[:sort] : 'id'
User.order(Arel.sql("#{column} ASC"))

Arel.sql не экранирует — он лишь помечает строку как "доверенную". Ответственность за валидацию лежит на разработчике.

Разбор:

  • В блоке выше безопасный путь построен через whitelist: params[:sort] сравнивается с allowed_columns, и только после этого имя колонки попадает в Arel.sql.
  • Ключевой момент: Arel.sql не экранирует строку, а лишь помечает её как доверенную часть SQL, поэтому защита строится именно на проверке списка разрешённых полей.
  • По шагам: читается входной параметр сортировки -> выполняется проверка in?(allowed_columns) -> при провале подставляется безопасный fallback id -> формируется ORDER BY.
  • Такая схема предотвращает SQL-инъекцию через order, где параметр обычно вставляется в запрос как идентификатор, а не как значение.
  • Частая ошибка — расширить список сортировок и забыть обновить whitelist; из-за этого либо ломается функциональность, либо появляется небезопасная ветка с прямой интерполяцией.

Массовое присваивание (mass assignment) и strong parameters

Уязвимость: злоумышленник отправляет PATCH /users/42 { "admin": true }, и если в контроллере используется User.update(params[:user]), — флаг admin присваивается.

Rails 4+ решает это через strong parameters:

def user_params
params.require(:user).permit(:name, :email)
# :admin не включён — будет вырезан из хэша
end

Разбор:

  • Фрагмент показывает конкретный сценарий, который стартует со строки def user_params и задаёт контекст выполнения.
  • Ключевые элементы блока: permit, они определяют основную логику примера.
  • По шагам код выполняется так: def user_params -> params.require(:user).permit(:name, :email) -> # :admin не включён — будет вырезан из хэша -> end.
  • Для безопасности критичны валидация входных данных и запрет небезопасной интерполяции пользовательских значений.
  • Типичная ошибка при развитии такого кода — смешивать бизнес-правила и инфраструктурные детали в одном месте; лучше разделять ответственность.

Механизм работает на уровне ActionController::Parameters:
require проверяет наличие ключа и выбрасывает ActionController::ParameterMissing при отсутствии;
permit возвращает новый хэш, содержащий только разрешённые ключи и их значения (вложенные структуры обрабатываются рекурсивно);
— значения не модифицируются — только фильтрация ключей.

Важно: permit не валидирует типы. permit(:age) пропустит "abc", но User.new(age: "abc") вызовет ошибку при save, если столбец ageinteger. Валидация типов — задача модели или отдельного слоя.


Утечки данных через логи

По умолчанию Rails фильтрует чувствительные параметры в логах:

# config/initializers/filter_parameter_logging.rb
Rails.application.config.filter_parameters += [:password, :token]

Разбор:

  • Фрагмент показывает конкретный сценарий, который стартует со строки # config/initializers/filter_parameter_logging.rb и задаёт контекст выполнения.
  • Ключевые элементы блока — config, initializers, filter_parameter_logging, rb, Rails, они определяют основную логику примера.
  • По шагам код выполняется так: # config/initializers/filter_parameter_logging.rb -> Rails.application.config.filter_parameters += [:password, :token].
  • Для безопасности критичны валидация входных данных и запрет небезопасной интерполяции пользовательских значений.
  • Типичная ошибка при развитии такого кода — смешивать бизнес-правила и инфраструктурные детали в одном месте; лучше разделять ответственность.

При логировании запроса:

Parameters: {"email"=>"timur@example.com", "password"=>"[FILTERED]"}

Разбор:

  • Фрагмент показывает конкретный сценарий, который стартует со строки Parameters: {"email"=>"timur@example.com", "password"=>"[FILTERED]"} и задаёт контекст выполнения.
  • Ключевые элементы блока — Parameters:, email, timur, example, com, они определяют основную логику примера.
  • По шагам код выполняется так: Parameters: {"email"=>"timur@example.com", "password"=>"[FILTERED]"}.
  • Для безопасности критичны валидация входных данных и запрет небезопасной интерполяции пользовательских значений.
  • Типичная ошибка при развитии такого кода — смешивать бизнес-правила и инфраструктурные детали в одном месте; лучше разделять ответственность.

Механизм работает на уровне ActionDispatch::Http::ParameterFilter:
— совпадение по имени параметра (точное или регулярное выражение);
— фильтрация происходит до записи в лог, включая Rails.logger, Sentry, Lograge;
— не защищает от утечек в binding.pry, puts params, или кастомных логгерах.

Рекомендация: не логировать params явно — использовать filtered_parameters.


Защита от XSS при рендеринге данных из БД

Данные из БД не считаются "безопасными" по умолчанию. В ERB:

<%= user.bio %> → HTML-экранируется (например, `<script>` → `&lt;script&gt;`)
<%= raw user.bio %> → выводится как есть (опасно!)
<%= user.bio.html_safe %> → помечает строку как доверенную (опасно!)

Разбор:

  • Фрагмент показывает конкретный сценарий, который стартует со строки <%= user.bio %> с HTML-экранированием (см. блок erb выше) и задаёт контекст выполнения.
  • Ключевые элементы блока: <%=, они определяют основную логику примера.
  • По шагам код выполняется так: <%= user.bio %> (экранирование) → <%= raw user.bio %> (без экранирования, опасно) → <%= user.bio.html_safe %> (помечает строку как доверенную, опасно).
  • Для безопасности критичны валидация входных данных и запрет небезопасной интерполяции пользовательских значений.
  • Типичная ошибка при развитии такого кода — смешивать бизнес-правила и инфраструктурные детали в одном месте; лучше разделять ответственность.

html_safe не очищает — он лишь устанавливает флаг html_safe? = true. Если в user.bio передан неэкранированный HTML с тегом script, браузер его выполнит.
Чистка требует отдельных инструментов: sanitize(user.bio), Loofah, Rails::Html::WhiteListSanitizer.


Тестирование — уровни и изоляция

Транзакционные тесты

По умолчанию Rails оборачивает каждый тест в transaction и откатывает её по завершении:


Разбор:
- Фрагмент показывает конкретный сценарий, который стартует со строки `Разбор:` и задаёт контекст выполнения.
- Ключевые элементы блока: `<%=`, `transaction`, они определяют основную логику примера.
- По шагам код выполняется так: `Разбор:` -> `- Фрагмент показывает конкретный сценарий, который стартует со строк` -> `- Ключевые элементы блока: `<%=`, они определяют основную логику при` -> `- По шагам код выполняется так: `<%= user.bio %> → HTML-экр`.
- Для безопасности критичны валидация входных данных и запрет небезопасной интерполяции пользовательских значений.
- Типичная ошибка при развитии такого кода — смешивать бизнес-правила и инфраструктурные детали в одном месте; лучше разделять ответственность.

# Внутри теста —
ActiveRecord::Base.connection.begin_transaction
# ... тест
ActiveRecord::Base.connection.rollback_transaction

Преимущества:

  • изоляция между тестами;
  • высокая скорость (никаких INSERT/DELETE в БД);
  • идемпотентность.

Ограничения:

  • не работают с CREATE TABLE, DROP INDEX, TRUNCATE (DDL не поддерживает вложенность в большинстве СУБД);
  • не тестируют поведение вне транзакции (например, after_commit коллбэки).

Для DDL-тестов используется use_transactional_tests = false и ручная очистка (через database_cleaner или ActiveRecord::Base.connection.execute("DELETE FROM ...")).


Фикстуры и Factories

Фикстуры (test/fixtures/users.yml) — это YAML-файлы, загружаемые до тестового набора:


Разбор:
- Фрагмент показывает конкретный сценарий, который стартует со строки `Преимущества:` и задаёт контекст выполнения.
- Ключевые элементы блока: `YAML`, `transaction`, они определяют основную логику примера.
- По шагам код выполняется так: `Преимущества:` -> `- изоляция между тестами;` -> `- высокая скорость (никаких `INSERT/DELETE` в БД);` -> `- идемпотентность.`.
- Для производительности важны индексы по условиям выборки, проверка плана выполнения и контроль N+1 при связях.
- Типичная ошибка при развитии такого кода — смешивать бизнес-правила и инфраструктурные детали в одном месте; лучше разделять ответственность.

# users.yml
timur:
name: Тимур
email: timur@example.com

Плюсы:

  • скорость загрузки (один INSERT на таблицу);
  • стабильные id, удобно для тестов ассоциаций;
  • не требуют Ruby-логики.

Минусы:

  • дублирование данных между фикстурами;
  • сложно поддерживать при изменении схемы;
  • не выражают бизнес-правила (например, "активный пользователь с профилем").

Factories (например, factory_bot):

Код ITЗагрузка примера кода…

Плюсы:

  • гибкость, композиция, наследование;
  • выражение бизнес-инвариантов в коде;
  • уникальные данные на каждый тест.

Минусы:

  • медленнее (каждый create — отдельный INSERT);
  • риск неявных зависимостей между фабриками.

Выбор зависит от типа теста:
unit-тесты моделей — factories (гибкость важнее скорости);
интеграционные/системные тесты — фикстуры (предсказуемость, быстродействие).


Мокирование БД

Для unit-тестов без обращения к БД используется мокирование:


Разбор:
- Фрагмент показывает конкретный сценарий, который стартует со строки `Плюсы:` и задаёт контекст выполнения.
- Ключевые элементы блока: `create`, `INSERT`, `unit`, `factories`, `unit`, они определяют основную логику примера.
- По шагам код выполняется так: `Плюсы:` -> `- гибкость, композиция, наследование;` -> `- выражение бизнес-инвариантов в коде;` -> `- уникальные данные на каждый тест.`.
- Для производительности важны индексы по условиям выборки, проверка плана выполнения и контроль N+1 при связях.
- Типичная ошибка при развитии такого кода — смешивать бизнес-правила и инфраструктурные детали в одном месте; лучше разделять ответственность.

allow(User).to receive(:find).with(42).and_return(double(id: 42, name: 'Тимур'))

Но:
— мокирование ActiveRecord-методов (where, save) часто приводит к хрупким тестам (тест зависит от реализации, а не поведения);
— лучше изолировать бизнес-логику в сервисные объекты, принимающие данные (не модели), и мокировать только внешние вызовы.

Антипаттерн:


Разбор:
- Фрагмент показывает конкретный сценарий, который стартует со строки `Но:` и задаёт контекст выполнения.
- Ключевые элементы блока: `where`, они определяют основную логику примера.
- По шагам код выполняется так: `Но:` -> `— мокирование ActiveRecord-методов (`where`, `save`) часто приводит ` -> `— лучше изолировать бизнес-логику в *сервисные объекты*, принимающие` -> `Антипаттерн:`.
- Практически важно добавить обработку ошибок и явные проверки входа, чтобы исключить скрытые падения в рантайме.
- Типичная ошибка при развитии такого кода — смешивать бизнес-правила и инфраструктурные детали в одном месте; лучше разделять ответственность.

# Плохо: тест зависит от внутреннего метода ActiveRecord
allow(user).to receive(:save).and_return(false)

Лучше:


Разбор:
- Фрагмент показывает конкретный сценарий, который стартует со строки `Лучше:` и задаёт контекст выполнения.
- Ключевые действия здесь связаны с подготовкой данных, выполнением операции и фиксацией результата.
- По шагам код выполняется так: `Лучше:`.
- Практически важно добавить обработку ошибок и явные проверки входа, чтобы исключить скрытые падения в рантайме.
- Типичная ошибка при развитии такого кода — смешивать бизнес-правила и инфраструктурные детали в одном месте; лучше разделять ответственность.

# Хорошо: сервис принимает валидные данные и возвращает результат
result = UserService.create(name: 'Тимур', email: 't@example.com')
expect(result.success?).to be true

Миграции в продакшене — стратегии без простоев

Прямое применение rails db:migrate в production несёт риски:
— блокировка таблиц при ALTER TABLE;
— несовместимость кода и схемы в момент развёртывания.

Three-step migration — стандартная практика:

  1. Развернуть код, совместимый со старой и новой схемой.
    Пример: добавление столбца phone без NOT NULL:
    — код проверяет наличие user.phone (в Rails часто user.phone.present?), а не полагается на столбец;
    — валидации не требуют phone.

  2. Применить миграцию (add_column :users, :phone, :string).
    — операция ADD COLUMN без NOT NULL и DEFAULT — мгновенная в PostgreSQL (12+), MySQL (8.0+);
    — данные не перезаписываются.

  3. Развернуть код, использующий новый столбец.
    — валидации, бизнес-логика, интерфейс.

Для несовместимых изменений (удаление столбца, изменение типа):

  • сначала убрать использование в коде (оставить столбец);
  • потом удалить столбец.

Для NOT NULL с DEFAULT:
— сначала add_column без NOT NULL;
— затем update_all порциями (чтобы не блокировать таблицу);
— затем change_column_null :users, :phone, false.

Инструменты — strong_migrations (гем, запрещающий опасные операции в Разработка), pg_repack (перестроение таблиц без блокировки).


Мониторинг и наблюдаемость

Метрики ActiveRecord

Rails 7+ интегрируется с ActiveSupport::Notifications, что позволяет собирать:

  • количество запросов в секунду;
  • 95-й и 99-й перцентили времени выполнения;
  • количество slow queries (например, > 100 мс).

Пример отправки в Prometheus через prometheus-client:


Разбор:
- Фрагмент показывает конкретный сценарий, который стартует со строки `---` и задаёт контекст выполнения.
- Ключевые элементы блока: `rails`, `db:migrate`, `production`, `ALTER`, `TABLE`, они определяют основную логику примера.
- По шагам код выполняется так: `---` -> `### Миграции в продакшене — стратегии без простоев` -> `Прямое применение `rails db:migrate` в production несёт риски:` -> `— блокировка таблиц при `ALTER TABLE`;`.
- Практически важно добавить обработку ошибок и явные проверки входа, чтобы исключить скрытые падения в рантайме.
- Типичная ошибка при развитии такого кода — смешивать бизнес-правила и инфраструктурные детали в одном месте; лучше разделять ответственность.

ActiveSupport::Notifications.subscribe('sql.active_record') do |*args|
event = ActiveSupport::Notifications::Event.new(*args)
DB_QUERY_DURATION.observe(event.duration / 1000.0) # в секундах
DB_QUERY_COUNT.increment
end

Алертинг по аномалиям

Типичные правила:

  • рост числа ROLLBACK в минуту (признак ошибок валидации или конфликтов optimistic locking);
  • увеличение времени выполнения SELECT к критическим таблицам (users, orders);
  • появление N+1: резкий скачок числа запросов на один HTTP-запрос.

Логирование slow queries

В PostgreSQL:
log_min_duration_statement = 200 — логировать запросы > 200 мс;
— в Rails: config.active_record.logger = ActiveSupport::Logger.new('log/slow_queries.log') с фильтром по duration.


Контекст — почему в Ruby доминирует ORM, а в других языках — нет

Это следствие архитектурных предпосылок языков и экосистем.

Язык/ЭкосистемаДоминирующий подходПричины
Ruby (Rails)ActiveRecord (full ORM)— Rails изначально строился как "full-stack framework" с соглашениями; — динамическая типизация позволяет легко внедрять методы в классы; — приоритет — скорость разработки MVP.
Java (Spring)JPA / Hibernate (Data Mapper ORM)— статическая типизация требует явного маппинга; — enterprise-традиции: разделение объектов и persistence; — поддержка сложных наследований, кэширования 2-го уровня.
GoRaw SQL / SQLBoiler / Ent (codegen)— отсутствие наследования и динамической диспетчеризации затрудняет ORM; — приоритет — производительность и прозрачность; — code generation компенсирует ручное написание.
RustDiesel (query builder) / sqlx (compile-time checks)— безопасность памяти запрещает "магические" маппинги времени выполнения; — compile-time проверка SQL (sqlx) — уникальное преимущество; — акцент на явном контроле.

В Ruby ORM стал стандартом, потому что:
— он органично вписался в философию Rails "convention over configuration";
— динамическая природа языка позволяет реализовать "магию" без потери читаемости для целевой аудитории;
— сообщество сконцентрировалось на вебе, где CRUD-операции доминируют.

Однако в высоконагруженных, аналитических или embedded-сценариях Ruby-разработчики всё чаще используют микрослои (Sequel) или raw SQL — что подтверждает: инструмент выбирается под задачу, а не наоборот.


Связанные статьи энциклопедии


Основа по протоколу

Базовый разбор HTTP и HTTPS находится в отдельной статье — HTTP как основа веб-интеграций.