ЛегкоRuby / Основы
Условные конструкции
Какие условные конструкции есть в Ruby? В чём разница между if/unless/case?
if — выполняет код если условие true
unless — наоборот, выполняет если false (эквивалент if !condition)
case/when — множественный выбор (аналог switch в других языках)
ternary — сокращённая форма: condition ? a : b
Все конструкции возвращают значение (в Ruby всё выражение):
x = if condition
"yes"
else
"no"
end
В Ruby можно писать if в конце строки (modifier):
puts "hello" if condition
ЛегкоRuby / Основы
Методы
Как определить метод? Что такое неявный возврат? Что возвращает метод без return?
def greet(name)
"Hello, #{name}"
end
greet("Alice") # => "Hello, Alice"
Метод возвращает значение последнего выражения (неявный возврат). return не обязателен.
def add(a, b)
a + b
end
add(2, 3) # => 5
Значения по умолчанию:
def greet(name = "World")
"Hello, #{name}"
end
greet # => "Hello, World"
greet("Bob") # => "Hello, Bob"
? в названии метода — соглашение: метод возвращает boolean:
def even?(n)
n % 2 == 0
end
! в названии — метод модифицирует объект на месте:
[1, 2, 3].sort # => [1, 2, 3]
[3, 1, 2].sort! # => [1, 2, 3] (исходный массив изменён)
СреднеRuby / ООП
Инкапсуляция
Что такое public, private, protected? В чём разница между private и protected?
public — метод доступен отовсюду (по умолчанию)
private — метод доступен только внутри объекта (нельзя вызвать с явным получателем: self.method — ошибка)
protected — метод доступен внутри объекта И для других объектов того же класса
class Person
def initialize(age)
@age = age
end
def older_than?(other)
@age > other.age # protected позволяет обратиться к age другого объекта Person
end
protected
def age
@age
end
end
Если бы age был private — other.age вызвало бы ошибку. protected нужен для сравнения объектов между собой.
ЛегкоRuby / Переменные
Символы
Что такое символы (Symbol)? Чем :name отличается от "name"? Когда использовать символы?
Символ — неизменяемая строка-константа. Каждый :name указывает на один и тот же объект в памяти.
"name".object_id != "name".object_id # разные объекты
:name.object_id == :name.object_id # один и тот же объект
Символы используются как ключи в хэшах, имена опций, имена методов:
# Хэш с символьными ключами (частый паттерн)
{ name: "Alice", age: 25 }
# Как ключи в options hash
def connect(host:, port:, ssl: true)
end
Ruby 3.1+ добавил frozen string literal по умолчанию — разница между Symbol и String уменьшилась, но символы всё ещё быстрее для ключей хэша.
ЛегкоRuby / Строки
Методы строк
Какие основные методы строк в Ruby вы знаете?
size / length — длина строки
upcase / downcase — регистр
strip — убирает пробелы по краям
capitalize — первая буква заглавная
reverse — разворачивает строку
include?(str) — содержит подстроку
split(separator) — разбивает на массив
join(separator) — склеивает массив в строку
start_with? / end_with? — проверка начала/конца
gsub(pattern, replacement) — глобальная замена
sub(pattern, replacement) — замена первого совпадения
to_i / to_f — конвертация в число
to_s — конвертация в строку
empty? — пустая ли строка
chars — массив символов
chomp — убирает \n в конце
squeeze — убирает дублирующиеся символы
ЛегкоRuby / Строки
Интерполяция
Что такое интерполяция строк? В чём разница одинарных и двойных кавычек?
Двойные кавычки поддерживают интерполяцию и escape-последовательности:
name = "Alice"
"Hello, #{name}" # => "Hello, Alice"
"2 + 2 = #{2 + 2}" # => "2 + 2 = 4"
Одинарные кавычки — строка как есть, без интерполяции:
'Hello, #{name}' # => "Hello, \#{name}"
Также в двойных кавычках работают escape-последовательности:
"\n" — перенос строки
"\t" — табуляция
"\\" — обратный слеш
Форматирование (sprintf):
"%s is %d years old" % ["Alice", 25]
# => "Alice is 25 years old"
format("%s is %d", "Alice", 25)
ЛегкоRuby / Коллекции и Enumerable
Массивы: создание и основные методы
Как создать массив в Ruby? Какие основные методы работы с массивами?
Создание:
[1, 2, 3] # литерал
Array.new(3, 0) # [0, 0, 0]
(1..5).to_a # [1, 2, 3, 4, 5]
%w[apple banana] # ["apple", "banana"]
Основные методы:
arr = [3, 1, 2]
arr.length / arr.size # 3
arr.push(4) / arr << 4 # [3, 1, 2, 4] — добавить в конец
arr.pop # 4 — удалить последний
arr.unshift(0) # [0, 3, 1, 2] — добавить в начало
arr.shift # 0 — удалить первый
arr.include?(1) # true
arr.reverse # [2, 1, 3]
arr.sort # [1, 2, 3]
arr.flatten # развернуть вложенные
arr.uniq # уникальные
arr.join(", ") # "3, 1, 2"
arr.sample # случайный элемент
arr.first / arr.last # первый / последний
arr.empty? # false
ЛегкоRuby / Коллекции и Enumerable
Хэши: создание и методы
Как создать хэш в Ruby? Какие основные методы?
Создание:
{ name: "Иван", age: 25 } # символы как ключи
{ "name" => "Иван" } # строки как ключи
Hash.new(0) # с дефолтом (0 для отсутствующих)
Hash.new { |h, k| h[k] = [] } # с дефолтом-массивом
Основные методы:
h = { a: 1, b: 2, c: 3 }
h[:a] # 1
h[:d] # nil
h.fetch(:d, 0) # 0 (с дефолтом)
h[:d] = 4 # добавить/изменить
h.keys # [:a, :b, :c]
h.values # [1, 2, 3]
h.key?(:a) # true
h.merge(d: 4) # объединить (новый хэш)
h.merge!(d: 4) # объединить (изменить оригинал)
h.delete(:a) # удалить ключ, вернуть значение
h.select { |k, v| v > 1 } # фильтровать
h.transform_values { |v| v * 2 } # изменить значения
h.dig(:a, :b, :c) # безопасный доступ к вложенным
ЛегкоRuby / Коллекции и Enumerable
Enumerable: any?, all?, none?, one?
Что делают методы any?, all?, none?, one? из модуля Enumerable?
Эти методы проверяют условие на элементах коллекции и возвращают true/false.
[1, 2, 3].any? { |x| x > 2 } # true (хотя бы один)
[1, 2, 3].all? { |x| x > 0 } # true (все подходят)
[1, 2, 3].none? { |x| x < 0 } # true (ни один не подходит)
[1, 2, 3].one? { |x| x > 2 } # true (ровно один)
Без блока — проверяют truthiness:
[nil, false].any? # false
[1, nil].any? # true
Работают с любым Enumerable: массивы, хэши, диапазоны.
СреднеRuby / Коллекции и Enumerable
Enumerable: group_by, partition, tally
Что делают group_by, partition, tally? Примеры использования.
group_by — группирует по результату блока:
["apple", "bat", "car", "ant"].group_by { |w| w[0] }
# => { "a"=>["apple", "ant"], "b"=>["bat"], "c"=>["car"] }
(1..6).group_by { |n| n.even? }
# => { false=>[1, 3, 5], true=>[2, 4, 6] }
partition — делит на два массива [true, false]:
(1..6).partition { |n| n.even? }
# => [[2, 4, 6], [1, 3, 5]]
tally — считает сколько раз каждый элемент встречается (Ruby 2.7+):
["a", "b", "a", "c", "b", "a"].tally
# => { "a"=>3, "b"=>2, "c"=>1 }
count с блоком — посчитать подходящие:
[1, 2, 3, 4, 5].count { |x| x.even? } # => 2
ЛегкоRuby / Исключения
Обработка исключений: begin/rescue
Как обрабатывать исключения в Ruby? Как устроен блок begin/rescue?
begin/rescue/ensure/else — конструкция для обработки ошибок:
begin
result = risky_operation
rescue StandardError => e
puts "Ошибка: #{e.message}"
# обработка ошибки
else
# выполняется если НЕ было ошибки
puts "Всё хорошо: #{result}"
ensure
# выполняется ВСЕГДА (ошибка или нет)
cleanup_resources
end
rescue перехватывает указанный класс исключений и его потомков.
=> e — сохраняет объект исключения в переменную e.
Можно несколько rescue:
rescue ArgumentError => e
# ...
rescue StandardError => e
# всё остальное
Типичные классы: StandardError, ArgumentError, TypeError, NoMethodError,
RuntimeError, ZeroDivisionError, IOError.
СреднеRuby / Исключения
raise и типы исключений
Как вызвать исключение вручную? Какие типы исключений бывают?
raise — выбрасывает исключение:
raise "Что-то пошло не так" # RuntimeError
raise ArgumentError, "Неверный аргумент" # ArgumentError
raise CustomError.new("детали") # своё исключение
Иерархия исключений:
Exception
├── StandardError # ловить нужно ЭТО
│ ├── ArgumentError
│ ├── TypeError
│ ├── NoMethodError
│ ├── RuntimeError
│ ├── ZeroDivisionError
│ ├── IOError
│ └── ...
├── SystemExit
├── SignalException
└── ...
Важно: никогда не ловите Exception — только StandardError и его потомки.
rescue без класса = rescue StandardError.
Пользовательское исключение:
class PaymentError < StandardError; end
raise PaymentError, "Недостаточно средств"
СреднеRuby / Исключения
retry в rescue
Что такое retry в блоке rescue? Как использовать?
retry — повторяет блок begin с начала. Полезно для повторных попыток:
attempts = 0
begin
attempts += 1
response = HTTParty.get("https://api.example.com/data")
rescue Net::OpenTimeout => e
if attempts < 3
sleep(2 ** attempts) # экспоненциальная задержка: 2, 4, 8
retry # повторить begin с начала
else
raise # пробросить исключение дальше
end
end
Осторожно: без счётчика attempts retry создаст бесконечный цикл.
В Rails чаще используют retry_on в ActiveJob:
retry_on Net::OpenTimeout, wait: 5.seconds, attempts: 3
СреднеRuby / Исключения
Исключения: практики и антипаттерны
Какие есть антипаттерны при работе с исключениями в Ruby?
Антипаттерны:
1. Пустой rescue — проглатывает ошибку без обработки:
rescue => e
# ничего не делаем — ОПАСНО
Лучше: хотя бы логировать Rails.logger.error(e.message)
2. rescue Exception — ловит ВСЁ, включая SystemExit:
rescue Exception => e # НЕ ДЕЛАЙТЕ ТАК
Правильно: rescue StandardError => e
3. Использование исключений для управления потоком:
begin
user = User.find(id)
rescue ActiveRecord::RecordNotFound
# лучше: User.find_by(id: id)
end
4. Слишком широкий rescue в начале:
rescue StandardError => e # ловит всё
Лучше: rescue конкретный класс (ArgumentError и т.д.)
Правильные практики:
— Логируй ошибку
— Используй ensure для очистки ресурсов
— Создавай свои классы исключений для бизнес-логики
— rescue конкретные классы, а не StandardError
СреднеRuby / Блоки, Proc, Lambda
&block и передача блока в метод
Как передать блок в метод? Что такое &block?
Неявный блок (yield):
def hello
yield "Мир"
end
hello { |name| puts "Привет, #{name}!" }
# => Привет, Мир!
Явный &block — превращает блок в Proc:
def hello(&block)
block.call("Мир")
end
hello { |name| puts "Привет, #{name}!" }
& в reverse — Proc в блок:
doubler = Proc.new { |x| x * 2 }
[1, 2, 3].map(&doubler) # => [2, 4, 6]
# &doubler превращает Proc обратно в блок для map
Передача метода как блока:
["hello", "world"].map(&:upcase) # => ["HELLO", "WORLD"]
# &:upcase — создаёт Proc, вызывающий upcase на объекте
block_given? — проверить, передан ли блок:
def hello
if block_given?
yield
else
puts "Нет блока"
end
end
СреднеRuby / Блоки, Proc, Lambda
Лямбды в Rails: scope, callback
Где лямбды используются в Rails? Приведи примеры.
Scope — именованные запросы:
class Post < ApplicationRecord
scope :published, -> { where(published: true) }
scope :recent, -> { where("created_at > ?", 1.week.ago) }
scope :by_author, ->(name) { where(author: name) }
end
Post.published.recent.by_author("Иван")
Callbacks с условиями:
class User < ApplicationRecord
before_save -> { self.email = email.downcase }
validates :password, length: { minimum: 8 }, if: -> { password.present? }
end
before_action в контроллерах:
before_action -> { authorize @post }, only: [:edit, :update]
Стратегия через хэш лямбд:
HANDLERS = {
confirmed: ->(order) { OrderMailer.confirmation(order).deliver_later },
cancelled: ->(order) { RefundService.call(order) }
}.freeze
Почему лямбды, а не Proc:
— Строгие к аргументам (упадёт сразу если забыл передать)
— return не ломает метод
— Короткий синтаксис -> () { }
СреднеRuby / Регулярные выражения
Регулярные выражения в Ruby
Что такое регулярные выражения? Какие основные методы работы с ними в Ruby?
Регулярное выражение — шаблон для поиска и замены в строках.
Создание:
/pattern/ # литерал
Regexp.new("pat") # из строки
Основные методы:
"hello world" =~ /world/ # => 6 (индекс совпадения)
"hello world".match(/world/) # => #<MatchData "world">
"hello 123".scan(/\d/) # => ["1", "2", "3"]
"hello".gsub(/[aeiou]/, "*") # => "h*ll*"
"hello 123".match?(/\d/) # => true (Ruby 2.4+)
Основные паттерны:
/\d/ — цифра /\D/ — НЕ цифра
/\w/ — буква/цифра/_ /\W/ — НЕ буква/цифра
/\s/ — пробел /\S/ — НЕ пробел
/./ — любой символ
/^/ — начало строки /$/ — конец строки
/[a-z]/ — диапазон /pattern/i — без учёта регистра
Квантификаторы:
/\d*/ — 0 или более /\d+/ — 1 или более
/\d?/ — 0 или 1 /\d{3}/ — ровно 3
/\d{2,4}/ — от 2 до 4
Группы и захват:
/(\d{4})-(\d{2})-(\d{2})/.match("2024-01-15")
# $1 => "2024", $2 => "01", $3 => "15"
СреднеRuby / Многопоточность
Многопоточность в Ruby: Thread и GIL
Как работает многопоточность в Ruby? Что такое GIL?
Thread — класс для создания потоков:
t1 = Thread.new { 3.times { puts "Поток 1"; sleep(1) } }
t2 = Thread.new { 3.times { puts "Поток 2"; sleep(1) } }
t1.join # ждать завершения
t2.join
GIL (Global Interpreter Lock) — в MRI Ruby только один поток
выполняет Ruby-код одновременно. Это значит:
— Потоки НЕ дают параллелизма для CPU-задач
— Но дают параллелизм для I/O (сеть, файлы, БД)
Race condition — проблема при работе с общими данными:
@counter = 0
threads = 10.times.map do
Thread.new { 1000.times { @counter += 1 } }
end
threads.each(&:join)
@counter # может быть < 10000!
Mutex — решение race condition:
mutex = Mutex.new
@counter = 0
threads = 10.times.map do
Thread.new do
1000.times { mutex.synchronize { @counter += 1 } }
end
end
Ractor (Ruby 3.0+) — настоящий параллелизм без GIL,
но с ограничениями (объекты должны быть thread-safe).
СреднеRuby / Многопоточность
Thread-safe в Rails
Что значит thread-safe? Почему это важно в Rails?
Thread-safe — код корректно работает при одновременном выполнении
в нескольких потоках.
В Rails (Puma) — несколько потоков обрабатывают запросы одновременно.
Поэтому:
НЕ thread-safe (опасно):
— Глобальные переменные ($var)
— Переменные класса (@@var)
— Модификация констант
— Общий mutable state между потоками
Thread-safe (безопасно):
— Локальные переменные (они создаются на каждый вызов)
— @instance переменные в контроллере (новый объект на запрос)
— Immutable объекты (замороженные)
— Базы данных (транзакции)
Rails по умолчанию thread-safe с Rails 4+:
config.threadsafe! — больше не нужен, включено всегда.
config.eager_load = true в production — загрузить весь код при старте.
config/puma.rb:
threads_count = ENV.fetch("RAILS_MAX_THREADS") { 5 }
# Puma создаёт пул потоков, каждый обрабатывает запрос
ЛегкоRuby / Внутреннее устройство
Everything is an Object
Что значит «в Ruby всё — объект»? Приведи примеры.
В Ruby всё является объектом — даже числа, строки, nil, true/false:
5.class # => Integer
"hello".class # => String
nil.class # => NilClass
true.class # => TrueClass
[1,2].class # => Array
:symbol.class # => Symbol
У всего есть методы (потому что всё наследуется от Object):
42.is_a?(Object) # => true
nil.is_a?(Object) # => true
"hi".respond_to?(:upcase) # => true
Даже классы — объекты:
String.class # => Class
Class.class # => Class
Методы можно вызывать цепочкой:
" hello ".strip.upcase.reverse # => "OLLEH"
Нет примитивов как в Java — всё объекты с методами.
СреднеRuby / Внутреннее устройство
method_missing
Что такое method_missing? Зачем нужен? Какие опасности?
method_missing — метод, вызываемый когда метод не найден.
class User
def initialize(data)
@data = data
end
def method_missing(name, *args)
if name.to_s.end_with?("?")
@data.key?(name.to_s.chomp("?"))
else
@data[name.to_s]
end
end
def respond_to_missing?(name, include_private = false)
true # важно! иначе respond_to? вернёт false
end
end
user = User.new("name" => "Иван", "age" => 25)
user.name # => "Иван"
user.age # => 25
user.name? # => true
Опасности:
— Ловит опечатки: user.nme вместо user.name — не упадёт
— Медленнее обычных методов
— Нужно переопределять respond_to_missing? тоже
Используется в: OpenStruct, динамических делегатах,
некоторых DSL (Rake, RSpec).
Правило: если можно обойтись без method_missing — обойдись.
СреднеRuby / Внутреннее устройство
freeze и frozen?
Что такое freeze в Ruby? Зачем нужен?
freeze — делает объект неизменяемым (immutable):
arr = [1, 2, 3].freeze
arr << 4 # FrozenError: can't modify frozen Array
str = "hello".freeze
str.upcase! # FrozenError
frozen? — проверяет, заморожен ли:
"hello".freeze.frozen? # => true
"hello".frozen? # => false
Зачем:
1. Безопасность — защита констант от изменения:
COLORS = %w[red green blue].freeze
2. Оптимизация памяти — Ruby не создаёт новые объекты
для одинаковых замороженных строк:
"hello".freeze # один объект в памяти
# без freeze — каждый раз новый объект
frozen_string_literal: true — в начале файла:
# frozen_string_literal: true
# Все строковые литералы в файле автоматически заморожены
"hello" << " world" # FrozenError
Важно: freeze замораживает только сам объект, не вложенные:
arr = [[1], [2]].freeze
arr << [3] # FrozenError
arr[0] << 10 # работает! Внутренний массив не заморожен
СреднеRuby / Внутреннее устройство
Monkey patching
Что такое monkey patching? Зачем нужен? Какие риски?
Monkey patching — изменение или добавление методов в существующий класс
во время выполнения программы:
class String
def shout
self.upcase + "!!!"
end
end
"hello".shout # => "HELLO!!!"
Используется:
— ActiveSupport в Rails добавляет методы в стандартные классы:
3.days.ago, "hello".blank?, [1,2,3].sum
— Гемы расширяют классы Ruby
— Быстрый фикс бага в чужой библиотеке
Риски:
— Конфликты: два гема добавляют метод с одинаковым именем
— Неочевидность: непонятно откуда появился метод
— Хрупкость: обновление Ruby/Rails может сломать патч
Рефайнменты (refinements) — безопасная альтернатива:
module ShoutString
refine String do
def shout
upcase + "!!!"
end
end
end
using ShoutString # действует только в этой области
"hello".shout # => "HELLO!!!"
# За пределами scope метод не виден
СреднеRuby on Rails / Основы Rails
Структура Rails
Знать и рассказать структуру папок Rails приложения.
app — основные файлы приложения
assets — картинки, стили, js
controllers — контроллеры
helpers — хелперы
jobs — задания
mailers — рассыльщики
models — модели
views — представления
layouts — макеты
config — конфигурация маршрутов, базы данных и т.д
environments — настройки сред приложения
locales — интернационализация
db — текущая схема базы данных, сиды
migrates — файлы миграции
lib — внешние модули
log — журналы логов
public — доступна извне как есть, статичные файлы и скомпилированные ассеты
test — структурирована по тестам моделей / контроллеров / интеграционным
tmp — временные файлы (такие как файлы кэша и pid)
vendor — код сторонних разработчиков, например, внешние гемы
ЛегкоRuby on Rails / Основы Rails
Scaffolding
Что такое scaffolding? Зачем он используется и где применяется?
Rails Scaffold - встроенный генератор, который запускает другие генераторы Rails, чтобы одной командой сгенерировать набор из модели, контроллера, вьюх, тестов, миграций и т.д. Предоставляется возможность создавать собственные предустановки генерации.
СреднеFrontend / Rails Frontend
form_with vs form_tag
Чем отличаются form_with и form_tag? Как form_with определяет POST или PATCH? Что значит model: [:admin, @category]?
form_with model: @article → привязана к модели
form_with url: articles_path → без модели, вручную
form_with + model сама определяет HTTP-метод:
@article.new_record? → POST /articles (создание)
@article persisted? → PATCH /articles/:id (обновление)
model: [:admin, @article] — массив:
:admin → namespace, префикс /admin/
@article → объект, определяет URL и метод
Без model пришлось бы писать вручную:
form_with url: admin_articles_path, method: :post # создание
form_with url: admin_article_path(@article), method: :patch # редактирование
form_with также подставляет значения полей из объекта:
@article.title = "Ruby"
→ f.text_field :title отрендерит value="Ruby"
Strong Parameters защищают от массового присвоения:
params.require(:article).permit(:title, :body)
— разрешены только title и body, всё остальное игнорируется
СреднеFrontend / Rails Frontend
redirect_to vs render
В чём разница между redirect_to и render? Что будет если после create использовать render вместо redirect_to и пользователь нажмёт F5?
redirect_to — браузер делает НОВЫЙ запрос (HTTP 302)
redirect_to articles_path
→ браузер получает 302
→ делает GET /articles
→ переменные обнуляются, данные в БД обновлены
render — браузер получает HTML сразу (без нового запроса)
render :new
→ браузер получает 200 OK
→ переменные сохраняются (@article с ошибками)
→ форма показывает ошибки валидации
Почему после create/update нужен redirect_to:
Если использовать render — при F5 браузер повторит POST
→ данные отправятся снова (дублирование)
→ redirect_to делает GET — F5 безопасно
Паттерн CRUD:
успех → redirect_to (защита от повторной отправки)
ошибка валидации → render (показать ошибки в форме)
status: :unprocessable_entity (422) нужен для Turbo:
без него Turbo думает что всё OK и не обновляет форму
СреднеFrontend / CSS
CSS Specificity
Что такое специфичность CSS? В каком порядке применяются стили при конфликте?
Specificity (специфичность) — алгоритм браузера для выбора,
какое CSS-правило применить при конфликте.
Приоритет от слабого к сильному:
1. !important — перебивает всё (избегать)
2. Inline стили (style="...") — 1000
3. #id — 100
4. .class, :pseudo-class, [attribute] — 10
5. element, ::pseudo-element — 1
6. * (универсальный) — 0
Пример подсчёта:
div .menu li a → 0,1,1,3 (1 класс + 3 элемента)
#nav .item:hover → 1,1,1,0 (1 id + 1 класс + 1 псевдокласс)
#nav .item:hover побеждает — выше специфичность
Правило: чем специфичнее селектор, тем выше приоритет.
При равной специфичности побеждает правило, объявленное позже.
ЛегкоFrontend / CSS
Box Model
Что такое CSS Box Model? Чем отличается box-sizing: content-box от border-box?
Каждый HTML-элемент — прямоугольная коробка из 4 слоёв:
┌─────────────────────────┐
│ margin │
│ ┌───────────────────┐ │
│ │ border │ │
│ │ ┌─────────────┐ │ │
│ │ │ padding │ │ │
│ │ │ ┌─────────┐ │ │ │
│ │ │ │ content │ │ │ │
│ │ │ └─────────┘ │ │ │
│ │ └─────────────┘ │ │
│ └───────────────────┘ │
└─────────────────────────┘
content-box (по умолчанию):
width: 100px + padding: 20px + border: 5px
→ реальная ширина = 100 + 20*2 + 5*2 = 150px
border-box (рекомендуется):
width: 100px — ВКЛЮЧАЕТ padding и border
→ реальная ширина = 100px (content сжимается)
Поэтому в проектах используют:
*, *::before, *::after { box-sizing: border-box; }
Это спасает от «съехавшей» вёрстки при добавлении padding/border.
СреднеFrontend / CSS
Flexbox vs Grid
В чём разница между Flexbox и CSS Grid? Когда использовать каждый?
Flexbox — одномерный (строка ИЛИ столбец):
навбар, кнопки в ряд, карточки в одну линию
Grid — двумерный (строки И столбцы одновременно):
галерея, таблица, дашборд, сложная раскладка страницы
Flexbox:
display: flex
justify-content — выравнивание по главной оси
align-items — по поперечной оси
flex-direction: row | column
gap — отступы между элементами
Grid:
display: grid
grid-template-columns: repeat(3, 1fr) — 3 равные колонки
grid-template-rows — строки
gap — отступы
Правило:
Компонент в одну линию → Flexbox
Двумерная раскладка → Grid
Не уверены → Flexbox (проще начать)
СреднеFrontend / Hotwire
Hotwire: Turbo vs Stimulus
Что такое Hotwire? Чем отличаются Turbo и Stimulus? Зачем они нужны в Rails?
Hotwire — подход Rails к фронтенду БЕЗ JavaScript-фреймворков.
Вместо React/Vue — сервер рендерит HTML, а Turbo/Stimulus его оживляют.
Turbo — работает с HTML поверх HTTP:
Turbo Drive — перехватывает клики по ссылкам, грузит через AJAX
(страница обновляется без полной перезагрузки)
Turbo Frames — обновляет часть страницы
(как iframe, но лучше: только нужный блок перерисовывается)
Turbo Streams — сервер отправляет HTML-обновления через WebSocket
(append, replace, remove элементов без JS)
Stimulus — минимальный JS для интерактивности:
data-controller="slider" — подключает JS-контроллер к элементу
data-action="click->slider#next" — обработчик событий
data-slider-index-value="0" — данные для контроллера
Разница:
Turbo — обновляет DOM с сервера (HTML over the wire)
Stimulus — добавляет интерактивность на клиенте (код-редактор, модалки)
Аналогия:
Turbo — официант (приносит готовые блюда)
Stimulus — приборы (нарезаешь еду сам)
СреднеRuby on Rails / Контроллеры
Flash-сообщения
Что такое flash и flash.now? В чём разница?
flash — сообщение, доступное в СЛЕДУЮЩЕМ запросе:
redirect_to @post, notice: "Создано"
redirect_to @post, alert: "Ошибка"
flash.now — в ТЕКУЩЕМ запросе (при render):
flash.now[:alert] = "Исправьте ошибки"
render :new
Ключи: notice (успех), alert (ошибка), можно любые.
Во layout:
<% flash.each do |type, msg| %>
<div class="<%= type %>"><%= msg %></div>
<% end %>
flash.keep — сохранить ещё на один запрос.
flash.discard — очистить.
СложноFrontend / Hotwire
Turbo Frames
Что такое Turbo Frames? Как они работают? Приведи пример использования.
Turbo Frame — тег <turbo-frame> который обновляется отдельно от страницы.
Как работает:
1. В HTML: <turbo-frame id="task_content">...</turbo-frame>
2. Клик по ссылке ВНУТРИ frame → Turbo делает AJAX-запрос
3. Из ответа берётся ТОЛЬКО содержимое этого frame (по id)
4. Остальная страница не обновляется
Пример — вкладки в задаче:
show.html.slim:
= turbo_frame_tag "task_content" do
= link_to "Задача", task_path(@task)
= link_to "Вывод", check_task_path(@task)
check.html.slim (ответ сервера):
= turbo_frame_tag "task_content" do
div Результат выполнения кода
Клик по "Вывод" → GET /tasks/:slug/check → сервер возвращает
только frame "task_content" → Turbo заменяет его на странице
Важно:
— id frame должен совпадать в запросе и ответе
— ссылка ВНЕ frame обновляет всю страницу (или можно указать
data: { turbo_frame: "task_content" })
— Форма с data: { turbo_frame: "task_content" } отправит AJAX
и ответ заменит только этот frame
СреднеFrontend / Asset Pipeline
Asset Pipeline в Rails
Что такое Asset Pipeline? Чем Sprockets отличается от Propshaft? Как подключить CSS/JS в Rails 8?
Asset Pipeline — система обработки CSS, JS, изображений в Rails.
Задачи:
1. Компиляция — Sass → CSS, TypeScript → JS, JSX → JS
2. Бандлинг — объединение файлов в один
3. Минификация — убирает пробелы, сокращает имена переменных
4. Фingerprinting — добавляет хэш в имя файла
application-a1b2c3.css → при изменении хэш меняется
→ браузер скачивает новую версию (нет кэша)
Sprockets (старый):
— встроенная компиляция Sass, CoffeeScript
— медленный, сложный
— gem 'sprockets-rails'
Propshaft (новый, Rails 7+):
— только fingerprinting и отдача файлов
— НЕТ компиляции — используй esbuild/vite для JS, Tailwind CLI для CSS
— быстрее и проще
— gem 'propshaft'
Rails 8 типичная схема:
CSS: Tailwind CLI (через gem 'tailwindcss-rails')
JS: esbuild (npm, собирает Stimulus-контроллеры)
Assets: Propshaft (отдаёт файлы)
Подключение в layout:
= stylesheet_link_tag "application"
= javascript_include_tag "application", type: "module"
СреднеFrontend / Asset Pipeline
Организация фронтенда в Rails
Как организовать CSS и JavaScript в Rails-приложении? Где хранить стили и контроллеры?
Rails 8 — типичная структура фронтенда:
app/javascript/
controllers/ — Stimulus-контроллеры
code_editor_controller.js
filter_controller.js
application.js — точка входа, импортирует все контроллеры
app/assets/stylesheets/
application.tailwind.css — главный CSS-файл
app/assets/images/ — картинки
Как это работает:
1. esbuild собирает app/javascript/application.js → app/assets/builds/application.js
2. Tailwind компилирует application.tailwind.css → app/assets/builds/application.css
3. Propshaft отдаёт файлы из app/assets/builds/ с fingerprinting
Stimulus-контроллер:
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["output"]
connect() { console.log("подключён") }
}
Подключается через data-атрибуты:
<div data-controller="filter">
<input data-filter-target="input">
</div>
Правила:
— Один контроллер = один файл
— Название файла = data-controller (filter_controller.js → data-controller="filter")
— Стили через Tailwind-классы, не кастомный CSS
— Сложную логику — в Service Object на сервере, не в JS
СреднеFrontend / CSS
CSS Positioning
Какие виды позиционирования есть в CSS? Чем отличаются position: static, relative, absolute, fixed, sticky? Как работает z-index?
Виды позиционирования:
static (по умолчанию):
— элемент в нормальном потоке
— top/left/right/bottom НЕ работают
— z-index НЕ работает
relative:
— элемент в нормальном потоке (занимает место)
— top/left/right/bottom СДВИГАЮТ от обычной позиции
— создаёт контекст позиционирования для дочерних absolute
— z-index работает
absolute:
— ВЫПАДАЕТ из нормального потока (не занимает место)
— позиционируется относительно ближайшего positioned-предка
(relative/absolute/fixed/sticky)
— если нет positioned-предка — относительно <html>
fixed:
— ВЫПАДАЕТ из нормального потока
— позиционируется относительно viewport (экрана)
— не скроллится вместе со страницей
— пример: шапка сайта, плавающая кнопка
sticky:
— комбо relative + fixed
— ведёт себя как relative, пока не доскроллишь до порога
— потом «прилипает» как fixed
— пример: заголовки в таблице, сайдбар
.header { position: sticky; top: 0; }
z-index — управляет порядком наложения:
— работает ТОЛЬКО для positioned-элементов (не static)
— выше z-index → ближе к пользователю (поверх других)
— stacking context: z-index сравнивается внутри одного контекста
Частая ошибка: z-index: 9999 не сработает если родитель
создаёт stacking context с более низким z-index.
СреднеFrontend / CSS
Отзывчивая вёрстка
Что такое отзывчивая вёрстка (responsive)? Как работают media queries? В чём разница между rem, em и px? Что такое mobile-first?
Отзывчивая вёрстка — сайт адаптируется к размеру экрана:
мобильный → планшет → десктоп
Media queries — условия для CSS:
@media (min-width: 768px) { ... } /* от 768px и больше */
@media (max-width: 767px) { ... } /* до 767px */
Mobile-first — сначала пишем стили для мобильных, потом добавляем для больших:
.card { width: 100%; } /* мобильный */
@media (min-width: 768px) { .card { width: 50%; } } /* планшет */
@media (min-width: 1024px) { .card { width: 33%; } } /* десктоп */
Desktop-first (хуже) — наоборот, начинаем с десктопа и урезаем.
Единицы измерения:
px — абсолютные, фиксированный размер
em — относительная, от font-size РОДИТЕЛЯ
parent { font-size: 16px } → child { padding: 1em } = 16px
rem — относительная, от font-size КОРНЯ (<html>)
html { font-size: 16px } → 1rem = 16px всегда
Практика:
— font-size → rem (масштабируется с настройками браузера)
— padding/margin → rem или em
— max-width вместо width (не ломается на широких экранах)
— CSS-функция clamp(): font-size: clamp(1rem, 2.5vw, 2rem)
Viewport meta — обязателен в <head>:
<meta name="viewport" content="width=device-width, initial-scale=1">
без него мобильный браузер покажет десктопную версию
ЛегкоFrontend / CSS
Псевдоклассы и псевдоэлементы
Что такое псевдоклассы и псевдоэлементы в CSS? В чём разница между : и ::? Какие самые полезные?
Псевдоклассы (:) — состояние или положение элемента:
:hover — курсор наведён
:focus — элемент в фокусе (input, button)
:focus-within — фокус на элементе ИЛИ его потомке
:active — момент нажатия
:first-child — первый потомок родителя
:last-child — последний потомок
:nth-child(2) — второй потомок
:nth-child(odd) — нечётные (1, 3, 5...)
:nth-child(even) — чётные (2, 4, 6...)
:not(.class) — все элементы БЕЗ этого класса
:disabled — отключённый input
Псевдоэлементы (::) — виртуальные элементы:
::before — создаёт элемент ПЕРЕД содержимым
::after — создаёт элемент ПОСЛЕ содержимого
::first-line — первая строка текста
::first-letter — первая буква
::placeholder — текст-подсказка в input
::selection — выделенный пользователем текст
::before и ::after — самые полезные:
.tooltip::after {
content: attr(data-tip); /* берёт текст из data-tip */
position: absolute;
background: black;
color: white;
}
Важно:
— ::before/::after НЕ существуют без content: "..."
— В CSS3 псевдоклассы с одним :, псевдоэлементы с двумя ::
— В CSS2 всё было с одним : (::before = :before) — работает, но устарело
СреднеFrontend / Hotwire
Turbo Drive
Что делает Turbo Drive? Какие события он генерирует? Почему не работает window.onload и как это исправить?
Turbo Drive — перехватывает клики по ссылкам и отправку форм,
делает AJAX-запрос вместо полной перезагрузки страницы.
Что происходит при клике по ссылке:
1. Turbo перехватывает клик
2. Делает fetch() по URL ссылки
3. Получает HTML ответ
4. Заменяет <body> и мерджит <head>
5. URL в браузере обновляется через History API
Проблема: JS не выполняется заново при навигации!
window.onload — сработает ТОЛЬКО при первой загрузке
при навигации Turbo не перезагружает страницу → onload не triggers
Решение — события Turbo:
document.addEventListener("turbo:load", () => { ... })
— срабатывает при каждой навигации (включая первую)
document.addEventListener("turbo:render", () => { ... })
— срабатывает при рендере (включая preview из кэша)
document.addEventListener("turbo:before-cache", () => { ... })
— перед кэшированием страницы (очистка)
Для Stimulus-контроллеров это не проблема —
connect() вызывается каждый раз при появлении элемента в DOM.
Отключить Turbo для ссылки:
= link_to "Выйти", logout_path, data: { turbo: false }
— заставит браузер сделать обычный запрос
turbo:load vs DOMContentLoaded:
DOMContentLoaded — один раз при загрузке страницы
turbo:load — каждый раз при навигации Turbo
СложноFrontend / Hotwire
Turbo Streams
Что такое Turbo Streams? Какие 7 действий поддерживаются? Когда использовать Streams, а когда Frames?
Turbo Streams — сервер отправляет HTML-инструкции для обновления DOM
без полного перерендера страницы.
7 действий:
append — добавить в конец контейнера
prepend — добавить в начало контейнера
before — вставить перед элементом
after — вставить после элемента
replace — заменить элемент целиком
update — обновить содержимое (без замены тега)
remove — удалить элемент
Синтаксис:
<%= turbo_stream.append "comments" do %>
<%= render @comment %>
<% end %>
Результат — специальный MIME-тип text/vnd.turbo-stream.html:
<turbo-stream action="append" target="comments">
<template>
<div id="comment_5">Новый комментарий</div>
</template>
</turbo-stream>
В контроллере:
respond_to do |format|
format.turbo_stream # рендерит create.turbo_stream.slim
format.html { redirect_to @post }
end
Через модель (broadcasts):
class Comment < ApplicationRecord
broadcasts_to :post # автоматически при create/update/destroy
end
→ отправляет Stream через WebSocket всем подписчикам
Streams vs Frames:
Frames — обновляет ОДИН блок при клике/submit (pull)
Streams — сервер ОБРАТНО обновляет любой элемент (push)
Frames — проще, достаточно для CRUD
Streams — для real-time, уведомлений, чатов
СреднеFrontend / Hotwire
Stimulus: targets, values, classes
Как устроен Stimulus-контроллер? Что такое targets, values и classes? Какие lifecycle-методы есть?
Stimulus-контроллер — JS-класс, привязанный к HTML через data-атрибут:
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["input", "output"]
static values = { count: Number, url: String }
static classes = ["active", "hidden"]
connect() { /* элемент появился в DOM */ }
disconnect() { /* элемент удалён из DOM */ }
}
Targets — ссылки на элементы внутри контроллера:
HTML: <div data-controller="search">
<input data-search-target="input">
<div data-search-target="output"></div>
</div>
JS: this.inputTarget — первый найденный (ошибка если нет)
this.inputTargets — массив всех
this.hasInputTarget — есть ли (boolean)
Values — типизированные данные в HTML:
HTML: <div data-controller="counter" data-counter-count-value="5">
JS: this.countValue // → 5
this.countValueChanged() // callback при изменении
this.countValue = 10 // обновит data-атрибут
Типы: String, Number, Boolean, Array, Object
Classes — CSS-классы для toggle:
HTML: <div data-controller="toggle" data-toggle-active-class="bg-green">
JS: this.activeClass // → "bg-green"
this.element.classList.add(this.activeClass)
Lifecycle:
connect() — элемент добавлен в DOM (как mounted)
disconnect() — элемент удалён из DOM (как unmounted)
Actions — обработчики событий:
HTML: <button data-action="click->counter#increment">
JS: increment(event) { this.countValue++ }
СреднеFrontend / Asset Pipeline
esbuild и сборка JavaScript
Зачем нужен esbuild в Rails? Что делает бандлер? Что такое import/export и tree shaking?
Проблема: браузеры не понимают:
— import/export между файлами
— npm-пакеты (node_modules)
— TypeScript, JSX
Бандлер (esbuild) решает все эти проблемы.
Что делает esbuild:
1. Берёт точку входа: app/javascript/application.js
2. Рекурсивно обходит все import
3. Собирает всё в ОДИН файл: app/assets/builds/application.js
4. Минифицирует (убирает пробелы, сокращает имена)
Скорость: esbuild написан на Go, в 10-100x быстрее Webpack.
import/export — модули в JavaScript:
// code_editor_controller.js
export default class CodeEditor extends Controller { ... }
// application.js
import CodeEditor from "./code_editor_controller"
// или автоматический импорт:
import "./controllers" // import all controllers
Tree shaking — удаление неиспользуемого кода:
import { map, filter } from "lodash"
→ в бандл попадут ТОЛЬКО map и filter, не вся библиотека
Команда сборки (из package.json):
"build": "esbuild app/javascript/*.* --bundle --outdir=app/assets/builds"
Watch-режим для разработки:
"watch": "esbuild ... --watch"
→ пересобирает при изменении файлов
Важно: esbuild НЕ делает:
— type checking (это TypeScript)
— полифиллы (это babel)
— горячую перезагрузку (это Vite)
СреднеFrontend / Rails Frontend
importmap vs esbuild
Какие три способа подключения JavaScript в Rails 7/8? Плюсы и минусы каждого?
Три подхода к JavaScript в Rails:
1. importmap (по умолчанию в rails new):
— браузер сам загружает ES-модули по HTTP
— НЕ нужен бандлер (npm, node_modules)
— config/importmap.rb — маппинг имён на URL
— Плюсы: простота, нет шага сборки
— Минусы: нет npm-пакетов, нет tree shaking, медленнее на больших проектах
2. esbuild (rails new -j esbuild):
— бандлер собирает всё в один файл
— npm-пакеты доступны (npm install ...)
— Плюсы: быстро, npm-экосистема, tree shaking
— Минусы: нужен Node.js, шаг сборки
3. Vite (rails new -j vite):
— современный бандлер с HMR (горячая перезагрузка)
— Плюсы: самый быстрый dev-опыт, Vue/React support
— Минусы: сложнее настроить, больше магии
Когда что:
— Простой проект, Hotwire → importmap (или esbuild)
— Средний/крупный проект → esbuild
— React/Vue фронтенд → Vite
Наш проект использует esbuild:
npm-пакеты: @hotwired/stimulus, @codemirror/...
Сборка: esbuild → app/assets/builds/application.js
СреднеFrontend / Rails Frontend
Turbolinks → Turbo: миграция
Чем Turbolinks отличается от Turbo? Что сломалось при миграции? Зачем DHH переписал Turbolinks?
Turbolinks (2013-2020):
— перехватывал ссылки, обновлял <body> через AJAX
— jQuery-эпоха: $(document).on("turbolinks:load", ...)
— проблемы: глобальный state, утечки памяти, баги с JS
Turbo (2020+, Rails 7):
— замена Turbolinks, переписан с нуля
— три части: Drive, Frames, Streams
— работает со Stimulus (не нужен jQuery)
Что сломалось при миграции Turbolinks → Turbo:
— turbolinks:load → turbo:load (события переименованы)
— $(document).ready → Stimulus connect()
— jQuery не чистил state при навигации → утечки памяти
— Turbolinks кэшировал страницу → старые данные показывались
Паттерны замены:
Turbolinks: Turbo:
$(document).on("turbolinks:load") document.addEventListener("turbo:load")
$(element).on("click") data-action="click->ctrl#method"
ручной DOM-манипуляцией Turbo Streams с сервера
turbo:load vs DOMContentLoaded:
DOMContentLoaded — только при полной перезагрузке
turbo:load — при каждой навигации через Turbo Drive
В Stimulus-контроллерах используй connect() — не turbo:load
История:
— rails-ujs (Rails 3-5) — jQuery, data-remote, data-confirm
— Turbolinks (Rails 4-6) — AJAX-навигация
— Turbo + Stimulus (Rails 7+) — полная замена без jQuery
СреднеRuby on Rails / Основы Rails
Rack
Что такое Rack?
Rack это промежуточное программное обеспечение, оно делит входящие HTTP-запросы на различные этапы, затем обрабатывает их по частям, после чего посылает ответ веб-приложения (контроллера).
Программа Rack состоит из двух отдельных компонентов: обработчика и адаптера, с помощью которых происходит обмен данными между веб-серверами и приложениями (фреймворками).
Какие серверы есть: WEBrick, Thin, Puma, Unicorn, Phusion Passenger, Iodine.
ЛегкоRuby on Rails / MVC и Роутинг
MVC
За что отвечают Model, View, Controller уровни в Rails?
MVC — это паттерн программирования, который подразумевает схему разделения данных приложения, пользовательского интерфейса и управляющей логики на три отдельных компонента.
ЛегкоRuby on Rails / MVC и Роутинг
Роутинг
Как работает роутинг? Что такое ресурсные роуты? Как они формируются?
Браузеры запрашивают страницы от Rails, выполняя запрос по URL, используя определенный метод HTTP, такой как GET, POST, PATCH, PUT и DELETE.
Роутинг распознает запрос по методу и по URL и направляет его в экшн контроллера или в приложение Rack.
Он также может генерировать пути и URL, избегая необходимость жестко прописывать строки в ваших вьюхах.
Ресурсный роутинг позволяет быстро объявлять все общие маршруты для заданного ресурсного контроллера. Вместо объявления отдельных маршрутов для экшнов index, show, new, edit, create, update и destroy, ресурсный маршрут объявляет их одной строчкой кода.
ЛегкоRuby on Rails / ActiveRecord
dependent
Что такое dependent связь?
Опция :dependent указывает, что необходимо сделать с зависимой моделью (моделями) при удалении текущей модели. Может принимать значения:
:delete — связанные объекты будут удалены прямо из базы данных без вызова метода destroy
:destroy — будет вызван destroy на связанных объектах
:nullify — внешний ключ будет установлен NULL
:restrict_with_error — при наличии связанного объекта вызовет ошибку
:restrict_with_exception — при наличии связанного объекта вызовется исключение
ЛегкоRuby on Rails / ActiveRecord
t.references
Что такое t.references?
Столбец таблицы в миграции, указывающий на принадлежность к другой таблице. Например, книга принадлежит автору:
class CreateBooks < ActiveRecord::Migration[5.2]
def change
create_table :books do |t|
t.references :author
end
end
end
ЛегкоRuby on Rails / ActiveRecord
ActiveRecord
Что такое ActiveRecord, и какие средства предоставляет для работы с объектами?
ActiveRecord это паттерн программирования. AR является популярным способом доступа к данным реляционных баз данных в объектно-ориентированном программировании. ActiveRecord еще называют буквой M в MVC — которая является слоем в системе, ответственным за представление бизнес-логики и данных.
Active Record упрощает создание и использование бизнес-объектов, данные которых требуют персистентного хранения в базе данных. Сама по себе эта реализация паттерна Active Record является описанием системы ORM (Object Relational Mapping). Active Record это фреймворк ORM.
Active Record предоставляет нам несколько механизмов, наиболее важными из которых являются способности для:
Представления моделей и их данных.
Представления связей между этими моделями.
Представления иерархий наследования с помощью связанных моделей.
Валидации моделей до того, как они станут персистентными в базе данных.
Выполнения операций с базой данных в объектно-ориентированном стиле.
ЛегкоRuby on Rails / ActiveRecord
scopes
Что такое скоупы (scopes)? Как использовать?
Скоупы позволяют задавать часто используемые запросы, к которым можно обращаться как к вызовам метода в связанных объектах или моделях. С помощью этих скоупов можно использовать такие методы как where, joins и includes. Все методы скоупов возвращают объект ActiveRecord::Relation, который позволяет вызывать на нем дополнительные методы (такие как другие скоупы).
Для определения простого скоупа мы используем метод scope внутри класса, передав запрос, который хотим запустить при вызове этого скоупа:
class Article < ApplicationRecord
scope :published, -> { where(published: true) }
end
ЛегкоRuby on Rails / ActiveRecord
Валидации
Что такое валидации? Как написать свои валидации? Для чего нужны валидации?
Валидации используются, чтобы быть уверенными, что только проверенные данные сохраняются в вашу базу данных. Например, для вашего приложения может быть важно, что каждый пользователь предоставил валидный электронный адрес.
Валидации на уровне модели - наилучший способ убедиться, что в базу данных будут сохранены только валидные данные. Они не зависят от базы данных, не могут быть обойдены конечными пользователями и удобны в тестировании и обслуживании.
Пример простейшей валидации:
class Person < ApplicationRecord
validates :name, presence: true
end
Person.create(name: "John Doe").valid? # => true
Person.create(name: nil).valid? # => false
Разработчик также может написать свои собственные правила валидации, которые будут располагаться в каталоге app/validators.
ЛегкоRuby on Rails / ActiveRecord
Связи моделей
Какие связи для связывания моделей в приложении Rails вы знаете?
Rails поддерживает шесть типов связей:
belongs_to
has_one
has_many
has_many :through
has_one :through
has_and_belongs_to_many
СреднеRuby on Rails / ActiveRecord
Примеры связей
Привести примеры использования has_many, belongs_to, has_and_belongs_to_many, has_one, has_many :through?
Фильм имеет множество сезонов, сезон принадлежит фильму и имеет множество серий. У каждого фильма может быть только один официальный сайт. В каждом фильме снимается множество актёров, при этом каждый актёр снимается в разных фильмах:
class Film < ApplicationRecord
has_many :seasons
has_many :episodes, through: :seasons
has_one :official_site
has_and_belongs_to_many :actors
end
class Season < ApplicationRecord
belongs_to :film
has_many :episodes
end
class Episode < ApplicationRecord
belongs_to :season
end
class OfficialSite < ApplicationRecord
belongs_to :film
end
class Actor < ApplicationRecord
has_and_belongs_to_many :films
end
СреднеRuby on Rails / ActiveRecord
has_many :through vs HABTM
Что лучше выбрать has_many :through или has_and_belongs_to_many?
Это зависит от контекста связи many-to-many.
Если планируется использование дополнительной логики в этой связи, создание дополнительных полей в соединительной таблице, то лучше отдать предпочтение has_many :through. В этом случае применяются промежуточные модели-связки.
В том случае, если достаточно простой соединительной таблицы, то можно обойтись has_and_belongs_to_many (т.н. HBTM).
СреднеRuby on Rails / ActiveRecord
Полиморфные связи
Что такое полиморфные связи?
Особый вид связи, при которой модель может принадлежать сразу нескольким моделям.
Например, картинку можно добавлять к статье, комментарию, пользователю.
class Picture < ApplicationRecord
belongs_to :imageable, polymorphic: true
end
class Article < ApplicationRecord
has_many :pictures, as: :imageable
end
class Comment < ApplicationRecord
has_many :pictures, as: :imageable
end
При этом картинка сохраняет в себе имя класса и id объекта, которому она принадлежит. У картинки имеются атрибуты imageable_id и imageable_type.
СреднеRuby on Rails / ActiveRecord
N+1 запрос
Что такое проблема N+1 запроса? Как можно решить проблему N+1 в Rails?
Проблема N+1 — когда на каждый элемент коллекции делается отдельный запрос к БД. Например, 10 клиентов + 1 запрос на адрес каждого = 11 запросов.
clients = Client.limit(10)
clients.each do |client|
puts client.address.postcode
end
Решение — метод includes, загружает все связанные записи одним запросом:
clients = Client.includes(:address).limit(10)
Будет всего два запроса:
SELECT * FROM clients LIMIT 10
SELECT addresses.* FROM addresses WHERE (addresses.client_id IN (1,2,3,4,5,6,7,8,9,10))
ЛегкоRuby on Rails / Views
Partial
Что такое partial и для чего используются?
Partial — это кусочек кода, который можно вынести в отдельный темплейт, для удобства использования и для использования в других представлениях.
ЛегкоRuby on Rails / Views
Haml и Slim
Что такое Haml, Slim? Какие плюсы их использования?
Haml и Slim — это шаблонизаторы, используются для удобства и минимизации написания кода в представлениях. Сокращает в несколько раз написание кода, нет проблем в закрывании тегов, не получится что тег не закрыт и код не работает. Меньше вероятность что можно ошибиться + лучше читаемость в коде.
ЛегкоRuby on Rails / Views
Расширения файлов
Что означает несколько расширений файла example.html.erb?
example — название файла. html — расширение, которое позволяет использовать стандартный язык разметки HyperText Markup Language. erb — позволяет включить использование кода написанного на языке Ruby вместе с языком разметки.
ЛегкоRuby on Rails / Views
ERB
Что такое ERB? Можете расшифровать аббревиатуру?
ERB — Embedded Ruby (встроенный Ruby)
ЛегкоRuby on Rails / Views
Presenter
Что такое presenter и для чего он нужен? Где применяется? В чем его основная задача?
Presenter - паттерн проектирования, простой класс (в Rails), использующийся для вынесения какой-либо логики по обработке моделей из слоя контроллеров и слоя представлений.
Например:
module Posts
class IndexPresenter
def posts
Posts.all
end
def authors
Authors.all
end
def post_published_count
Post.published_count
end
end
end
Так будет выглядеть экшн index в контроллере:
def index
@presenter = Posts::IndexPresenter.new
end
Так это будет представлено во view:
<p>Всего опубликовано: <%= @presenter.published_count %></p>
<%= @presenter.authors %>
ЛегкоRuby on Rails / Продвинутые темы
ActiveJob
Что такое ActiveJob? Когда его использовать?
Active Job - это фреймворк для объявления заданий и их запуска на разных бэкендах очередей. Эти задания могут быть чем угодно: от регулярно запланированных чисток до списаний с карт или рассылок. В общем, всем, что может быть выделено в небольшие работающие части и запускаться параллельно.
Имеет встроенные адаптеры для планировщиков фоновых задач:
Sidekiq
Resque
Delayed Job
и т.д..
ЛегкоRuby on Rails / Продвинутые темы
Asset Pipeline
Что такое Asset Pipeline?
Asset Pipeline (файлопровод) - фреймворк для соединения и минимизации, или сжатия ассетов JavaScript и CSS. Он также добавляет возможность писать эти ассеты на других языках и препроцессорах, таких как CoffeeScript, Sass и ERB. Это позволяет автоматически комбинировать ассеты приложения с ассетами других гемов.
Первой особенностью файлопровода является соединение ассетов, что может уменьшить количество запросов, необходимых браузеру для отображения страницы. Браузеры ограничены в количестве запросов, которые они могут выполнить параллельно, поэтому меньшее количество запросов может означать более быструю загрузку вашего приложения.
Второй особенностью файлопровода является минимизация или сжатие ассетов. Для файлов CSS это выполняется путем удаления пробелов и комментариев. Для JavaScript могут быть применены более сложные процессы. Можно выбирать из набора встроенных опций или определить свои.
Третьей особенностью файлопровода является то, что он позволяет писать эти ассеты на языке более высокого уровня с дальнейшей прекомпиляцией до фактического ассета. Поддерживаемые языки по умолчанию включают Sass для CSS, CoffeeScript для JavaScript и ERB для обоих.
ЛегкоRuby on Rails / Продвинутые темы
Serializer
Что такое serializer и для чего он нужен? Где применяется? В чем его основная задача?
Сериализация (serialization) - процесс перевода каких-либо структур данных в последовательность битов. Обратный процесс называется десериализация (deserialization).
Сериализация используется для передачи объектов по сети и сохранения их в файлы. Например: сериализация заполненного объекта в XML-документ с последующей передачей документа по HTTP или протоколам электронной почты.
Также часто используется для преобразования информации в формат JSON.
В Rails интерфейс базовой сериализации представлен модулем ActiveModel::Serialization. Вам необходимо объявить хэш, содержащий атрибуты, которые вы хотите сериализовать. Атрибуты должны быть строками, не символами.
Что касается JSON, то Active Model также предоставляет модуль ActiveModel::Serializers::JSON для сериализации/десериализации JSON.
ЛегкоRuby on Rails / Продвинутые темы
i18n
Что такое i18n (интернационализация)?
Адаптация приложения к особенностям региона, в котором он будет использоваться.
Название i18n происходит от английского слова internationalization, между первой и последней буквами i и n 18 букв.
Гем i18n, поставляемый с Ruby on Rails (начиная с Rails 2.2), представляет простой и расширяемый фреймворк для перевода приложения на язык, отличный от английского, а также изменения формата даты, времени, валюты и т.д.
Rails автоматически добавляет все файлы .rb и .yml из директории config/locales к пути загрузки переводов.
СреднеRuby on Rails / Продвинутые темы
Кеширование
Как реализовано кеширование в рельсах?
Кэширование означает хранение контента, генерируемого в цикле запрос-отклик, и повторное использование его при ответе на подобные запросы. Кэширование значительно ускоряет загрузку страниц, снижает количество запросов к серверу.
Виды кэширования:
Кэширование страницы — начиная с Rails 4 добавляется гемом actionpack-page_caching
Кэширование экшна — начиная с Rails 4 добавляется гемом actionpack-action_caching
Кэширование фрагмента — позволяет фрагменту логики вьюхи быть обернутым в блок кэша и обслуженным из хранилища кэша для последующего запроса
Кэширование матрешкой (Russian doll caching) — можно вкладывать кэшированные фрагменты в другие кэшированные фрагменты
СреднеRuby on Rails / Продвинутые темы
Service Objects
Что такое Service Objects, Form Objects, View Objects, Query Objects, для чего они нужны?
Это обычные классы Ruby, которые применяются для рефакторинга Rails-приложения, инкапсулируя часть логики моделей / представлений / контроллеров.
Service Objects — используются, когда одновременно задействованы несколько моделей, когда производятся сложные действия с моделями.
Form Objects — используются, когда отправка одной формы изменяет несколько моделей.
View Objects — используются, когда большой метод внутри модели используется только для отображения данных.
Query Objects — используются для сложных SQL запросов, утяжеляющих модели/контроллеры.
ЛегкоRuby on Rails / Контроллеры
before_action и коллбэки контроллера
Что такое before_action, after_action, around_action? Для чего используются?
Это коллбэки контроллера, выполняющиеся до, после или вокруг экшена.
before_action — самый частый. Проверка авторизации, загрузка данных:
before_action :set_post, only: [:show, :edit, :update, :destroy]
before_action :authenticate_user!, except: [:index, :show]
after_action — после экшена. Логирование, заголовки.
around_action — оборачивает экшен:
around_action :wrap_in_transaction
Подводные камни:
— skip_before_action может сломать логику безопасности
— Если before_action не делает render/redirect, выполнение продолжается
— Порядок важен — выполняются в порядке объявления
— Не злоупотребляйте — сложная цепочка трудно отлаживать
ЛегкоRuby on Rails / Контроллеры
Strong Parameters
Что такое Strong Parameters? Зачем нужны?
Strong Parameters — защита от mass assignment. Запрещает передавать
параметры в модель напрямую из params, требуя явного whitelist:
def create
@post = Post.create(post_params)
end
private
def post_params
params.require(:post).permit(:title, :body, :category_id)
end
require(:post) — обязателен ключ :post.
permit(...) — разрешены только указанные атрибуты.
Вложенные параметры:
params.require(:post).permit(:title, comments: [:body, :author])
Без permit — ActiveModel::ForbiddenAttributesError.
ЛегкоRuby on Rails / Контроллеры
render vs redirect_to
В чём разница между render и redirect_to?
render — отрисовывает шаблон в текущем запросе. Не создаёт новый запрос:
render :edit # шаблон edit
render json: @post # JSON
render plain: "OK" # текст
redirect_to — HTTP-redirect (302). Браузер делает новый запрос:
redirect_to @post
redirect_to posts_url
redirect_back fallback_location: posts_url
Правила:
— Успешный create/update/destroy → redirect_to (Post/Redirect/Get)
— Ошибка валидации → render (сохраняем @объект с ошибками)
— render не останавливает метод! Нужно: return render :edit
ЛегкоRuby on Rails / Контроллеры
params в контроллере
Что такое params в Rails-контроллере?
params — хеш-подобный объект (ActionController::Parameters)
с данными запроса:
— Параметры маршрута: params[:id], params[:controller], params[:action]
— Query string: /posts?page=2 → params[:page]
— Данные формы: params[:post][:title]
— JSON body (если Content-Type: application/json)
Методы:
params.require(:post).permit(:title, :body) # strong params
params[:id] # может быть nil
params.fetch(:page, 1) # с дефолтом
params.to_unsafe_h # весь хеш (осторожно!)
СреднеRuby on Rails / Контроллеры
Sessions и Cookies
Как работают сессии и куки в Rails?
Cookies — данные в браузере:
cookies[:theme] = "dark"
cookies.permanent[:remember_token] = token # на 20 лет
cookies.signed[:user_id] = 42 # подписанное
cookies.encrypted[:secret] = "data" # зашифрованное
Sessions — данные между запросами для одного пользователя:
session[:user_id] = @user.id
session.delete(:user_id)
По умолчанию CookieStore — хранится в браузере (зашифровано, ~4KB).
Другие хранилища: ActiveRecordStore, RedisStore.
CookieStore: удобно, но не храните чувствительные данные!
СреднеRuby on Rails / Контроллеры
rescue_from
Как обрабатывать исключения в контроллере? Что такое rescue_from?
rescue_from — обработчик исключений на уровне контроллера:
class ApplicationController < ActionController::Base
rescue_from ActiveRecord::RecordNotFound, with: :not_found
rescue_from Pundit::NotAuthorizedError, with: :forbidden
private
def not_found
render file: Rails.root.join("public/404.html"), status: :not_found
end
def forbidden
render file: Rails.root.join("public/403.html"), status: :forbidden
end
end
Для конкретного экшена:
def show
@post = Post.find(params[:id])
rescue ActiveRecord::RecordNotFound
redirect_to posts_path, alert: "Не найден"
end
В API: rescue_from + render json: { error: "..." }, status: :not_found
СреднеRuby on Rails / Контроллеры
Concerns в контроллерах
Что такое concerns? Как применять в контроллерах?
Concerns — модули для переиспользования кода между контроллерами.
app/controllers/concerns/paginatable.rb:
module Paginatable
extend ActiveSupport::Concern
included do
helper_method :current_page
end
private
def current_page
params[:page] || 1
end
end
Использование:
class PostsController < ApplicationController
include Paginatable
def index
@posts = Post.page(current_page)
end
end
ActiveSupport::Concern даёт:
— included { } — блок при включении
— class_methods { } — добавляет методы класса
Не выносите в concerns бизнес-логику — для этого Service Objects.
ЛегкоRuby on Rails / API
Rails API mode
Что такое Rails в режиме API? Чем отличается от обычного?
rails new app --api — приложение без views, helpers, assets.
Отличия:
— ApplicationController < ActionController::API (не Base)
— Нет модулей для views, cookies, flash
— Нет asset pipeline
— По умолчанию рендерит JSON
— Тоньше middleware — быстрее
ApiController включает: rendering JSON, strong params, rescue_from.
Не включает: Cookies, Sessions, Flash, Helpers.
Можно добавить модули вручную:
class ApplicationController < ActionController::API
include ActionController::Cookies
end
СреднеRuby on Rails / API
Рендеринг JSON: JBuilder, сериализаторы
Какие способы рендеринга JSON есть в Rails?
1. render json: — простой вариант:
render json: @post
render json: @post, only: [:id, :title]
render json: @post, include: :comments
2. JBuilder — DSL для JSON (app/views/posts/show.json.jbuilder):
json.extract! @post, :id, :title, :body
json.author @post.author.name
json.comments @post.comments, :id, :body
3. Сериализаторы (jsonapi-serializer, blueprinter):
class PostSerializer
include JSONAPI::Serializer
attributes :id, :title, :body
belongs_to :author
end
4. as_json на уровне модели:
def as_json(options = {})
super(only: [:id, :title])
end
Простые API: render json:
Сложные структуры: JBuilder или сериализаторы.
СреднеRuby on Rails / API
REST API: HTTP-методы и статус-коды
Как спроектировать REST API в Rails?
HTTP-методы → экшены:
GET /posts → index → 200
GET /posts/:id → show → 200
POST /posts → create → 201
PATCH /posts/:id → update → 200
DELETE /posts/:id → destroy → 204
Коды ошибок:
400 Bad Request — невалидные данные
401 Unauthorized — не авторизован
403 Forbidden — нет прав
404 Not Found — не найдено
422 Unprocessable Entity — ошибки валидации
Пример create:
def create
post = Post.new(post_params)
if post.save
render json: post, status: :created
else
render json: { errors: post.errors.full_messages },
status: :unprocessable_entity
end
end
СреднеRuby on Rails / API
API Versioning и CORS
Как версионировать API и настроить CORS?
Версионирование через URL (самый популярный):
namespace :api do
namespace :v1 do
resources :posts
end
end
→ /api/v1/posts
Структура: app/controllers/api/v1/posts_controller.rb
V2 может наследовать V1:
class Api::V2::PostsController < Api::V1::PostsController
CORS (Cross-Origin Resource Sharing) — gem rack-cors:
# config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do
allow do
origins 'https://myapp.com'
resource '*',
headers: :any,
methods: [:get, :post, :put, :patch, :delete, :options]
end
end
Без CORS браузер блокирует AJAX-запросы с другого домена.
ЛегкоRuby on Rails / Аутентификация
Devise
Что такое Devise? Какие модули входят?
Devise — гем для аутентификации (на базе Warden).
Модули:
— Database Authenticatable — вход по email/password (bcrypt)
— Registerable — регистрация
— Recoverable — восстановление пароля
— Rememberable — «запомнить меня» (cookie)
— Trackable — отслеживание входов (IP, дата)
— Validatable — валидации email/password
— Confirmable — подтверждение email
— Lockable — блокировка после неудачных попыток
— Omniauthable — OAuth (Google, GitHub)
Настройка:
rails generate devise:install
rails generate devise User
rails db:migrate
Использование:
before_action :authenticate_user!
current_user # текущий пользователь
user_signed_in? # авторизован?
СреднеRuby on Rails / Аутентификация
Сессии vs токены
В чём разница между аутентификацией через сессии и токены?
Сессионная (cookie-based):
— Сервер хранит сессию, браузер получает cookie
— Cookie отправляется автоматически с каждым запросом
— Stateful (состояние на сервере)
— CSRF-защита обязательна
— Для веб-приложений (MVC)
Токенная (token-based):
— Клиент получает токен при логине
— Отправляет в заголовке: Authorization: Bearer <token>
— Stateless (без состояния на сервере)
— Нет CSRF (нет cookies)
— Для SPA, мобильных приложений, API
JWT (JSON Web Token):
— Содержит payload (user_id, срок действия)
— Подписан секретным ключом
— Не требует БД для проверки
— Минус: нельзя отозвать без чёрного списка
В Rails: Devise = сессии, devise-jwt / knockout = токены.
СреднеRuby on Rails / Аутентификация
has_secure_password
Что такое has_secure_password? Как использовать без Devise?
has_secure_password — встроенная аутентификация (ActiveModel::SecurePassword).
Требует: gem bcrypt + поле password_digest в таблице.
class User < ApplicationRecord
has_secure_password
# добавляет виртуальные: password, password_confirmation
end
user = User.create!(email: "...", password: "secret")
user.authenticate("secret") # → user
user.authenticate("wrong") # → false
В контроллере:
def create
user = User.find_by(email: params[:email])
if user&.authenticate(params[:password])
session[:user_id] = user.id
redirect_to root_path
else
flash.now[:alert] = "Неверный email или пароль"
render :new
end
end
Автоматически добавляет валидации presence + confirmation.
СреднеRuby on Rails / Аутентификация
Авторизация: Pundit и CanCanCan
Чем отличаются Pundit и CanCanCan?
Pundit — политико-ориентированный:
# app/policies/post_policy.rb
class PostPolicy
def initialize(user, post)
@user, @post = user, post
end
def update?
@user.admin? || @user == @post.author
end
end
# В контроллере: authorize @post
# Во view: policy(@post).update?
CanCanCan — декларативный:
# app/models/ability.rb
class Ability
include CanCan::Ability
def initialize(user)
can :read, Post
can :manage, Post, author_id: user.id if user
can :manage, :all if user&.admin?
end
end
# В контроллере: authorize! :read, @post
Pundit: проще, объектно-ориентированный, легче тестировать.
CanCanCan: удобен для сложных систем прав.
ЛегкоRuby on Rails / Тестирование
RSpec: describe, context, it
Как устроен RSpec? Что такое describe, context, it?
RSpec — фреймворк тестирования Ruby/Rails.
— describe — группировка по методу/функции
— context — группировка по условию/состоянию
— it — отдельный тест
RSpec.describe User, type: :model do
describe "#admin?" do
context "when role is admin" do
let(:user) { User.new(role: :admin) }
it { expect(user.admin?).to be true }
end
context "when role is user" do
let(:user) { User.new(role: :user) }
it { expect(user.admin?).to be false }
end
end
end
Типы: type: :model, :controller, :request, :system, :job.
ЛегкоRuby on Rails / Тестирование
FactoryBot
Что такое FactoryBot? Чем лучше fixtures?
FactoryBot — создание тестовых данных.
FactoryBot.define do
factory :user do
email { "test@example.com" }
password { "password123" }
name { "Test User" }
trait :admin do
role { :admin }
end
end
end
Использование:
create(:user) # сохраняет в БД
create(:user, :admin) # с trait
create(:user, name: "Иван") # с переопределением
build(:user) # не сохраняет
attributes_for(:user) # возвращает хеш
Плюсы перед fixtures:
— Явное создание в каждом тесте
— Легко комбинировать через traits
— Последовательности: sequence(:email) { |n| "user#{n}@test.com" }
— Не ломаются при изменении данных
СреднеRuby on Rails / Тестирование
let, let! и before
В чём разница между let, let! и before?
let — ленивая инициализация. Вычисляется при первом обращении:
let(:user) { create(:user) }
# НЕ создаётся пока не обратишься
let! — принудительная. Выполняется до каждого теста:
let!(:post) { create(:post) }
# Создаётся ДО каждого it
before — произвольный код до тестов:
before { login_as(create(:user)) }
Правила:
— let — по умолчанию (экономит время если не используется)
— let! — когда данные нужны в БД до теста
— before — для сложной настройки (логин, stub)
— let внутри context переопределяет let из describe
СреднеRuby on Rails / Тестирование
Моки и стабы (mocks/stubs)
Что такое моки и стабы? Зачем нужны?
Стаб (stub) — подмена возвращаемого значения:
allow(User).to receive(:count).and_return(42)
# User.count вернёт 42 без БД
Мок (mock) — ожидание вызова (stub + проверка):
expect(Mailer).to receive(:welcome).with(user)
# Тест упадёт если Mailer.welcome не вызван
double — объект-заглушка:
user = double("User", admin?: true, name: "Админ")
instance_double — строгая заглушка:
user = instance_double(User, admin?: true)
# Упадёт если у User нет метода admin?
Когда использовать:
— Внешние API, email — стабы
— Проверка что метод вызван — моки
— Изоляция тестов — не трогать БД
Правило: не мокайте тестируемый объект.
СреднеRuby on Rails / Тестирование
Request specs и System specs
В чём разница между request specs и system specs?
Request specs — тестируют HTTP-запрос/ответ (рекомендуются):
get "/api/v1/posts"
expect(response).to have_http_status(200)
expect(JSON.parse(response.body).size).to eq(3)
post "/api/v1/posts", params: { post: { title: "Test" } }
System specs (Capybara) — end-to-end с реальным браузером:
visit "/posts"
click_on "Новый пост"
fill_in "Title", with: "Мой пост"
click_on "Создать"
expect(page).to have_text("Мой пост")
Controller specs — устаревают, не рекомендуются.
Пирамида тестов:
много unit (model) → меньше request → мало system.
СреднеRuby on Rails / Тестирование
Shoulda Matchers и SimpleCov
Что такое Shoulda Matchers и SimpleCov?
Shoulda Matchers — лаконичные тесты Rails-паттернов:
it { should validate_presence_of(:title) }
it { should validate_uniqueness_of(:email) }
it { should belong_to(:user) }
it { should have_many(:comments).dependent(:destroy) }
it { should validate_length_of(:password).is_at_least(6) }
SimpleCov — покрытие тестами:
# spec/spec_helper.rb (в самом начале):
require 'simplecov'
SimpleCov.start 'rails' do
add_filter '/test/'
minimum_coverage 80
end
После тестов: coverage/index.html — отчёт по файлам и строкам.
Рекомендации:
— 80%+ — хороший уровень
— Покрывайте критические пути, не геттеры/сеттеры
— minimum_coverage — тесты упадут если покрытие ниже
ЛегкоRuby on Rails / Безопасность
SQL-инъекции
Что такое SQL-инъекция? Как Rails защищает?
SQL-инъекция — вставка SQL-кода через пользовательские данные.
Опасно:
User.where("email = '#{params[:email]}'")
Безопасно:
User.where("email = ?", params[:email])
User.where(email: params[:email])
ActiveRecord экранирует параметры при:
— Хеш-синтаксисе: where(name: params[:name])
— Placeholder: where("name = ?", params[:name])
Опасные методы (быть осторожным):
— where с интерполяцией строки
— order("#{params[:sort]}") — возможна инъекция!
— find_by_sql, exec_query
Правило: НИКОГДА не интерполируйте params в SQL.
ЛегкоRuby on Rails / Безопасность
XSS-атаки
Что такое XSS? Как Rails защищает?
XSS (Cross-Site Scripting) — внедрение JavaScript в страницу.
Rails автоматически экранирует HTML:
<%= @comment.body %>
# <script>alert(1)</script> → <script>alert(1)</script>
Опасно (без экранирования):
<%= raw @comment.body %> # НЕ экранирует!
<%= @comment.body.html_safe %> # НЕ экранирует!
sanitize — экранирование с whitelist тегов:
<%= sanitize @comment.body, tags: %w[b i a p] %>
Content Security Policy:
# config/initializers/content_security_policy.rb
Rails.application.config.content_security_policy do |p|
p.default_src :self
end
СреднеRuby on Rails / Безопасность
CSRF-защита
Что такое CSRF? Как Rails защищает?
CSRF — атака, когда вредоносный сайт отправляет запрос
от имени авторизованного пользователя.
Защита Rails (protect_from_forgery):
— Для каждой формы генерируется authenticity_token
— Rails проверяет токен при POST/PUT/PATCH/DELETE
— По умолчанию включено в ApplicationController
В forms: <%= form_with %> — токен автоматически.
В AJAX: Rails-UJS добавляет X-CSRF-Token.
В API (без cookies): CSRF не актуален:
skip_before_action :verify_authenticity_token
# или
protect_from_forgery with: :null_session
СреднеRuby on Rails / Безопасность
credentials.yml.enc и секреты
Как Rails хранит секреты?
config/credentials.yml.enc — зашифрованный файл с секретами.
Редактирование:
EDITOR=vim rails credentials:edit
Содержимое (YAML):
secret_key_base: "..."
aws:
access_key_id: "..."
Доступ в коде:
Rails.application.credentials.secret_key_base
Rails.application.credentials.dig(:aws, :access_key_id)
Ключ расшифровки: config/master.key (НЕ коммитить!).
Без master.key нельзя расшифровать credentials.
Переменные окружения:
ENV["DATABASE_PASSWORD"]
gem dotenv-rails — загружает .env файл
ЛегкоRuby on Rails / Фоновые задачи
ActiveJob: основы
Что такое ActiveJob? Как создать фоновую задачу?
ActiveJob — абстракция над очередями задач в Rails.
Создание:
rails generate job send_welcome_email
class SendWelcomeEmailJob < ApplicationJob
queue_as :default
def perform(user_id)
user = User.find(user_id)
UserMailer.welcome(user).deliver_now
end
end
Запуск:
SendWelcomeEmailJob.perform_later(user.id) # асинхронно
SendWelcomeEmailJob.perform_now(user.id) # синхронно
Параметры: только простые типы (String, Integer) и ActiveRecord
(сериализуется через GlobalID).
Отложенный запуск:
SendWelcomeEmailJob.set(wait: 5.minutes).perform_later(user.id)
SendWelcomeEmailJob.set(queue: :urgent).perform_later(user.id)
config.active_job.queue_adapter = :sidekiq
СреднеRuby on Rails / Фоновые задачи
Sidekiq и Redis
Что такое Sidekiq? Как он работает?
Sidekiq — обработчик фоновых задач. Использует Redis для очередей.
Архитектура: Client → Redis (хранит задачи) → Sidekiq Worker (выполняет).
Очереди:
queue_as :mailers
queue_as :urgent
Конфигурация (config/sidekiq.yml):
:concurrency: 5
:queues:
- urgent
- default
- mailers
Запуск: bundle exec sidekiq
Sidekiq Web UI:
require 'sidekiq/web'
mount Sidekik::Web => '/sidekiq'
Retry: по умолчанию 25 попыток с экспоненциальной задержкой.
После 25 неудач → dead jobs (видны в Web UI, можно восстановить).
sidekiq_options retry: 5 # кол-во попыток
sidekiq_options retry: false # без повторов
ЛегкоRuby on Rails / Фоновые задачи
perform_later vs perform_now
В чём разница между perform_later и perform_now?
perform_later — ставит задачу в очередь (асинхронно):
SendEmailJob.perform_later(user.id)
# Мгновенно возвращается, выполнится в фоне
perform_now — выполняет синхронно в текущем процессе:
SendEmailJob.perform_now(user.id)
# Блокирует до завершения
Отложенный запуск:
SendEmailJob.set(wait: 5.minutes).perform_later(user.id)
SendEmailJob.set(wait_until: Date.tomorrow.noon).perform_later
Правило: если задача > 100мс — perform_later.
В тестах: perform_now (или заглушить perform_later).
СреднеRuby on Rails / Фоновые задачи
Retry и dead jobs
Как обрабатывать ошибки в фоновых задачах?
ActiveJob — retry_on:
class MyJob < ApplicationJob
retry_on Net::OpenTimeout, wait: 5.seconds, attempts: 3
discard_on ActiveJob::DeserializationError # игнорировать
end
Sidekiq — автоматический retry:
— 25 попыток по умолчанию с экспоненциальной задержкой
— 15с, 30с, 60с, 3м, 7м, ... до 21 дня
— После 25 неудач → dead jobs
Dead jobs:
— Хранятся 6 месяцев в Redis
— Видны в Sidekiq Web UI → Dead tab
— Можно восстановить вручную
Обработка ошибок:
def perform
# логика
rescue StandardError => e
Rails.logger.error("Job failed: #{e.message}")
raise # пробросить для retry
end
ЛегкоRuby on Rails / Среды и конфигурация
Среды Rails: development, test, production
Какие среды бывают? Чем отличаются?
development:
— Автоперезагрузка кода
— Подробные логи (SQL, время рендеринга)
— Страницы ошибок с трейсбеком
— config.cache_classes = false
test:
— Транзакционные тесты (данные откатываются)
— config.cache_classes = true
— Тихий режим
production:
— Код закеширован, не перезагружается
— Минимум логов
— Стандартные страницы 404/500
— config.cache_classes = true
— config.eager_load = true
— config.force_ssl = true (рекомендуется)
Установка: RAILS_ENV=production rails server
По умолчанию: development
ЛегкоRuby on Rails / Среды и конфигурация
database.yml и ENV
Как настраивается подключение к БД?
config/database.yml — отдельная конфигурация для каждой среды:
default: &default
adapter: postgresql
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
development:
<<: *default
database: myapp_development
production:
<<: *default
url: <%= ENV["DATABASE_URL"] %>
DATABASE_URL — приоритетнее остальных настроек.
pool — размер пула соединений (обычно = кол-во потоков).
Переменные окружения:
ENV["DATABASE_URL"] # из панели хостинга / Docker
gem dotenv-rails # загружает .env файл
.env — НЕ коммитить! .env.example — шаблон (коммитится).
ЛегкоRuby on Rails / Среды и конфигурация
Initializers
Что такое initializers в Rails?
Initializers — файлы в config/initializers/, выполняются при запуске.
Загружаются после фреймворка и гемов, в алфавитном порядке.
Примеры:
# config/initializers/cors.rb
Rails.application.config.middleware.insert_before 0, Rack::Cors do ...
# config/initializers/devise.rb
Devise.setup do |config|
config.mailer_sender = 'no-reply@example.com'
end
# config/initializers/time_formats.rb
Time::DATE_FORMATS[:short] = "%d %b %H:%M"
Правила:
— Один файл на гем/фичу
— Имена в snake_case
— Нужен перезапуск для применения изменений
СреднеRuby on Rails / Среды и конфигурация
Конфигурация приложения
Какие основные файлы конфигурации есть в Rails?
config/application.rb — общие настройки:
config.time_zone = 'Moscow'
config.i18n.default_locale = :ru
config.load_defaults 7.1
config/environments/ — настройки по средам:
development.rb, test.rb, production.rb
config/routes.rb — маршруты
config/database.yml — подключение к БД
config/storage.yml — хранилища файлов (local, S3)
config/cable.yml — WebSocket (Redis, async)
config/puma.rb — настройки сервера (воркеры, потоки)
config/credentials.yml.enc — секреты
ЛегкоRuby on Rails / Миграции
Миграции: основы
Что такое миграции? Зачем нужны? Как создать?
Миграции — способ изменения структуры БД на Ruby (без SQL).
Создание:
rails generate migration CreatePosts title:string body:text
rails generate migration AddEmailToUsers email:string:uniq
rails generate migration RemoveAgeFromUsers age:integer
Пример миграции:
class CreatePosts < ActiveRecord::Migration[7.1]
def change
create_table :posts do |t|
t.string :title, null: false
t.text :body
t.references :user, foreign_key: true
t.timestamps
end
add_index :posts, :title
end
end
Команды:
rails db:migrate # выполнить миграции
rails db:rollback # откатить последнюю
rails db:rollback STEP=3 # откатить 3 миграции
rails db:migrate:status # статус миграций
change — автоматически обратимая миграция (Rails сам создаёт down).
Если необратима — используйте up/down:
def up
add_column :users, :admin, :boolean, default: false
end
def down
remove_column :users, :admin
end
ЛегкоElixir / Pattern Matching
Что такое Pattern Matching
Что такое Pattern Matching в Elixir? Чем отличается от оператора = в Ruby?
Pattern Matching — механизм сравнения структуры данных и извлечения значений. В Elixir оператор = не присваивание, а сопоставление.
# В Ruby:
x = 1 # присваивание
# В Elixir:
x = 1 # сопоставление (match)
1 = x # match: 1 == 1
2 = x # MatchError: нет совпадения
{a, b} = {1, 2} # a = 1, b = 2
[head | tail] = [1, 2, 3] # head = 1, tail = [2, 3]
%{name: name} = %{name: "Alice", age: 25} # name = "Alice"
Pattern Matching работает в:
- присваивании переменных
- заголовках функций (function head)
- case/cond
- with
СреднеRuby on Rails / Миграции
Data migrations и best practices
Как мигрировать данные? Какие best practices миграций?
Миграция данных — изменение самих данных, а не структуры:
class PopulateUserNames < ActiveRecord::Migration[7.1]
def up
User.find_each do |user|
user.update!(name: "#{user.first_name} #{user.last_name}")
end
end
def down
# обычно невозможно откатить
end
end
Best practices:
— find_each вместо all (загрузка пакетами)
— Не используйте модели в миграциях — они могут измениться
Лучше: execute("UPDATE users SET ...")
— Одна миграция = одно изменение
— Тестируйте миграции: rails db:migrate && rails db:rollback
— Добавляйте индексы для внешних ключей
— null: false для обязательных полей
— default: для новых колонок с NOT NULL
Опасно в production:
— remove_column без down — данные потеряны навсегда
— rename_column — сломает существующий код
— change_column — необратимо
ЛегкоRuby on Rails / Миграции
Schema.rb и структура БД
Что такое schema.rb? Зачем нужен?
db/schema.rb — автоматически генерируемый файл, отражающий
текущую структуру БД после всех миграций.
Генерируется после rails db:migrate.
Зачем:
— Быстрый взгляд на структуру БД без чтения всех миграций
— rails db:schema:load — создаёт БД из schema.rb (быстрее migrate)
— Версионирование структуры БД в git
schema.rb vs migrations:
— schema.rb — текущее состояние (итог)
— migrations — история изменений (как пришли к итогу)
Когда использовать:
— Новая БД: rails db:schema:load (быстро)
— Существующая БД: rails db:migrate (применить новые миграции)
Важно: schema.rb коммитится в git.
config.active_record.dump_schema_after_migration = false — отключить.
ЛегкоRuby on Rails / Mailers
ActionMailer
Что такое ActionMailer? Как отправить email в Rails?
ActionMailer — отправка email в Rails.
Создание:
rails generate mailer UserMailer welcome
# app/mailers/user_mailer.rb
# app/views/user_mailer/welcome.html.erb
# app/views/user_mailer/welcome.text.erb
Пример:
class UserMailer < ApplicationMailer
default from: 'no-reply@example.com'
def welcome(user)
@user = user
mail(to: @user.email, subject: 'Добро пожаловать!')
end
end
Вызов:
UserMailer.welcome(user).deliver_now # синхронно
UserMailer.welcome(user).deliver_later # через ActiveJob (фон)
Настройка (config/environments/development.rb):
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
address: 'smtp.gmail.com',
port: 587,
user_name: ENV['GMAIL_USER'],
password: ENV['GMAIL_PASSWORD'],
authentication: 'plain'
}
В development можно использовать letter_opener gem —
письма открываются в браузере вместо отправки.
СреднеRuby on Rails / Mailers
deliver_now vs deliver_later
В чём разница между deliver_now и deliver_later?
deliver_now — отправляет синхронно, блокирует текущий запрос:
UserMailer.welcome(user).deliver_now
# Пользователь ждёт пока email отправится
deliver_later — ставит в очередь (ActiveJob), не блокирует:
UserMailer.welcome(user).deliver_later
# Мгновенно возвращается, email отправится в фоне
deliver_later с настройками:
UserMailer.welcome(user).deliver_later(wait: 5.minutes)
UserMailer.welcome(user).deliver_later(wait_until: 1.hour.from_now)
Правило: в контроллерах — ВСЕГДА deliver_later.
deliver_now — только в rake-задачах, консоли, тестах.
Если очередь не настроена — deliver_later выполняет сразу inline.
config.action_mailer.deliver_later_queue_name = :mailers — своя очередь.
ЛегкоRuby on Rails / ActiveStorage
ActiveStorage: загрузка файлов
Что такое ActiveStorage? Как загрузить и отобразить файл?
ActiveStorage — загрузка и хранение файлов в Rails.
Настройка: config/storage.yml
local:
service: Disk
root: <%= Rails.root.join("storage") %>
Модель:
class User < ApplicationRecord
has_one_attached :avatar
has_many_attached :documents
end
Загрузка:
user.avatar.attach(params[:avatar])
user.avatar.attached? # => true
Отображение:
<%= image_tag user.avatar %>
<%= link_to "Скачать", rails_blob_path(user.avatar, disposition: "attachment") %>
Валидация:
validates :avatar, presence: true,
content_type: ['image/png', 'image/jpg'],
size: { less_than: 5.megabytes }
Варианты (миниатюры):
<%= image_tag user.avatar.variant(resize_to_limit: [100, 100]) %>
Хранилища: local (диск), Amazon S3, Google Cloud, Azure.
config.active_storage.service = :local # :amazon в production
СреднеRuby on Rails / ActiveStorage
ActiveStorage: хранилища и cleanup
Какие хранилища поддерживает ActiveStorage? Как удалять файлы?
Хранилища (config/storage.yml):
local: # диск (development/test)
service: Disk
root: <%= Rails.root.join("storage") %>
amazon: # Amazon S3 (production)
service: S3
access_key_id: <%= Rails.credentials.dig(:aws, :access_key_id) %>
secret_access_key: <%= Rails.credentials.dig(:aws, :secret_access_key) %>
bucket: myapp-production
config/environments/production.rb:
config.active_storage.service = :amazon
Gemfile: gem "aws-sdk-s3" # для Amazon S3
Удаление файлов:
user.avatar.purge # синхронно
user.avatar.purge_later # через ActiveJob (рекомендуется)
user.documents.each { |d| d.purge_later }
Cleanup — удаление потерянных файлов:
# Задача по расписанию:
OrphanedBlobJob.set(cron: "0 3 * * *").perform_later
Миграция с Paperclip/Carrierwave:
— PaperClip устарел, миграция на ActiveStorage встроена
ЛегкоRuby on Rails / ActiveRecord
Enums в ActiveRecord
Что такое enum в Rails? Как использовать?
enum — перечисления в модели, хранятся как integer в БД:
class Post < ApplicationRecord
enum status: { draft: 0, published: 1, archived: 2 }
enum category: { article: 0, news: 1, review: 2 }
end
Использование:
post = Post.new
post.draft! # установить статус
post.published? # => false
post.status # => "draft"
Post.published # все опубликованные
Post.where(status: :published) # то же самое
Методы, создаваемые enum:
post.published! # установить
post.published? # проверить
Post.statuses # => { "draft"=>0, "published"=>1, "archived"=>2 }
Важно: не удаляйте значения из середины enum —
нарушится маппинг. Новые значения добавляйте в конец.
ЛегкоRuby on Rails / ActiveRecord
pluck и select
В чём разница между pluck и select?
pluck — возвращает массив значений одной колонки (без создания объектов):
User.pluck(:email)
# => ["ivan@mail.ru", "anna@mail.ru"]
# SQL: SELECT email FROM users
User.pluck(:id, :email)
# => [[1, "ivan@mail.ru"], [2, "anna@mail.ru"]]
select — загружает объекты ActiveRecord с выбранными колонками:
User.select(:id, :email)
# => [#<User id: 1, email: "ivan@mail.ru">, ...]
Разница:
User.pluck(:email) # ["ivan@mail.ru"] — массив строк
User.select(:email) # [#<User>] — массив объектов
pluck быстрее — не создаёт объекты, не загружает все колонки.
find_each vs all:
User.all.each { } # загружает ВСЕ в память
User.find_each { } # по 1000 записей (batch)
СреднеRuby on Rails / ActiveRecord
Transactions
Что такое транзакция в ActiveRecord? Зачем нужна?
Транзакция — группа операций, которые выполняются вместе.
Если одна упала — все откатываются (all-or-nothing).
ActiveRecord::Base.transaction do
account.update!(balance: account.balance - 100)
target.update!(balance: target.balance + 100)
Transfer.create!(from: account, to: target, amount: 100)
end
# Если любой update! упадёт — все три откатятся
Важно: используйте bang-методы (save!, update!) внутри транзакции.
save без ! вернёт false вместо исключения — транзакция не откатится.
С block:
User.transaction do
user.destroy!
user.posts.delete_all
end
Исключения, откатывающие транзакцию:
— ActiveRecord::RecordInvalid, RecordNotSaved
— Любое исключение, но не return!
СреднеRuby on Rails / ActiveRecord
Counter cache и touch
Что такое counter_cache и touch?
counter_cache — кеширование количества связей:
# Без counter_cache (COUNT-запрос каждый раз):
user.posts.count # SELECT COUNT(*) FROM posts WHERE user_id = 1
# С counter_cache:
class Post < ApplicationRecord
belongs_to :user, counter_cache: true
end
# Нужна колонка: add_column :users, :posts_count, :integer, default: 0
user.posts_count # Берёт из колонки, без SQL!
touch — обновляет updated_at родителя:
class Comment < ApplicationRecord
belongs_to :post, touch: true
end
# При создании/удалении комментария — post.updated_at обновляется
# Полезно для кеширования: post обновился → кеш сбросился
Сбросить счётчики:
User.find_each { |u| User.reset_counters(u.id, :posts) }
СреднеRuby on Rails / ActiveRecord
STI (Single Table Inheritance)
Что такое STI в Rails? Когда использовать?
STI — несколько моделей в одной таблице, разделённых по типу.
# Таблица vehicles: type, name, speed, seats
class Vehicle < ApplicationRecord; end
class Car < Vehicle; end
class Bicycle < Vehicle; end
Car.create!(name: "Toyota", speed: 180, seats: 5)
Bicycle.create!(name: "Горный", speed: 30)
Vehicle.all # все транспортные средства
Car.all # только машины (WHERE type = 'Car')
Колонка type — обязательна, хранит имя класса.
Rails автоматически добавляет WHERE type IN ('Car').
Когда использовать:
— Модели с общими полями, но разным поведением
— Небольшое различие между типами
Когда НЕ использовать:
— Сильно разные поля у подтипов (много NULL)
— Лучше отдельные таблицы или polymorphic
Минусы: одна большая таблица, много NULL-колонок.
СреднеRuby on Rails / MVC и Роутинг
namespace, scope, member, collection
Какие способы группировки маршрутов есть в Rails?
namespace — добавляет префикс URL и модуль контроллера:
namespace :admin do
resources :posts
end
# /admin/posts → Admin::PostsController
scope — префикс URL без модуля:
scope '/api' do
resources :posts
end
# /api/posts → PostsController
member — действие над конкретным ресурсом:
resources :posts do
member do
post :publish # POST /posts/:id/publish
end
end
collection — действие над всей коллекцией:
resources :posts do
collection do
get :search # GET /posts/search
end
end
shallow — короткие URL для вложенных:
resources :users do
resources :posts, shallow: true
end
# /users/:user_id/posts (index, create)
# /posts/:id (show, edit, update, destroy)
ЛегкоRuby on Rails / Views
Helpers, layouts, yield
Что такое helpers, layouts и yield во views?
Layouts — обёртка для всех страниц:
# app/views/layouts/application.html.erb
<html>
<body>
<%= yield %> <!-- сюда вставится содержимое страницы -->
</body>
</html>
content_for / yield — именованные блоки:
# В шаблоне:
<% content_for :title, "Моя страница" %>
# В layout:
<title><%= yield :title %></title>
Helpers — методы для views (и контроллеров):
# app/helpers/application_helper.rb
module ApplicationHelper
def formatted_date(date)
date.strftime("%d.%m.%Y") if date
end
end
# Во view:
<%= formatted_date(@post.created_at) %>
Встроенные helpers:
link_to "Текст", path
button_to "Удалить", post_path(@post), method: :delete
form_with model: @post
image_tag "logo.png"
number_to_currency(1000) # => "1,000.00 ₽"
СреднеRuby on Rails / Продвинутые темы
ActionCable (WebSockets)
Что такое ActionCable? Для чего нужен?
ActionCable — фреймворк для real-time через WebSockets.
Channel (канал):
# app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
def subscribed
stream_from "chat_#{params[:room]}"
end
def receive(data)
ActionCable.server.broadcast("chat_#{params[:room]}", data)
end
def unsubscribed
# cleanup
end
end
Сервер — broadcast откуда угодно:
ActionCable.server.broadcast("chat_1", { message: "Привет!" })
Клиент (JS):
const cable = ActionCable.createConsumer()
const chat = cable.subscriptions.create("ChatChannel", { room: 1 })
chat.received = (data) => { appendMessage(data) }
Использование: чаты, уведомления, live-обновления.
Нужен Redis в production: config/cable.yml → redis adapter.
Turbo Streams — альтернатива для простых случаев (без JS).
СреднеRuby on Rails / Продвинутые темы
Concerns в моделях
Как использовать concerns в моделях?
Concerns — модули для переиспользования кода между моделями.
# app/models/concerns/searchable.rb
module Searchable
extend ActiveSupport::Concern
included do
scope :search, ->(q) { where("title ILIKE ?", "%#{q}%") }
end
class_methods do
def fuzzy_search(q)
where("title ILIKE ?", "%#{q}%")
end
end
end
class Post < ApplicationRecord
include Searchable
end
class Article < ApplicationRecord
include Searchable
end
Post.search("ruby") # работает
Article.fuzzy_search("rails") # тоже работает
ActiveSupport::Concern даёт:
— included { } — блок выполняется при include
— class_methods { } — добавляет методы класса
Не путать с Service Objects — concerns для общих методов моделей,
Service Objects для бизнес-логики.
ЛегкоDocker
Что такое Docker
Что такое Docker? Зачем он нужен? Чем контейнер отличается от виртуальной машины?
Docker — инструмент для упаковки приложения и его зависимостей в контейнер. Контейнер работает одинаково на любой машине.
Контейнер vs ВМ:
ВМ — полный клон ОС с ядром. Тяжёлый (гигабайты), медленный запуск.
Контейнер — использует ядро хоста. Лёгкий (мегабайты), мгновенный запуск.
Образ (image) — шаблон для создания контейнеров. Создаётся из Dockerfile.
Контейнер (container) — запущенный экземпляр образа.
Реестр (registry) — хранилище образов (Docker Hub).
Dockerfile — файл с инструкциями для сборки образа:
FROM ruby:3.3
WORKDIR /app
COPY . .
RUN bundle install
CMD ["rails", "server"]
ЛегкоDocker
docker-compose
Что такое docker-compose? Зачем нужен, когда можно просто Docker?
docker-compose — инструмент для запуска нескольких контейнеров одновременно. Описывает всю инфраструктуру в одном файле docker-compose.yml.
Пример — Rails + PostgreSQL:
services:
app:
build: .
ports:
- "3000:3000"
depends_on:
- db
db:
image: postgres:16
volumes:
- pgdata:/var/lib/postgresql/data
Команды:
docker compose up — запустить всё
docker compose up -d — в фоне
docker compose down — остановить
docker compose build — пересобрать образы
Без compose тебе пришлось бы вручную запускать каждый контейнер, подключать сети, пробрасывать порты.
ЛегкоElixir / Основы Elixir
Что такое Elixir
Что такое Elixir? На чём он работает? Зачем его учить Rails-разработчику?
Elixir — функциональный язык программирования, работает на виртуальной машине BEAM (Erlang VM).
Особенности:
- Функциональный (нет mutable state, нет классов, нет наследования)
- Поддерживает hot code reload — можно обновлять код без остановки приложения
- Fault-tolerant — процесс упал, остальные продолжают работать (supervisor tree)
- Распараллеливание — миллионы легковесных процессов одновременно
- OTP — фреймворк для построения отказоустойчивых систем
Зачем Rails-разработчику:
- Phoenix — веб-фреймворк, конкурент Rails по скорости
- LiveView — realtime UI без JavaScript
- Nerves — программирование встраиваемых устройств
- Расширение Rails-приложения: websocket, background jobs, code battle
ЛегкоElixir / Основы Elixir
Функции в Elixir
Как объявить функцию в Elixir? Чем анонимная функция отличается от именованной?
Именованная функция (внутри модуля):
defmodule Math do
def add(a, b) do
a + b
end
def greet(name) do
"Hello, #{name}"
end
end
Math.add(2, 3) # => 5
Math.greet("Alice") # => "Hello, Alice"
Анонимная функция (first-class citizen):
greet = fn name -> "Hello, #{name}" end
greet.("Alice") # => "Hello, Alice"
Обрати внимание: анонимная функция вызывается с точкой greet.(...), именованная без Math.greet(...).
Capture operator:
add = &Math.add/2
add.(2, 3) # => 5
Сокращённая запись анонимной функции:
square = &(&1 * &1)
square.(5) # => 25
ЛегкоElixir / Основы Elixir
Списки и кортежи
Что такое списки (List) и кортежи (Tuple)? Чем они отличаются?
List — связный список, добавление в начало O(1), доступ по индексу O(n):
[1, 2, 3]
[1 | [2, 3]] # => [1, 2, 3] (добавление в голову)
list = [1, 2, 3]
[0 | list] # => [0, 1, 2, 3]
Tuple — contiguous block of memory, доступ по индексу O(1), размер фиксирован:
{:ok, "result"}
person = {"Alice", 25}
elem(person, 0) # => "Alice"
person |> elem(0) # => "Alice"
Когда использовать:
List — коллекция переменного размера (много элементов, добавление/удаление)
Tuple — фиксированное количество элементов (результат операции: {:ok, data} / {:error, reason})
СреднеElixir / Pattern Matching
Pattern Matching в функциях
Как использовать Pattern Matching в определении функций?
В Elixir можно определять несколько вариантов одной функции с разным pattern matching. Elixir выбирает первый совпавший вариант (как case/when в Ruby).
defmodule Math do
def factorial(0), do: 1
def factorial(n) when n > 0, do: n * factorial(n - 1)
end
factorial(5) # => 120
factorial(0) # => 1
Анонимные функции тоже поддерживают pattern matching:
handle_result = fn
{:ok, value} -> "Success: #{value}"
{:error, reason} -> "Error: #{reason}"
end
handle_result.({:ok, 42}) # => "Success: 42"
handle_result.({:error, "bad"}) # => "Error: bad"
Это заменяет if/else и case — код чище и декларативнее.
СреднеElixir / Pattern Matching
Pin operator
Что такое pin operator ^? Зачем он нужен?
Pin operator (^) заставляет Elixir использовать текущее значение переменной, а не переприсваивать его.
Без ^:
x = 1
x = 2 # x теперь 2 (переменная переприсвоена)
С ^:
x = 1
^x = 1 # match: 1 == 1
^x = 2 # MatchError: 1 != 2
Пример в функции:
defmodule Greeter do
def greet(name, role) when role == :admin do
"Hello, #{name} (admin)"
end
def greet(name, _role) do
"Hello, #{name}"
end
end
С pin operator в case:
expected = 42
case some_value do
^expected -> "exactly 42"
other -> "got #{other}"
end
СреднеRuby / Окружение и диагностика
Запускается не та версия Ruby
Вы установили нужную версию Ruby через rbenv, но команда ruby -v показывает другую (системную) версию. В чём причина и как это исправить?
Причина: менеджер версий (rbenv, rvm, asdf) не инициализирован в текущей оболочке, либо в $PATH системный Ruby стоит раньше.
Диагностика:
which ruby # /usr/bin/ruby — системный (неправильно)
# /home/user/.rbenv/shims/ruby — через rbenv (правильно)
which -a ruby # показать все ruby в системе
Решение для rbenv — добавить в ~/.bashrc (или ~/.zshrc):
export PATH="$HOME/.rbenv/bin:$PATH"
eval "$(rbenv init -)"
Затем:
source ~/.bashrc
rbenv rehash
Приоритет выбора версии (от высокого к низкому):
1. Переменная окружения RBENV_VERSION
2. .ruby-version в текущей папке (rbenv local)
3. ~/.rbenv/version (rbenv global)
4. Системный Ruby (/usr/bin/ruby)
Типичная ошибка: установлен rbenv, но не прописан eval "$(rbenv init -)" в rc-файл оболочки.
СреднеRuby / Окружение и диагностика
Гем установлен, но require падает
Вы выполнили gem install pry, но в Ruby-скрипте require 'pry' выдаёт LoadError. Гем точно установлен. В чём проблема?
Причина: гем установлен для одной версии Ruby, а скрипт запускается другой.
У каждой версии Ruby — своя директория с гемами:
gem env gemdir # покажет путь к гемам текущего Ruby
ruby -v # текущая версия
Проверить:
gem list pry # установлен ли для текущего Ruby?
Частые сценарии:
- Установили гем под Ruby 3.1, а запускаете скрипт под Ruby 3.3
- Используете системный Ruby вместо rbenv/rvm
- Забыли bundle install перед запуском
Правильный подход — использовать Bundler:
bundle install # установить гемы из Gemfile
bundle exec ruby script.rb # запустить с нужными версиями гемов
Проверить, откуда загрузился гем:
ruby -e "require 'pry'; puts Gem.loaded_specs['pry'].full_gem_path"
СреднеRuby / Окружение и диагностика
bundle exec vs ruby
Скрипт работает с bundle exec ruby script.rb, но падает с обычным ruby script.rb. Почему так происходит и что делает bundle exec?
bundle exec гарантирует, что Ruby использует именно те версии гемов, которые указаны в Gemfile.lock.
Без bundle exec:
- Ruby ищет гемы по $LOAD_PATH — массиву директорий
- Берёт первый найденный гем, независимо от версии
- Может подхватить несовместимую версию
С bundle exec:
- Bundler подменяет $LOAD_PATH, добавляя только нужные версии
- Все зависимости разрешены по Gemfile.lock
- Гарантированно воспроизводимое окружение
Посмотреть $LOAD_PATH:
ruby -e 'puts $LOAD_PATH'
bundle exec ruby -e 'puts $LOAD_PATH' # будет включать гемы из Gemfile
$LOAD_PATH — это массив директорий, где Ruby ищет файлы при require.
Первый найденный файл побеждает — поэтому порядок директорий важен.
Проверить, какой файл реально загрузился:
$LOADED_FEATURES.grep(/some_gem/)
СреднеRuby / Окружение и диагностика
Гем удалён, но команда всё ещё работает
Вы выполнили gem uninstall rails, но команда rails -v всё ещё показывает версию. Как такое возможно?
Причин может быть несколько:
1. Несколько версий Ruby — удалили из одной, команда доступна из другой:
which rails # откуда берётся команда?
gem list rails # установлен ли для текущего Ruby?
2. Binstub остался — исполняемый файл в bin/ или /usr/local/bin:
which rails # /usr/local/bin/rails или bin/rails
file $(which rails) # посмотреть тип файла
3. Другой гем предоставляет команду rails (например, railties)
4. Spring кеширует — в Rails-проекте:
spring stop # остановить Spring
Диагностика:
which -a rails # все исполняемые файлы rails
gem environment # полная информация об окружении
bundle show rails # где гем в текущем проекте
Правильная очистка:
gem uninstall rails -a # удалить все версии
which rails # проверить, осталась ли команда
СреднеRuby / Окружение и диагностика
Что такое $LOAD_PATH
Интервьюер просит проверить '$LOAD_PATH' и посмотреть приоритет загрузки. Что это такое, как проверить и зачем это нужно?
$LOAD_PATH (или коротко $:) — глобальная переменная Ruby, массив директорий, в которых Ruby ищет файлы при вызове require.
Проверить:
ruby -e 'puts $LOAD_PATH'
ruby -e '$LOAD_PATH.each_with_index { |p, i| puts "#{i}: #{p}" }'
bundle exec ruby -e 'puts $LOAD_PATH' # с учётом Gemfile
Ruby идёт по директориям слева направо и берёт первый найденный файл.
Отсюда конфликты: если в двух директориях есть файл с одинаковым именем,
загрузится тот, что стоит раньше в $LOAD_PATH.
Когда это важно:
- Конфликты версий гемов
- Отладка: какой файл реально загрузился
- Понимание, почему require находит (или не находит) файл
Проверить, откуда загрузился конкретный файл:
ruby -e "require 'rails'; puts $LOADED_FEATURES.grep(/rails/)"
СреднеRuby on Rails / Диагностика и проблемы
PostgreSQL: connection refused
Вы запускаете rails db:migrate и получаете ошибку connection refused. PostgreSQL установлен и работает. В чём может быть проблема?
Возможные причины и проверка:
1. Неправильный порт в config/database.yml:
По умолчанию PostgreSQL слушает порт 5432, но если установлен второй инстанс — может быть 5433.
Проверить порт PostgreSQL:
sudo -u postgres psql -c "SHOW port;"
Или посмотреть в postgresql.conf.
2. PostgreSQL слушает не на том хосте:
В database.yml указан host: localhost, но PostgreSQL слушает только Unix-сокет.
Или наоборот: host: 127.0.0.1, а PostgreSQL принимает только на /var/run/postgresql/.
3. Неверные учётные данные в database.yml:
Проверить: username, password, host, port.
4. Файл database.yml не создан (нет config/database.yml):
Rails использует дефолтные значения, которые могут не совпадать с вашей установкой.
Диагностика:
cat config/database.yml # проверить настройки
sudo -u postgres psql # подключиться напрямую
pg_isready # доступен ли PostgreSQL
ss -tlnp | grep 5432 # слушает ли порт
Алгоритм: сначала подключиться напрямую (psql), потом сверить настройки с database.yml.
СреднеRuby on Rails / Диагностика и проблемы
Таблица создана, но в schema.rb её нет
Вы выполнили миграцию, таблица в базе данных создалась (проверили через psql), но в db/schema.rb она не появилась. Почему?
Причина: формат схемы установлен в :sql вместо :ruby.
По умолчанию Rails использует формат :ruby — генерирует db/schema.rb.
Но если в конфиге указано:
config.active_record.schema_format = :sql
То структура БД будет сохранена в db/structure.sql (сырой SQL-дамп).
Проверить:
ls db/schema.rb db/structure.sql # какой файл существует?
Также возможные причины:
- Миграция упала в середине, но таблица успела создаться
- Забыли запустить rails db:migrate RAILS_ENV=test
- Файл schema.rb закрыт для записи
Решение: проверить формат в config/application.rb или config/environments/*.rb
и запустить rails db:schema:dump (или db:structure:dump).
ЛегкоRuby on Rails / Диагностика и проблемы
Тесты падают: database does not exist
rails server и rails console работают нормально, но при запуске тестов (rails test) возникает ошибка: database 'app_test' does not exist. Почему?
Причина: Rails использует разные базы данных для разных окружений:
- development → app_development (создана, работает)
- test → app_test (не создана)
- production → app_production
По умолчанию rails server и rails console запускаются в development.
rails test запускается в test окружении — нужна отдельная база.
Решение:
RAILS_ENV=test rails db:create # создать тестовую базу
RAILS_ENV=test rails db:migrate # применить миграции
# Или одной командой:
rails db:test:prepare # подготовить тестовую БД
Или проще — при настройке проекта:
rails db:create # создаст все базы (development + test)
rails db:migrate # миграции для development
rails db:test:prepare # скопировать схему в test
ЛегкоRuby on Rails / Диагностика и проблемы
Новая колонка не видна в консоли
Вы добавили колонку через миграцию (rails db:migrate прошёл успешно), но в rails console вызов Model.column_names не показывает новую колонку. В чём проблема?
Причина: консоль была открыта ДО выполнения миграции.
ActiveRecord кеширует схему таблиц при первой загрузке модели.
Решение — обновить кеш схемы прямо в консоли:
Model.reset_column_information
Или просто перезапустить консоль:
exit
rails console
Почему так происходит:
- При запуске консоли ActiveRecord загружает описание таблиц
- Это описание хранится в памяти до конца сессии
- Новая миграция меняет БД, но консоль не знает об этом
Проверить, что миграция действительно прошла:
rails db:migrate:status
# должна быть отметка "up" рядом с нужной миграцией
СреднеRuby on Rails / Диагностика и проблемы
Изменения в коде не применяются
Вы сохранили изменения в контроллере, перезапустили rails server, но в браузере всё равно старый результат. В чём может быть дело?
Возможные причины (от частых к редким):
1. Spring кеширует код приложения:
spring stop # остановить Spring
# Или отключить: DISABLE_SPRING=1 rails server
2. Кеширование в development:
config.action_controller.perform_caching = false
# Проверить в config/environments/development.rb
3. Браузерный кеш:
Ctrl+Shift+R (жёсткое обновление)
# Или открыть в режиме инкогнито
4. Редактируете не тот файл:
puts "HERE" в контроллере
# Если не видно в логах — открыт не тот файл
5. Запущено несколько серверов:
lsof -i :3000 # проверить, что слушает порт
kill -9 <PID> # убить старый процесс
В development-режиме Rails автоматически перезагружает код при каждом запросе,
но Spring может вмешиваться и кешировать.
СреднеRuby on Rails / Диагностика и проблемы
link_to :delete отправляет GET
В шаблоне написано link_to 'Удалить', item_path(@item), method: :delete, но при клике отправляется GET-запрос вместо DELETE. Почему?
Причина: не работает JavaScript-обработчик для не-GET запросов.
Ссылки (<a>) по умолчанию отправляют GET. Чтобы отправить DELETE,
Rails генерирует атрибут data-method="delete" и полагается на
JavaScript (rails-ujs или Turbo), который перехватывает клик и
отправляет правильный HTTP-метод через форму.
Решение зависит от версии Rails:
Rails 6 и ниже — нужен rails-ujs:
//= require rails-ujs в application.js
Rails 7 (Turbo) — другой синтаксис:
link_to 'Удалить', item_path(@item), data: { turbo_method: :delete }
Rails 7 (без Turbo, с rails-ujs):
link_to 'Удалить', item_path(@item), data: { method: :delete }
Или использовать button_to (работает без JavaScript):
button_to 'Удалить', item_path(@item), method: :delete
# button_to генерирует <form>, а не <a> — форма поддерживает любой метод
Лучший подход: для деструктивных действий использовать button_to.
СложноRuby on Rails / Диагностика и проблемы
bundle install: native extension error
При запуске bundle install вы получаете ошибку: Gem::Ext::BuildError: ERROR: Failed to build gem native extension. Что делать?
Причина: гем содержит C-код, для компиляции которого не хватает системных библиотек.
Частые виновники:
- pg (PostgreSQL клиент) — нужен libpq-dev
- nokogiri (XML парсер) — нужен libxml2-dev, libxslt1-dev
- ffi — нужен build-essential
- sqlite3 — нужен libsqlite3-dev
- rmagick (ImageMagick) — нужен libmagickwand-dev
Решение (Ubuntu/Debian):
sudo apt install build-essential libpq-dev libxml2-dev libxslt1-dev libsqlite3-dev
Для pg конкретно:
sudo apt install libpq-dev
# macOS: brew install postgresql
Для nokogiri:
sudo apt install libxml2-dev libxslt1-dev
# Или использовать встроенные библиотеки:
bundle config build.nokogiri --use-system-libraries
Алгоритм:
1. Прочитать полное сообщение об ошибке — там указан конкретный гем
2. Найти системные зависимости этого гема (обычно в документации)
3. Установить зависимости: sudo apt install ...
4. Повторить bundle install
ЛегкоGit / Основы Git
VCS и Git: что и зачем
Что такое VCS? Что такое Git? Почему его используют?
VCS (Version Control System) — система контроля версий —
программа для работы с изменяющейся информацией.
Git — распределённая система контроля версий, которая даёт
возможность отслеживать изменения в файлах и работать
совместно с другими разработчиками.
Подход Git к хранению данных — набор снимков файловой системы.
При каждом сохранении Git запоминает, как выглядит каждый файл,
и сохраняет ссылку на этот снимок.
Почему Git:
— Распределённый: полная копия репозитория у каждого
— Быстрый: большинство операций локально
— Ветвление: лёгкое создание и слияние веток
— Целостность: каждое изменение проверяется SHA-1 хешем
ЛегкоGit / Основы Git
Создание и подключение репозитория
Как создать репозиторий, подключить внешний репозиторий?
Создание нового репозитория и подключение к GitHub:
git init # инициализация локального репозитория
git add . # добавить все файлы в staging
git commit -m "first commit" # первый коммит
git remote add origin git@github.com:username/project.git
git push -u origin master # отправить на GitHub
remote — указатель на внешний репозиторий.
origin — стандартное имя для основного remote.
Посмотреть remote:
git remote -v
git remote remove origin # удалить remote
ЛегкоGit / Основы Git
Клонирование удалённого репозитория
Как загрузить удалённый репозиторий?
git clone — создаёт локальную копию удалённого репозитория:
git clone git@github.com:username/project.git
Клон по SSH (git@github.com:...) — нужна настройка SSH-ключа.
Клон по HTTPS (https://github.com/...) — логин/пароль или токен.
Клон в другую папку:
git clone git@github.com:user/repo.git my-folder
Клон определённой ветки:
git clone -b branch-name git@github.com:user/repo.git
ЛегкоGit / Основы Git
Коммит и история коммитов
Что такое коммит? Как посмотреть историю коммитов?
Коммит (commit) — подтверждение изменений, снимок состояния проекта.
Каждый коммит имеет уникальный SHA-1 хеш.
История коммитов:
git log # полная история
git log --oneline # краткий формат (хеш + сообщение)
git log --oneline -10 # последние 10 коммитов
git log --graph --oneline # с визуализацией веток
git log --author="username" # фильтр по автору
git log --since="2 weeks ago" # фильтр по дате
Показать изменения в коммите:
git show abc1234 # содержимое конкретного коммита
git diff abc1234^..abc1234 # что изменилось в коммите
ЛегкоGit / Основы Git
Состояния файлов в Git
Какие состояния файлов существуют в системе Git?
В Git файлы могут находиться в одном из трёх состояний:
1. Зафиксированный (committed) — файл сохранён в локальной базе
2. Изменённый (modified) — файл изменён, но не зафиксирован
3. Подготовленный (staged) — изменённый файл отмечен для
включения в следующий коммит
Три области проекта:
— .git/ (Git directory) — метаданные и база объектов
— Рабочий каталог (working directory) — файлы проекта
— Staging area (index) — подготовленные файлы
Переходы:
modified → git add → staged → git commit → committed
ЛегкоGit / Основы Git
git commit: флаги -am, -a, -m
git commit — в каких случаях писать -am, -a и -m?
-m "message" — создать коммит с сообщением для файлов
из staging area (предварительно добавленных через git add):
git add some.file
git commit -m "Your message here"
-a — автоматически добавить в staging все отслеживаемые
(индексированные) файлы. Новые (неотслеживаемые) файлы
в коммит НЕ попадут:
git commit -a -m "Your message here"
-am — объединение флагов -a и -m:
git commit -am "Your message here"
Разница:
-m — только файлы из staging (после git add)
-a — все отслеживаемые файлы автоматически
-am — все отслеживаемые + сообщение
ЛегкоGit / Основы Git
SSH-ключ для Git
Для чего нужен SSH-ключ?
SSH-ключи используются для авторизации на сервисах
(GitHub, GitLab, серверы) без ввода пароля.
SSH-ключ состоит из двух частей:
— id_rsa — закрытая часть. Должна быть доступна только вам.
Никому не давайте доступ. Один ключ можно использовать
на нескольких машинах, но это увеличивает риск.
— id_rsa.pub — открытая часть. Можно показывать всем.
Добавляется в ~/.ssh/authorized_keys на сервере или
в настройки GitHub/GitLab.
Генерация ключа:
ssh-keygen -t ed25519 -C "your@email.com"
Добавить ключ в ssh-agent:
eval "$(ssh-agent -s)"
ssh-add ~/.ssh/id_ed25519
Проверить подключение к GitHub:
ssh -T git@github.com
ЛегкоGit / Ветвление и слияние
Ветки в Git
Что такое ветка в Git?
Ветка (branch) — организованная система ссылок на коммиты.
Ветка по умолчанию — master (или main).
При каждом коммите ветка сдвигается вперёд автоматически.
Основные операции:
git branch # список веток
git branch feature # создать ветку
git checkout feature # переключиться на ветку
git checkout -b feature # создать + переключиться
git switch -c feature # новый способ (Git 2.23+)
Ответвление от основной ветки делается для работы
с определённой фичей, исправлением бага или экспериментом.
Ветка — это просто указатель (ref) на коммит.
Создание ветки — очень быстрая операция.
СреднеGit / Ветвление и слияние
merge и rebase: отличия
Отличия между командами merge и rebase?
Два способа включить изменения из одной ветки в другую:
merge — создаёт коммит-слияние (merge commit):
git checkout main
git merge feature
— Сохраняет полную историю
— Создаёт новый коммит с двумя предками
— Безопасно для публичных веток
— История может быть нелинейной
rebase — переписывает коммиты поверх целевой ветки:
git checkout feature
git rebase main
— Находится общий предок двух веток
— Дельта каждого коммита сохраняется
— Текущая ветка ставится на коммит целевой
— Изменения применяются одно за другим
— История становится линейной и аккуратной
— Хеши коммитов переписываются!
rebase даёт чистую историю, но переписывает хеши.
merge сохраняет реальную историю.
ЛегкоGit / Ветвление и слияние
Золотое правило rebase
Какое золотое правило rebase?
Не перемещайте (rebase) коммиты, которые вы уже отправили
в публичный репозиторий.
Правило вытекает из свойства rebase переписывать историю
коммитов с новыми хешами.
Если сделать rebase публичных коммитов:
— У других разработчиков будут конфликты при pull
— История разойдётся
— Git будет считать коммиты разными
Безопасно:
git rebase main # свою feature-ветку на main
Опасно:
git rebase main # если main — публичная ветка
Если запушили — используйте merge, не rebase.
ЛегкоGit / Ветвление и слияние
Удаление веток
Как удалить ветку локально и с удалённого репозитория?
Удалить локальную ветку:
git branch -d feature-branch # безопасное удаление (только если слита)
git branch -D feature-branch # принудительное удаление
Удалить ветку с удалённого репозитория (GitHub):
git push origin --delete feature-branch
Удалить отслеживающую ветку (если удалённая удалена):
git fetch --prune # очистить устаревшие refs
git branch -vv # показать, какие ветки отслеживают
git remote prune origin # удалить stale отслеживающие ветки
Проверить, слита ли ветка:
git branch --merged main # ветки, слитые в main
СреднеGit / Ветвление и слияние
cherry-pick: перенос коммитов
Как перенести изменения из одной ветки в другую?
Способ 1 — cherry-pick (перенос конкретного коммита):
git log --oneline -5 # найти хеш нужного коммита
git checkout target-branch # перейти в целевую ветку
git cherry-pick abc1234 # применить коммит
cherry-pick создаёт НОВЫЙ коммит с теми же изменениями,
но другим хешем.
Способ 2 — создать ветку и смержить в обе:
git checkout -b feature main # создать ветку от main
# ... сделать изменения, закоммитить
git checkout main
git merge feature # слить в main
git checkout other-branch
git merge feature # слить в другую ветку
cherry-pick удобнее для единичных коммитов.
merge — для целых наборов изменений.
ЛегкоGit / Ветвление и слияние
Pull requests
Что такое запросы на слияние (pull requests)? Как их создавать?
Pull Request (PR) — запрос на слияние вашей ветки с основной.
Удобная система взаимодействия между автором изменений
и хозяином репозитория.
Позволяет:
— Обсуждать изменения (комментарии к строкам кода)
— Вносить правки до слияния
— Запускать CI/CD проверки
— Требовать аппрувы (code review)
Как создать PR:
1. Создать ветку и закоммитить изменения
2. Запушить ветку на GitHub
3. Зайти на GitHub → "Compare & pull request"
4. Описать изменения, назначить ревьюеров
5. После аппрува — "Merge pull request"
В GitLab — Merge Request (MR). Суть та же.
ЛегкоGit / Рабочий процесс
Отправка изменений на удалённый репозиторий
Как отправить свои изменения на удалённый репозиторий?
Стандартный workflow:
git add . # добавить изменения в staging
git commit -m "Commit message" # зафиксировать
git push # отправить на удалённый
Первый push новой ветки:
git push -u origin feature-branch # -u = --set-upstream
Последующие push:
git push # запомнила после -u
Загрузить чужие изменения перед push:
git pull --rebase # получить + переместить свои коммиты наверх
Push с принудительной перезаписью (ОСТОРОЖНО):
git push --force # перезаписать удалённую историю
git push --force-with-lease # безопаснее — не перезапишет чужие коммиты
ЛегкоGit / Рабочий процесс
Получение последних изменений
Как загрузить последние изменения с определённой ветки?
git pull — получить и слить изменения:
git pull origin main # получить main и слить с текущей
git pull --rebase — получить и переместить:
git pull --rebase # ваши коммиты будут поверх полученных
Предпочтительный вариант — чистая история без merge-коммитов
git fetch — только получить (без слияния):
git fetch origin # скачать все изменения
git fetch origin main # скачать только main
git log origin/main # посмотреть, что нового
git merge origin/main # слить когда готовы
Разница:
— pull = fetch + merge
— pull --rebase = fetch + rebase
— fetch — безопаснее, даёт контроль
ЛегкоGit / Рабочий процесс
Изменение последнего коммита (amend)
Как добавить изменения в уже созданный коммит? Как изменить название коммита?
git commit --amend — изменить последний коммит:
Добавить забытые файлы в последний коммит:
git add forgotten_file.rb
git commit --amend --no-edit # добавить без изменения сообщения
Изменить сообщение последнего коммита:
git commit --amend -m "New message"
Изменить и файлы, и сообщение:
git add .
git commit --amend -m "Updated commit"
Важно: amend создаёт НОВЫЙ хеш коммита.
Если вы уже запушили этот коммит — нужно:
git push --force-with-lease
Но! Не делайте amend коммитов, которые уже в публичной ветке
и другие разработчики могли на них основываться.
ЛегкоGit / Рабочий процесс
Git vs SVN
Разница между Git и SVN (Subversion)?
Главное отличие: Git — распределённая VCS, SVN — централизованная.
Преимущества Git:
— Сервер не нужен. Можно работать полностью локально
— Если сервер «прилёг» — коммиты в локальный репозиторий
продолжают работать, а когда сервер вернётся — push
— Шифрование «из коробки» (SHA-1 хеши)
— Служебная информация только в .git/ в корне проекта,
а не в каждом каталоге (как .svn у Subversion)
— Быстрое ветвление и слияние
SVN особенности:
— Централизованная модель: нужен сервер для几乎所有 операций
— Нумерация коммитов — последовательная (1, 2, 3...)
— Лучше работает с большими бинарными файлами
— Можно клонировать поддерево каталога
В продакшене Git — стандарт де-факто.
СреднеGit / Рабочий процесс
Gitflow: модель ветвления
Что такое Gitflow?
Gitflow — модель ветвления для управления релизами.
Основное правило GitHub Flow: всё, что в master (main) —
гарантированно стабильно и готово к деплою в любой момент.
Основные ветки в Gitflow:
— main/master — стабильный код (production)
— develop — ветка разработки (интеграция фич)
— feature/* — отдельные фичи (от develop)
— release/* — подготовка к релизу
— hotfix/* — срочные исправления (от main)
Workflow:
1. Создать feature-ветку от main
2. Разработать, коммитить в feature-ветку
3. Открыть Pull Request в main
4. Code review, тесты
5. Слить в main после аппрува
Каждая новая ветвь создаётся от main.
Вносить правки напрямую в main — нельзя.
СреднеGit / Проблемы и решения
Push прошёл, но на продакшене изменений нет
Вы сделали git push, команда прошла без ошибок, но на продакшене (или на тестовом сервере) изменений нет. В чём может быть проблема?
Возможные причины:
1. Запушили не в ту ветку:
git branch -vv # посмотреть, куда привязана текущая ветка
git log origin/main # проверить, есть ли коммиты в main
2. Push был в origin, а деплой настроен с другого remote:
git remote -v # показать все remote'ы
# origin → github.com:you/repo
# deploy → production-server
3. Запушили в свою feature-ветку, а не в main:
git push origin feature-branch # запушили сюда
git push origin main # а нужно было сюда
4. CI/CD не прошёл — изменения не попали на сервер:
Проверить статус pipeline (GitHub Actions, GitLab CI и т.д.)
5. На сервере не выполнили миграции:
Даже если код обновился, БД может быть старой.
На сервере: rails db:migrate RAILS_ENV=production
Диагностика:
git log --oneline -5 # последние коммиты
git diff origin/main...HEAD # что не в origin/main
git remote -v # куда пушите
ЛегкоGit / Проблемы и решения
Конфликты слияния: unmerged paths
Вы делаете git merge main в свою ветку, возникают конфликты. Вы исправили файлы вручную, но git status всё ещё показывает 'unmerged paths'. Что пропущено?
После ручного разрешения конфликтов нужно сообщить Git, что файлы исправлены:
git add <файл> # отметить файл как разрешённый
# Или все сразу:
git add .
Затем завершить слияние:
git commit # (без -m — Git предложит сообщение о слиянии)
Полный алгоритм разрешения конфликтов:
1. git merge main # начать слияние
2. Git помечает файлы с конфликтами: <<<<<<<, =======, >>>>>>>
3. Открыть файлы, удалить маркеры, оставить нужный код
4. git add . # ← ЭТО ЧАСТО ЗАБЫВАЮТ
5. git commit # завершить слияние
Проверить статус:
git status # зелёный — готово, красный — ещё конфликт
Отменить слияние, если запутались:
git merge --abort # вернуться к состоянию до слияния
СреднеGit / Проблемы и решения
Случайный коммит не в ту ветку
Вы сделали коммит в main, хотя хотели сделать его в feature-ветке. Как перенести коммит, не потеряв работу?
Решение через git cherry-pick:
1. Запомнить хэш коммита:
git log --oneline -3
# abc1234 Мой важный коммит
2. Переключиться на нужную ветку:
git checkout feature-branch
3. Перенести коммит:
git cherry-pick abc1234
4. Вернуться в main и откатить коммит:
git checkout main
git reset --soft HEAD~1 # сохранить изменения в staging
# Или:
git reset --mixed HEAD~1 # вернуть изменения в рабочую директорию
# Или (ОСТОРОЖНО — теряет изменения):
git reset --hard HEAD~1 # удалить коммит полностью
Важно: если вы уже запушили коммит в общий репозиторий —
не используйте reset. Используйте git revert:
git revert abc1234 # создаст обратный коммит
ЛегкоБазы данных / Основы БД
Реляционная база данных
Что такое реляционная база данных?
Реляционная БД — набор данных с предопределёнными связями между ними.
Данные организованы в виде таблиц из столбцов и строк.
— Столбцы — определённый тип данных (имя, возраст, email)
— Строки — набор связанных значений (одна запись/сущность)
— Каждая ячейка — значение атрибута
Связи между таблицами реализуются через ключи (primary key, foreign key).
Примеры реляционных БД: PostgreSQL, MySQL, SQLite, Oracle, SQL Server.
Примеры нереляционных: MongoDB (документы), Redis (ключ-значение),
Neo4j (графы), Cassandra (колоночная).
ЛегкоБазы данных / Основы БД
Таблица, кортеж, первичный ключ
Что такое таблица, кортеж? Что такое primary key?
Таблица — набор элементов данных в виде столбцов (с уникальным именем)
и строк. Содержит определённое число столбцов, но любое количество строк.
Кортеж — это набор именованных значений заданного типа (одна строка таблицы).
Primary key (PK) — подмножество столбцов, которое уникально идентифицирует
строку. Каждая строка однозначно определяется одним или несколькими
уникальными значениями.
Свойства PK:
— Не позволяет создавать одинаковые записи (строк)
— Обеспечивает логическую связь между таблицами
— Не может быть NULL
— В таблице только один PK
В Rails: по соглашению используется столбец id (auto-increment),
который автоматически создаётся для каждой записи.
create_table :users do |t|
t.string :name
t.timestamps
end
# id создаётся автоматически как PRIMARY KEY
ЛегкоБазы данных / Основы БД
Связи между таблицами: foreign key
Как реализованы связи между таблицами? Что такое foreign key?
Между таблицами существуют три вида связей:
Один-ко-многим (1:N):
Один пользователь — много постов
users(id) ← posts(user_id)
Один-к-одному (1:1):
Один пользователь — один профиль
users(id) ← profiles(user_id) с UNIQUE
Многие-ко-многим (N:N):
Много студентов — много курсов
Нужна промежуточная таблица (join table):
students(id), courses(id), enrollments(student_id, course_id)
Foreign key (FK) — столбец в дочерней таблице, ссылающийся
на PK родительской. Обеспечивает ссылочную целостность.
В Rails:
belongs_to :user # posts.user_id → users.id
has_many :posts # users.id ← posts.user_id
has_many :through # для N:N через промежуточную таблицу
По соглашению Rails: столбец FK называется как модель + _id
(user_id, post_id).
СреднеБазы данных / Основы БД
Нормализация и денормализация
Что такое нормализация и денормализация базы данных?
Нормализация — преобразование структуры БД к виду, отвечающему
нормальным формам (НФ). Цель: устранить избыточность и аномалии.
Нормальные формы (основные):
— 1НФ: атомарные значения (не массивы в ячейках)
— 2НФ: все неключевые столбцы зависят от всего PK
— 3НФ: нет транзитивных зависимостей между неключевыми столбцами
Денормализация — намеренное нарушение нормальных форм
для ускорения чтения за счёт добавления избыточных данных.
Когда денормализовать:
— Частые сложные JOIN-запросы замедляют работу
— Read-heavy нагрузка (много чтений, мало записей)
— Кэширование вычисляемых полей (counter_cache в Rails)
Пример денормализации:
# Нормализовано: отдельная таблица
posts(id, title), comments(id, post_id, text)
# Денормализовано: счётчик в posts
posts(id, title, comments_count)
# comments_count обновляется триггером/counter_cache
ЛегкоБазы данных / Основы БД
Redis vs PostgreSQL: почему быстрее
Основная причина, по которой Redis работает быстрее PostgreSQL?
Причина в месте хранения данных:
— Redis хранит данные в оперативной памяти (RAM)
— PostgreSQL хранит данные на жёстком диске (SSD/HDD)
RAM на порядки быстрее диска:
— RAM: ~100 нс доступ (наносекунды)
— SSD: ~100 мкс доступ (микросекунды, в 1000 раз медленнее)
Redis — in-memory key-value хранилище:
— Нет парсинга SQL-запросов
— Нет планировщика запросов
— Нет сложных транзакций
— Данные в памяти — прямой доступ по ключу
Но: Redis теряет данные при перезагрузке (если не настроена
персистенция). PostgreSQL — надёжное хранение, ACID-транзакции,
сложные запросы, но медленнее.
Обычно используются вместе: PostgreSQL — основное хранилище,
Redis — кэш и сессии.
ЛегкоRuby / Основы
Целочисленное деление
Почему в Ruby 1660 / 100 не равно 16.6? Что нужно сделать, чтобы получить 16.6?
Если все аргументы арифметического выражения — целые числа (Integer),
то результат будет целым числом. Если хотя бы одно число с плавающей
запятой (Float), то результат будет Float.
1660 / 100 #=> 16 (Integer)
1660.0 / 100 #=> 16.6 (Float)
1660 / 100.0 #=> 16.6 (Float)
Чтобы получить 16.6, нужно чтобы хотя бы один операнд был Float:
1660.fdiv(100) #=> 16.6
1660.to_f / 100 #=> 16.6
ЛегкоБазы данных / SQL запросы
SELECT: оператор запроса
Как работает SELECT оператор?
SELECT — оператор запроса, возвращающий набор данных из БД.
Состоит из предложений (разделов):
SELECT — список возвращаемых столбцов
FROM — источник данных (таблица, join, подзапрос)
WHERE — фильтр строк
GROUP BY — группировка с агрегатными функциями
HAVING — фильтр групп (после GROUP BY)
ORDER BY — сортировка
LIMIT — ограничение количества строк
Порядок выполнения:
FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT
Пример:
SELECT department, COUNT(*), AVG(salary)
FROM employees
WHERE age > 25
GROUP BY department
HAVING COUNT(*) > 5
ORDER BY AVG(salary) DESC
LIMIT 10;
СреднеБазы данных / SQL запросы
Виды JOIN
Какие бывают виды JOIN? Как каждый работает?
INNER JOIN — только совпадающие строки из обеих таблиц:
SELECT * FROM users
INNER JOIN posts ON users.id = posts.user_id
# Только пользователи с постами
LEFT OUTER JOIN — все строки из левой + совпадающие из правой:
SELECT * FROM users
LEFT JOIN posts ON users.id = posts.user_id
# Все пользователи, даже без постов (posts = NULL)
RIGHT OUTER JOIN — все строки из правой + совпадающие из левой:
# То же что LEFT, но таблицы меняются местами
FULL OUTER JOIN — объединение LEFT и RIGHT:
# Все строки из обеих таблиц, NULL где нет совпадений
CROSS JOIN — декартово произведение:
# Каждая строка первой × каждая строка второй
# Не требует условия ON
SELECT * FROM colors CROSS JOIN sizes
# 3 цвета × 4 размера = 12 строк
В Rails:
User.joins(:posts) # INNER JOIN
User.left_joins(:posts) # LEFT JOIN
User.includes(:posts) # LEFT OUTER JOIN (eager loading)
ЛегкоБазы данных / SQL запросы
INSERT, UPDATE, DELETE
Как работают INSERT, UPDATE, DELETE операторы?
INSERT — добавление строк в таблицу:
INSERT INTO users (name, email)
VALUES ('Alice', 'alice@mail.com');
INSERT INTO users (name, email)
SELECT name, email FROM archive;
UPDATE — обновление значений:
UPDATE users
SET name = 'Bob', updated_at = NOW()
WHERE id = 1;
Важно: без WHERE обновятся ВСЕ строки!
DELETE — удаление записей:
DELETE FROM users WHERE id = 1;
Важно: без WHERE удалятся ВСЕ строки!
В Rails:
User.create(name: 'Alice') # INSERT
User.update(name: 'Bob') # UPDATE
User.delete(id) # DELETE (без callbacks)
User.destroy(id) # DELETE (с callbacks)
ЛегкоБазы данных / SQL запросы
WHERE и HAVING: отличия
Чем HAVING отличается от WHERE?
WHERE — фильтрует строки ДО группировки:
SELECT department, COUNT(*)
FROM employees
WHERE age > 25 # фильтр по строкам
GROUP BY department
HAVING — фильтрует группы ПОСЛЕ группировки:
SELECT department, COUNT(*)
FROM employees
GROUP BY department
HAVING COUNT(*) > 5 # фильтр по группам
Порядок выполнения:
FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY
Правило:
— WHERE — для условий на отдельные строки
— HAVING — для условий на агрегаты (COUNT, SUM, AVG)
Можно использовать оба:
SELECT department, AVG(salary) as avg_sal
FROM employees
WHERE age > 25 # фильтр строк
GROUP BY department
HAVING AVG(salary) > 50000 # фильтр групп
ЛегкоБазы данных / SQL запросы
Оператор BETWEEN
Что такое BETWEEN в SQL? Как работает?
BETWEEN — оператор для проверки вхождения значения в диапазон.
Включает ГРАНИЧНЫЕ значения.
Синтаксис:
SELECT * FROM products WHERE price BETWEEN 100 AND 500;
# Эквивалентно:
SELECT * FROM products WHERE price >= 100 AND price <= 500;
С датами:
SELECT * FROM orders
WHERE created_at BETWEEN '2024-01-01' AND '2024-12-31';
NOT BETWEEN — вне диапазона:
SELECT * FROM products WHERE price NOT BETWEEN 100 AND 500;
Важные нюансы:
— Включает обе границы (closed interval)
— Для дат: BETWEEN '2024-01-01' AND '2024-01-31'
НЕ включит 31 января 15:30 — нужно:
created_at >= '2024-01-01' AND created_at < '2024-02-01'
— Работает с числами, датами, строками (по алфавиту)
В Rails:
Product.where(price: 100..500) # BETWEEN
Product.where.not(price: 100..500) # NOT BETWEEN
Order.where(created_at: Date.today.beginning_of_month..Date.today.end_of_month)
ЛегкоБазы данных / SQL запросы
LIKE и ILIKE: поиск по шаблону
Чем LIKE отличается от ILIKE?
LIKE — поиск по шаблону (case-sensitive, регистрозависимый):
SELECT * FROM users WHERE name LIKE 'Иван%';
# Найдёт: Иван, Иванов, Иванова
# НЕ найдёт: иван, ИВАН
ILIKE — то же, но case-insensitive (нечувствителен к регистру):
SELECT * FROM users WHERE name ILIKE 'иван%';
# Найдёт: Иван, иван, ИВАН, Иванов, иванова
Шаблоны:
— % — любое количество символов (0 и более)
LIKE '%test%' # содержит 'test' где угодно
— _ — ровно один символ
LIKE '_ван' # Иван, Ован, но не Ван
Производительность:
— LIKE 'prefix%' — может использовать индекс (B-tree)
— LIKE '%suffix' — full scan, индекс не поможет
— LIKE '%middle%' — full scan
— Для быстрого поиска: полнотекстовый поиск или GIN-индекс
ILIKE доступен только в PostgreSQL.
В MySQL: LIKE всегда case-insensitive (зависит от collation).
В SQLite: LIKE case-insensitive для ASCII.
В Rails:
User.where('name LIKE ?', "#{params[:q]}%")
User.where('name ILIKE ?', "%#{params[:q]}%") # PostgreSQL only
User.where('name LIKE ?', "%#{params[:q]}%".downcase) # универсально
СреднеБазы данных / Индексы
Индексы: зачем и как
Что такое индексы? Для чего используются? Плюсы, минусы?
Индекс — объект БД, создаваемый для повышения производительности
поиска данных. Формируется из значений столбцов и указателей
на соответствующие строки таблицы.
Без индекса — последовательный просмотр всей таблицы (full scan).
С индексом — поиск по оптимизированной структуре (B-дерево).
Плюсы:
— Ускорение SELECT, WHERE, JOIN, ORDER BY
— Быстрый поиск по ключу
— Уникальные индексы гарантируют целостность
Минусы:
— Замедляют INSERT, UPDATE, DELETE (нужно обновлять индекс)
— Занимают дополнительную память
— Лишние индексы — пустая трата ресурсов
Когда создавать индекс:
— Столбцы в WHERE, JOIN, ORDER BY
— Часто запрашиваемые поля
— "Золотое правило": индекс под каждый частый запрос
В Rails:
add_index :users, :email, unique: true
add_index :posts, [:user_id, :created_at] # составной
add_index :articles, :body, using: :gin # полнотекстовый
СложноБазы данных / Индексы
Виды индексов
Какие виды индексов бывают?
По структуре:
— B-дерево (B-tree) — по умолчанию в PostgreSQL
— Хэш-индекс — точное совпадение (оператор =)
— GIN — полнотекстовый поиск, массивы, JSONB
— GiST — геоданные, диапазоны, полнотекстовый
— BRIN — для огромных таблиц с естественной сортировкой
По количеству столбцов:
— Простой (один столбец)
— Составной (несколько столбцов)
Порядок столбцов важен! (a, b) ≠ (b, a)
По характеристике:
— Уникальный (UNIQUE) — гарантирует уникальность значений
— Частичный (WHERE condition) — индексирует подмножество строк
— Покрывающий — содержит все столбцы запроса
— Полнотекстовый (инвертированный) — для поиска по тексту
— Функциональный — индекс по выражению: LOWER(email)
По связи с таблицей:
— Кластерный — физически упорядочивает данные
— Некластерный — отдельная структура с указателями
В PostgreSQL:
CREATE INDEX idx_email ON users (email); -- B-tree
CREATE INDEX idx_lower ON users (LOWER(email)); -- функциональный
CREATE INDEX idx_active ON users WHERE active; -- частичный
CREATE INDEX idx_tags ON articles USING gin(tags); -- GIN
СреднеБазы данных / Индексы
Полнотекстовый поиск
Что такое полнотекстовый поиск? Как работает?
Полнотекстовый поиск — поиск по содержимому текста с учётом
морфологии (окончания, склонения), ранжированием по релевантности.
В отличие от LIKE '%word%':
— LIKE ищет точную подстроку, медленный на больших текстах
— Полнотекстовый поиск знает, что «бег» = «бегать» = «бежал»
Как работает:
1. Текст разбивается на токены (лексемы)
2. Токены нормализуются (стемминг — приведение к основе)
3. Строится инвертированный индекс: слово → список документов
4. При поиске запрос тоже нормализуется и ищется в индексе
В PostgreSQL:
-- Создать колонку tsvector для индексации
ALTER TABLE articles ADD COLUMN search_vector tsvector;
-- Обновить вектор
UPDATE articles SET search_vector =
to_tsvector('russian', title || ' ' || body);
-- GIN-индекс для быстрого поиска
CREATE INDEX idx_articles_search ON articles USING gin(search_vector);
-- Поиск
SELECT * FROM articles
WHERE search_vector @@ to_tsquery('russian', 'рубей & релс');
# Найдёт: Ruby, Ruby on Rails, руби, рельсы
В Rails:
— gem pg_search — обёртка над PostgreSQL full-text search
— gem searchkick — через Elasticsearch/Meilisearch
— gem ransack — простой поиск (не полнотекстовый)
ЛегкоБазы данных / Транзакции
Транзакции в БД
Что такое транзакции?
Транзакция — группа последовательных операций с БД,
представляющая логическую единицу работы с данными.
Транзакция выполняется либо целиком и успешно (Commit),
либо не выполняется вообще (Rollback), и тогда не производит
никакого эффекта.
Свойства ACID:
— Atomicity (Атомарность) — все или ничего
— Consistency (Согласованность) — БД переходит из одного
корректного состояния в другое
— Isolation (Изолированность) — транзакции не влияют
друг на друга
— Durability (Долговечность) — после Commit данные сохраняются
В SQL:
BEGIN; # начать транзакцию
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT; # зафиксировать
-- или ROLLBACK; # откатить
В Rails:
ActiveRecord::Base.transaction do
account1.debit(100)
account2.credit(100)
end # Commit автоматически, или ROLLBACK при exception
ЛегкоRuby / ООП
Геттеры и сеттеры
Как создать геттер и сеттер методы в Ruby?
С помощью методов:
- attr_reader — создаёт геттер (метод чтения)
- attr_writer — создаёт сеттер (метод записи)
- attr_accessor — объединяет attr_reader и attr_writer
Без attr_* (вручную):
class Tovar
def price=(price)
@price = price
end
def price
@price
end
end
С attr_accessor:
class Tovar
attr_accessor :price
end
СложноБазы данных / Транзакции
Уровни изолированности транзакций
Расскажите об уровнях изолированности транзакций.
Уровень изолированности — степень защиты от несогласованности
данных при параллельном выполнении транзакций.
Проблемы параллельного доступа:
— Lost update (потерянное обновление) — две транзакции
меняют одни данные, одна перезаписывает другую
— Dirty read (грязное чтение) — чтение незафиксированных данных
— Non-repeatable read (неповторяющееся чтение) — при повторном
чтении данные изменились
— Phantom read (фантомное чтение) — при повторном чтении
появились/исчезли строки
Уровни (от слабого к сильному):
1. Read uncommitted — видны незафиксированные изменения
Грязное чтение возможно
2. Read committed — видны только зафиксированные данные
PostgreSQL по умолчанию. Нет dirty read
3. Repeatable read — повторное чтение вернёт те же данные
Нет dirty + non-repeatable read
4. Serializable — транзакции выполняются как будто последовательно
Максимальная изоляция, минимальная производительность
Чем выше уровень — лучше согласованность, но меньше параллелизм.
В PostgreSQL по умолчанию: Read committed.
В Rails: ActiveRecord::Base.transaction(isolation: :serializable)
СреднеБазы данных / Транзакции
Журнал транзакций (WAL)
Что такое журнал транзакций SQL?
Журнал транзакций (WAL — Write-Ahead Log) — файл, содержащий
записи всех транзакций, произошедших в БД.
WAL — последовательный по своей природе, делится на виртуальные
файлы журнала (VLF).
Для чего нужен:
— Восстановление незавершённых транзакций после сбоя
— Rollback транзакций
— Высокая доступность (replication)
— Point-in-time recovery (восстановление на момент времени)
— Резервное копирование: полное + инкрементальное
Принцип WAL:
— Изменения сначала пишутся в журнал, потом в файлы данных
— Если сбой произошёл между ними — при перезапуске БД
накатит (replay) журнал и восстановит состояние
В PostgreSQL:
— pg_wal/ директория хранит WAL-файлы
— Настройки: wal_level, max_wal_size, wal_keep_size
СреднеБазы данных / Масштабирование
Репликация данных
Что такое репликация, для чего нужна?
Репликация — техника масштабирования БД. Данные с одного сервера
постоянно копируются на один или несколько других (реплики).
Позволяет распределить нагрузку: чтение с реплик, запись на master.
Два основных подхода:
Master-Slave (Primary-Replica):
— Один master — принимает записи
— Несколько slave — принимают чтение
— Slave асинхронно копирует данные с master
— Простой: slave может немного отставать (replication lag)
Master-Master (Active-Active):
— Оба сервера принимают и чтение, и запись
— Сложнее в настройке, возможны конфликты
— Редко используется
В PostgreSQL:
— Streaming replication (встроенная)
— Logical replication (по таблицам)
— Настройка: primary_conninfo в recovery.conf
В Rails:
— config.database.yml с replica:
replica:
database: myapp
host: replica-server
— ActiveRecord.reading_role = :reading
СреднеБазы данных / Масштабирование
Шардинг и партиционирование
Что такое шардинг (партиционирование)?
Шардинг — разделение БД на части, каждая на отдельном сервере.
В отличие от репликации (копирование данных), шардинг —
разделение данных.
Вертикальный шардинг — выделение таблиц на отдельные серверы:
Сервер 1: users, profiles
Сервер 2: posts, comments
Просто, но не решает проблему огромных таблиц
Горизонтальный шардинг — разделение одной таблицы на серверы:
users_shard_1: id 1..1_000_000 (Сервер 1)
users_shard_2: id 1_000_001..2_000_000 (Сервер 2)
Для огромных таблиц, не умещающихся на одном сервере
Партиционирование в PostgreSQL (без шардинга):
CREATE TABLE logs (
id serial, created_at timestamp, data text
) PARTITION BY RANGE (created_at);
CREATE TABLE logs_2024_q1 PARTITION OF logs
FOR VALUES FROM ('2024-01-01') TO ('2024-04-01');
Когда шардинг нужен:
— Таблицы > 100GB
— Нагрузка, с которой один сервер не справляется
— Географическое распределение данных
СложноБазы данных / Масштабирование
Блокировочные и версионные СУБД
Что такое блокировочные и версионные СУБД?
Два подхода к управлению параллельным доступом:
Блокировочные (Lock-based) СУБД:
— При изменении данных строка БЛОКИРУЕТСЯ (lock)
— Другие транзакции ждут снятия блокировки
— Гарантирует строгую консистентность
— Возможны deadlocks (взаимоблокировки)
— Пример: MySQL с InnoDB (по умолчанию)
Транзакция 1: UPDATE users SET name = 'A' WHERE id = 1 -- lock
Транзакция 2: UPDATE users SET name = 'B' WHERE id = 1 -- ждёт...
Версионные (MVCC — Multi-Version Concurrency Control) СУБД:
— При UPDATE создаётся НОВАЯ версия строки
— Старая версия доступна для чтения другим транзакциям
— Читатели не блокируют писателей, писатели не блокируют читателей
— Нужен VACUUM для очистки старых версий
— Пример: PostgreSQL, Oracle, MySQL (InnoDB тоже использует MVCC)
Транзакция 1: UPDATE → создаёт версию 2 (видна после COMMIT)
Транзакция 2: SELECT → видит версию 1 (старую)
Преимущества MVCC:
— Высокая пропускная способность при OLTP
— Чтение не блокирует запись
— Меньше deadlocks
Недостатки MVCC:
— Bloat (раздувание) от старых версий строк
— Нужен VACUUM для очистки
— Больше потребление памяти
СреднеБазы данных / PostgreSQL
pgBouncer: пул соединений
pgBouncer — что это и зачем нужно?
pgBouncer — пул соединений (connection pooler) для PostgreSQL.
Проблема: каждое соединение в PostgreSQL — отдельный процесс.
1000 подключений = 1000 процессов = много памяти.
Создание нового соединения — дорого.
pgBouncer решает это:
— Приложение подключается к pgBouncer (как к PostgreSQL)
— pgBouncer переиспользует существующие соединения
— Меньше процессов на сервере PostgreSQL
Режимы пулинга:
Session pooling — соединение привязано к сессии клиента.
Пока клиент не отключится, соединение не переиспользуется.
Transaction pooling — соединение привязано к транзакции.
После COMMIT/ROLLBACK соединение возвращается в пул.
Наиболее популярный режим.
Statement pooling — соединение на каждый оператор.
Максимальная эффективность, но не поддерживает транзакции.
В Rails + pgBouncer:
database.yml → host: pgbouncer-host
PgBouncer → PostgreSQL (мало соединений)
Rails (много процессов) → PgBouncer (переиспользует)
СреднеБазы данных / PostgreSQL
PgQ: очередь на базе PostgreSQL
Что такое PgQ и как работает?
PgQ — система очередей на базе PostgreSQL (из skytools).
Если написать очередь на БД вручную — она будет медленной
с большой нагрузкой. PgQ избегает этого за счёт
использования внутренней механики PostgreSQL.
Особенности:
— Транзакционная: каждое событие доставляется хотя бы один раз
— События достаются пачками (batch)
— Нужно быть внимательным, чтобы не обработать одно событие
дважды (например, при аварии обработчика)
Альтернативы:
— Sidekiq (Redis) — стандарт для Rails
— GoodJob, QueueClassic — очереди на PostgreSQL
— RabbitMQ, Kafka — внешние брокеры сообщений
В Rails обычно используют Sidekiq (Redis-based).
PgQ/GoodJob — когда не хочется добавлять Redis в стек.
СложноБазы данных / PostgreSQL
Индексы PostgreSQL
Как устроены и функционируют индексы в PostgreSQL?
Основные типы индексов PostgreSQL:
B-tree (по умолчанию):
— Сбалансированное дерево, ускоряет =, <, >, <=, >=, BETWEEN, LIKE('prefix%')
— Подходит для большинства случаев
Hash:
— Только точное совпадение (=)
— Редко используется, B-tree обычно лучше
GIN (Generalized Inverted Index):
— Полнотекстовый поиск (tsvector)
— Массивы, JSONB, hstore
— Медленнее обновляется, быстрее ищет
GiST (Generalized Search Tree):
— Геометрические данные, диапазоны, полнотекстовый
— Гибкий, но зависит от реализации оператора
BRIN (Block Range Index):
— Для огромных таблиц с естественным порядком (time-series)
— Хранит только мин/макс для блоков страниц
— Очень компактный, но менее точный
Частичный индекс:
CREATE INDEX idx_active_users ON users (email) WHERE active = true;
Покрывающий индекс (Index-only scan):
CREATE INDEX idx_covering ON posts (user_id, title, created_at);
— Запрос получит все данные из индекса, без обращения к таблице
СреднеБазы данных / PostgreSQL
Транзакции в PostgreSQL
Как воплощён и работает механизм транзакций в PostgreSQL?
Транзакция в PostgreSQL:
BEGIN; # начать
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT; # зафиксировать
-- или ROLLBACK; # откатить все изменения
Если ни COMMIT ни ROLLBACK не вызваны — транзакция висит открытой.
Посмотреть висячие транзакции:
SELECT * FROM pg_stat_activity WHERE state = 'idle in transaction';
Висячие транзакции опасны:
— Блокируют другие транзакции (locks)
— Раздувают WAL
— Могут остановить всю БД
MVCC (Multi-Version Concurrency Control):
— PostgreSQL не перезаписывает строки при UPDATE
— Создаёт новую версию строки (tuple)
— Старая версия видна другим транзакциям
— VACUUM очищает устаревшие версии
В Rails:
ActiveRecord::Base.transaction do
account1.update!(balance: new_balance1)
account2.update!(balance: new_balance2)
end
# exception → ROLLBACK, иначе COMMIT
СреднеБазы данных / PostgreSQL
Системы репликации PostgreSQL
Какие системы репликации есть в PostgreSQL? Зачем нужны?
Репликация — копирование данных между серверами PostgreSQL.
Зачем: отказоустойчивость, масштабирование чтения, бэкап.
Физическая (streaming) репликация:
— Копирует изменения на уровне блоков (WAL)
— Реплика — точная копия master (все БД)
— Асинхронная или синхронная
— Реплика только для чтения
— Настройка: primary_conninfo в postgresql.conf
Логическая репликация:
— Копирует изменения на уровне строк (INSERT/UPDATE/DELETE)
— Можно реплицировать отдельные таблицы
— Реплика может быть на другой версии PostgreSQL
— Гибче, но медленнее физической
— Настройка: CREATE PUBLICATION / CREATE SUBSCRIPTION
Синхронная репликация:
— Master ждёт подтверждения от replica перед COMMIT
— Данные не потеряются при падении master
— Медленнее (зависит от сети до replica)
— synchronous_commit = on, synchronous_standby_names
Асинхронная репликация (по умолчанию):
— Master не ждёт подтверждения
— Быстрее, но возможна потеря данных при падении master
— replication lag может достигать секунд/минут
Каскадная репликация:
— Replica может быть источником для других replica
— Снижает нагрузку на master
СреднеБазы данных / PostgreSQL
Синхронные и асинхронные операции
Что такое синхронные и асинхронные операции в PostgreSQL?
Синхронные операции:
— Клиент отправляет запрос и ждёт результат
— Блокируется пока сервер не ответит
— Простая модель программирования
— Медленнее при множестве параллельных запросов
Пример: обычный SELECT/INSERT — клиент ждёт завершения.
Асинхронные операции:
— Клиент отправляет запрос и продолжает работу
— Результат обрабатывается когда готов (callback/poll)
— Не блокирует другие операции
— Сложнее в реализации, но эффективнее
В контексте репликации:
— Синхронная: master ждёт подтверждения записи на replica
Гарантия данных, но выше задержка
— Асинхронная: master не ждёт replica
Быстрее, но возможна потеря данных при сбое
В контексте PostgreSQL драйверов:
— Синхронный:PG::Connection.exec — ждёт результат
— Асинхронный: PG::Connection.send_query + get_result
Можно отправить несколько запросов и получать результаты
по мере готовности
В Rails:
— По умолчанию синхронные запросы к БД
— Async: ActiveRecord::Base.async do User.count end (Rails 7+)
СреднеБазы данных / Диагностика
Запрос стал медленнее без изменений
Приложение, базу, сервер не трогали — но спустя какое-то время запрос стал работать медленнее. Почему?
Возможные причины:
1. Устаревшая статистика:
PostgreSQL собирает статистику для планировщика запросов.
Если статистика устарела — планировщик выберет плохой план.
Решение: ANALYZE; или VACUUM ANALYZE;
2. Раздувание таблицы (bloat):
При UPDATE/DELETE PostgreSQL создаёт новые версии строк.
Мёртвые строки накапливаются — таблица разрастается.
Решение: VACUUM (авто) или VACUUM FULL (полная перестройка)
3. Изменение данных:
Данные выросли — индекс перестал помещаться в RAM.
Решение: EXPLAIN ANALYZE — проверить план запроса
4. Планировщик сменил план:
При росте данных planner может решить использовать
sequential scan вместо index scan (и ошибиться).
Решение: ANALYZE; или увеличить статистику:
ALTER TABLE users ALTER COLUMN status SET STATISTICS 500;
Диагностика:
EXPLAIN ANALYZE SELECT ...; # план + реальное время
SELECT pg_size_pretty(pg_total_relation_size('users')); # размер
SELECT * FROM pg_stat_user_tables WHERE relname = 'users';
СреднеБазы данных / Диагностика
Типичные узкие места в БД
Типичные bottle necks (узкие места) в базах данных?
Типичные узкие места (bottlenecks) в БД:
1. Отсутствующие индексы:
— Full table scan вместо index scan
— Решение: EXPLAIN ANALYZE + CREATE INDEX
2. N+1 запросы (в Rails):
— 100 пользователей → 100 запросов за их постами
— Решение: includes(), eager_load(), joins()
3. Медленные JOIN-ы:
— Объединение больших таблиц без индексов
— Решение: индексы на FK, покрывающие индексы
4. Блокировки (locks):
— Долгие транзакции блокируют другие запросы
— Решение: короткие транзакции, избегать idle in transaction
5. Раздутые таблицы (bloat):
— Мёртвые строки от UPDATE/DELETE
— Решение: autovacuum, VACUUM FULL
6. Неоптимальные типы данных:
— TEXT вместо VARCHAR, UUID вместо INTEGER для PK
— Решение: выбирать типы под задачу
7. Слишком много соединений:
— Каждый connection = отдельный процесс (в PostgreSQL)
— Решение: pgBouncer, connection pool
8. Отсутствие партиционирования:
— Огромные таблицы логов/аудита
— Решение: PARTITION BY RANGE/LIST
ЛегкоБазы данных / Диагностика
SQL Injection и XSS
Объяснить разницу между SQL Injection и XSS (CSS Injection)?
SQL Injection — атака на БАЗУ ДАННЫХ:
— Вредоносный SQL-код вставляется в запрос к БД
— Выполняется на сервере БД
Пример:
# Опасно!
query = "SELECT * FROM users WHERE name = '#{params[:name]}'"
# Если params[:name] = "'; DROP TABLE users; --"
# Выполнится: SELECT * FROM users WHERE name = ''; DROP TABLE users; --'
Защита (в Rails — автоматически):
User.where(name: params[:name]) # параметризованный запрос
User.where("name = ?", params[:name]) # плейсхолдер
Никогда НЕ делать: User.where("name = '#{params[:name]}'")
XSS (Cross-Site Scripting) — атака на БРАУЗЕР:
— Вредоносный JavaScript вставляется в HTML-страницу
— Выполняется в браузере пользователя
Пример:
<!-- Если вывести без экранирования: -->
<div><%= raw params[:comment] %></div>
<!-- params[:comment] = "<script>stealCookies()</script>" -->
Защита (в Rails — по умолчанию):
<%= params[:comment] %> # экранируется автоматически
<%= sanitize params[:comment] %> # разрешить безопасные теги
Не использовать raw() для пользовательского ввода
Разница:
— SQL Injection → атака на сервер/БД
— XSS → атака на браузер/клиента
ЛегкоRuby / ООП
attr_reader, attr_writer, attr_accessor
Что такое attr_reader, attr_writer, attr_accessor?
Все классы наследуют методы Module.
attr_reader, attr_writer, attr_accessor являются его методами.
attr_reader создаёт переменную экземпляра и метод-геттер:
attr_reader :name
# Эквивалентно:
def name
@name
end
attr_writer создаёт метод-сеттер:
attr_writer :name
# Эквивалентно:
def name=(name)
@name = name
end
attr_accessor объединяет функционал attr_reader и attr_writer.
ЛегкоDevOps и CI/CD / Системные проблемы
Address already in use
При запуске rails server появляется ошибка: Address already in use - bind(2) for 127.0.0.1:3000. Что делать?
Причина: порт 3000 занят другим процессом (старый сервер, другая программа).
Найти процесс:
lsof -i :3000 # показать PID и имя процесса
# Или:
fuser 3000/tcp # показать PID
Убить процесс:
kill -9 <PID> # убить конкретный процесс
# Или одной командой:
fuser -k 3000/tcp # убить всё, что занимает порт 3000
Альтернатива — запустить на другом порту:
rails server -p 3001 # использовать порт 3001
Почему так происходит:
- Предыдущий сервер не завершился корректно (Ctrl+C не нажали)
- Spring запустил фоновый процесс
- Другой проект тоже запущен на 3000
Профилактика:
spring stop # остановить все процессы Spring
pkill -f "rails server" # убить все rails server процессы
СреднеDevOps и CI/CD / Системные проблемы
rails new зависает
Команда rails new myapp зависает и ничего не происходит. В чём причина и как это исправить?
Возможные причины:
1. Spring завис — самая частая причина:
spring stop
DISABLE_SPRING=1 rails new myapp
2. Нет интернета — bundle install внутри rails new не может скачать гемы:
Проверить: ping -c 3 rubygems.org
Запустить без bundle: rails new myapp --skip-bundle
Потом: cd myapp && bundle install
3. За прокси — нужна настройка:
export HTTP_PROXY=http://proxy:port
export HTTPS_PROXY=http://proxy:port
4. Старый кеш RubyGems:
gem sources -c # очистить кеш
gem sources -u # обновить индекс
Алгоритм:
1. spring stop
2. Проверить интернет
3. Запустить с DISABLE_SPRING=1
4. Если всё равно висит — добавить --skip-bundle и установить гемы вручную
СреднеDevOps и CI/CD / Системные проблемы
Системная диагностика: базовые команды
Назовите основные команды для диагностики проблем в терминале Linux/macOS, которые полезны Ruby/Rails-разработчику.
Процессы и порты:
ps aux # все запущенные процессы
ps aux | grep ruby # найти процессы Ruby
lsof -i :3000 # кто занимает порт 3000
kill -9 <PID> # убить процесс
top / htop # мониторинг ресурсов
Файлы и директории:
which ruby # где находится Ruby
which -a ruby # все Ruby в системе
ls -la # подробный список файлов
du -sh * # размер директорий
df -h # свободное место на диске
Сеть:
curl -I http://localhost:3000 # проверить HTTP-ответ
ping rubygems.org # доступность сервера
ss -tlnp # открытые порты
Логи:
tail -f log/development.log # логи Rails в реальном времени
tail -f /var/log/postgresql/... # логи PostgreSQL
grep "ERROR" log/development.log # искать ошибки
Окружение:
env # все переменные окружения
echo $PATH # показать PATH
echo $RAILS_ENV # текущее окружение Rails
СложноDevOps и CI/CD / Системные проблемы
Docker: контейнер не запускается
Вы запускаете docker compose up, но контейнер с приложением падает (exited with code 1). Как найти причину?
Алгоритм диагностики:
1. Посмотреть логи упавшего контейнера:
docker compose logs app
docker compose logs --tail 50 app # последние 50 строк
2. Если контейнер уже остановлен:
docker compose ps -a # статус контейнеров
docker logs <container_id> # логи конкретного контейнера
3. Частые причины:
- bundle install не прошёл (забыли пересобрать образ):
docker compose build --no-cache
- База данных ещё не готова, а приложение уже стартует:
depends_on не дожидается готовности, использовать healthcheck
- Неправильные переменные окружения в .env или docker-compose.yml
- Порт занят на хосте: ports: "3000:3000" — проверить lsof -i :3000
- Файлы не скопированы: проверить Dockerfile (COPY . .)
4. Зайти внутрь контейнера для отладки:
docker compose exec app bash
docker compose exec app sh # если нет bash (Alpine)
5. Перезапуск с нуля:
docker compose down -v # удалить контейнеры и volumes
docker compose up --build # пересобрать и запустить
СреднеRuby / Основы
Проблемы чисел с плавающей точкой
Почему в Ruby 24.0 * 0.1 не равно 2.4? Как правильно работать с дробными числами?
Компьютеры используют binary floating point формат, который не может
точно представить числа вроде 0.1, 0.2 или 0.3.
Когда код компилируется, 0.1 уже округляется до ближайшего числа
в этом формате, что приводит к ошибке округления ДО вычисления.
24.0 * 0.1 #=> 2.4000000000000004 (не ровно 2.4!)
0.1 + 0.2 #=> 0.30000000000000004 (не ровно 0.3!)
Решения:
1. Для денег — используйте Integer (копейки/центы):
2499 вместо 24.99
2. BigDecimal для точных вычислений:
require 'bigdecimal'
BigDecimal("24.0") * BigDecimal("0.1") #=> 0.24e1
3. Сравнение с допуском (epsilon):
(a - b).abs < 0.0001
СреднеRuby / Основы
Struct и OpenStruct
Что такое Struct и OpenStruct в Ruby? Когда их использовать?
Struct — упрощённый способ создания классов с предопределёнными атрибутами.
Создаёт класс с геттерами и сеттерами.
Point = Struct.new(:x, :y)
p = Point.new(1, 2)
p.x #=> 1
p.y = 5 #=> 5
p[:x] #=> 1 (доступ по индексу)
OpenStruct — позволяет создавать объекты с любыми атрибутами динамически.
Требует require 'ostruct'.
require 'ostruct'
person = OpenStruct.new
person.name = "Alice"
person.age = 30
person.name #=> "Alice"
Разница:
- Struct — фиксированный набор атрибутов, быстрый
- OpenStruct — любые атрибуты, медленный (использует method_missing)
Когда использовать:
- Struct — для простых DTO, вместо хеша с известными ключами
- OpenStruct — редко, в основном для парсинга/тестов
- В продакшене лучше Plain Ruby классы или dry-struct
СреднеRuby / Основы
Способы вызова методов
Какие способы вызова методов есть в Ruby? В чём разница между .send и .call?
1. Обычный вызов:
obj.method_name(arg)
2. .send — вызывает любой метод, включая приватные:
obj.send(:method_name, arg)
obj.__send__(:method_name, arg) # безопаснее (нельзя переопределить)
3. .call — вызывает Proc/Lambda/Method объект:
my_proc = Proc.new { |x| x * 2 }
my_proc.call(5) #=> 10
my_proc.(5) #=> 10 (синтаксический сахар)
my_proc[5] #=> 10 (ещё один вариант)
4. method(:name).call — получает Method объект и вызывает:
method(:puts).call("hello")
5. .eval — выполняет строку как Ruby код:
eval("2 + 2") #=> 4
# Очень медленный, опасный (code injection), НЕ используется
.send используют для метапрограммирования и динамического вызова.
.call — для Proc/Lambda.
ЛегкоRuby / Основы
Safe navigation operator (&.)
Что такое safe navigation operator (&.) в Ruby? Чем отличается от try в Rails?
&. (safe navigation) — вызывает метод только если объект не nil.
Если объект nil, возвращает nil вместо NoMethodError.
user&.name
# Эквивалентно:
user.nil? ? nil : user.name
Цепочка:
user&.profile&.avatar_url
Важные отличия от Rails try:
- &. проверяет на nil, но НЕ на false:
false&.to_s #=> "" (вызов произойдёт!)
- &. не вычисляет аргументы если объект nil:
obj&.foo(expensive_call()) # expensive_call НЕ вызовется если obj nil
- try в ActiveSupport всегда вычисляет аргументы
&. быстрее try — реализован на уровне парсера, а не Ruby кода.
СреднеRuby / ООП
self в Ruby
Что означает ключевое слово self в Ruby? Какие значения оно принимает?
self — ссылка на текущий объект (контекст выполнения).
Внутри метода экземпляра — self это сам объект:
class User
def greet
"Hello from #{self.name}"
end
end
Внутри определения класса — self это класс:
class User
puts self #=> User
def self.all
# self здесь = User (метод класса)
end
end
class << self открывает синглтон-класс — все методы становятся методами класса:
class User
class << self
def find(id)
# метод класса User.find
end
end
end
self используется для:
- Определения методов класса (def self.method)
- Вызова других методов того же объекта (self.other_method)
- Явного указания атрибута (self.name = "Alice" а не name = "Alice")
ЛегкоRuby / ООП
super: вызов родительского метода
Как работает ключевое слово super в Ruby? Что будет если вызвать super без аргументов?
super вызывает метод с тем же именем из родительского класса.
class Animal
def speak
"sound"
end
end
class Dog < Animal
def speak
super + " woof!"
end
end
Dog.new.speak #=> "sound woof!"
Варианты вызова:
- super — передаёт ВСЕ аргументы текущего метода в родительский
- super() — вызывает БЕЗ аргументов (пустые скобки важны!)
- super(arg1, arg2) — передаёт конкретные аргументы
Частая ошибка:
def initialize(name:)
@name = name
super # передаёт name: в родительский initialize
end
Если родительский initialize не принимает аргументов:
def initialize(name:)
@name = name
super() # скобки обязательны, иначе ArgumentError
end
СреднеRuby on Rails / Legacy-проекты
Чтение чужого кода: как разобраться
Вы пришли на новый проект. Бизнес-логика в views, SQL в контроллерах, контроллеры по 500 строк. Нет документации. Как разобраться?
Навык чтения чужого кода — главный навык на legacy.
Вы будете читать код в 10 раз больше, чем писать.
Алгоритм разбора legacy:
1. Найти точку входа:
Роут → Контроллер → Модель → Service
config/routes.rb — карта приложения
2. Следовать за запросом:
Открыть браузер → DevTools → Network → кликнуть
→ увидеть URL → найти в routes → найти controller action
3. Нарисовать карту:
Пометить: "этот метод вызывает тот, который зовёт API,
а результат используется в этом колбэке"
Зарисовать на бумаге / diagrams.net
4. Искать "запахи" (code smells):
— Метод > 30 строк — скорее всего делает слишком много
— Метод с 5 параметрами — слишком много ответственностей
— Комментарий "TODO: refactor" — кто-то уже запутался
— if/else на 5 уровней — упростить можно
— send(dynamic_method) — вызов по строке, хардкор
5. Записать вопросы:
"Зачем этот callback?"
"Почему два поля делают одно и то же?"
"Почему validate в before_save, а не в validation?"
Спросить у команды — не угадывать.
Инструменты:
— grep / ripgrep — найти все использования метода
— git blame — кто и когда написал эту строку (спросить автора)
— git log --follow file.rb — история изменений файла
— rails routes — список всех endpoints
— rubocop -l (lint) — найти проблемные места автоматически
— traceroute gem — найти неиспользуемые роуты/экшены
Не делать:
— Не рефакторить "пока разбираетесь" — сломаете
— Не судить автора — он работал в других условиях
— Не переписывать с нуля — это ловушка (second-system effect)
Делать:
— Документировать по ходу — комментарии, README
— Писать тесты на то, что поняли
— Задавать вопросы — это не слабость
Как ИИ помогает:
ИИ объяснит что делает конкретный метод за 5 секунд.
"Объясни этот код" — и получите детальный разбор.
Но ИИ НЕ знает контекст: "а почему ОНО так работает?"
Потому что 2 года назад клиент потребовал фичу за 1 день.
Исторический контекст — только от команды.
СложноRuby / ООП
Синглтон-методы и синглтон-классы
Что такое синглтон-методы и синглтон-классы в Ruby?
Синглтон-метод — метод, принадлежащий только одному объекту.
cat = Animal.new
dog = Animal.new
def dog.bark
"WOOF!"
end
dog.bark #=> "WOOF!"
cat.bark #=> NoMethodError
dog.singleton_methods #=> [:bark]
Методы класса — это тоже синглтон-методы:
class User
def self.all
# self.all — синглтон-метод объекта User
end
end
Синглтон-класс — анонимный класс, хранящий синглтон-методы.
Встраивается в цепочку поиска метода:
dog.singleton_class #=> #<Class:#<Animal:0x...>>
dog.singleton_class.superclass #=> Animal
Поиск метода: singleton_class -> Animal -> Object -> BasicObject
class << dog
def bark
"WOOF!"
end
end
# То же самое, что def dog.bark
СреднеRuby / ООП
extend self в модуле
Что произойдёт если в модуле сделать extend self? Зачем это нужно?
extend self делает инстанс-методы модуля доступными как методы модуля
(можно вызывать без создания экземпляра и без include).
Без extend self:
module MyModule
def greet
"Hello!"
end
end
MyModule.greet #=> NoMethodError
MyModule.greet # не работает напрямую!
С extend self:
module MyModule
extend self
def greet
"Hello!"
end
end
MyModule.greet #=> "Hello!" # работает!
Эквивалентно:
module MyModule
def greet
"Hello!"
end
module_function :greet
end
Зачем: создание утилитных модулей, которые используются
и как mixin (include) и как namespace (Module.method).
ЛегкоRuby / Окружение и диагностика
Серверы приложений Ruby
Какие серверы приложений существуют для Ruby/Rails? В чём разница?
Основные серверы:
Puma — текущий стандарт в Rails (по умолчанию).
- Многопоточный + мультипроцессный (clustered mode)
- Быстрый, хорошо справляется с медленными запросами
- Поддерживает concurrent I/O
Unicorn — predecessor Puma.
- Форкает процессы (workers), каждый обрабатывает по одному запросу
- Простой и надёжный, но не многопоточный
- Не подходит для медленных I/O (long polling)
Passenger (Phusion Passenger) — коммерческий + бесплатный.
- Встраивается в Nginx/Apache
- Автоматическое управление процессами
- Удобен для простого деплоя
WeBrick — встроенный сервер Ruby.
- Только для разработки, не для продакшена
- Однопоточный, медленный
Thin — EventMachine-based, редко используется сейчас.
Рекомендация для продакшена: Puma (стандарт Rails).
СреднеRuby / Окружение и диагностика
Benchmark: измерение скорости кода
Как измерить скорость работы Ruby кода? Чем benchmark отличается от benchmark-ips?
Стандартный Benchmark (встроен в Ruby):
require 'benchmark'
Benchmark.bm do |x|
x.report("each:") { (1..1_000_000).each { |i| i } }
x.report("map:") { (1..1_000_000).map { |i| i } }
end
benchmark-ips (gem) — замеряет итерации в секунду:
require 'benchmark/ips'
Benchmark.ips do |x|
x.report("each") { (1..1000).each {} }
x.report("map") { (1..1000).map {} }
x.compare! # показывает разницу в процентах
end
Разница:
- Benchmark — измеряет время выполнения (секунды)
- benchmark-ips — измеряет итерации/сек (более точный),
автоматически подбирает количество итераций,
показывает сравнение в % между методами
Установка: gem install benchmark-ips
СреднеRuby / Внутреннее устройство
Ruby интерпретаторы
Какие существуют реализации Ruby интерпретатора? Чем они отличаются?
MRI (Matz's Ruby Interpreter) / CRuby — основная реализация.
- Написан на C
- Самый распространённый, стандарт de facto
- Использует GIL (Global Interpreter Lock)
JRuby — Ruby на платформе JVM.
- Настоящая многопоточность (без GIL)
- Доступ к Java библиотекам
- Быстрее для некоторых задач, медленнее для других
- Совместим с большинством Ruby-кода
TruffleRuby — Ruby на GraalVM.
- Самый быстрый для многих бенчмарков
- JIT-компиляция через Graal
- Экспериментальный, растущая совместимость
Rubinius — Ruby написанный на Ruby (большая часть).
- Исторически важен, сейчас не развивается активно
В продакшене 99% случаев — MRI/CRuby (тот самый ruby который
устанавливается через rbenv/rvm).
СреднеRuby / Внутреннее устройство
JIT-компиляция в Ruby
Что такое JIT-компиляция? Как JIT работает в Ruby?
JIT (Just-In-Time) компиляция — оптимизация часто вызываемых методов
путём компиляции их в машинный код на лету (во время работы программы).
Цель JIT — пропустить шаги интерпретации для горячих методов.
В Ruby (начиная с 2.6) есть MJIT:
- Компилирует часто вызываемые методы в C код
- Затем компилирует C в машинный код через GCC/Clang
- Отключён по умолчанию (включается --jit)
Ruby 3.1+: YJIT (от Shopify):
- Написан на Rust, встроен в CRuby
- Быстрее разогревается чем MJIT
- Включён по умолчанию в Ruby 3.3+
Плюсы JIT:
- Ускорение долгоживущих процессов (серверы)
- Бесплатное ускорение без изменения кода
Минусы:
- Cold start медленнее (нужно время на компиляцию)
- Больше потребление памяти
- Для коротких скриптов бесполезен
ЛегкоRuby / Основы
Типы данных в Ruby
Какие типы данных используются в Ruby? Что такое массив? хэш? строка? число? символ?
Числа (Numeric):
5 # Integer — целое число
4.5 # Float — число с плавающей точкой
2+3i # Complex — комплексное число
Rational(2, 3) # Rational — рациональная дробь ⅔
Логический тип (Boolean):
true и false
Всё есть true, кроме false и nil
Массивы (Array):
[1, 2, 3] # целочисленный
[1, "two", 3.0] # смешанный (гетерогенный)
Динамические, неограниченные по размеру
push/pop для стека, обширная библиотека методов
Строки (String):
'hello' # одинарные кавычки (принято по умолчанию)
"hello #{name}" # двойные (поддерживают интерполяцию)
'2' + '2' #=> "22" (конкатенация)
Не ограничены по длине, динамические
Хэши (Hash):
{ key: "value" } # символы как ключи
{ "key" => "value" }# строки как ключи
Ассоциативные массивы — ключ → значение
Диапазоны (Range):
1..10 # включительно (1, 2, ..., 10)
1...10 # исключая конец (1, 2, ..., 9)
array[3..] # бесконечный диапазон (Ruby 2.6+)
Символы (Symbol):
:name, :status
Один экземпляр на имя (экономия памяти):
:slovo.object_id == :slovo.object_id #=> true
"slovo".object_id == "slovo".object_id #=> false
Неизменяемые, часто используются как ключи хэшей
nil (NilClass):
nil — единственное значение "пустоты"
false и nil — единственные "falsy" значения в Ruby
ЛегкоRuby / Основы
loop, while, map, each
Что такое loop, while, map, each? Чем отличаются?
loop, while — управляющие конструкции, создающие циклы,
повторение кода по условию/без условий:
loop { puts "infinity"; break } # бесконечный цикл
while x < 10 do x += 1 end # цикл по условию
each, map — итераторы, перебирают все элементы у объекта
(унаследованы от Enumerable). Принимают блоки и выполняют
код для элементов коллекций (массивов, диапазонов, хэшей):
[1, 2, 3].each { |x| puts x } # перебор
[1, 2, 3].map { |x| x * 2 } # преобразование
Итераторы предпочтительнее циклов — меньше ошибок, чище код.
ЛегкоRuby / Коллекции и Enumerable
each и map: отличия
Чем отличается each от map?
each занимается просто перебором, возвращает исходный массив:
[1, 2, 3].each { |x| puts x } # печатает, возвращает [1, 2, 3]
map занимается перебором и возвращает НОВЫЙ массив с результатами:
[1, 2, 3].map { |x| x * 2 } #=> [2, 4, 6]
map! — изменяет исходный массив
each используется для побочных эффектов (печать, запись).
map — для трансформации данных.
ЛегкоRuby / Основы
Другие циклы и итераторы
Какие ещё циклы и итераторы есть в Ruby?
Циклы:
until x > 10 do x += 1 end # противоположность while
for item in array do ... end # перебор (редко используется)
Числовые итераторы:
3.times { puts "hi" } # повтор 3 раза
1.upto(5) { |n| puts n } # от 1 до 5
5.downto(1) { |n| puts n } # от 5 до 1
1.step(10, 2) { |n| puts n } # 1, 3, 5, 7, 9
СреднеRuby / Коллекции и Enumerable
inject и reduce
Назовите отличия inject и reduce.
inject — алиас reduce. Это один и тот же метод.
[1, 2, 3].reduce(:+) #=> 6
[1, 2, 3].inject(:+) #=> 6 (то же самое)
ЛегкоRuby / Переменные
Типы переменных и области видимости
Какие переменные бывают, где они используются, где они доступны (области видимости)?
Локальные переменные (variable) — доступны только в текущей области:
def method
x = 1 # x видна только внутри method
end
Переменные экземпляра (@variable) — доступны во всех методах объекта:
class Person
def initialize(name)
@name = name
end
end
При первом вызове возвращают nil
Переменные класса (@@variable) — общая для всех экземпляров:
class Counter
@@count = 0 # одна на все экземпляры и наследники
end
Опасна: легко создать баг при наследовании
Глобальные переменные ($variable) — видна отовсюду:
$debug = true # доступна в любом месте программы
Почти никогда не используется, сложно отследить кто изменил
Константы (CONSTANT) — все заглавные:
PI = 3.14 # можно изменить, но Ruby выдаст warning
ЛегкоRuby / Переменные
Переменные @ и @@
Что такое переменная с одной @ и переменная с двумя @@?
Переменные экземпляра (@variable) — начинаются с @.
Доступны в методах экземпляра класса, где они определены.
При первом вызове возвращают nil.
Переменные класса (@@variable) — начинаются с @@.
Их область видимости — класс, в котором они определены,
и все экземпляры данного класса.
ЛегкоRuby / Внутреннее устройство
require и require_relative
Чем require отличается от require_relative?
require подключает файлы/гемы по относительному пути
в строгом соответствии ./1/ruby.rb, начиная с корня приложения.
Загружает один раз (повторный вызов вернёт false).
require "json" # поиск в $LOAD_PATH
require "./lib/my_file" # с указанием пути от корня
require_relative подключает файлы без относительного пути
и без указания расширения файла, запускает из той же директории,
где лежит файл запуска. Тоже загружает один раз.
require_relative "1/ruby.rb"
load — загружает файл КАЖДЫЙ РАЗ (нужно указывать расширение).
В Rails: Zeitwerk автозагружает по конвенции — require почти не нужен.
СреднеRuby / ООП
Модули и классы: отличия
Что такое модуль в Ruby? Какая разница между классом и модулем?
Модули в Ruby похожи на классы — содержат методы, константы,
другие модули и определения классов.
Задаются как классы, только слово module вместо class.
В отличие от классов:
- Создать объекты на основе модуля нельзя (нет new)
- Модуль не может иметь подклассы (нет наследования)
- Модули — одиночки, нет иерархии
Вместо этого модули добавляют функциональность класса
или отдельного объекта через include/extend/prepend.
ЛегкоRuby / ООП
Наследование в Ruby
Как организовано наследование в Ruby?
Наследование в Ruby — прямое (один родитель):
class Animal
end
class Dog < Animal
end
В Ruby всё в конечном счёте принадлежит классу BasicObject:
str = "Я - строка"
str.class #=> String
str.class.superclass #=> Object
str.class.superclass.superclass #=> BasicObject
Множественное наследование невозможно, но модули (include/extend)
дают тот же эффект.
СреднеRuby / ООП
include, extend, prepend
Чем отличается include от extend? Что такое prepend?
include — методы модуля становятся инстанс-методами класса:
class Person
include Greetable
end
Person.new.greet #=> "Hi!"
Необходимо создать экземпляр класса
extend — методы модуля становятся методами класса:
class Person
extend Greetable
end
Person.greet #=> "Hi!"
Без создания экземпляра класса
prepend — модуль вставляется перед классом в цепочке поиска метода:
Методы модуля устанавливаются первоочередными
Позволяет переопределить метод и вызвать super
ЛегкоRuby / ООП
Множественное наследование
Реализация множественного наследования в Ruby?
Множественное наследование в Ruby недоступно (у класса один родитель).
Реализация возможна через модули с помощью include/extend:
module A
def method_a; "a"; end
end
module B
def method_b; "b"; end
end
class C
include A, B
end
C.new.method_a #=> "a"
C.new.method_b #=> "b"
СреднеRuby / Блоки, Proc, Lambda
proc, lambda, block
Что такое proc, lambda, block? И какие отличия есть между ними?
Это анонимные функции — кусочки Ruby кода.
Block — код в фигурных скобках или do..end:
[1, 2, 3].each { |x| puts x }
Не является объектом, нельзя сохранить в переменную
Proc — объект, хранящий блок:
p = Proc.new { |x| x * 2 }
p.call(5) #=> 10
Нестрогий к аргументам (лишние игнорирует, недостающие = nil)
return выходит из метода-обёртки
Lambda — строгий Proc:
l = -> (x) { x * 2 }
l.call(5) #=> 10
Строгий к аргументам (ArgumentError при несовпадении)
return выходит только из лямбды
Все три — класса Proc:
Proc.new { }.class #=> Proc
-> { }.class #=> Proc
-> { }.lambda? #=> true
ЛегкоБазы данных / Распределённые БД
Репликация: Leader-Replica
Что такое репликация? Кто такой Leader и Replica? Зачем нужны несколько копий данных?
Репликация — копирование данных на несколько серверов.
Зачем:
— Надёжность: один сервер упал — данные живы на других
— Скорость: можно читать с ближайшего сервера
— Нагрузка: распределить запросы между серверами
Leader (Master, Primary) — главный сервер.
Все записи (INSERT, UPDATE, DELETE) идут через него.
Он решает, что записывать и в каком порядке.
Replica (Slave, Secondary) — копия лидера.
Получает данные от Leader и обновляется.
Обычно используется только для чтения (SELECT).
Схема:
Клиент ──WRITE──→ Leader ──копирует──→ Replica 1
Leader ──копирует──→ Replica 2
Клиент ──READ──→ Replica 1 (быстро, ближе к клиенту)
Аналогия из жизни:
Leader — редактор газеты, пишет статью
Replicas — типографии в разных городах, печатают копии
Читатель (клиент) берёт газету в ближайшем киоске
ЛегкоБазы данных / Распределённые БД
Синхронная vs Асинхронная репликация
Чем отличается синхронная репликация от асинхронной? Какие у них плюсы и минусы?
Синхронная репликация:
Leader записывает данные И ЖДЁТ подтверждения от реплик.
Только после подтверждения от ВСЕХ — отвечает клиенту "OK".
+ Данные на всех нодах всегда одинаковы (нет лага)
- Медленно: скорость записи = скорость самой медленной реплики
- Если одна реплика недоступна — запись блокируется
Асинхронная репликация:
Leader записывает данные и СРАЗУ отвечает клиенту "OK".
Реплики обновляются позже, в фоне.
+ Быстро: клиент не ждёт реплики
- Есть лаг: реплики могут быть устаревшими на несколько миллисекунд/секунд
- Если Leader упадёт — последние записи могут потеряться
Пример:
Синхронная: клиент ──WRITE──→ Leader ──ждёт──→ Replica OK → клиенту "OK"
Асинхронная: клиент ──WRITE──→ Leader → клиенту "OK" → потом копирует на Replica
В реальности чаще всего используют асинхронную — ради скорости.
PostgreSQL по умолчанию — асинхронная репликация.
ЛегкоБазы данных / Распределённые БД
Eventual Consistency
Что такое Eventual Consistency? Что значит 'в конечном счёте'?
Eventual Consistency (согласованность в конечном счёте) —
модель, при которой реплики НЕ обязаны быть одинаковыми прямо сейчас,
но ГАРАНТИРУЮТ стать одинаковыми через какое-то время.
Ключевое слово: eventually = "в конечном счёте / в конце концов".
Что это значит на практике:
— В любой момент времени реплики могут отдавать РАЗНЫЕ данные
— Но если перестать писать, все реплики догонят и станут одинаковыми
— Лаг обычно от миллисекунд до секунд
Пример из жизни:
Вы загрузили фото в Instagram.
Ваш друг открывает профиль — фото ещё не видно (реплика не обновилась).
Через 2 секунды обновляет страницу — фото появилось.
Это eventual consistency.
Противоположность — Strong Consistency:
Все реплики всегда одинаковы. Клиент всегда читает актуальные данные.
Но это медленнее.
Зачем используют Eventual:
— Быстрее (запись не ждёт реплики)
— Доступнее (система работает даже если часть реплик недоступна)
— Подходит для большинства реальных задач
ЛегкоБазы данных / Распределённые БД
Stale Read
Что такое Stale Read? Почему клиент может читать старые данные?
Stale Read (устаревшее чтение) — когда клиент читает данные,
которые уже НЕ являются актуальными, потому что реплика ещё не обновилась.
Как это происходит:
1. Клиент A записывает: user.name = "Иван"
2. Leader сохраняет: user.name = "Иван"
3. Клиент A читает user → попадает на Replica 1
4. Replica 1 ещё не обновилась → возвращает user.name = "Ольга"
5. Клиент A только что записал "Иван", а читает "Ольга" — это stale read
Почему это проблема:
— Пользователь обновил профиль, но видит старую версию
— Отменил заказ, но он всё ещё отображается как активный
— Перевёл деньги, но баланс не изменился
Когда возникает:
— При асинхронной репликации (почти всегда)
— Когда запрос на чтение попадает на отстающую реплику
— При высокой нагрузке, когда репликация не успевает
Stale read — это НЕ баг, это норма в eventual consistency системах.
Вопрос в том: как с этим бороться, когда это критично.
ЛегкоБазы данных / Распределённые БД
Read-After-Write Consistency
Что такое Read-After-Write Consistency? Чем отличается от Strong Consistency?
Read-After-Write Consistency (чтение после записи) — гарантия:
"Если клиент записал данные, он сразу увидит эту запись при чтении".
Тот же самый клиент. Свои собственные данные.
Важно: это гарантия только для АВТОРА записи.
Другие клиенты могут видеть старые данные — это нормально.
Сравнение уровней гарантий (от слабого к сильному):
Eventual Consistency:
Все в конечном счёте увидят новые данные. Никаких гарантий когда.
Read-After-Write:
Автор записи видит свои изменения сразу.
Другие — когда-нибудь потом.
Strong (Linearizable) Consistency:
Все клиенты всегда видят самые свежие данные.
Максимальная гарантия, но самая медленная.
Пример:
Вы поменяли аватарку.
Eventual: вы можете видеть старую аватарку
Read-After-Write: вы сразу видите новую аватарку, другие — позже
Strong: все сразу видят новую аватарку
Read-After-Write — это компромисс между скоростью и консистентностью.
Для большинства API этого достаточно.
СреднеБазы данных / Распределённые БД
CAP-теорема
Что такое CAP-теорема? Почему нельзя иметь всё одновременно?
CAP-теорема говорит: в распределённой системе можно выбрать только ДВА из трёх:
C — Consistency (Согласованность)
Все ноды видят одни и те же данные в один момент времени.
A — Availability (Доступность)
Каждый запрос получает ответ (без ошибки), даже если ноды упали.
P — Partition Tolerance (Устойчивость к разделению)
Система работает даже если сеть между нодами оборвалась.
Почему нельзя все три:
Сеть ВСЕГДА может сломаться (P обязателен в реальном мире).
Значит реальный выбор между C и A:
CP — жертвуем доступностью ради консистентности
Пока реплики не синхронизируются — система отвечает ошибкой.
Пример: HBase, MongoDB (по умолчанию)
AP — жертвуем консистентностью ради доступности
Система всегда отвечает, но данные могут быть старыми.
Пример: Cassandra, DynamoDB (в режиме eventual)
Как это связано с нашей задачей:
— Eventual consistency — это AP (доступность важнее)
— Strong consistency — это CP (консистентность важнее)
— Задача "read-after-write" — попытка получить C для автора,
оставаясь в AP для остальных
СреднеRuby on Rails / Legacy-проекты
Data Migration: безопасное изменение данных
Нужно переименовать колонку у 10M записей. Или добавить NOT NULL на таблицу, где есть NULL. Как не положить продакшен?
Data migration — миграция, которая меняет ДАННЫЕ, а не схему.
На большой таблице (10M+ строк) это опасно.
Правило: schema migration (DDL) и data migration (DML) — РАЗДЕЛЯТЬ.
Проблема 1: add_column с default на большой таблице:
# Старый PostgreSQL (< 11):
add_column :users, :active, :boolean, default: true
# PostgreSQL переписывает ВСЮ таблицу → lock на 10 минут
# PostgreSQL 11+:
add_column :users, :active, :boolean, default: true
# Мгновенно! PostgreSQL использует "fast default"
Проблема 2: remove_column:
remove_column :users, :old_field
# Старый код (ещё не задеплоенный) обращается к old_field → crash
Решение: два релиза
Релиз 1: убрать из кода все обращения к old_field
Релиз 2: remove_column в миграции
Проблема 3: rename_column:
rename_column :users, :name, :full_name
# Все User.where(name: ...) сломаются
Решение: три релиза
Релиз 1: добавить full_name (alias, скопировать данные)
Релиз 2: перевести код на full_name
Релиз 3: удалить name
Проблема 4: Data migration на 10M записей:
# Плохо: всё в одной транзакции
User.update_all(role: :active) # lock на 5 минут
# Хорошо: батчами
User.find_in_batches(batch_size: 1000) do |batch|
User.where(id: batch).update_all(role: :active)
sleep(0.1) # дать БД передохнуть
end
# Ещё лучше: через Sidekiq
class MigrateUserJob < ApplicationJob
def perform(user_id)
User.find(user_id).update!(role: :active)
end
end
User.pluck(:id).each { |id| MigrateUserJob.perform_later(id) }
Проблема 5: NOT NULL constraint:
# На таблице есть NULL-значения:
change_column_null :users, :email, false # → PG::NotNullViolation
Решение:
1. Data migration: UPDATE users SET email = '' WHERE email IS NULL
2. Schema migration: change_column_null :users, :email, false
Разные миграции, разный порядок!
Чеклист data migration:
— Сколько строк в таблице? (SELECT COUNT(*))
— Сколько времени займёт? (EXPLAIN ANALYZE на тестовых данных)
— Блокирует ли таблицу? (SELECT pg_locks)
— Можно откатить? (нужен down метод)
— Запустить на staging с копией production данных
Как ИИ помогает:
ИИ напишет батчированную миграцию с find_in_batches.
Но ИИ НЕ знает: у вас 1000 строк или 10M, какой RPS,
можно ли залочить таблицу на 1 секунду или нельзя никогда.
Решение о safety принимает разработчик, знающий продакшен.
СреднеБазы данных / Распределённые БД
Паттерн: Sticky Sessions
Как Sticky Sessions решают проблему stale read? В чём идея и какие минусы?
Идея: все запросы одного клиента всегда идут на одну и ту же ноду.
Как это работает:
1. Клиент делает первый запрос
2. Балансировщик назначает ему конкретную ноду (по IP, cookie, session)
3. Все последующие запросы этого клиента — на ту же ноду
4. Клиент пишет на Leader → Leader реплицирует →
раз клиент всегда читает с одной ноды, она успеет обновиться
(запись быстрая, следующий запрос через сотни мс)
Пример:
Балансировщик видит cookie user_id=42 → всегда роутит на Replica 1
Клиент 42 пишет данные → Leader реплицирует на Replica 1
Клиент 42 читает → идёт на Replica 1 → данные уже свежие
Плюсы:
+ Простая реализация (настройка балансировщика)
+ Нет дополнительной нагрузки на БД
+ Работает для большинства случаев
Минусы:
- Неравномерная нагрузка: одна нода перегружена, другая простаивает
- При падении ноды клиент переходит на другую — stale read вернётся
- Не гарантирует read-after-write при быстром чтении после записи
- Не работает, если клиент переключает устройство/браузер
СреднеБазы данных / Распределённые БД
Паттерн: Token/Version Consistency
Как решить stale read с помощью токенов версий? Что клиент передаёт при чтении?
Идея: при записи сервер возвращает версию/токен.
При чтении клиент передаёт этот токен, и реплика ждёт,
пока не обновится до нужной версии.
Как это работает:
1. Клиент: POST /users { name: "Иван" }
2. Сервер записывает, возвращает: { version: 42 }
3. Клиент: GET /users/1, заголовок: If-Version: 42
4. Реплика проверяет: "у меня версия 40, нужно догнать до 42"
5. Реплика ждёт обновления (или спрашивает Leader)
6. Когда версия >= 42 — возвращает свежие данные
Вместо version можно использовать:
— Timestamp: "я записал в 12:00:05, дай данные не старше"
— WAL offset: позиция в логе репликации
— Vector clock: векторная часы (более сложный вариант)
Плюсы:
+ Строгая гарантия: клиент ВИДИТ свои записи
+ Работает при failover (не привязан к конкретной ноде)
+ Гибкость: клиент решает, когда нужна строгая консистентность
Минусы:
- Реплика может ждать (latency spike) если сильно отстаёт
- Нужна поддержка на уровне БД или middleware
- Клиент должен хранить и передавать токен в каждом запросе
- Сложнее реализовать, чем sticky sessions
СреднеБазы данных / Распределённые БД
Паттерн: Quorum Reads/Writes (R + W > N)
Что такое Quorum reads/writes? Как формула R+W>N гарантирует свежие данные?
Идея: читать и писать с нескольких реплик одновременно.
Если при записи подтвердили W реплик, а при чтении спросили R реплик,
и R + W > N (общее число реплик) — хотя бы одна реплика
в пересечении будет свежей.
N — общее количество реплик
W — сколько реплик должны подтвердить запись
R — сколько реплик спросить при чтении
Пример: N=3, W=2, R=2
Запись: клиент пишет → Leader отправляет на 3 реплики
ждёт подтверждения от 2 (W=2) → отвечает "OK"
Реплики A, B подтвердили. C — ещё нет.
Чтение: клиент читает → спрашивает 2 реплики (R=2)
Получает данные от A (свежие!) и C (устаревшие)
Сравнивает версию/таймстемп → берёт свежие от A
Пересечение: W=2 (A,B) + R=2 (A,C) = реплика A общая
значит хотя бы один свежий ответ гарантирован
Варианты настройки:
N=3, W=2, R=2 — баланс (quorum)
N=3, W=3, R=1 — быстрые чтения, медленные записи
N=3, W=1, R=3 — быстрые записи, медленные чтения
N=3, W=1, R=1 — максимальная скорость, НО нет гарантии (1+1=2 < 3)
Плюсы:
+ Математическая гарантия свежести данных
+ Не нужен токен на клиенте
+ Автоматический failover (если одна нода упала — quorum из оставшихся)
Минусы:
- Latency: ждём несколько реплик (и при записи, и при чтении)
- При падении части нод может не хватить quorum → запись/чтение недоступны
- Выше нагрузка на сеть (больше межнодового трафика)
Используют: Cassandra, DynamoDB, Riak
СреднеБазы данных / Распределённые БД
Паттерн: Consensus-протоколы (Raft, Paxos)
Как Raft и Paxos обеспечивают строгую консистентность? Что такое consensus?
Consensus (консенсус) — алгоритм, при котором все ноды договариваются
о состоянии данных. Запись считается успешной, когда majority (большинство)
нод её подтвердили.
Raft — самый популярный consensus-алгоритм (понятнее чем Paxos).
Как работает Raft:
1. Выбирается Leader (через голосование)
2. Все записи идут через Leader
3. Leader отправляет запись всем Followers
4. Ждёт подтверждения от majority (N/2 + 1)
5. Только после этого запись считается подтверждённой (committed)
6. Чтение тоже идёт через Leader (или через read-index механизм)
Важно: majority — это больше половины.
5 нод → нужно 3 подтверждения
3 ноды → нужно 2 подтверждения
Что при падении Leader:
— Оставшиеся ноды начинают выборы нового Leader
— Новый Leader гарантирует, что все committed записи сохранены
— Система продолжает работу, если живо majority
Плюсы:
+ Строгая линеаризация (linearizability) — максимальная гарантия
+ Прозрачно для клиента (не нужно передавать токены)
+ Автоматический failover с гарантиями
Минусы:
- Высокая latency: каждая запись ждёт round-trip на majority нод
- При partition теряется доступность (если нет majority)
- Leader — bottleneck (все записи через него)
- Сложная реализация (обычно берут готовые решения)
Используют: etcd, Consul (Raft), ZooKeeper (Zab), CockroachDB, Spanner
СреднеБазы данных / Распределённые БД
Паттерн: Client-Side Cache / Write-Through
Как кэширование на стороне клиента помогает избежать stale read?
Идея: после записи клиент сохраняет результат у себя (в кэше).
При следующем чтении — берёт из кэша, а не из БД.
БД успевает реплицировать данные, и когда кэш протухнет (TTL) —
данные уже свежие на всех репликах.
Как это работает:
1. Клиент: POST /users { name: "Иван" }
2. Сервер записывает в БД, возвращает: { name: "Иван" }
3. Клиент кэширует: cache["user:1"] = { name: "Иван" }, TTL=5sec
4. Клиент: GET /users/1
5. Проверяет кэш → есть! → возвращает из кэша (мгновенно, без запроса в БД)
6. Через 5 секунд кэш протухает → следующий запрос идёт в БД → данные уже свежие
Где хранить кэш:
— В памяти клиента (браузер, мобильное приложение)
— На API-сервере (Redis, Memcached) — для множества инстансов
— CDN (для публичных данных)
Write-Through вариант:
При записи — одновременно обновляем и БД, и кэш.
Следующее чтение всегда попадёт в кэш с актуальными данными.
Плюсы:
+ Нулевая latency на чтение своих записей
+ Не нужно менять БД или балансировщик
+ Снижает нагрузку на БД
Минусы:
- Другие клиенты видят старые данные (пока кэш не протух)
- Сложная инвалидация кэша (когда данные меняются извне)
- Если несколько API-инстансов — нужен distributed cache (Redis)
- Риск рассинхронизации кэша и БД
СреднеБазы данных / Распределённые БД
Паттерн: Leader-Only Reads
Что если всегда читать с Leader? Какие плюсы и минусы?
Идея: чтение идёт только на Leader, где данные гарантированно свежие.
Реплики используются только для других целей (аналитика, бэкап).
Как это работает:
1. Клиент: POST /users { name: "Иван" } → Leader
2. Leader записывает, подтверждает
3. Клиент: GET /users/1 → ТОЖЕ на Leader
4. Leader возвращает свежие данные (он же сам только что записал)
Варианты применения:
— Все чтения через Leader
— Только "чтения после записи" через Leader (по сигналу от клиента)
— Чтение через Leader в течение N секунд после записи
В PostgreSQL:
SET default_transaction_read_only = on; — на репликах
Чтение идёт на primary (leader), реплики — для отчётов
Плюсы:
+ 100% гарантия read-after-write (данные на Leader всегда свежие)
+ Простая реализация (на уровне роутинга запросов)
+ Не нужны токены, кэши, quorum
Минусы:
- Leader — bottleneck: все запросы через одну ноду
- Реплики простаивают (не используются для обслуживания чтения)
- Высокая latency если Leader далеко от клиента географически
- Leader — единая точка отказа (SPOF): упал → всё встало
- Не масштабируется: добавление реплик не помогает с нагрузкой
Когда подходит:
— Небольшая нагрузка на чтение
— Данные критичны (финансы, платежи)
— Географически один регион
СреднеБазы данных / Распределённые БД
Паттерн: Per-Query Consistency (гибридный подход)
Как Cassandra и DynamoDB позволяют выбирать консистентность для каждого запроса?
Идея: клиент выбирает уровень консистентности ДЛЯ КАЖДОГО ЗАПРОСА отдельно.
Для важных операций — строгая консистентность (медленнее).
Для неважных — eventual (быстрее).
Уровни консистентности (на примере Cassandra):
ONE — записать/прочитать с одной реплики
Самый быстрый, но нет гарантий. Для лайков, просмотров.
QUORUM — записать/прочитать с majority реплик (N/2 + 1)
Баланс скорости и гарантий. Для профилей, настроек.
ALL — все реплики должны подтвердить
Максимальная гарантия, но упала одна нода — всё встало.
Для критичных данных (пароли, баланс).
LOCAL_QUORUM — quorum в локальном дата-центре
Для multi-DC: быстро локально, без межконтинентальных задержек.
Как это выглядит в коде (Cassandra):
# Запись профиля — важно, используем QUORUM
session.query("INSERT INTO users ...", consistency: :quorum)
# Запись лайка — не важно, используем ONE
session.query("INSERT INTO likes ...", consistency: :one)
# Чтение профиля после редактирования — важно
session.query("SELECT * FROM users WHERE id = ?", consistency: :quorum)
# Чтение ленты — не критично, eventual ok
session.query("SELECT * FROM feed", consistency: :one)
Плюсы:
+ Гибкость: разные гарантии для разных операций
+ Можно оптимизировать: строго только где нужно
+ Один кластер БД обслуживает все сценарии
Минусы:
- Сложнее reasoning: разные запросы — разные гарантии
- Нужна дисциплина в команде: все должны понимать уровни
- Ошибка в выборе уровня — stale read вернётся
Используют: Cassandra, DynamoDB, CosmosDB, ScyllaDB
СложноБазы данных / Распределённые БД
Задача: Read-After-Write в распределённой системе
На собеседовании вам дают задачу:
У вас есть API поверх распределённой БД (несколько нод).
Данные записываются на Leader и асинхронно реплицируются.
Используется Eventual Consistency.
Проблема: клиент записывает данные, сразу читает — и получает старую версию,
потому что попал на отстающую реплику (stale read).
Задача: предложите способы обеспечить Read-After-Write Consistency —
чтобы клиент всегда видел свои последние записи.
Для каждого способа опишите плюсы и минусы.
Подсказка: вспомните 7 паттернов из карточек выше.
=== 7 ПАТТЕРНОВ РЕШЕНИЯ ===
1. STICKY SESSIONS
Все запросы клиента на одну ноду.
+: простая реализация
-: при падении ноды — stale read возвращается
2. TOKEN / VERSION
Сервер возвращает версию при записи, клиент передаёт при чтении.
Реплика ждёт пока не догонит до нужной версии.
+: строгая гарантия, работает при failover
-: latency spike, нужна поддержка в БД
3. QUORUM READS/WRITES (R+W>N)
Читать и писать с нескольких реплик. Пересечение гарантирует свежесть.
+: математическая гарантия, автоматический failover
-: выше latency, при падении нод может не хватить quorum
4. CONSENSUS (Raft/Paxos)
Запись подтверждается majority нод через алгоритм консенсуса.
+: строгая линеаризация, прозрачна для клиента
-: максимальная latency, сложная реализация
5. CLIENT-SIDE CACHE
После записи кэшировать результат, читать из кэша, а не из БД.
+: нулевая latency, не трогаем БД
-: другие клиенты видят старые данные, сложная инвалидация
6. LEADER-ONLY READS
Читать только с Leader, где данные всегда свежие.
+: 100% гарантия, простая реализация
-: Leader — bottleneck, не масштабируется
7. PER-QUERY CONSISTENCY
Клиент выбирает уровень консистентности для каждого запроса.
+: гибкость, оптимизация где можно
-: нужна дисциплина, ошибка = stale read
=== КАК ВЫБРАТЬ ===
Высоконагруженный API: Per-Query + Quorum (варианты 7 + 3)
Быстрый фикс, мало трафика: Sticky Sessions (вариант 1)
Финансы, критичные данные: Consensus (вариант 4)
Много чтений, свой кэш: Client-Side Cache (вариант 5)
На собеседовании: назовите 3-4 варианта с +/- и обоснуйте выбор.
СреднеRuby on Rails / Инфраструктура Rails
Memory Leak в Ruby-процессе
Процесс Ruby (Puma worker) растёт до 2GB и падает (OOM Killer). Что такое memory leak в Ruby? Как найти и починить?
Memory leak (утечка памяти) — когда объекты создаются, но никогда не удаляются GC.
Процесс растёт и растёт, пока не упадёт.
Частые причины в Rails:
1. Глобальные переменные / константы-хранилища:
$cache = {} # растёт бесконечно, GC не чистит
CACHE << user # каждый запрос добавляет, никто не удаляет
2. Массивы/хэши в long-running процессах (Sidekiq, Puma):
class MyJob < ApplicationJob
@@processed = [] # растёт с каждой джобой
end
3. Забыли .limit / .find_each:
User.all.each { |u| ... } # загрузил 500 000 пользователей в память
# Правильно:
User.find_each { |u| ... } # по 1000 за раз
4. Замыкания (closures) держат ссылки:
def process
big_array = (1..1_000_000).to_a
proc { big_array.size } # big_array никогда не удалится
end
Как найти:
— gem 'memory_profiler' — показать кто аллоцирует
— gem 'derailed_benchmarks' — бенчмарк memory
— NewRelic / Datadog — график памяти по времени
— Sentry — алерт при OOM
Как ИИ помогает:
ИИ найдёт forgot .limit или глобальный массив в коде.
Но ИИ НЕ увидит, что leak происходит только при определённом traffic pattern
в продакшене. Profiling и мониторинг — ваша задача.
Что сделать на практике:
1. Посмотреть график памяти (Datadog/NewRelic)
2. Воспроизвести: ab -n 10000 http://localhost:3000/api/endpoint
3. Запустить memory_profiler на этот endpoint
4. Найти кто аллоцирует и почему не чистится
СреднеRuby on Rails / Инфраструктура Rails
Connection Pool: откуда берутся ошибки
В продакшене появляются ошибки: ActiveRecord::ConnectionTimeoutError или PG::ConnectionBad. Что такое connection pool и почему он заканчивается?
Connection Pool — набор заранее созданных подключений к БД.
Каждый поток (Puma thread, Sidekiq thread) берёт соединение из пула.
Если свободных нет — ждёт. Если ждёт дольше timeout — ошибка.
Как это работает:
Puma: 5 threads → нужен pool минимум 5
Sidekiq: 25 concurrency → нужен pool минимум 25
config/database.yml:
pool: 5 # максимум 5 одновременных подключений к БД
Почему пул заканчивается:
1. Долгий запрос: 5 потоков ждут БД → 6-й поток не может получить connection
2. Не закрытое соединение: забыли .release в raw SQL
3. N+1: 100 запросов в одном action = долго держат connection
4. Thread mismatch: Puma 5 threads, pool: 2 → 3 потока в очереди
Как диагностировать:
— rails dbconsole: SELECT * FROM pg_stat_activity; — сколько active connections
— Логи: "could not obtain a connection from the pool"
— NewRelic: график "Database connections"
Как чинить:
1. Увеличить pool (но не больше max_connections PostgreSQL):
pool: <%= ENV.fetch("RAILS_MAX_THREADS", 5) %>
2. Убрать долгие запросы (N+1, missing index)
3. Настроить PostgreSQL:
# postgresql.conf
max_connections = 200 # по умолчанию 100
4. PgBouncer — пулинг на уровне PostgreSQL:
Позволяет 1000 Rails connections → 50 реальных PostgreSQL connections
(transaction-level pooling)
Формула:
Puma processes × Puma threads + Sidekiq concurrency = минимальный pool
Умножить на количество приложений, подключённых к одной БД
СреднеRuby on Rails / Legacy-проекты
Рефакторинг: когда и как
Менеджер просит новую фичу. Код — лапша. Делать фичу в этом коде или сначала рефакторить? Что такое Boy Scout Rule?
Главное правило: не рефакторить "на потом".
Рефакторинг = часть работы над фичей.
Boy Scout Rule:
"Оставь код чище, чем ты его нашёл."
Не нужно переписывать весь файл. Достаточно:
— Вынести метод, пока разбираешься
— Добавить тест на код, который меняешь
— Убрать закомментированный код
— Переименовать непонятную переменную
Когда рефакторить:
— Добавляете фичу и не понимаете код → сначала упростить
— Дублирование: копипаста в 3 местах → вынести в метод
— Тест на новую фичу требует mock 10 зависимостей → упростить связи
Когда НЕ рефакторить:
— Код работает и вы его НЕ трогаете
— Нет тестов и нет времени их написать
— Рефакторинг не приносит бизнес-ценности
— "Мне не нравится как написано" — не причина
Стратегии рефакторинга:
1. Extract Method — вынести кусок в отдельный метод:
# Было:
def process
data = fetch_from_api
data.map { |d| d["value"] * 1.2 }
data.reject { |d| d > 100 }
data.sort.reverse
# ... ещё 20 строк
end
# Стало:
def process
data = fetch_from_api
normalized = normalize(data)
filtered = filter(normalized)
sorted = sort_desc(filtered)
end
2. Replace Conditional with Polymorphism:
# Было:
def price
case type
when 'book' then base * 0.9
when 'premium' then base * 1.5
when 'sale' then base * 0.5
end
end
# Стало (STI):
class BookProduct < Product
def price = base * 0.9
end
3. Move Method — метод в правильный класс:
# User#calculate_order_total — это про Order
→ Order#total
4. Introduce Parameter Object:
# Было:
def create_order(user, product, quantity, discount, gift_wrap)
# Стало:
def create_order(order_params)
Опасный рефакторинг (не делать без тестов):
— Переименование методов (breaks external callers)
— Изменение public API
— Перемещение между модулями
— Любой рефакторинг без запуска тестов после
Как ИИ помогает:
ИИ мгновенно вынесет метод, переименует переменные, упростит if/else.
Но ИИ НЕ знает: "а этот метод вызывается из 10 мест,
включая rake task, который запускается по cron на продакшене".
ИИ может сломать то, о чём не знает. Review каждого изменения.
СреднеRuby on Rails / Legacy-проекты
Undocumented Code: нет даже README
На проекте нет документации. Ни README, ни комментариев. Как понять что делает приложение? Как начать документировать?
Недокументированный проект — норма, не исключение.
Документация устаревает, а код — всегда актуален.
Порядок разбора (от быстрого к глубокому):
1. README.md — если есть:
— Как запустить? (setup instructions)
— Что делает приложение? (purpose)
— Как запустить тесты?
2. config/routes.rb — карта всех endpoints:
Ресурсы = основные сущности приложения
Namespace = модули/домены
3. db/schema.rb — структура данных:
Таблицы = бизнес-сущности
Foreign keys = связи между сущностями
Индексы = важные поля (по ним ищут)
4. Gemfile — технологии и зависимости:
gem 'stripe' → есть платежи
gem 'sidekiq' → фоновые задачи
gem 'devise' → аутентификация
gem 'pundit' → авторизация
5. app/jobs/ — фоновые процессы:
Какие процессы работают в фоне?
Рассылки, импорт, синхронизация?
6. app/services/ (если есть) — бизнес-логика:
Главные процессы приложения
Что документировать в первую очередь:
1. Как запустить проект (setup guide):
— Ruby version, Node version, PostgreSQL version
— bundle install, rails db:setup
— export ENV_VAR=... (не забудьте .env.example)
— rails server
2. Архитектурные решения (ADR — Architecture Decision Records):
docs/adr/001-why-sidekiq-not-resque.md
"Мы выбрали Sidekiq потому что..."
3. Схема доменов:
docs/architecture.md
"Пользователь → делает Заказ → содержит Товары → Оплата через Stripe"
4. Deployment process:
docs/deployment.md
"Merge в main → CI → deploy to staging → ручная проверка → deploy to production"
Инструменты:
— rails-erd gem — автоматически сгенерировать ER-диаграмму
— annotate gem — комментарии-схемы в модели (зависит от CPU)
— Yard / RDoc — документация из кода
— diagrams.net — нарисовать архитектуру
Антипаттерны документации:
— Дублировать код в комментариях: "x = x + 1 # прибавляем 1"
— Устаревшие комментарии: "используем Rails 5" (а уже 7)
— Документировать ОЧЕВИДНОЕ
— Документация вместо понятного кода
Как ИИ помогает:
ИИ сгенерирует README по коду проекта за минуту.
ИИ нарисует схему связей между моделями.
Но ИИ НЕ знает: "а это legacy-поле, его используют
только в старом мобильном приложении v1, уже никем не поддерживаемом".
Бизнес-контекст — только от команды.
СреднеRuby on Rails / Инфраструктура Rails
Sidekiq: очередь растёт, джобы падают
Sidekiq-очередь растёт до 100 000 джоб. Джобы падают, retry создают avalanche effect. Как диагностировать и чинить?
Avalanche effect (лавинообразный эффект):
Джоба падает → Sidekiq retry → падает снова → retry с задержкой
+ новые джобы постоянно добавляются → очередь растёт экспоненциально.
Частые причины падений:
1. Timeout: внешнее API не отвечает за 30 секунд
Net::ReadTimeout при HTTP запросе
2. Memory: Sidekiq process OOM, worker убит
Обработка 10000 записей в памяти
3. Not idempotent: джоба выполнена наполовину, retry дублирует данные
Отправили email, потом retry → клиент получил 2 письма
4. Exception в середине: обработали 500 из 1000 записей, упали
При retry начинаем СНАЧАЛА → первые 500 обработаются повторно
Что делать:
1. Idempotency — джоба безопасна для повторного выполнения:
class SendEmailJob < ApplicationJob
def perform(user_id)
return if user_already_emailed?(user_id) # guard clause
UserMailer.welcome(user_id).deliver_now
mark_emailed!(user_id)
end
end
2. Батчинг вместо обработки всего сразу:
# Плохо:
User.all.each { |u| process(u) } # OOM на 500K записей
# Хорошо:
User.find_in_batches(batch_size: 500) do |batch|
batch.each { |u| process(u) }
end
3. Circuit breaker — временно отключить падающий сервис:
gem 'circuitbox'
Не пытаться вызвать API, если он уже упал 5 раз подряд
4. Dead jobs queue — лимит retry:
sidekiq_options retry: 3, dead: true
После 3 retry → dead queue → алерт → ручное разбирательство
5. Мониторинг:
Sidekiq Web UI: /sidekiq — размер очереди, retry count
Sentry: алерт при росте очереди
Как ИИ помогает:
ИИ напишет idempotent джобу с guard clause.
Но СТРАТЕГИЮ обработки ошибок (retry vs dead vs circuit breaker)
вы определяете на основе бизнес-требований.
СреднеRuby on Rails / Инфраструктура Rails
Блокировки БД: row locks, deadlocks
В логах: PG::LockNotAvailable или deadlock detected. Что такое блокировки в БД? Почему возникают deadlocks?
Блокировка (lock) — механизм БД, чтобы две транзакции не меняли
одни и те же данные одновременно.
Когда возникает lock:
Transaction A: UPDATE users SET balance = 100 WHERE id = 1
Transaction B: UPDATE users SET balance = 200 WHERE id = 1
B ждёт пока A не завершится (commit или rollback).
Deadlock (взаимная блокировка):
Transaction A: UPDATE users SET ... WHERE id = 1 (lock id=1)
Transaction B: UPDATE users SET ... WHERE id = 2 (lock id=2)
Transaction A: UPDATE users SET ... WHERE id = 2 (ждёт B)
Transaction B: UPDATE users SET ... WHERE id = 1 (ждёт A)
→ Обе ждут друг друга = deadlock. PostgreSQL убьёт одну из них.
В Rails это выглядит так:
ActiveRecord::Deadlocked
PG::Error: ERROR: deadlock detected
Как избежать:
1. Одинаковый порядок обновления:
ВСЕГДА обновлять записи по id по возрастанию
sort_by(&:id) перед обновлением
2. Короткие транзакции:
# Плохо:
User.transaction do
user.update!(balance: ...)
sleep(5) # внешний API
order.update!(status: ...) # lock держится 5 секунд!
end
# Хорошо:
result = call_external_api # БЕЗ транзакции
User.transaction do
user.update!(balance: ...)
order.update!(status: result)
end
3. Optimistic locking:
add_column :products, :lock_version, :integer, default: 0
# ActiveRecord автоматически проверит версию при update
# Если кто-то изменил раньше — ActiveRecord::StaleObjectError
4. advisory locks для бизнес-логики:
ActiveRecord::Base.connection.execute(
"SELECT pg_advisory_lock(12345)"
)
# Только один процесс может держать этот lock
5. SELECT FOR UPDATE (pessimistic):
User.transaction do
user = User.lock.find(1) # заблокировать строку
user.update!(balance: user.balance - 100)
end
Как ИИ помогает:
ИИ добавит lock_version, перепишет транзакцию.
Но ИИ не знает, что deadlock возникает только в пятницу вечером
при определённом traffic pattern. Читать PostgreSQL logs — ваша задача.
СреднеRuby on Rails / Инфраструктура Rails
Медленные запросы: EXPLAIN ANALYZE
Endpoint отвечает 5 секунд. Как понять почему? Что такое EXPLAIN ANALYZE и как читать его вывод?
EXPLAIN ANALYZE — команда PostgreSQL, которая показывает:
— КАК БД выполняет запрос (план)
— СКОЛЬКО времени занимает каждый шаг (реальное время)
Как использовать:
В rails console:
User.joins(:orders).where(orders: { status: 'active' }).explain
→ ActiveRecord сгенерирует EXPLAIN для запроса
В psql:
EXPLAIN ANALYZE SELECT * FROM users WHERE email = 'test@mail.com';
Что смотреть в выводе:
Seq Scan (sequential scan) — БД читает ВСЮ таблицу. ПЛОХО.
→ Нужен индекс по этому полю
Index Scan — БД использует индекс. ХОРОШО.
→ Запрос быстрый
Bitmap Heap Scan — использует индекс, но читает много строк.
→ Может быть ок, или нужен более точный индекс
Sort — сортировка без индекса, в памяти.
→ На больших данных будет медленно
Nested Loop — для каждой записи из одной таблицы ищет в другой.
→ Проверить join, возможно нужен индекс по foreign key
Типичные проблемы и решения:
1. Нет индекса:
WHERE email = '...' → нет index on email → Seq Scan
Добавить: add_index :users, :email
2. Функция в WHERE:
WHERE LOWER(email) = '...' → index не используется
Решение: add_index :users, "lower(email)"
Или: WHERE email ILIKE '...' (case-insensitive)
3. SELECT * вместо нужных колонок:
User.all вместо User.select(:id, :name)
На 100 колонок / 1M строк = передача GB данных
4. Missing foreign key index:
JOIN orders ON orders.user_id = users.id
→ add_index :orders, :user_id
Инструменты:
— rails console: User.explain
— Bullet gem: алерт при N+1
— Rack Mini Profiler: показать SQL запросы на странице
— PostgreSQL slow query log: log_min_duration_statement = 500
Как ИИ помогает:
ИИ добавит .includes, создаст индекс, перепишет запрос.
Но ИИ не знает: у вас 100 строк или 10M. Index на 100 строк не нужен.
Решение принимаете вы на основе данных.
СреднеRuby on Rails / Инфраструктура Rails
Медленный deploy: причины и оптимизация
Deploy занимает 10 минут. Asset compilation на production. Docker image 2GB. Как ускорить?
Типичный Rails deploy:
git push → CI → build → deploy → restart → health check
Каждый шаг может быть узким местом.
Что тормозит:
1. bundle install:
Gemfile на 200 гемов → скачивание + компиляция native extensions (pg, nokogiri)
Решение: Docker layer caching — копировать Gemfile отдельно
COPY Gemfile Gemfile.lock ./
RUN bundle install
COPY . .
Gemfile не меняется → cache hit → мгновенно
2. Asset precompilation:
rails assets:precompile компилирует JS/CSS/SCSS
На production может занимать 2-5 минут
Решения:
— Компилировать в CI, не на сервере
— Использовать esbuild/vite (быстрее чем Sprockets)
— Кэшировать node_modules и tmp/cache/assets
3. Миграции БД:
add_column :users, :bio, :text на таблице 50M строк
PostgreSQL залочит таблицу на минуты
Решение:
— add_column с default: → PostgreSQL 11+ не блокирует
— remove_column → сначала убрать из кода, потом через релиз
— data migrations — отдельные джобы, не в deploy
4. Docker image слишком большой:
Ruby:3.2 образ = 1GB
+ gems + assets = 2GB
Решение: multi-stage build
FROM ruby:3.2 AS builder
RUN bundle install && rails assets:precompile
FROM ruby:3.2-slim # slim = 200MB вместо 1GB
COPY --from=builder /app /app
5. Restart всех Puma workers:
kill -USR2 master_pid — phased restart
По одному воркеру, но каждый грузит приложение
Решение: preload_app! + phased restart
Метрики для мониторинга:
— CI time: от push до готового artifact
— Deploy time: от artifact до живого pod
— Downtime: сколько пользователи видят ошибку
Как ИИ помогает:
ИИ напишет Dockerfile с multi-stage, оптимизирует Gemfile.
Но стратегию zero-downtime deploy (blue-green, canary, rolling)
выбираете вы на основе инфраструктуры.
СреднеRuby on Rails / Инфраструктура Rails
Graceful Shutdown: что происходит при деплое
При деплое Puma перезапускается. Что происходит с текущими запросами? Что такое graceful shutdown и зачем он нужен?
Проблема: при деплое процесс убивается.
Если в этот момент обрабатывается запрос — клиент получит ошибку.
Если выполняется Sidekiq job — данные обработаны наполовину.
Graceful shutdown (плавное завершение) — дать процессу завершить
текущую работу перед остановкой.
Как работает Puma:
1. Получает сигнал TERM (от Kubernetes, systemd, Capistrano)
2. Перестаёт принимать НОВЫЕ запросы
3. Даёт текущим запросам завершиться (timeout: 30 секунд)
4. Если не успели — форсированно убивает
5. Запускается новая версия
Настройка в config/puma.rb:
workers 2
threads 1, 5
worker_timeout 30 # сколько ждать перед kill
preload_app! # загрузить app один раз, fork workers
on_worker_boot do
# подключиться к БД после fork
ActiveRecord::Base.establish_connection
end
Sidekiq graceful shutdown:
Sidekiq при TERM:
1. Перестаёт брать новые джобы
2. Текущие джобы дорабатывают (timeout: 25 секунд)
3. Не завершённые → push обратно в Redis
4. Процесс завершается
# config/initializers/sidekiq.rb
Sidekiq.configure_server do |config|
config.timeout = 25
end
Kubernetes и graceful shutdown:
spec:
terminationGracePeriodSeconds: 60
1. Kubernetes отправляет SIGTERM
2. Ждёт 60 секунд
3. Если жив — SIGKILL (форсированное убийство)
Проблемы без graceful shutdown:
— Пользователь видит 502 в момент деплоя
— Sidekiq job выполнена наполовину → данные в неконсистентном состоянии
— Файл загружен наполовину → битый файл на диске
— Транзакция открыта → lock в БД на 30 секунд
Как ИИ помогает:
ИИ напишет правильный puma.rb и sidekiq initializer.
Но timeout значения (30s? 60s?) зависят от ваших SLA
и времени выполнения запросов. Это определяете вы.
СреднеRuby on Rails / Инфраструктура Rails
Мониторинг: Sentry, APM, логи
На продакшене что-то сломалось. Как узнать? Что такое APM, Sentry, structured logging?
Мониторинг — способность ЗНАТЬ что происходит в продакшене
без ручной проверки.
Три слоя мониторинга:
1. Errors — Sentry / Rollbar
Автоматически ловит все unhandled exceptions
Показывает: stack trace, user info, request params, breadcrumbs
Алерт в Slack при росте ошибок
# config/initializers/sentry.rb
Sentry.init do |config|
config.dsn = Rails.application.credentials.sentry_dsn
config.environment = Rails.env
end
2. Performance (APM) — NewRelic / Datadog / AppSignal
Показывает: время запроса, время SQL, время view
Трейсы: какой запрос куда ходил и сколько ждал
Графики: response time, throughput, error rate, memory
Находит:
— "Endpoint /api/orders отвечает 3 секунды"
— "SELECT * FROM products занимает 80% времени"
— "Memory растёт каждый деплой — leak"
3. Logs — ELK / Loki + Grafana
Structured logging = логи в JSON формате
Легко искать и фильтровать
# Вместо:
puts "User logged in"
# Использовать:
Rails.logger.info({ event: "user_login", user_id: user.id, ip: request.ip })
Поиск в логах:
event: "user_login" AND ip: "1.2.3.*"
error: true AND controller: "PaymentsController"
Что мониторить в первую очередь:
— Error rate (ошибки / запросы) — >1% = проблема
— P95 response time — 95% запросов укладываются в X ms
— Queue size (Sidekiq) — растёт = что-то не успевает
— Memory usage — растёт = leak
— Database connections — >80% pool = скоро OOM
Alerting (алерты):
— Error rate > 1% → Slack #alerts
— Response time p95 > 2s → Slack #alerts
— Sidekiq queue > 10000 → Slack #alerts
— Memory > 1.5GB → PagerDuty (ночью будит дежурного)
Как ИИ помогает:
ИИ настроит Sentry, напишет structured logging.
Но ЧТО мониторить и КАКИЕ thresholds — знаете вы из бизнес-контекста.
"P95 > 500ms — это норма или катастрофа?" — зависит от приложения.
СреднеRuby on Rails / Инфраструктура Rails
Race Condition: когда два потока ломают данные
Два Sidekiq-джоба одновременно списывают баланс. Или два запроса создают дублирующую запись. Что такое race condition и как с ним бороться?
Race condition (состояние гонки) — когда результат зависит
от порядка выполнения параллельных операций.
Пример 1: Списание баланса:
Job A: читает balance = 1000
Job B: читает balance = 1000
Job A: записывает balance = 1000 - 500 = 500
Job B: записывает balance = 1000 - 300 = 700
Итог: balance = 700, а должно быть 200. 300 рублей потеряно.
Пример 2: Дублирование записи:
Request A: проверяет unique? → да, создаёт
Request B: проверяет unique? → да, создаёт (A ещё не закончил)
Итог: две одинаковые записи.
Как чинить:
1. Atomic update (для чисел):
# Плохо:
user.update!(balance: user.balance - 500)
# Хорошо:
User.update_counters(user.id, balance: -500)
# SQL: UPDATE users SET balance = balance - 500 WHERE id = ?
2. Database unique constraint:
add_index :subscriptions, [:user_id, :plan_id], unique: true
# БД гарантированно не допустит дубль
# В коде: rescue ActiveRecord::RecordNotUnique
3. SELECT FOR UPDATE (pessimistic locking):
User.transaction do
user = User.lock.find(user_id)
user.update!(balance: user.balance - 500)
end
# Другая транзакция будет ждать
4. Optimistic locking:
add_column :users, :lock_version, :integer, default: 0
# update проверит lock_version, если изменился — StaleObjectError
5. Redis distributed lock:
lock_key = "process_order:#{order_id}"
Redis.current.set(lock_key, "1", nx: true, ex: 30)
# Только один процесс возьмёт lock
6. Idempotency key:
class CreatePaymentJob < ApplicationJob
def perform(order_id, idempotency_key)
return if ProcessedKey.exists?(key: idempotency_key)
# ... обработка ...
ProcessedKey.create!(key: idempotency_key)
end
end
Как ИИ помогает:
ИИ перепишет на atomic update, добавит unique index.
Но ИИ не знает: "а может ли джоба вызваться дважды?"
Это вы должны знать из бизнес-логики.
СреднеRuby on Rails / Инфраструктура Rails
Zero-Downtime Deploy: стратегии
Как деплоить без остановки сервиса? Что такое rolling deploy, blue-green, canary?
Zero-downtime deploy — пользователи не замечают обновления.
Ни одной 502 ошибки.
Стратегии:
1. Rolling Deploy (по умолчанию в Kubernetes):
— Запущено 4 pod (instance)
— Kubernetes убивает 1, запускает новый
— Когда новый healthy — убивает следующий
— Всегда живо минимум 3 из 4
Проблема: если новая версия сломана — 25% запросов упадут
перед тем как rollback сработает.
2. Blue-Green Deploy:
— Blue = текущая версия (живая)
— Green = новая версия (разворачивается параллельно)
— Когда Green healthy → переключаем traffic (load balancer)
— Если проблема → переключаем обратно на Blue
+ Мгновенный rollback
- Нужна двойная инфраструктура (2x серверов)
- Миграции БД должны быть совместимы с ОБЕИМИ версиями
3. Canary Deploy:
— Новая версия запускается для 5% трафика
— Мониторим 15 минут: error rate, latency
— Если ок → увеличиваем до 25% → 50% → 100%
— Если проблема → откатываем 5% обратно
+ Ловим баги на малом трафике
- Сложнее инфраструктура
- Нужен feature flags для разделения
Важное правило для миграций:
Деплой происходит в два релиза:
Релиз 1: Код работает со СТАРОЙ И НОВОЙ схемой БД
Миграция: add_column (backward compatible)
Релиз 2: Код использует только новую схему
Миграция: remove_old_column
Нарушение = ошибка на продакшене.
Инструменты:
— Kubernetes: rolling update (default)
— Heroku: preboot (blue-green автоматически)
— Capistrano: release folders + symlink switch
— Feature flags (Flipper): включить фичу для canary % пользователей
Как ИИ помогает:
ИИ напишет Kubernetes manifests, Dockerfile, migration strategy.
Но выбор стратегии (rolling vs blue-green vs canary) зависит от
размера команды, бюджета инфраструктуры, SLA.
СреднеRuby on Rails / Инфраструктура Rails
ИИ в разработке: что делегировать, что проверять
Вы используете ИИ для написания кода. Что ИИ делает хорошо, а где он ошибается? Как правильно работать с ИИ-ассистентом?
ИИ отлично генерирует шаблонный код. Но вы — последняя линия защиты.
ИИ делает хорошо:
— CRUD-контроллеры, модели, миграции
— RSpec тесты на happy path
— N+1: добавить .includes, переписать на .joins
— Документация, комментарии
— Regex, парсинг, форматирование
— Объяснение чужого кода
ИИ делает ПЛОХО:
— Архитектурные решения (не знает размер вашей команды и roadmap)
— Race conditions (пишет как будто всё sequential)
— Миграции на production (не знает размер таблицы и RPS)
— Безопасность (может сгенерировать код с уязвимостями)
— Бизнес-контекст (не знает "почему так, а не иначе")
— Связи между модулями (не видит полную картину проекта)
Правильный workflow:
1. Вы описываете задачу максимально точно
2. ИИ генерирует код
3. Вы ЧИТАЕТЕ каждую строку (code review)
4. Проверяете: security, edge cases, performance
5. Запускаете тесты
6. Если ошибка — ИИ фиксит, вы снова проверяете
Красные флаги в коде от ИИ:
— SQL без параметров: "WHERE email = '#{params[:email]}'" → injection!
— Глобальное состояние: $cache, @@all_users → memory leak
— .all.each вместо .find_each → OOM на больших таблицах
— Без .limit на запросах → загрузка всей таблицы
— Нет обработки nil → NoMethodError
— Сложный код без тестов → наверняка сломается
Навыки, которые стали важнее:
1. Code review — теперь 50% вашей работы
2. Формулирование задач — точный промпт = точный код
3. Архитектурное мышление — ИИ не решит за вас
4. Debugging — ИИ не имеет доступа к вашему продакшену
5. Security — вы ловите уязвимости до того как они попадут в код
СреднеRuby on Rails / Инфраструктура Rails
API Versioning: не сломать мобильное приложение
Вы изменили ответ API. Мобильное приложение перестало работать. Как правильно версионировать API?
Проблема: мобильное приложение уже у пользователей на телефонах.
Вы не можете заставить всех обновиться одновременно.
Изменили API → старые версии приложения сломались.
Стратегии versioning:
1. URL-based (самый распространённый):
/api/v1/users
/api/v2/users
# config/routes.rb
namespace :api do
namespace :v1 do
resources :users
end
namespace :v2 do
resources :users
end
end
2. Header-based:
GET /api/users
Accept: application/vnd.myapp.v2+json
3. Parameter-based (не рекомендуется):
GET /api/users?version=2
Правила безопасных изменений:
BACKWARD COMPATIBLE (можно без новой версии):
— Добавить новое поле в ответ: { name: "...", email: "..." } → + avatar_url
— Добавить новый optional параметр
— Добавить новый endpoint
BREAKING CHANGE (нужна новая версия):
— Переименовать поле: name → full_name
— Удалить поле из ответа
— Изменить тип: integer → string
— Изменить структуру: { user: {} } → { data: { user: {} } }
Workflow при breaking change:
1. Создать /api/v2/ с новой структурой
2. /api/v1/ продолжает работать
3. Мобильная команда обновляет приложение на v2
4. Когда все пользователи обновились → удалить v1
5. Обычно v1 живёт 3-6 месяцев
Deprecation headers:
response.headers["X-API-Deprecated"] = "v1 will be removed on 2025-06-01"
response.headers["Link"] = "</api/v2/users>; rel=\"successor-version\""
Как ИИ помогает:
ИИ сгенерирует namespace routing, serializers.
Но что является breaking change, а что нет — решаете вы.
ИИ может добавить поле и не сказать, что старые клиенты сломаются.
СреднеRuby on Rails / Инфраструктура Rails
Background Jobs: что делать в фоне
Что отправлять в Sidekiq, а что выполнять синхронно? Как определить —foreground или background?
Правило: если пользователю нужен РЕЗУЛЬТАТ прямо сейчас — foreground.
Если результат нужен ПОЗЖЕ — background.
Foreground (в запросе):
— Чтение данных (show, index)
— Создание записи, пользователь видит результат
— Авторизация, пользователь ждёт ответа
— Простые вычисления (< 100ms)
Background (Sidekiq):
— Отправка email (пользователь не ждёт)
— Обработка изображений (resize, оптимизация)
— Генерация PDF/CSV отчётов
— Интеграции: отправка в CRM, уведомления в Slack
— Массовые операции: импорт 10000 записей из CSV
— Периодические задачи: ежедневная агрегация, очистка
Серая зона (зависит от контекста):
— Обновление поискового индекса (можно в фоне)
— Обновление статистики/счётчиков (можно в фоне)
— Валидация данных (обычно foreground)
Паттерны:
1. Fire and forget:
SendWelcomeEmailJob.perform_later(user.id)
# Отправили и забыли, результат не нужен
2. Polling (клиент спрашивает статус):
# Создаём задачу
report = Report.create!(status: :pending)
GenerateReportJob.perform_later(report.id)
# Клиент периодически спрашивает:
GET /api/reports/1 → { status: "pending" }
GET /api/reports/1 → { status: "completed", url: "..." }
3. WebSocket/ActionCable (push-уведомление):
GenerateReportJob.perform_later(report.id)
# Когда готово — Job отправляет через ActionCable:
ActionCable.server.broadcast("user_#{user.id}", { event: "report_ready" })
4. Webhook (уведомить внешний сервис):
ProcessPaymentJob.perform_later(order.id)
# Когда готово — отправляем webhook на URL клиента
Ошибки при работе с фонами:
— Передать object вместо id:
SendEmailJob.perform_later(user) # user сериализуется, может быть устаревшим
SendEmailJob.perform_later(user.id) # правильно: загрузим свежего из БД
— Долгая джоба без прогресса:
# Плохо: импорт 100K записей в одной джобе
ImportUsersJob.perform_later(csv_path)
# Лучше: разбить на батчи
csv.each_slice(1000) { |batch| ImportBatchJob.perform_later(batch) }
Как ИИ помогает:
ИИ определит, что email → background, чтение → foreground.
Но сложные бизнес-решения ("обновлять ли статистику в фоне?")
требуют понимания product requirements.
СреднеRuby on Rails / Legacy-проекты
God Object: модель на 2000 строк
Вы пришли на проект и увидели: класс User — 2000 строк, 50 методов, 30 has_many, 20 колбэков. Что такое God Object и как его разобрать?
God Object (божественный объект) — класс, который делает ВСЁ.
Нарушает принцип единственной ответственности (SRP).
Изменил одно поле — сломалась рассылка, статистика и оплата.
Признаки God Object:
— Файл > 300 строк
— > 10 public методов
— > 5 колбэков (after_save, before_create и т.д.)
— Зависит от > 10 других моделей
— Тесты на эту модель занимают 1000 строк
— Любое изменение страшно — "а что ещё сломается?"
Пример типичного God Object:
class User < ApplicationRecord
has_many :orders, :subscriptions, :notifications, :posts, ...
after_save :send_welcome_email, :update_stats, :notify_admin
after_create :create_stripe_customer, :setup_trial, :log_event
before_destroy :cancel_subscriptions, :archive_orders, :send_farewell
def full_name; end
def display_name; end
def admin?; end
def can_access?(resource); end
def calculate_lifetime_value; end
def generate_report; end
def send_weekly_digest; end
def export_to_csv; end
def process_payment(amount); end
# ... ещё 40 методов
end
Как разбирать (стратегия):
1. Вынести в Service Objects — бизнес-процессы:
UserPaymentService.new(user).process(1000)
UserReportService.new(user).generate
2. Вынести в Query Objects — сложные запросы:
ActiveUsersQuery.new(User).call
UsersWithExpiredSubscriptionQuery.new.call
3. Вынести в Form Objects — сложные формы:
RegistrationForm.new(params) # создаёт User + Subscription + sends email
4. Вынести в Concerns — общее поведение:
module Billable ( Stripe integration )
module Notifiable ( email/push логика )
5. Вынести в отдельные модели (STI или новые таблицы):
UserBillingProfile вместо полей billing_* в User
Порядок рефакторинга:
1. Написать тесты на текущее поведение (characterization tests)
2. Вынести один метод/группу за раз
3. Запустить тесты после каждого изменения
4. Не трогать несколько групп одновременно
Как ИИ помогает:
ИИ может вынести метод в Service Object за 10 секунд.
Но ИИ НЕ видит 30 скрытых зависимостей между методами User.
Он удалит колбэк, не зная, что его вызывает Sidekiq job в 3 местах.
Стратегию разборки планируете вы.
СреднеRuby on Rails / Legacy-проекты
Callback Hell: 15 действий при сохранении
after_save запускает send_email, update_stats, notify_admin, sync_crm... Добавили поле — всё сломалось. Что такое callback hell и как из него выбраться?
Callback Hell — когда модельные колбэки создают цепочку
неявных действий, которые невозможно отследить.
Пример:
class Order < ApplicationRecord
after_create :reserve_inventory
after_create :send_confirmation_email
after_create :notify_warehouse
after_create :update_user_stats
after_create :sync_to_crm
after_save :recalculate_totals
after_save :update_search_index
after_commit :push_to_analytics
def reserve_inventory
Inventory.reserve(items) # может упасть → откатит create?
end
end
Проблемы:
1. Неявность: Order.create запускает 8 действий. Разработчик не знает.
2. Порядок: после change статуса срабатывает after_save, но order ещё не saved
3. Тестирование: чтобы протестировать email, нужно создать заказ
и mock 7 других колбэков
4. Производительность: Order.create делает 8 HTTP/SQL запросов
5. Отладка: "Почему отправилось 3 письма?" — ищи в колбэках
Антипаттерны:
— after_save { send_email } → email отправляется при ЛЮБОМ сохранении
— Колбэк дергает внешнее API → Order.create занимает 5 секунд
— Колбэк падает → весь save откатывается (или нет, если after_commit)
Как чинить:
1. Заменить колбэки на явные Service Objects:
# Было:
after_create :send_confirmation_email
# Стало:
class CreateOrderService
def call(params)
order = Order.create!(params)
OrderMailer.confirmation(order).deliver_later # явно
NotifyWarehouseJob.perform_later(order.id) # явно
order
end
end
2. Оставить колбэки ТОЛЬКО для data integrity:
# OK — это про целостность данных:
before_validation :normalize_email
before_create :generate_uuid
after_destroy :nullify_references
# УБРАТЬ — это про бизнес-процессы:
after_save :send_email → Service Object
after_create :sync_crm → Service Object
after_update :recalculate → Service Object
3. Использовать события (event-driven):
ActiveSupport::Notifications.instrument("order.created", order: order)
# Подписчики в отдельных файлах решают что делать
Пошаговый план:
1. Список всех колбэков модели
2. Каждый: "это data integrity или бизнес-логика?"
3. Бизнес-логику → в Service Object
4. Data integrity → оставить
5. Тесты на каждый вынесенный метод
Как ИИ помогает:
ИИ перепишет колбэк в Service Object.
Но ИИ не знает: "а этот колбэк вызывается из 5 мест —
через Order.create, Order.update, из Sidekiq, из rake task..."
Все 5 мест нужно обновить — это ваша ответственность.
СреднеRuby on Rails / Legacy-проекты
Нет тестов: как начать покрывать legacy
Проект 3 года, 50 моделей, 0 тестов. С чего начать? Что такое characterization tests?
Legacy без тестов — это ходьба по минному полю.
Любое изменение может сломать что угодно, и вы не узнаете об этом.
Проблема: нельзя написать тесты сразу на всё.
50 моделей × 10 методов = 500 тестов. Это месяцы работы.
Пока пишете — код меняется. Тесты устаревают.
Стратегия (приоритеты):
1. Characterization Tests — зафиксировать текущее поведение:
Не "как должно работать", а "как работает СЕЙЧАС".
Запускаете код, записываете что вернул → это и есть тест.
it "processes payment" do
result = PaymentService.new(user).process(1000)
expect(result.status).to eq("pending") # не "success"!
# Даже если это баг — сначала фиксируем, потом чиним
end
2. Приоритет покрытия:
— Критичные бизнес-процессы (оплата, регистрация)
— Места, которые сейчас рефакторите
— Места, где чаще всего бывают баги (Sentry подскажет)
— Public API / методы, которые используют другие команды
3. Не тратьте время на:
— Геттеры/сеттеры
— Simple CRUD без логики
— Тесты на фреймворк (validates_presence_of)
Что добавить в проект первым:
1. RSpec + FactoryBot + Shoulda Matchers
2. SimpleCov (показывает % покрытия)
3. Brakeman (security)
4. Rubocop (стиль, не на CI — только warning)
Workflow при работе с legacy:
1. Получили таск
2. Написать тест на текущее поведение (characterization)
3. Убедиться что тест проходит
4. Изменить код
5. Убедиться что тест всё ещё проходит (или намеренно изменился)
6. Отправить PR
Скелет теста для legacy-метода:
describe "#calculate_discount" do
subject { service.calculate_discount }
context "with regular customer" do
let(:customer) { create(:customer, tier: :regular) }
it { is_expected.to eq(0) } # фиксируем текущее поведение
end
end
Как ИИ помогает:
ИИ сгенерирует тест по существующему методу за секунды.
Но ИИ не знает: "а этот метод вызывается с nil в проде каждый вторник"
— edge case'ы из бизнес-контекста добавляете вы.
ИИ может сгенерировать тесты, которые проходят,
но не покрывают реальные баги. Review — обязательно.
СреднеRuby on Rails / Legacy-проекты
Rails Upgrade: миграция с 5 на 7
Проект на Rails 5.2. Нужно обновить до Rails 7. С чего начать? Какие подводные камни?
Rails upgrade — всегда пошаговый. Никогда не перепрыгивать версии.
Правило: обновлять на одну MINOR версию за раз.
5.2 → 6.0 → 6.1 → 7.0 → 7.1
На каждом шаге:
1. Обновить Gemfile: gem 'rails', '~> 6.0.0'
2. Запустить bundle update rails
3. Запустить rails app:update (применить изменения конфигов)
4. Запустить тесты
5. Починить всё что сломалось
6. Закоммитить
7. Следующая версия
Типичные проблемы при upgrade:
1. Deprecation warnings → ошибки:
Rails 5: belongs_to необязателен
Rails 6.1: belongs_to обязателен по умолчанию (optional: false)
Решение: добавить optional: true или foreign key
2. Изменившийся API:
Rails 5: update_attribute (без валидации)
Rails 6: update_attribute deprecated → update_column
Или: смена поведения ActiveStorage, ActionMailbox
3. Удалённые методы:
Rails 5: find_or_create_by!
Rails 6: работает, но create_or_find_by! — новый метод
Rails 7: некоторые методы удалены
4. Зависимости (gems):
Gem может не поддерживать новую версию Rails.
Проверить: rubygems.org → versions → runtime dependencies
Решение: обновить gem, найти замену, или форкнуть
5. Config файлы:
rails app:update покажет diff для каждого конфига
Нужно решить: принять новый, оставить старый, или объединить
6. Asset Pipeline:
Rails 5: Sprockets
Rails 7: importmap / esbuild / vite
Это отдельный проект на неделю+
7. Autoloading:
Rails 5: classic (autoload_paths)
Rails 7: zeitwerk (строгие правила命名)
Zeitwerk не прощает: CONST = vs const_set, вложенные модули
Чеклист перед upgrade:
— Все тесты проходят на текущей версии
— CI зелёный
— Есть бэкап БД
— Деплой на staging first
— Deprecation warnings изучены (они подсказка что сломается)
Как ИИ помогает:
ИИ знает ВСЕ deprecations и изменения API между версиями.
"Что изменилось в Rails 6.1?" — ИИ ответит мгновенно.
Но ИИ не знает: "а этот gem используется в 3 местах
и у него нет поддержки Rails 7".
Проверка совместимости всех gems — ваша задача.
СреднеRuby on Rails / Legacy-проекты
Gem Dependency Hell: заброшенные гемы
Проект зависит от gem 'rails_admin', который не обновлялся 4 года. Обновляешь Rails — gem ломается. Что делать?
Dependency Hell (ад зависимостей) — когда gems конфликтуют
друг с другом или с версией Rails.
Типичная ситуация:
— rails_admin последний комит 2 года назад
— Он зависит от rails < 7.0
— Вы обновляете Rails до 7 → rails_admin ломается
— bundle update не помогает — нет новой версии гема
Варианты решения (от простого к сложному):
1. Обновить gem:
Проверить: есть ли новая версия?
github.com/rails_admin/rails_admin → Releases
Может быть beta/rc с поддержкой Rails 7
2. Форкнуть и починить:
fork → исправить совместимость → указать в Gemfile:
gem 'rails_admin', github: 'your-org/rails_admin', branch: 'rails-7'
3. Найти замену:
rails_admin → administrate, activeadmin, avid
Но замена = переписать все кастомные страницы
4. Изолировать:
Вынести admin-панель в отдельное приложение (mountable engine)
Оно живёт на старом Rails, основное приложение обновляется
5. Переписать:
Если функционала немного — проще написать свой
Как избежать в будущем:
— Перед добавлением gem: проверить last commit, issues, PR count
— Популярные гемы: > 1000 stars, активные maintainers
— Minimal dependencies: gem с 3 зависимостями лучше чем с 30
— Периодически: bundle audit (security vulnerabilities)
— Dependabot / Renovate — автоматические PR с обновлениями
Команды для диагностики:
bundle outdated # какие гемы устарели
bundle audit # security уязвимости
bundle viz # дерево зависимостей
gem install gemnasium-parser # анализ совместимости
Как ИИ помогает:
ИИ найдёт альтернативу, форкнёт и починит совместимость.
Но решение "форкнуть vs заменить vs переписать" зависит от
бюджета, сроков и объёма кастомизации. Это бизнес-решение.
СреднеRuby on Rails / Legacy-проекты
Монолит: как выжить с 500K строк
Проект — один монолит на 500 000 строк. 30 разработчиков наступают друг другу на ноги. Деплой = деплой всего. Что делать?
Монолит — норма. Большинство Rails-проектов — монолиты.
Проблема не в монолите, а в неорганизованном монолите.
Что болит при большом монолите:
— Deploy: изменил 1 строку → деплой всего приложения (10 минут)
— Конфликты: 30 разработчиков меняют одни файлы
— Тесты: полный прогон 40 минут → никто не запускает
— Случайные поломки: изменение в billing сломало рассылку
— Onboarding: новичок разбирается месяц
Стратегии (не microservices!):
1. Модульный монолит (Module Boundaries):
Организовать код по доменам, а не по типу:
# Вместо:
app/models/user.rb, order.rb, payment.rb
app/services/...
# Структура по доменам:
app/
billing/
models/, services/, controllers/
notifications/
models/, services/, controllers/
catalog/
models/, services/, controllers/
Каждая команда владеет своим доменом.
2. Engines (Rails Engines):
Вынести домен в mountable engine:
# engines/billing/lib/billing/engine.rb
module Billing
class Engine < ::Rails::Engine
isolate_namespace Billing
end
end
Engine = отдельный namespace, свои модели, роуты, views.
Но в рамках одного приложения (одна БД, один деплой).
3. Strangler Fig Pattern:
Новую функциональность — в отдельный сервис.
Старую — постепенно вырезать.
# роутер направляет:
/api/v2/orders → новый микросервис
/api/v1/orders → старый монолит
Постепенно strangler "обвивает" старую систему.
4. Ускорение тестов:
— Запускать только изменённые: TEST_CHANGED_ONLY=1
— Параллельный запуск: parallel_tests
— TestProf: let_it_be (shared context), before_all
Когда реально нужны микросервисы:
— 5+ независимых команд
— Разные требования к масштабированию
(billing = мало запросов, catalog = много)
— Разные технологии (одна часть на Ruby, другая на Go)
— Чёткие границы между доменами
Как ИИ помогает:
ИИ разобьёт God Object на модули, создаст Engine, напишет тесты.
Но решение "какие границы между доменами" — архитектурное.
ИИ не знает, что billing и notifications — одна команда,
а catalog — другая. Организацию команд вы определяете.
СреднеRuby on Rails / Новые проекты
rails new: как начать проект правильно
Нужно создать новый Rails-проект с нуля. Какие флаги использовать? Что настроить до первой строчки бизнес-кода?
rails new myapp --skip-jbuilder --skip-action-mailbox --skip-action-text
Что включить обязательно:
— devise (аутентификация)
— pundit (авторизация)
— sidekiq (фоновые задачи)
— rspec-rails + factory_bot_rails (тесты)
— rubocop (стиль кода)
— brakeman (безопасность)
Что настроить сразу:
— .ruby-version — зафиксировать версию Ruby
— Gemfile — группы :development, :test, :production
— .env.example — шаблон переменных окружения
— .editorconfig — одинаковые отступы у всей команды
— .gitignore — не коммитить .env, логи, tmp
— docker-compose.yml — для локальной разработки
(PostgreSQL, Redis, Sidekiq — один docker-compose up)
Что НЕ делать:
— Не добавлять гемы "на всякий случай"
— Не начинать с дизайн-системы — начни с минимального UI
— Не копировать структуру старого проекта один-в-один
Как ИИ помогает:
ИИ сгенерирует Gemfile с нужными гемами под задачу.
ИИ напишет docker-compose.yml за 30 секунд.
ИИ настроит CI/CD pipeline по описанию.
Но ИИ НЕ знает: какие гемы уже использует команда,
какие у вас внутренние соглашения по структуре.
Архитектурные решения — за людьми.
СреднеRuby on Rails / Новые проекты
Монолит vs Микросервисы: когда выбирать
Начинаешь новый проект. Нужно ли сразу дробить на микросервисы? Или лучше монолит? Когда микросервисы оправданы?
Монолит (Majestic Monolith):
— Один код, одна база данных, один деплой
— Проще разрабатывать, тестировать, деплоить
— DHH (создатель Rails) рекомендует монолит для 95% проектов
— Проблемы начинаются при 20+ разработчиках или
когда разные части нужно деплоить независимо
Микросервисы:
— Каждый сервис — отдельное приложение
— Своя база данных, свой деплой
— Плюсы: независимый деплой, разные технологии,
команда владеет своим сервисом
— Минусы: сложность сети, distributed transactions,
debugging между сервисами — ад, нужен DevOps
Когда микросервисы ОПРАВДАНЫ:
— 10+ разработчиков с чёткими командами
— Разные требования к нагрузке (одна часть — x100 другой)
— Разные SLA (payment — 99.99%, блог — 99%)
— Нужно использовать разные языки (ML на Python, API на Go)
Стратегия для нового проекта:
1. Начни с монолита
2. Выдели границы (bounded contexts) в коде
3. Когда мониторинг покажет узкие места —
выдели в сервис ТОЛЬКО эту часть
Как ИИ помогает:
ИИ нарисует архитектурную схему по описанию.
ИИ предложит границы сервисов на основе домена.
Но ИИ НЕ знает: сколько у вас разработчиков,
какой бюджет на инфраструктуру, как будет расти проект.
Решение "монолит или сервисы" — всегда за командой.
СреднеRuby on Rails / Новые проекты
12-Factor App: принципы конфигурации
Что такое 12-Factor App? Какие принципы важны для Rails-разработчика? Зачем знать это на новом проекте?
12-Factor App — манифест от создателей Heroku (2011).
12 правил для приложений, которые работают в облаке.
Самые важные для Rails:
1. Codebase — один код в Git, много деплоев
(dev, staging, production — один репозиторий)
3. Config — конфигурация в переменных окружения
НЕ в коде! DATABASE_URL, SECRET_KEY_BASE, API_KEY
В Rails: ENV['KEY'], credentials.yml.enc, .env
4. Backing services — БД, Redis, S3 — как ресурсы
Меняешь PostgreSQL → Amazon RDS? Меняешь только URL.
В Rails: config/database.yml берёт DATABASE_URL
5. Build, Release, Run — разделить стадии
Build: bundle install, assets:precompile
Release: container image с версией
Run: запустить процесс (puma, sidekiq)
6. Processes — приложение НЕ хранит состояние
Сессия — в Redis/БД, не в памяти процесса
Файлы — в S3, не на диске сервера
Иначе при перезапуске всё потеряется
7. Port binding — приложение self-contained
Rails сам поднимает HTTP-сервер (Puma)
Не нужен Apache/Nginx перед ним (в development)
10. Dev/Prod parity — минимум различий
Одинаковые версии Ruby, PostgreSQL, Redis
Docker помогает: одинаковый контейнер везде
Как ИИ помогает:
ИИ сгенерирует docker-compose.yml по 12-factor.
ИИ проверит config на хардкод секретов.
Но ИИ НЕ знает: какие переменные нужны именно вам,
как устроен ваш деплой-процесс.
СреднеRuby on Rails / Новые проекты
CI/CD Pipeline: GitHub Actions для Rails
Нужно настроить автоматическую проверку и деплой для нового Rails-проекта. Что должно быть в pipeline?
CI (Continuous Integration) — автоматическая проверка
при каждом push/PR:
— bundle install — установить зависимости
— rubocop — проверить стиль кода
— brakeman — проверить безопасность
— rspec — запустить тесты
— Если хоть один шаг упал — PR не мержить
CD (Continuous Deployment) — автоматический деплой:
— После мержа в main → деплой на staging
— После тега (v1.2.0) → деплой на production
— Rollback: вернуться к предыдущей версии
Минимальный .github/workflows/ci.yml:
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
services:
postgres: ...
redis: ...
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
- run: bundle install
- run: bundle exec rubocop
- run: bundle exec rspec
Что добавляют на grown-up проектах:
— Parallel jobs (rspecsplit на 4 параллельных потока)
— Code coverage (SimpleCov + Coveralls)
— Deploy на Heroku/AWS/VPS после мержа
— Slack-уведомления о падениях
Как ИИ помогает:
ИИ сгенерирует CI/CD конфиг с нуля за минуту.
ИИ исправит упавший pipeline по логу ошибки.
Но ИИ НЕ знает: какие у вас секреты в GitHub Secrets,
как устроен ваш деплой на конкретный сервер.
СреднеRuby on Rails / Новые проекты
Puma: настройка workers и threads
Rails-приложение запущено на Puma. Что означают workers, threads, preload_app? Как настроить под свой сервер?
Puma — веб-сервер для Rails (по умолчанию с Rails 5+).
Workers (процессы):
— Каждый worker — отдельный процесс Ruby
— Изолированная память (нет shared state)
— Больше workers = больше памяти
— Стабильность: если один worker упал, остальные живут
— workers_count = CPU cores (обычно)
Threads (потоки внутри worker):
— Делят память внутри процесса
— Rails thread-safe благодаря GVL (GIL)
— Количество threads = сколько запросов обрабатывает
один worker одновременно
— threads 5, 5 — минимум и максимум одинаковые
— threads_count = 5 (для I/O-тяжёлых можно больше)
preload_app:
— Загрузить приложение ОДИН раз перед fork workers
— Экономит память (copy-on-write)
— НО: нужно reconnect к БД после fork!
config/puma.rb:
workers Integer(ENV.fetch("WEB_CONCURRENCY") { 2 })
threads_count = Integer(ENV.fetch("RAILS_MAX_THREADS") { 5 })
threads threads_count, threads_count
preload_app!
plugin :tmp_restart
Формула: сервер на 1 ГБ RAM ≈ 2-3 workers × 5 threads
Каждый worker ≈ 150-300 МБ (зависит от приложения)
Как ИИ помогает:
ИИ сгенерирует config/puma.rb под характеристики сервера.
ИИ объяснит, почему 50 threads — это плохо.
Но ИИ НЕ знает: реальное потребление памяти вашего приложения.
Нужно измерить (monitoring) — потом настроить.
СреднеRuby on Rails / Новые проекты
Error Handling API: как возвращать ошибки
API возвращает ошибки. Какой формат правильный? HTTP-статусы + JSON body? Что возвращать при разных ошибках?
Правильный формат ошибки в API:
— HTTP статус — МАШИНА понимает что случилось
— JSON body — ЧЕЛОВЕК (фронтендер) понимает детали
HTTP статусы (самые частые):
400 Bad Request — клиент послал ерунду
401 Unauthorized — не авторизован (нет токена)
403 Forbidden — авторизован, но нет прав
404 Not Found — ресурс не найден
422 Unprocessable Entity — валидация не прошла
500 Internal Server Error — наша вина (баг)
Формат JSON body (JSON:API):
{
"errors": [
{
"status": "422",
"title": "Validation Error",
"detail": "Email не может быть пустым",
"source": { "pointer": "/data/attributes/email" }
}
]
}
Или проще (многие так делают):
{
"error": "Email не может быть пустым",
"code": "validation_error"
}
В Rails:
rescue_from ActiveRecord::RecordNotFound do |e|
render json: { error: "Not found" }, status: :not_found
end
rescue_from ActiveRecord::RecordInvalid do |e|
render json: { errors: e.record.errors.full_messages },
status: :unprocessable_entity
end
Что НЕ делать:
— Не возвращать 200 с {"error": "..."} — это ломает контракт
— Не возвращать stack trace в production (безопасность!)
— Не использовать один статус для всех ошибок
Как ИИ помогает:
ИИ сгенерирует error handler для всего API.
ИИ подскажет правильный HTTP статус по ситуации.
Но ИИ НЕ знает: какие ошибки ожидает ваш фронтенд,
какой формат уже используется в существующих эндпоинтах.
СреднеRuby on Rails / Новые проекты
Поиск: PostgreSQL full-text → Elasticsearch
Нужен поиск по приложениям. Когда достаточно PostgreSQL, а когда нужен Elasticsearch/Meilisearch? Как развивать поиск?
Уровень 1 — ILIKE (самый простой):
Task.where("title ILIKE ?", "%#{params[:q]}%")
— Работает для 100-1000 записей
— Не понимает морфологию ( "задач" ≠ "задача")
— Медленный на больших таблицах
Уровень 2 — PostgreSQL Full-Text Search:
Task.where("to_tsvector('russian', title) @@ to_tsquery('russian', ?)",
params[:q])
— Понимает морфологию: "задача" найдёт "задачи"
— Ранжирование по релевантности (ts_rank)
— Можно добавить индекс GIN для скорости
— Достаточно для 90% Rails-проектов
Уровень 3 — Elasticsearch / Meilisearch / Typesense:
— Когда нужен поиск по НЕСКОЛЬКИМ моделям одновременно
— Когда нужен autocomplete (подсказки при вводе)
— Когда нужен faceted search (фильтры + подсчёт)
— Когда данных миллионы
Meilisearch — проще для нового проекта:
— Устанавливается как Docker-контейнер
— Индексация через гем meilisearch-rails
— Из коробки: typos, фильтры, сортировка
Стратегия для нового проекта:
1. Начни с PostgreSQL full-text
2. Добавь индекс GIN
3. Когда PostgreSQL перестанет справляться —
добавь Meilisearch
Как ИИ помогает:
ИИ напишет scope для полнотекстового поиска.
ИИ настроит интеграцию с Meilisearch за минуты.
Но ИИ НЕ знает: какие поля важны для поиска,
какие фильтры нужны пользователям.
СложноRuby on Rails / Новые проекты
Multitenancy: SaaS с несколькими клиентами
Нужно сделать SaaS-приложение, где у каждого клиента свои данные. Как изолировать данные? Какой подход выбрать?
Multitenancy — одна программа, много клиентов (tenants).
Примеры: Shopify, Notion, Basecamp.
Подход 1 — Shared Database, Shared Schema:
— Все данные в одной БД, таблица tenants
— Каждая запись: tenant_id
— Все запросы: WHERE tenant_id = current_tenant
— Плюсы: просто начать, одна БД
— Минусы: легко забыть WHERE и показать чужие данные
— Гем: acts_as_tenant
Подход 2 — Shared Database, Separate Schema:
— Одна БД PostgreSQL, но у каждого tenant свой schema
— tenant_a.users, tenant_b.users
— Плюсы: изоляция данных на уровне БД
— Минусы: миграции нужно прогонять по всем schemas
— Гем: apartment
Подход 3 — Separate Database:
— У каждого tenant своя база данных
— Плюсы: полная изоляция
— Минусы: дорого, сложно управлять миграциями
Что выбрать для нового проекта:
— Начни с подхода 1 (tenant_id + acts_as_tenant)
— Добавь Row-Level Security в PostgreSQL:
ALTER TABLE tasks ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON tasks
USING (tenant_id = current_setting('app.tenant_id')::uuid);
— Это НЕ пропустит запрос без tenant_id даже если забыл WHERE
Как ИИ помогает:
ИИ сгенерирует мультитенантную модель с scope.
ИИ настроит acts_as_tenant за минуты.
Но ИИ НЕ знает: какие данные общие между tenants,
как будет выглядеть биллинг, нужна ли кастомизация.
СреднеRuby on Rails / Новые проекты
Testing Strategy: что тестировать в новом проекте
Начинаешь новый проект. Что тестировать? Что НЕ тестировать? Как не потратить половину времени на тесты?
Пирамида тестов для нового Rails-проекта:
Снизу вверх:
1. Unit-тесты (модели) — МНОГО, быстрые:
— Валидации (presence, uniqueness, format)
— Методы модели (calculate_total, full_name)
— Scopes (active, recent, by_user)
— Это основа, без неё никак
2. Integration-тесты (сервисы, взаимодействия) — средне:
— Service Objects (CreateOrder, ImportData)
— Background Jobs (perform)
— Mailers (что письмо отправляется)
— Взаимодействие модель + событие
3. System-тесты (пользовательские сценарии) — мало:
— Критические пути: регистрация, оплата, создание
— НЕ тестируй каждый click и hover
— 5-10 system-тестов на весь проект
Что НЕ тестировать:
— Rails-фреймворк (belongs_to работает — тестирует Rails)
— Гемы (Devise логинит — тестирует Devise)
— Simplecov (покрытие) — не цель, а инструмент
— Очевидные вещи (getter/setter)
Правило для нового проекта:
— Начни с тестов моделей (самые полезные)
— Добавь 3-5 system-тестов на критические пути
— Остальное — по мере роста
Как ИИ помогает:
ИИ сгенерирует тест из контроллера или модели.
ИИ напишет FactoryBot-фабрику по схеме.
Но ИИ НЕ знает: какие сценарии критичны для бизнеса,
какие edge cases были в продакшене.
СреднеRuby on Rails / Новые проекты
Hotwire vs SPA: когда какой фронтенд
Начинаешь новый Rails-проект. Какой фронтенд выбрать: Hotwire (Turbo + Stimulus) или SPA (React/Vue)? Когда какой подход лучше?
Hotwire (Turbo + Stimulus):
— Сервер рендерит HTML, JS только обновляет части
— Turbo Drive — навигация без перезагрузки страницы
— Turbo Frames — обновление частей страницы
— Turbo Streams — real-time обновления через WebSocket
— Stimulus — лёгкий JS для интерактивности
— Плюсы: один язык (Ruby), меньше кода, SEO из коробки
— Минусы: сложная интерактивность (drag-and-drop,
complex forms) — мучительно
SPA (React / Vue / Svelte):
— Бэкенд — JSON API, фронтенд — отдельное приложение
— Плюсы: полная свобода UI, rich interactions,
огромная экосистема компонентов
— Минусы: два стека, два деплоя, больше сложности,
SEO нужен SSR (Next.js / Nuxt)
Когда Hotwire:
— CRUD-приложения (админки, CRM, блоги)
— Команда знает Ruby, но не JS
— Нужен быстрый MVP
— SEO важен (контентные сайты)
— 80% Rails-проектов
Когда SPA:
— Real-time dashboard с графиками
— Сложные формы (конструкторы, редакторы)
— Мобильное приложение (React Native — тот же React)
— Фронтенд-команда отдельно от бэкенда
Стратегия для нового проекта:
1. Начни с Hotwire
2. Когда Hotwire начинает мешать —
выдели ЭТУ часть в SPA
3. Не нужно "всё или ничего" — можно миксовать
Как ИИ помогает:
ИИ пишет Turbo/Stimulus код так же легко как React.
ИИ переведёт ERB-шаблон в React-компонент.
Но ИИ НЕ знает: какие навыки у вашей команды,
какая интерактивность нужна пользователям.
СреднеRuby on Rails / Новые проекты
Feature Flags: Flipper для безопасных релизов
Нужно выкатить новую фичу, но боишься, что всё сломается. Как выкатывать безопасно? Что такое Feature Flags?
Feature Flag (фича-флаг) — переключатель функции в коде:
— Включён → пользователь видит новую фичу
— Выключен → пользователь видит старый вариант
— БЕЗ нового деплоя
Гем Flipper:
# Включить фичу для всех
Flipper.enable(:new_dashboard)
# Включить для конкретного пользователя
Flipper.enable_actor(:new_dashboard, user)
# Включить для 10% пользователей
Flipper.enable_percentage(:new_dashboard, 10)
# В коде:
if Flipper.enabled?(:new_dashboard, current_user)
render "new_dashboard"
else
render "old_dashboard"
end
Сценарии использования:
1. Canary release — выкатили на 1% пользователей,
смотрим метрики. Если ок — увеличиваем до 100%
2. A/B тестирование — 50% видят вариант А,
50% — вариант Б. Смотрим конверсию
3. Kill switch — если фича сломалась,
выключаем одной кнопкой БЕЗ отката
4. Preview для QA — включили фичу только для
тестировщиков, они проверяют на production
Адаптеры хранения (Flipper):
— Flipper-ActiveRecord — в БД (для начала)
— Flipper-Redis — в Redis (быстрее)
— Flipper UI — веб-интерфейс для управления флагами
Правило: удаляй флаги после раскатки!
Флаги — временный инструмент, не архитектура.
Как ИИ помогает:
ИИ добавит Flipper в проект за минуты.
ИИ обернёт код в if Flipper.enabled? по описанию.
Но ИИ НЕ знает: какие метрики смотреть при canary,
каков порог "достаточно хороший" результат.
СреднеRuby on Rails / Новые проекты
Docker Compose: среда разработки с нуля
Новый проект, нужно настроить среду разработки через Docker. Что должно быть в docker-compose.yml? Как не усложнить?
Docker Compose — один файл, одна команда,
вся среда разработки готова.
Минимальный docker-compose.yml для Rails:
services:
db:
image: postgres:16
environment:
POSTGRES_PASSWORD: password
volumes:
- pgdata:/var/lib/postgresql/data
ports:
- "5432:5432"
redis:
image: redis:7
ports:
- "6379:6379"
sidekiq:
build: .
command: bundle exec sidekiq
depends_on: [db, redis]
env_file: .env
volumes:
pgdata:
Что нужно для нового проекта:
— PostgreSQL — основная БД
— Redis — кэш, сессии, Sidekiq
— Sidekiq — фоновые задачи
— (опционально) Meilisearch — если нужен поиск
— (опционально) Mailpit/MailCatcher — тестовая почта
Что НЕ контейнеризировать в development:
— Само Rails-приложение! Запускай локально:
bin/rails s — быстрее, проще дебажить
— Только внешние сервисы (db, redis) в Docker
.env для development:
DATABASE_URL=postgres://postgres:password@localhost:5432/myapp_dev
REDIS_URL=redis://localhost:6379/0
SECRET_KEY_BASE=dev_only_secret
После git clone новый разработчик делает:
cp .env.example .env
docker compose up -d
bin/setup
bin/rails s
Как ИИ помогает:
ИИ сгенерирует docker-compose.yml под стек.
ИИ добавит любой сервис (Elasticsearch, MinIO).
Но ИИ НЕ знает: какие порты заняты на вашей машине,
какие сервисы нужны именно этому проекту.
ЛегкоСети / Интернет и браузер
Веб-приложение и клиент-серверная архитектура
Что такое веб-приложение? Что такое клиент-серверная архитектура?
Веб-приложение — программа, доступная через браузер по сети (HTTP).
Работает по клиент-серверной архитектуре:
Клиент (браузер) — отправляет запросы, отображает UI.
Сервер — обрабатывает запросы, работает с БД, возвращает ответ.
Клиент и сервер общаются через протокол HTTP.
Клиент не знает, как устроен сервер. Сервер не знает, кто клиент.
В Rails: браузер → роутер → контроллер → модель → БД → view → HTML → браузер.
СреднеСети / Интернет и браузер
Что происходит при вводе URL в браузере
Когда пользователь вводит запрос в адресной строке браузера или кликает на ссылку, что происходит? Расскажи про этапы рендера.
1. DNS-запрос — браузер резолвит домен в IP-адрес
2. TCP-соединение — handshake (SYN → SYN-ACK → ACK)
3. TLS-рукопожатие — если HTTPS
4. HTTP-запрос — браузер отправляет GET-запрос
5. Сервер обрабатывает запрос и возвращает HTTP-ответ (HTML)
6. Браузер начинает рендер:
- Парсинг HTML → DOM-дерево
- Парсинг CSS → CSSOM
- Композиция DOM + CSSOM → Render Tree
- Layout (вычисление размеров и позиций)
- Paint (отрисовка пикселей)
- Composite (сборка слоёв)
7. Загрузка и выполнение JS (блокирует рендер, если нет async/defer)
ЛегкоСети / HTTP и протоколы
Как работает HTTP
Как работает HTTP? Из чего состоит HTTP-запрос?
HTTP — текстовый протокол прикладного уровня (клиент-серверный).
HTTP-запрос состоит из трёх частей:
1. Стартовая строка: METHOD /path HTTP/1.1
2. Заголовки (headers): Host, Content-Type, Authorization и т.д.
3. Тело (body): опционально, данные для POST/PUT/PATCH
Пример:
GET /users HTTP/1.1
Host: example.com
Accept: application/json
HTTP-ответ:
1. Стартовая строка: HTTP/1.1 200 OK
2. Заголовки
3. Тело (HTML, JSON, файл и т.д.)
ЛегкоСети / HTTP и протоколы
Стартовая строка HTTP-запроса
Из чего состоит стартовая строка HTTP-запроса?
Стартовая строка (request line) состоит из трёх элементов:
1. HTTP-метод: GET, POST, PUT, PATCH, DELETE
2. URI: /users/1?page=2
3. Версия протокола: HTTP/1.1
Пример:
GET /users/1 HTTP/1.1
Для ответа (status line):
HTTP/1.1 200 OK
Состоит из: версия, статус-код, причина (reason phrase).
ЛегкоСети / HTTP и протоколы
Структура HTTP-запроса и тело
Что из себя представляет HTTP-запрос? Структура? Есть ли тело у всех HTTP-запросов?
HTTP-запрос — текстовое сообщение от клиента серверу.
Структура:
1. Стартовая строка (method + URI + version)
2. Заголовки (Content-Type, Authorization, Accept ...)
3. Пустая строка (разделитель)
4. Тело (body) — опционально
Тело есть НЕ у всех запросов:
- GET, HEAD, DELETE, OPTIONS — обычно без тела
- POST, PUT, PATCH — с телом (JSON, form-data, файл)
Тело может отсутствовать даже у POST (не рекомендуется, но допустимо).
ЛегкоСети / HTTP и протоколы
HTTP vs HTTPS
В чем отличие протокола HTTP от HTTPS?
HTTPS = HTTP + TLS/SSL (шифрование).
HTTP:
- Данные передаются в открытом виде
- Порт 80
- Уязвим к перехвату (MITM)
HTTPS:
- Данные зашифрованы (TLS)
- Порт 443
- Проверка подлинности сервера через сертификат
- Современный стандарт — TLS 1.3
Без HTTPS нельзя: HSTS, HTTP/2 (практически), trustworthy API.
В Rails: force_ssl в конфиге принудительно перенаправляет на HTTPS.
СреднеСети / HTTP и протоколы
PUT vs PATCH
Чем PUT-запрос отличается от PATCH?
PUT — полная замена ресурса. Нужно передать ВСЕ поля.
PATCH — частичное обновление. Передаём только изменённые поля.
Пример (ресурс user: {name: "Alice", email: "a@mail.ru"}):
PUT /users/1 → заменяет целиком
{ "name": "Bob", "email": "b@mail.ru" }
PATCH /users/1 → обновляет только name
{ "name": "Bob" }
Если в PUT не указать email — он станет null.
В Rails: update (PATCH) vs update! — но оба делают PATCH по REST-конвенции.
СреднеСети / HTTP и протоколы
PUT vs POST и идемпотентность
Чем PUT отличается от POST? Что такое идемпотентность HTTP-методов?
POST — создание нового ресурса. Не идемпотентный.
PUT — замена существующего ресурса. Идемпотентный.
Идемпотентность — повторный запрос даёт тот же результат:
POST /users → создаёт нового пользователя каждый раз (3 запроса = 3 записи)
PUT /users/1 → обновляет одну и ту же запись (3 запроса = 1 запись)
Таблица идемпотентности:
GET — идемпотентный, безопасный
PUT — идемпотентный
PATCH — может быть идемпотентным
DELETE — идемпотентный
POST — НЕ идемпотентный
ЛегкоСети / HTTP и протоколы
GET vs POST
В чем разница между GET и POST?
GET:
- Получение данных (чтение)
- Параметры в URL (query string): /users?name=alice
- Безопасный (не меняет состояние сервера)
- Идемпотентный
- Кэшируется браузером
- Тело игнорируется (по спецификации)
POST:
- Создание/отправка данных
- Параметры в теле запроса
- НЕ безопасный (меняет состояние)
- НЕ идемпотентный (повтор = новая запись)
- Не кэшируется
В формах Rails: method: :get для поиска, method: :post для создания.
СреднеСети / HTTP и протоколы
GET с телом запроса
Может ли GET-запрос иметь тело? Можно ли в теле GET-запроса отправить картинку на сервер?
По RFC 7231: у GET нет запрещённого тела, но оно не имеет семантики.
Сервер МОЖЕТ проигнорировать тело GET-запроса.
Некоторые прокси и кэши отбрасывают тело GET.
Отправить картинку через GET — плохая практика:
- GET предназначен для чтения
- Картинка — изменение состояния сервера → нужен POST
- URL имеет ограничение длины
Правильно:
POST /uploads с файлом в теле (multipart/form-data)
или PUT /images/123 с бинарником в теле
В практике: GET с телом — почти всегда ошибка проектирования.
ЛегкоСети / HTTP и протоколы
GET с телом в теории
Можно ли в GET в теории поместить тело?
Технически — да. HTTP-спецификация не запрещает тело в GET.
Но:
- RFC 7231 говорит: тело запроса GET не имеет определённой семантики
- Сервер МОЖЕТ его проигнорировать
- Прокси, кэши и фреймворки могут отбросить тело
- Elasticsearch использовал GET с телом для поиска — это известное
исключение, но его критиковали
На практике: никогда не полагайтесь на тело в GET.
Используйте query-параметры или POST для сложных запросов.
ЛегкоСети / HTTP и протоколы
HTTP-метод для частичного обновления по REST
Какой HTTP-метод используется для обновления небольшого кусочка по REST?
PATCH — метод для частичного обновления ресурса.
Пример:
PATCH /users/1
{ "email": "new@mail.ru" }
Обновит только email, остальные поля останутся без изменений.
PUT заменил бы ресурс целиком (все поля обязательны).
В Rails routes: resources :users — автоматически добавляет PATCH.
ЛегкоСети / HTTP и протоколы
Приватный ресурс без авторизации
Что будет, если попытаться обратиться к приватному ресурсу без авторизации?
Сервер вернёт ошибку:
401 Unauthorized — если пользователь не прошёл аутентификацию
(нет токена, неверный логин/пароль)
403 Forbidden — если пользователь аутентифицирован,
но нет прав на этот ресурс
Разница:
401 — «Кто ты? Представься»
403 — «Я знаю кто ты, но тебе сюда нельзя»
В Rails:
before_action :authenticate_user! → 401 если не залогинен
before_action :authorize_admin! → 403 если не админ
CanCanCan / Pundit — для авторизации (403)
СложноСети / HTTP и протоколы
REST API для каталога автомобилей
Как бы вы спроектировали REST API для каталога автомобилей (ресурсы, методы, коды ответов)?
Ресурс: cars (автомобили)
Метод URI Действие Код ответа
─────── ─────────────── ────────────── ──────────
GET /cars Список 200
GET /cars/1 Детали 200
POST /cars Создать 201 (Created)
PATCH /cars/1 Обновить часть 200
PUT /cars/1 Заменить 200
DELETE /cars/1 Удалить 204 (No Content)
Фильтрация через query-параметры:
GET /cars?brand=toyota&year=2024&sort=price
Ошибки:
400 — невалидные параметры
401 — не авторизован
404 — /cars/999 не найден
422 — невалидные данные при создании
В Rails routes:
resources :cars
Генерирует все 6 маршрутов автоматически.
СреднеСети / HTTP и протоколы
Фильтрация: GET-параметры vs POST-тело
Как реализовать фильтрацию: параметры в GET vs тело POST, и когда оправдан POST для непубличных фильтров?
По REST: фильтрация — это чтение → GET с query-параметрами:
GET /products?category=electronics&min_price=100
Когда оправдан POST для фильтрации:
- Слишком сложные фильтры (вложенные, массивы) — тело POST удобнее
- Тело GET может отбрасываться прокси
- Конфиденциальные данные в фильтрах (не попадут в логи URL, историю)
- ElasticSearch-style: POST /products/_search с JSON в теле
Правило: если фильтр bookmarkable/cachable — GET.
Если сложный или приватный — POST.
В Rails:
# GET
Product.where(category: params[:category])
# POST с复杂 фильтрами
Product.search(params[:filters])
ЛегкоСети / Коды ответов и заголовки
Семейства кодов ответов HTTP
Какие семейства кодов ответов HTTP знаешь? Чем код ответа 200 отличается от 201?
Семейства (первая цифра):
1xx — информационные (100 Continue)
2xx — успех (200 OK, 201 Created, 204 No Content)
3xx — перенаправление (301 Moved Permanently, 302 Found, 304 Not Modified)
4xx — ошибка клиента (400 Bad Request, 401, 403, 404)
5xx — ошибка сервера (500 Internal Server Error, 502, 503)
200 vs 201:
200 OK — запрос успешен, тело содержит результат
201 Created — ресурс создан (POST), обычно с Location header
и телом созданного ресурса
В Rails:
render json: @user, status: :ok # 200
render json: @user, status: :created # 201
render json: @user, status: :no_content # 204
ЛегкоСети / Коды ответов и заголовки
Код ответа 400
Что такое код ответа 400? Что означает?
400 Bad Request — сервер не понял запрос из-за ошибки клиента.
Причины:
- Невалидный JSON в теле
- Отсутствуют обязательные параметры
- Неверный формат данных (строка вместо числа)
- Некорректный синтаксис запроса
Пример:
POST /users { "name": "" } → 400 (имя не может быть пустым)
В Rails:
render json: { errors: user.errors }, status: :bad_request
# или через respond_to_with_error
ЛегкоСети / Коды ответов и заголовки
Код ответа 500
Что такое код ответа 500? Что означает?
500 Internal Server Error —通用 серверная ошибка.
Что произошло:
- Необработанное исключение в коде
- Ошибка подключения к БД
- Ошибка в бизнес-логике
Важно: 500 не раскрывает детали ошибки клиенту (безопасность).
Детали логируются на сервере (Rails: log/production.log).
Родственные коды:
502 Bad Gateway — прокси не получил ответ от upstream
503 Service Unavailable — сервер перегружен / на обслуживании
В Rails:
rescue_from StandardError do |e|
Rails.logger.error e.message
render json: { error: "Internal error" }, status: 500
end
ЛегкоСети / Коды ответов и заголовки
Часто используемые HTTP-заголовки
Какие чаще всего используются заголовки? Приведи примеры.
Заголовки запроса (Request):
Host: example.com — целевой хост (обязательный в HTTP/1.1)
Content-Type: application/json — формат тела
Authorization: Bearer token — аутентификация
Accept: application/json — ожидаемый формат ответа
User-Agent: Mozilla/5.0 — информация о клиенте
Cookie: session=abc123 — куки
Заголовки ответа (Response):
Content-Type: text/html — формат тела ответа
Set-Cookie: token=xyz — установить куку
Location: /users/5 — URI нового ресурса (при 201)
Cache-Control: no-cache — управление кэшем
Access-Control-Allow-Origin: * — CORS
В Rails:
response.headers["X-Custom"] = "value"
render json: data, content_type: "application/json"
СреднеСети / Коды ответов и заголовки
Редирект и ссылка в ответе
Если мы сделали запрос, и произошел редирект, как достать ссылку, которая ведет к редиректу из этого запроса?
При редиректе сервер возвращает:
- Статус-код: 301, 302, 303, 307 или 308
- Заголовок Location: https://new-url.com/path
Новый URL берётся из заголовка Location.
Коды редиректа:
301 — постоянный (кэшируется, метод может смениться на GET)
302 — временный (браузер может сменить метод)
307 — временный, метод сохраняется
308 — постоянный, метод сохраняется
В Rails:
redirect_to @user # 302 по умолчанию
redirect_to @user, status: 301
HTTP-клиенты (Ruby):
response = Net::HTTP.get_response(URI(url))
response["Location"] # → URL редиректа
response.code # → "302"
СреднеСети / API и тестирование
SOAP vs REST
В чем разница SOAP от REST API?
SOAP — протокол, REST — архитектурный стиль.
SOAP:
- Строгий стандарт (WSDL-контракт)
- Только XML
- Встроенная обработка ошибок, безопасность (WS-Security)
- Тяжёлый, много boilerplate
- Используется в enterprise, банках, платёжных системах
REST:
- Лёгкий, использует стандарты HTTP (методы, коды, заголовки)
- Любой формат (JSON, XML, HTML)
- Нет строгого контракта (OpenAPI — опционально)
- Stateless
- Де-факто стандарт для веб-API
Сравнение:
SOAP REST
Формат XML только Любой (JSON)
Протокол Свой HTTP
Контракт WSDL OpenAPI (опц.)
Сложность Высокая Низкая
Кэширование Нет Через HTTP
ЛегкоСети / API и тестирование
Что такое REST API
Что такое REST API?
REST (Representational State Transfer) — архитектурный стиль для API.
Принципы:
1. Ресурсы — всё есть ресурс (URI): /users, /posts/1
2. HTTP-методы — CRUD через GET/POST/PUT/PATCH/DELETE
3. Stateless — каждый запрос содержит всю нужную информацию
4. Единый интерфейс — стандартные методы и коды ответов
5. Представление — клиент получает представление ресурса (JSON)
Пример REST API для постов:
GET /posts — список
GET /posts/1 — один пост
POST /posts — создать
PATCH /posts/1 — обновить
DELETE /posts/1 — удалить
REST не протокол, а набор рекомендаций. Строгое следование — редкость.
ЛегкоСети / API и тестирование
SOAP и JSON
Можно ли в SOAP отправить JSON?
Нет. SOAP по спецификации использует только XML.
SOAP-сообщение — это XML-конверт (Envelope):
<soap:Envelope>
<soap:Header>...</soap:Header>
<soap:Body>...</soap:Body>
</soap:Envelope>
JSON не может заменить XML в SOAP — структура конверта требует XML.
Если нужен JSON — используйте REST API или gRPC (с protobuf).
СреднеСети / API и тестирование
Как протестировать API
Как протестировать API веб-приложения? Какие инструменты применимы?
Уровни тестирования API:
1. Ручное тестирование (exploratory):
- curl: curl -X POST http://api.example.com/users -d '{"name":"A"}'
- Postman / Insomnia — GUI-клиенты
- HTTPie — curl с человеческим лицом
2. Интеграционные тесты:
- Rails: ActionDispatch::IntegrationTest
- RSpec: request specs (spec/requests/)
- Проверяют полный цикл: запрос → роут → контроллер → БД → ответ
3. Контрактные тесты:
- OpenAPI / Swagger — валидация по схеме
4. Нагрузочное тестирование:
- Apache JMeter, k6, Locust
Что проверять:
- Статус-коды (200, 201, 400, 404)
- Структуру JSON-ответа
- Валидацию данных
- Аутентификацию/авторизацию
- Граничные случаи
ЛегкоСети / API и тестирование
Инструменты для тестирования API
Какие инструменты для тестирования API можешь назвать?
GUI-клиенты:
Postman — самый популярный, коллекции, автотесты
Insomnia — легче Postman, open-source
Bruno — offline, файлы хранятся рядом с кодом
CLI:
curl — стандарт де-факто, есть везде
HTTPie — curl с удобным синтаксисом: http POST api.com/users name=Alice
В коде:
RSpec + rack-test (request specs)
Minitest + ActionDispatch::IntegrationTest
Нагрузка:
k6 — JS-скрипты, современный
Apache JMeter — Java, мощный, сложный
Locust — Python
Документация + тестирование:
Swagger UI — интерактивная документация по OpenAPI
ЛегкоСети / API и тестирование
Библиотеки для HTTP-запросов
Какие библиотеки использовал для HTTP-запросов?
Ruby:
Net::HTTP — стандартная библиотека (встроена)
HTTParty — простой DSL для API-клиентов
Faraday — гибкий, middleware-based (от команды Omise)
RestClient — простой REST-клиент
Typhoeus — параллельные запросы (обёртка над libcurl)
Пример HTTParty:
class GithubApi
include HTTParty
base_uri "api.github.com"
def user(name)
self.class.get("/users/#{name}")
end
end
В Rails-тестах:
get "/users", headers: { "Authorization" => token }
post "/users", params: { name: "Alice" }, as: :json
ЛегкоСети / API и тестирование
Что такое токен
Что такое токен? Для чего используется?
Токен — строка, подтверждающая аутентификацию пользователя.
Альтернатива сессиям (cookies) для API.
JWT (JSON Web Token) — популярный формат:
header.payload.signature
Как работает:
1. Пользователь логинится (email + пароль)
2. Сервер генерирует JWT, отправляет клиенту
3. Клиент отправляет токен в каждом запросе:
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
4. Сервер проверяет подпись токена (без запроса в БД — stateless)
Зачем:
- Stateless аутентификация (масштабирование)
- Мобильные приложения (cookies неудобны)
- SSO (Single Sign-On)
- Микросервисы (один токен для всех сервисов)
В Rails: gem devise-jwt, gem jwt
ЛегкоСети / API и тестирование
Авторизация
Что отвечает за авторизацию?
Авторизация — проверка прав доступа (что пользователь может делать).
Аутентификация (кто ты?) vs Авторизация (что тебе можно?).
Инструменты в Rails:
Pundit — простой, policy-объекты для каждого ресурса
CanCanCan — ability-класс, проверка через can? :edit, @post
Пример Pundit:
# app/policies/post_policy.rb
class PostPolicy < ApplicationPolicy
def update?
record.user == user || user.admin?
end
end
# Контроллер
authorize @post # → вызовет PostPolicy#update?
HTTP-уровень:
401 Unauthorized — не аутентифицирован
403 Forbidden — не авторизован (нет прав)
ЛегкоСети / Форматы данных
Что такое JSON
Что такое JSON?
JSON (JavaScript Object Notation) — текстовый формат обмена данными.
Типы данных: строка, число, boolean, null, массив, объект.
Пример:
{
"name": "Alice",
"age": 30,
"active": true,
"tags": ["ruby", "rails"],
"address": null
}
Почему популярен:
- Легче XML (нет закрывающих тегов)
- Нативная поддержка в JS (JSON.parse / JSON.stringify)
- Человекочитаемый
- Поддерживается всеми языками
В Ruby:
require "json"
JSON.parse('{"name":"Alice"}') # => {"name"=>"Alice"}
{name: "Alice"}.to_json # => '{"name":"Alice"}'
В Rails: render json: @users — автоматическая сериализация.
ЛегкоСети / Форматы данных
Тело HTTP-запроса и форматы
Что может быть в теле HTTP-запроса? Какие форматы данных могут передаваться?
Тело запроса (body) может содержать любые данные.
Формат указывается в заголовке Content-Type.
Форматы:
application/json — JSON (самый частый для API)
application/x-www-form-urlencoded — данные формы (key=value&key2=value2)
multipart/form-data — файлы + поля формы
text/xml — XML (SOAP)
text/plain — простой текст
application/octet-stream — бинарные данные (файл)
Примеры:
Content-Type: application/json
{"title": "Hello", "body": "World"}
Content-Type: multipart/form-data
[файл image.png + поле name=Alice]
ЛегкоСети / Форматы данных
Тело HTTP-ответа и форматы
Что приходит в теле ответа от сервера? Какие бывают форматы?
Тело ответа зависит от заголовка Content-Type:
Частые форматы:
text/html — HTML-страница (браузер рендерит)
application/json — JSON (для API, SPA)
application/xml — XML (SOAP, RSS)
text/css — стили
application/javascript — JS-скрипт
image/png, image/jpeg — картинка
application/octet-stream — бинарный файл (скачивание)
Пример (JSON API):
HTTP/1.1 200 OK
Content-Type: application/json
{"id": 1, "name": "Alice", "email": "alice@mail.ru"}
Пустое тело:
204 No Content — пустой ответ (после DELETE)
304 Not Modified — пустой ответ (кэш актуален)
В Rails:
respond_to do |format|
format.html
format.json { render json: @users }
end
СреднеСети / Сети и уровни
TCP vs UDP
В чем различие TCP и UDP?
TCP (Transmission Control Protocol):
- Надёжная доставка (гарантия порядка, повтор при потере)
- Установление соединения (3-way handshake)
- Контроль потока и перегрузки
- Медленнее UDP
- Используется: HTTP, SSH, SMTP, FTP
UDP (User Datagram Protocol):
- Без гарантии доставки (fire-and-forget)
- Нет соединения, нет порядка пакетов
- Быстрее, меньше накладных расходов
- Используется: DNS, видеостриминг, онлайн-игры, VoIP
Сравнение:
TCP UDP
Надёжность Да Нет
Скорость Медленнее Быстрее
Порядок Гарантирован Нет
Соединение Нужно Не нужно
Примеры HTTP, SSH DNS, видео
QUIC (HTTP/3) — поверх UDP, но с надёжностью TCP.
СреднеСети / Сети и уровни
Модель OSI и уровень HTTP
Расскажи про OSI. На каком уровне находится HTTP?
OSI — 7 уровней сетевой модели:
7. Application — HTTP, FTP, DNS, SMTP (для пользователя)
6. Presentation — SSL/TLS, JPEG, JSON (кодирование/шифрование)
5. Session — управление сессиями
4. Transport — TCP, UDP (доставка между хостами)
3. Network — IP, маршрутизация
2. Data Link — Ethernet, MAC-адреса
1. Physical — кабели, радиоволны, биты
HTTP — уровень 7 (Application).
TCP — уровень 4 (Transport).
IP — уровень 3 (Network).
Упрощённая модель TCP/IP (4 уровня):
Application → HTTP, DNS
Transport → TCP, UDP
Internet → IP
Network Access → Ethernet, Wi-Fi
СреднеСети / Сети и уровни
Что такое CORS
Что такое CORS? Это связано именно с HTTP?
CORS (Cross-Origin Resource Sharing) — механизм безопасности браузера.
Регулирует: может ли скрипт с origin A обращаться к API на origin B.
Origin = протокол + домен + порт.
https://app.com ≠ http://app.com ≠ https://api.app.com
Без CORS браузер блокирует запрос (Same-Origin Policy).
Сервер должен вернуть заголовок:
Access-Control-Allow-Origin: https://app.com
или Access-Control-Allow-Origin: * (разрешить всем)
Preflight-запрос:
Браузер сначала отправляет OPTIONS-запрос.
Если сервер разрешает — браузер отправляет основной запрос.
CORS — это механизм HTTP (использует заголовки).
Но ограничение налагает БРАУЗЕР, не сервер.
curl/Postman — не подвержены CORS.
В Rails:
gem "rack-cors"
config.middleware.use Rack::Cors do
allow { origins "*"; resource "*", headers: :any, methods: :any }
end
СложноСети / Микросервисы
Коммуникация между микросервисами
Какие способы коммуникации между микросервисами знаешь: брокеры сообщений (например, Kafka), REST, RPC? Их особенности, плюсы и минусы.
Три основных способа:
1. REST (HTTP API):
+ Простота, стандарты HTTP
+ Легко отладить (curl, Postman)
- Синхронный — ждёт ответ (coupling по времени)
- Зависимость от доступности сервиса
2. RPC (gRPC, JSON-RPC):
+ Быстрый (бинарный протокол в gRPC)
+ Строгая типизация (protobuf)
- Синхронный
- Тесное связывание (shared contract)
- Сложнее отладить
3. Брокеры сообщений (Kafka, RabbitMQ):
+ Асинхронный — отправитель не ждёт
+ Loose coupling — сервисы не знают друг о друге
+ Буферизация при сбоях (durable queues)
- Сложность инфраструктуры
- Eventual consistency
- Сложнее отладить
Выбор:
REST — простые запросы, CRUD
RPC — высокая производительность между своими сервисами
Kafka — событийная архитектура, логирование, streaming
СложноСети / Микросервисы
Партиции в Kafka
Что такое партиции (partitions) в Kafka? Как они устроены и зачем нужны?
Партиция — единица параллелизма в Kafka.
Topic разбит на партиции — упорядоченные, append-only логи.
Каждая партиция хранится на одной машине (replica на других).
Зачем:
- Параллелизм — разные consumer-ы читают разные партиции одновременно
- Масштабирование —吞吐量 пропорциональна количеству партиций
- Порядок гарантируется ВНУТРИ партиции (не между)
Как сообщение попадает в партицию:
- По ключу: hash(key) % num_partitions → одна партиция
(сообщения с одним ключом в одном порядке)
- Без ключа: round-robin (произвольная партиция)
Consumer Group:
- Каждый consumer в группе читает свои партиции
- Партиций ≥ consumer-ов (иначе кто-то простаивает)
Schema:
Topic "orders"
├── Partition 0: [msg1, msg2, msg3, ...]
├── Partition 1: [msg4, msg5, msg6, ...]
└── Partition 2: [msg7, msg8, msg9, ...]
СложноСети / Микросервисы
Transactional Outbox
Что такое паттерн transactional outbox? Для чего нужен? Какие альтернативы существуют?
Проблема: нужно обновить БД и отправить сообщение в Kafka — атомарно.
Если отправить в Kafka, а потом БД упадёт — сообщение уже ушло (орфанное).
Если сначала БД, потом Kafka — можем забыть отправить.
Transactional Outbox:
В той же транзакции записываем сообщение в outbox-таблицу:
BEGIN
UPDATE orders SET status = 'paid' WHERE id = 1
INSERT INTO outbox (event_type, payload) VALUES ('order.paid', '{...}')
COMMIT
Отдельный процесс (CDC / poller) читает outbox и отправляет в Kafka.
Плюсы:
- Атомарность (одна транзакция)
- Гарантия доставки (outbox не потеряется)
- At-least-once доставка
Альтернативы:
- Transactional messaging (Kafka transactions) — встроенная поддержка
- Dual write с компенсацией — сложнее, ненадёжно
- Debezium CDC — читает WAL PostgreSQL и отправляет в Kafka напрямую
В Rails: gem outboxer, или самописный poller + outbox-таблица.