Связи
Сущности предметной области не существуют сами по себе. Они часто зависят друг от друга. На уровне базы данных такие связи задаются через внешние ключи или даже промежуточные таблицы, как в случае связи «многие ко многим».
ORM использует эти ключи для работы со связями. Он добавляет множество полезных методов, которые упрощают работу с зависимыми сущностями: выборкой, добавлением, модификацией и удалением.
Представим, что мы создаем блог, и нам понадобится сущность Post
. Пользователи связаны с постами «один ко многим»:
- Один пользователь может быть автором многих постов
- У одного поста всегда один автор
Структура
Для поддержки такой связи при создании таблицы постов нужно добавить внешний ключ на таблицу пользователей. Далее приведен пример файла миграции, который создает таблицу posts
с внешним ключом creator
таблицы users
. Что такое миграции и как с ними работать, мы разберем в следующих уроках.
Рассмотрим пример:
class CreatePosts < ActiveRecord::Migration[6.0]
def change
create_table :posts do |t|
t.string :state, null: true
t.string :title
t.text :body
# Поле, которое будет внешним ключом
t.references :creator, null: false, foreign_key: {to_table: :users}
t.timestamps
end
end
end
По умолчанию ActiveRecord не воспринимает внешние ключи как что-то особенное. Она требует явного указания связи на уровне моделей. Для этого в каждой из моделей определяется специальный метод, через который будет происходить взаимодействие между связанными сущностями. Имя этого метода произвольно. Оно выбирается так, чтобы лучше отражать суть связи: у поста есть автор, у каждого автора есть посты.
В следующем примере мы используем методы belongs_to
и has_many
для установки связей между моделями. При этом используется параметр class_name
для указания класса, с которым устанавливается связь. Это особенно полезно в случаях, когда имя ассоциации не совпадает с именем соответствующего класса.
В данном контексте belongs_to
и has_many
обозначают «принадлежит к» и «имеет много» соответственно.
Так это выглядит на практике:
# post.rb
class Post < ApplicationRecord
belongs_to :creator, class_name: 'User'
end
В этом примере Post
принадлежит User
, который обозначен как creator
.
# user.rb
class User < ApplicationRecord
# Во множественном числе, потому что это коллекция
has_many :posts, foreign_key: 'creator_id'
# У каждого пользователя много постов
# has_many определяется у модели, имеющей внешние ключи в других таблицах
end
А здесь каждый User
«имеет много» posts
, связанных через внешний ключ creator_id
.
Метод has_many
также поддерживает соглашение для определения имени внешнего ключа. Только здесь оно определяется не по имени метода, а по имени модели, в которой описывается связь. Для модели User это будет user_id
. В нашем случае такая логика не работает, поэтому имя свойства указано явно.
CRUD
Теперь ActiveRecord знает о связях и позволяет работать с ними напрямую:
user = User.find(1)
# SELECT "posts".* FROM "posts" WHERE "posts"."creator_id" = 1
user.posts.each do |post|
puts post
end
Обращение к коллекции зависимых сущностей возвращает специальный объект, который может использоваться как массив.
Есть и другой способ взаимодействовать с зависимостями. Вызов posts
как метода позволяет управлять этой коллекцией, например, удалить или добавить новый пост:
# Создаем новый пост и автоматически связываем его с пользователем
post = user.posts.build(title: 'title', body: 'body') # Параметры поста можно передать в build
post.save
# Выводим все посты пользователя
user.posts # => [#<Post id: 1, title: "title", body: "body", creator_id: 1, ...>]
# Находим пост по id
post2 = user.posts.find(1)
post == post2 # => true
# Удаляем все посты пользователя
user.posts.destroy_all
То же самое происходит и с другой стороны связи:
post = Post.find(1)
post.creator.first_name
# Устанавливаем пользователя
post.creator = user
post.save
При работе со связями важно переключиться от мышления через таблицы и ключи к сущностям и связям. Технически это значит, что код опирается на сами сущности, а не их идентификаторы:
# Плохо
post.creator_id = user.id
# Хорошо
post.creator = user
Выборки
Все типы связей в ActiveRecord поддерживают построение запросов на выборку:
# В запрос будет включено условие по creator_id, равным текущему пользователю
# SELECT "posts".* FROM "posts" WHERE "posts"."creator_id" = 1 AND "posts"."state" = 'active'
user.posts.where(state: 'active')
Самостоятельная работа
- Повторите шаги из теории.
- Создайте еще несколько моделей - Урок, Пользователь, Участник Курса.
- Создайте связь многие-ко-многим между пользователем и курсом.
Дополнительные материалы
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.