Ruby on RailsJuniorTechnical

В чём разница между 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 — устанавливает внешний ключ в NULL
  • dependent: :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.build created объект не будет автоматически иметь 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.

Sources

Related topics