Статьи

Подтвердите свои знания о рельсовых ассоциациях

таблица данных реляционной базы данных связанный символ векторные иллюстрации концепция плоский

Rails 5 скоро появится (сейчас он на RC1), поэтому, готовясь к этой основной версии, самое время вернуться к основам. Сегодня мы собираемся обсудить ассоциации ActiveRecord .

Ассоциации значительно упрощают выполнение различных операций с записями в вашем коде. Доступно несколько типов ассоциаций:

  • Один к одному
  • Один ко многим
  • Многие-ко-многим
  • Полиморфный один ко многим

В этой статье мы собираемся обсудить все из них, а также некоторые варианты для дальнейшей настройки. Исходный код доступен на GitHub .

Ассоциации один-ко-многим

Для начала создайте новое приложение Rails:

$ rails new OhMyRelations -T 

Для этой демонстрации используется вариант релиза Rails 5, но все, что обсуждается в этой статье, относится к Rails 3 и 4.

Ассоциация «один ко многим», вероятно, является наиболее распространенным и широко используемым типом. Идея довольно проста: запись A может иметь много записей B, а запись B принадлежит только одной записи A. Для каждой записи B необходимо хранить id записи A, к которой она принадлежит — этот id называется внешним ключом .

Давайте попробуем это на практике. Предположим, у нас есть пользователь, у которого может быть куча постов. Прежде всего, создайте модель User :

 $ rails g model User name:string 

Что касается таблицы posts , то она должна содержать внешний ключ, и условным user_id является именование этого столбца после связанной таблицы, поэтому в нашем случае это будет user_id (обратите внимание на единственную форму):

 $ rails g model Post user:references body:text 

user:references — это отличный способ определить внешний ключ — он автоматически user_id соответствующий столбец user_id и добавит к нему индекс. Внутри вашей миграции вы увидите что-то вроде:

 t.references :user, foreign_key: true 

Конечно, вы также можете сказать:

 $ rails g model Post user_id:integer:index body:text 

Примените ваши миграции:

 $ rake db:migrate 

Не забывайте, что сами модели должны быть оснащены специальными методами для правильного установления отношений. Пока мы использовали ключевое слово links при генерации миграции, модель Post уже будет иметь следующую строку:

 belongs_to :user 

Тем не менее, User должен быть изменен вручную:

модели / user.rb

 [...] has_many :posts [...] 

Обратите внимание на форму множественного числа («post s ») для названия отношения. Для отношения принадлежащих вам используется форма единственного числа («пользователь»).

Это было не так сложно, правда? Теперь связь установлена, и вы можете использовать такие методы, как:

  • user.posts — ссылки на сообщения пользователя
  • user.posts << post — устанавливает новое отношение между пользователем и публикацией
  • post.user — ссылается на владельца поста
  • user.posts.build({ }) — создает новую запись для пользователя, но пока не сохраняет ее в базе данных. Он заполняет атрибут user_id в сообщении. Это похоже на высказывание Post.new({user_id: user.id}) .
  • user.posts.create({ }) — создает новое сообщение и сохраняет его в базе данных.
  • post.build_user — так же, как и выше, создание нового пользователя без его сохранения
  • post.create_user — то же, что и выше, создает и сохраняет пользователя в базе данных

Давайте обсудим некоторые параметры, которые вы можете установить при определении отношений. Предположим, например, что вы хотите, belongs_to отношение belongs_to называлось author , а не user :

модели / post.rb

 [...] belongs_to :author [...] 

Однако этого недостаточно, поскольку Rails использует аргумент :author для получения имени связанной модели и внешнего ключа. Пока у нас нет модели с именем Author , мы должны указать фактическое имя класса:

модели / post.rb

 [...] belongs_to :author, class_name: 'User' [...] 

Пока все хорошо, но таблица posts также не имеет поля author_id , поэтому нам нужно переопределить опцию: :foreign_key :

модели / post.rb

 [...] belongs_to :author, class_name: 'User', foreign_key: 'user_id' [...] 

Теперь внутри вашей консоли вы можете сделать что-то вроде:

 $ post = Post.new $ post.create_author 

и все должно работать просто отлично.

Обратите внимание, что для ассоциации has_many также доступны опции :foreign_key и :foreign_key . Более того, используя эти параметры, вы можете установить отношение, где модель ссылается на себя, как описано здесь .

Другой распространенный параметр, который вы можете установить, это :dependent , обычно для отношения has_many . Зачем нам это нужно? Предположим, у пользователя по имени Джон есть куча сообщений. Затем, внезапно, Джон удаляется из базы данных — ну, это происходит … Но как насчет его сообщений? У них все еще есть столбец user_id с идентификатором Джона, но эта запись больше не существует! Эти сообщения называются осиротевшими записями и могут привести к различным проблемам, поэтому вы, вероятно, захотите позаботиться о таком сценарии.

