В чём разница между has_one, has_many, belongs_to и has_many :through?
belongs_to — внешний ключ в этой таблице; has_one/has_many — ключ в другой таблице (1:1 и 1:N); has_many :through — N:M через промежуточную модель с возможностью хранить дополнительные поля на связи.
Ассоциации ActiveRecord: четыре типа
Ассоциации в Rails — это макросы, которые добавляют методы для работы со связанными моделями и описывают отношения на уровне объектов Ruby, отражая внешние ключи в БД.
belongs_to
Указывает, что эта модель содержит внешний ключ. Добавляет метод для получения связанного объекта.
# Таблица comments имеет колонку post_id (INTEGER)
class Comment < ApplicationRecord
belongs_to :post # обязательная связь (Rails 5+)
belongs_to :author, class_name: "User" # другое имя ключа: author_id
belongs_to :category, optional: true # разрешает post_id = NULL
end
comment = Comment.find(1)
comment.post # SELECT * FROM posts WHERE id = comment.post_id LIMIT 1
comment.post_id # => 42 (сам внешний ключ)
comment.build_post # создаёт новый Post, не сохраняя
С Rails 5 belongs_to по умолчанию обязателен — валидация упадёт, если внешний ключ nil. Используйте optional: true для необязательных связей.
has_one
Обратная сторона belongs_to: внешний ключ живёт в другой таблице. Связь «один к одному».
# Таблица profiles имеет колонку user_id
class User < ApplicationRecord
has_one :profile # SELECT * FROM profiles WHERE user_id = ?
has_one :avatar, class_name: "Attachment",
as: :attachable # полиморфная связь
end
user = User.find(1)
user.profile # => Profile объект или nil
user.build_profile(bio: "Ruby dev") # создаёт без сохранения
user.create_profile!(bio: "Ruby dev") # создаёт и сохраняет
has_many
Один объект имеет много связанных. Возвращает ActiveRecord::CollectionProxy.
# Таблица posts имеет колонку user_id
class User < ApplicationRecord
has_many :posts, dependent: :destroy # при удалении User удаляем его Posts
has_many :comments, through: :posts # через промежуточную таблицу
has_many :active_posts, -> { where(published: true) },
class_name: "Post" # scope-ассоциация
end
user = User.find(1)
user.posts # SELECT * FROM posts WHERE user_id = 1
user.posts.create!(title: "Hi") # INSERT INTO posts (user_id, title) ...
user.posts.count # SELECT COUNT(*) FROM posts WHERE user_id = 1
user.posts.pluck(:id) # [1, 2, 3] — только id, без объектов
has_many :through
Связь «многие ко многим» через промежуточную join-таблицу с моделью. Позволяет добавлять данные на промежуточную запись.
# Таблицы: users, courses, enrollments
# enrollments: user_id, course_id, enrolled_at, grade
class Enrollment < ApplicationRecord
belongs_to :user
belongs_to :course
# дополнительные поля: enrolled_at, grade, status
end
class User < ApplicationRecord
has_many :enrollments
has_many :courses, through: :enrollments # JOIN через enrollments
end
class Course < ApplicationRecord
has_many :enrollments
has_many :students, through: :enrollments, source: :user
end
# Использование
user.courses # SELECT courses.* FROM courses INNER JOIN enrollments ON ...
user.courses.map(&:name) # без N+1 — один JOIN-запрос
# Доступ к данным промежуточной таблицы
user.enrollments.find_by(course: course).grade # => "A"
# Альтернатива: has_and_belongs_to_many (HABTM)
# Не создаёт модель для join-таблицы — используйте только если
# join-таблица не нужна как самостоятельная сущность
dependent: опции при удалении
dependent: :destroy— удаляет связанные записи через Ruby (запускает callbacks), N запросовdependent: :delete_all— один SQL DELETE без callbacks (быстро, но небезопасно при наличии зависимых callbacks)dependent: :nullify— устанавливает внешний ключ в NULLdependent: :restrict_with_error— запрещает удаление, если есть связанные записи
Подводные камни
- N+1 при обходе ассоциаций:
users.each { |u| u.posts.count }— N+1 запросов. ИспользуйтеUser.includes(:posts)илиUser.joins(:posts).select("users.*, COUNT(posts.id)"). - belongs_to обязателен по умолчанию в Rails 5+: если забыть
optional: true, запись с nil-внешним ключом не пройдёт валидацию, хотя на уровне БД это может быть допустимо. - dependent: :destroy на большие коллекции: удаление пользователя с 100 000 постов запустит 100 000 DELETE-запросов. Лучше
dependent: :delete_allили фоновый job. - has_many :through vs HABTM: HABTM (
has_and_belongs_to_many) не создаёт модель — нельзя добавить поля к join-таблице позже без рефактора. Предпочитайтеhas_many :throughпри любой неопределённости. - Полиморфные ассоциации и индексы: полиморфная
belongs_to :attachable, polymorphic: trueтребует составного индекса на(attachable_type, attachable_id), иначе seq scan. - Кеширование ассоциаций:
user.postsкешируется в объекте; повторный вызов не делает запрос. Чтобы перезагрузить —user.posts.reload. Это может привести к устаревшим данным в долгоживущих объектах. - scope в ассоциации не применяется при build:
has_many :active_posts, -> { where(published: true) }— приuser.active_posts.buildcreated объект не будет автоматически иметьpublished: true.
Common mistakes
- Сводить associations к названию метода без lifecycle и failure path.
- Игнорировать модель runtime: Rails 8.1 строит приложение вокруг Rack, routing, controllers, Active Record, views и conventions over configuration.
- Не отделять validation, authorization, transaction boundary и business logic.
- Менять похожие API местами без учёта семантики ошибок и ownership.
What the interviewer is testing
- Объясняет associations через конкретную точку lifecycle в Ruby on Rails.
- Приводит корректный минимальный пример без вымышленных методов или callbacks.
- Называет edge cases: пустые значения, ошибки, транзакции, безопасность или concurrency.