Опция :dependent принимает следующие значения:

  • :destroy — все связанные объекты будут удалены один за другим (в отдельном запросе). Соответствующие обратные вызовы будут выполняться до и после удаления.
  • :delete_all — все связанные объекты будут удалены в одном запросе. Никакие обратные вызовы не будут выполнены.
  • :nullify — внешние ключи для связанных объектов будут установлены в NULL . Никакие обратные вызовы не будут выполнены.
  • :restrict_with_exception — если есть какие-либо связанные записи, будет сгенерировано исключение.
  • :restrict_with_error — если есть какие-либо связанные записи, к владельцу будет добавлена ​​ошибка (запись, которую вы пытаетесь удалить).

Итак, как вы можете видеть, есть много способов справиться с этим сценарием. Для этой демонстрации я буду использовать :destroy :

модели / user.rb

 [...] has_many :posts, dependent: :destroy [...] 

Интересно то, что belongs_to также поддерживает параметр :dependent — он может быть установлен либо :destroy либо :delete ). Однако в отношениях один ко многим я настоятельно не рекомендую вам выбирать эту опцию.

Стоит также упомянуть, что в Rails 5 по умолчанию вы не можете создать запись, если ее родитель не существует. По сути, это означает, что вы не можете сделать:

 Post.create({user_id: nil}) 

потому что, очевидно, нет такого пользователя.

Эта новая функция может быть отключена для всего приложения путем настройки следующего файла инициализатора:

конфиг / Инициализаторы / active_record_belongs_to_required_by_default.rb

 Rails.application.config.active_record.belongs_to_required_by_default = false # default is true 

Также вы можете установить :optional параметр для отдельных отношений:

 belongs_to :author, optional: true 

Индивидуальные ассоциации

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

 $ rails g model Address street:string city:string country:string user_id:integer:index $ rake db:migrate 

Для пользователя просто сказать:

модели / user.rb

 has_one :address 

Имея это на месте, вы можете вызывать методы, такие как

  • user.address — выбирает связанный адрес
  • user.build_address — такой же, как метод, предоставляемый belongs_to ; создает новый адрес, но не
    сохранить его в базе данных.
  • user.create_address — создает новый адрес и сохраняет его в базе данных.

Отношение has_one позволяет вам определять :class_name :foreign_key :dependent :foreign_key и другие параметры, как с has_many .

Многие ко многим Ассоциациям

Ассоциация «Имеет и принадлежит многим»

Ассоциации «многие ко многим» немного сложнее и могут быть установлены двумя способами. Прежде всего, давайте обсудим прямую связь , без каких-либо промежуточных моделей. Это называется «имеет и принадлежит многим» или просто «HABTM».

Предположим, пользователь может зарегистрироваться во многих различных событиях, и событие может содержать много пользователей. Для достижения этой цели нам нужна отдельная таблица (часто называемая «таблицей соединений»), в которой хранятся отношения между пользователями и событиями. Эта таблица должна иметь специальное имя: users_events . По сути, это просто сочетание имен двух таблиц, между которыми мы устанавливаем связь.

Во-первых, создайте events :

 $ rails g model Event title:string 

Теперь промежуточный стол:

 $ rails g migration create_events_users user:references event:references $ rake db:migrate 

Обратите внимание на имя промежуточной таблицы — Rails ожидает, что она будет состоять из имен двух таблиц (`events` и` users`). Кроме того, имя высшего порядка (`events`) должно быть первым (` events> users`, потому что буква «e» предшествует букве «u»). Последний шаг — добавить has_and_belongs_to_many к обеим моделям:

модель / user.rb

 [...] has_and_belongs_to_many :events [...] 

модель / event.rb

 [...] has_and_belongs_to_many :users [...] 

Теперь вы можете вызывать методы, такие как:

  • user.events
  • user.events << [event1, event2] — создает отношения между пользователем и группой событий
  • user.events.destroy(event1) — уничтожает связь между записями (фактические записи не будут удалены). Существует также метод delete который делает то же самое, за исключением того, что он не запускает обратные вызовы
  • user.event_ids — аккуратный метод, который возвращает массив идентификаторов из коллекции
  • user.event_ids = [1,2,3] — делает коллекцию содержащей только объекты, идентифицированные предоставленными значениями первичного ключа.
    Обратите внимание, что если коллекция изначально содержала другие объекты, они будут удалены.
  • user.events.create({}) — создает новый объект и добавляет его в коллекцию

has_and_belongs_to_many принимает опции :foreign_key и :foreign_key , которые мы уже обсуждали. Тем не менее, он также поддерживает некоторые другие специальные настройки:

  • :association_foreign_key — по умолчанию Rails использует имя отношения для поиска внешнего ключа в промежуточной таблице, которая, в свою очередь, используется для поиска связанного объекта. Так, например, если вы скажете has_and_belongs_to_many :users , будет использован столбец user_id . Однако это не всегда удобно, поэтому для определения имени пользовательского столбца можно использовать :association_foreign_key .
  • :join_table — эта опция может использоваться для переопределения имени для промежуточной таблицы (в нашем случае это называется users_events ).

Вот и все. Тем не менее has_and_belongs_to_many — довольно жесткий способ определения ассоциаций «многие ко многим», поскольку вы не можете работать с моделью отношений независимо. Во многих случаях вы захотите сохранить некоторые дополнительные данные для каждого отношения или определить дополнительные обратные вызовы, которые нельзя сделать с помощью отношения HABTM. Поэтому я собираюсь показать вам другой, более удобный способ решения той же задачи.

Ассоциация «Много через»

Другим способом определения связи «многие ко многим» является использование типа « имеет множество через ». Предположим, у нас есть список игр, и время от времени проводятся соревнования по этим играм. Многие пользователи могут участвовать во многих соревнованиях. Помимо установления отношения «многие ко многим» между пользователями и играми, мы также хотим хранить дополнительную информацию о каждой регистрации, такую ​​как категория соревнования (любительская, полупрофессиональная, профессиональная и т. Д.)

Прежде всего, создайте новую модель Game :

 $ rails g model Game title:string 

Нам также нужна промежуточная таблица, но на этот раз с моделью:

 $ rails g model Enrollment game:references user:references category:string $ rake db:migrate 

Для модели Enrollment все было настроено автоматически:

модели / enrollment.rb

 [...] belongs_to :game belongs_to :user [...] 

Настройте две другие модели:

модели / user.rb

 [...] has_many :enrollments has_many :games, through: :enrollments [...] 

модели / game.rb

 [...] has_many :enrollments has_many :users, through: :enrollments [...] 

Здесь мы явно указываем промежуточную модель для установления этого отношения. Теперь вы можете работать с каждой регистрацией как с независимой организацией, что более удобно. Обратите внимание, что если имя исходной ассоциации не может быть автоматически выведено из имени ассоциации, вы можете использовать опцию :source и соответственно установить ее значение.

Итак, для подведения итогов используем has_many :through предпочтительнее has_and_belongs_to_many , однако в простых случаях вы можете придерживаться последнего решения.

«Имеет один через» ассоциации

Еще один интересный тип ассоциации — это сквозное соединение , которое несколько похоже на то, что мы видели в предыдущем разделе. Идея состоит в том, что модель сопоставляется с другой через промежуточную модель. Предположим, что у пользователя есть кошелек, а у кошелька — история платежей. Прежде всего, создайте модель Purse :

 $ rails g model Purse user:references funds:integer 

user_id будет внешним ключом для установления связи между пользователем и его кошельком. А теперь модель PaymentHistory :

 $ rails g model PaymentHistory purse:references $ rake db:migrate 

Теперь настройте такие модели:

модели / user.rb

 has_one :purse has_one :payment_history, through: :purse 

модели / purse.rb

 belongs_to :user has_one :payment_history 

модели / payment_history.rb

 belongs_to :purse 

Этот тип отношений используется редко, но иногда может пригодиться.

Полиморфные Ассоциации

Полиморфные ассоциации могут спасти день в определенных сценариях. Несмотря на страшное имя, идея проста: у вас есть модель, которая может принадлежать множеству разных моделей в одной ассоциации. Предположим, вы собираетесь делать игры и пользователей комментируемыми. Конечно, у вас могут быть две отдельные модели, называемые UserComment и GameComment но в целом комментарии очень похожи, за исключением того, что они принадлежат разным моделям. Именно тогда в игру вступают полиморфные ассоциации.

Создайте новую модель Comment :

 $ rails g model Comment body:text commentable_id:integer:index commentable_type:string 

Возможно, вы уже поняли идею. commentable_id — это внешний ключ для установления связи с другими таблицами. commentable_type , в свою очередь, будет содержать фактическое имя модели, которой принадлежит комментарий. Миграция:

 create_table :comments do |t| t.text :body t.integer :commentable_id t.string :commentable_type t.timestamps end add_index :comments, :commentable_id 

может быть переписан как:

 create_table :comments do |t| t.text :body t.references :commentable, polymorphic: true, index: true t.timestamps end add_index :comments, :commentable_id 

Мы уже видели метод references раньше, но на этот раз он также поставляется с опцией :polymorphic .

Применить миграцию:

 $ rake db:migrate 

Модель Comment будет иметь ассоциацию belongs_to , но с небольшим поворотом:

модели / comment.rb

 [...] belongs_to :commentable, polymorphic: true [...] 

Пока мы называем наши поля :commentable_id и :commentable_type , все отношение должно называться commentable .

Теперь User и Game модели:

модели / user.rb

 [...] has_many :comments, as: :commentable [...] 

модели / game.rb

 [...] has_many :comments, as: :commentable [...] 

:as является специальным параметром, объясняющим, что это полиморфная ассоциация. Теперь загрузите консоль и попробуйте запустить:

 $ u = User.create $ u.comments.create({body: 'test'}) 

Внутри таблицы commentable_type будет установлено значение User а для commentable_id — идентификатор пользователя. Ваша полиморфная ассоциация работает отлично, и теперь вы можете легко сделать другие модели комментируемыми!

Вывод

В этой статье мы обсудили различные типы ассоциаций, доступных в Rails. Мы видели, как их настроить и настроить. Официальная документация содержит хорошую ссылку для каждой ассоциации, поэтому обязательно ознакомьтесь с ней, если вы еще этого не сделали.

Надеюсь, эта статья была полезной проверкой ваших знаний. Оставайтесь с нами и до скорой встречи!