Spec-Zone.ru › Ruby on Rails 7.0

модуль ActiveRecord::Associations::ClassMethods

Ассоциации представляют собой набор макро-подобных методов класса для связывания объектов через внешние ключи. Они выражают такие отношения, как «Проект имеет одного руководителя проекта» или «Проект принадлежит портфолио». Каждый макрос добавляет ряд методов в класс, которые специализируются в зависимости от символа коллекции или ассоциации и хэша опций. Он работает практически так же, как собственные attr* методы Ruby.

class Project < ActiveRecord::Base
  belongs_to              :portfolio
  has_one                 :project_manager
  has_many                :milestones
  has_and_belongs_to_many :categories
end

Класс проекта теперь имеет следующие методы (и больше) для упрощения навигации и манипулирования его отношениями:

  • Project#portfolio, Project#portfolio=(portfolio), Project#reload_portfolio

  • Project#project_manager, Project#project_manager=(project_manager), Project#reload_project_manager

  • Project#milestones.empty?, Project#milestones.size, Project#milestones, Project#milestones<<(milestone), Project#milestones.delete(milestone), Project#milestones.destroy(milestone), Project#milestones.find(milestone_id), Project#milestones.build, Project#milestones.create

  • Project#categories.empty?, Project#categories.size, Project#categories, Project#categories<<(category1), Project#categories.delete(category1), Project#categories.destroy(category1)

Предупреждение

Не создавайте ассоциаций с теми же именами, что и методы экземпляров ActiveRecord::Base. Поскольку ассоциация добавляет метод с таким же именем в свою модель, использование ассоциации с тем же именем, что и предоставленное ActiveRecord::Base, переопределит метод, унаследованный через ActiveRecord::Base, и вызовет ошибки. Например, attributes и connection были бы плохим выбором для имён ассоциаций, поскольку эти имена уже существуют в списке ActiveRecord::Base методов экземпляров.

Автоматически сгенерированные методы

См. также Публичные методы экземпляров ниже для получения дополнительной информации.

Единичные ассоциации (один к одному)

                                  |            |  belongs_to  |
generated methods                 | belongs_to | :polymorphic | has_one
----------------------------------+------------+--------------+---------
other                             |     X      |      X       |    X
other=(other)                     |     X      |      X       |    X
build_other(attributes={})        |     X      |              |    X
create_other(attributes={})       |     X      |              |    X
create_other!(attributes={})      |     X      |              |    X
reload_other                      |     X      |      X       |    X
other_changed?                    |     X      |      X       |
other_previously_changed?         |     X      |      X       |

Коллекционные ассоциации (один ко многим / многие ко многим)

                                  |       |          | has_many
generated methods                 | habtm | has_many | :through
----------------------------------+-------+----------+----------
others                            |   X   |    X     |    X
others=(other,other,...)          |   X   |    X     |    X
other_ids                         |   X   |    X     |    X
other_ids=(id,id,...)             |   X   |    X     |    X
others<<                          |   X   |    X     |    X
others.push                       |   X   |    X     |    X
others.concat                     |   X   |    X     |    X
others.build(attributes={})       |   X   |    X     |    X
others.create(attributes={})      |   X   |    X     |    X
others.create!(attributes={})     |   X   |    X     |    X
others.size                       |   X   |    X     |    X
others.length                     |   X   |    X     |    X
others.count                      |   X   |    X     |    X
others.sum(*args)                 |   X   |    X     |    X
others.empty?                     |   X   |    X     |    X
others.clear                      |   X   |    X     |    X
others.delete(other,other,...)    |   X   |    X     |    X
others.delete_all                 |   X   |    X     |    X
others.destroy(other,other,...)   |   X   |    X     |    X
others.destroy_all                |   X   |    X     |    X
others.find(*args)                |   X   |    X     |    X
others.exists?                    |   X   |    X     |    X
others.distinct                   |   X   |    X     |    X
others.reset                      |   X   |    X     |    X
others.reload                     |   X   |    X     |    X

Переопределение сгенерированных методов

Методы ассоциаций генерируются в модуле, включенном в класс модели, что упрощает переопределения. Таким образом, исходный сгенерированный метод можно вызвать с помощью super:

class Car < ActiveRecord::Base
  belongs_to :owner
  belongs_to :old_owner

  def owner=(new_owner)
    self.old_owner = self.owner
    super
  end
end

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

Кардинальность и ассоциации

Активные записи ассоциации могут использоваться для описания отношений один-к-одному, один-ко-многим и многие-ко-многим между моделями. Каждая модель использует ассоциацию для описания своей роли в отношении. Ассоциация belongs_to всегда используется в модели, которая имеет внешний ключ.

Один-к-одному

Используйте has_one в базовой, и belongs_to в связанной модели.

class Employee < ActiveRecord::Base
  has_one :office
end
class Office < ActiveRecord::Base
  belongs_to :employee    # foreign key - employee_id
end

Один-ко-многим

Используйте has_many в базовой, и belongs_to в связанной модели.

class Manager < ActiveRecord::Base
  has_many :employees
end
class Employee < ActiveRecord::Base
  belongs_to :manager     # foreign key - manager_id
end

Многие-ко-многим

Существует два способа построения отношения многие-ко-многим.

Первый способ использует ассоциацию has_many с опцией :through и моделью соединения, поэтому есть два этапа ассоциаций.

class Assignment < ActiveRecord::Base
  belongs_to :programmer  # foreign key - programmer_id
  belongs_to :project     # foreign key - project_id
end
class Programmer < ActiveRecord::Base
  has_many :assignments
  has_many :projects, through: :assignments
end
class Project < ActiveRecord::Base
  has_many :assignments
  has_many :programmers, through: :assignments
end

Для второго способа используйте has_and_belongs_to_many в обеих моделях. Это требует таблицы соединения, которая не имеет соответствующей модели или первичного ключа.

class Programmer < ActiveRecord::Base
  has_and_belongs_to_many :projects       # foreign keys in the join table
end
class Project < ActiveRecord::Base
  has_and_belongs_to_many :programmers    # foreign keys in the join table
end

Выбор способа построения отношения многие-ко-многим не всегда прост. Если вам нужно работать с моделью отношения как со своей сущностью, используйте has_many :through. Используйте has_and_belongs_to_many при работе со схемами устаревшего типа или когда вы никогда не работаете напрямую с самим отношением.

Это ассоциация belongs_to или has_one?

Оба выражают отношение 1-к-1. Разница в основном в том, где разместить внешний ключ, который находится в таблице для класса, объявляющего отношение belongs_to.

class User < ActiveRecord::Base
  # I reference an account.
  belongs_to :account
end

class Account < ActiveRecord::Base
  # One user references me.
  has_one :user
end

Таблицы для этих классов могут выглядеть следующим образом:

CREATE TABLE users (
  id bigint NOT NULL auto_increment,
  account_id bigint default NULL,
  name varchar default NULL,
  PRIMARY KEY  (id)
)

CREATE TABLE accounts (
  id bigint NOT NULL auto_increment,
  name varchar default NULL,
  PRIMARY KEY  (id)
)

Несохраненные объекты и ассоциации

Вы можете манипулировать объектами и ассоциациями до их сохранения в базе данных, но есть некоторые особенности поведения, о которых вам следует знать, в основном связанные с сохранением связанных объектов.

Вы можете установить опцию :autosave для has_one, belongs_to, has_many или has_and_belongs_to_many ассоциации. Установка её в значение true всегда сохранит члены, а установка в значение false никогда не сохранит члены. Дополнительные сведения об опции :autosave доступны по адресу AutosaveAssociation.

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

  • Присвоение объекта ассоциации has_one автоматически сохраняет этот объект и заменяемый объект (если таковой есть), чтобы обновить их внешние ключи — за исключением случая, когда родительский объект не сохранён (new_record? == true).

  • Если одно из этих сохранений завершается ошибкой (потому что один из объектов недействителен), генерируется исключение ActiveRecord::RecordNotSaved, и назначение отменяется.

  • Если вы хотите назначить объект ассоциации has_one без его сохранения, используйте метод #build_association (документированный ниже). Заменяемый объект по-прежнему будет сохранён, чтобы обновить его внешний ключ.

  • Назначение объекта ассоциации belongs_to не сохраняет объект, поскольку поле внешнего ключа принадлежит родителю. Он также не сохраняет родителя.

Коллекции

  • Добавление объекта в коллекцию (has_many или has_and_belongs_to_many) автоматически сохраняет этот объект, за исключением случая, когда родительский объект (владелец коллекции) ещё не сохранён в базе данных.

  • Если сохранение любого из добавляемых в коллекцию объектов (через push или аналогично) завершается ошибкой, то push возвращает false.

  • Если сохранение завершается ошибкой при замене коллекции (через association=), генерируется исключение ActiveRecord::RecordNotSaved, и назначение отменяется.

  • Вы можете добавить объект в коллекцию без автоматического сохранения, используя метод collection.build (документированный ниже).

  • Все несохраненные (new_record? == true) члены коллекции автоматически сохраняются при сохранении родителя.

Настройка запроса

Ассоциации создаются из объектов Relation, и вы можете использовать синтаксис Relation, чтобы их настроить. Например, для добавления условия:

class Blog < ActiveRecord::Base
  has_many :published_posts, -> { where(published: true) }, class_name: 'Post'
end

Внутри блока -> { ... } вы можете использовать все обычные методы Relation.

Доступ к объекту владельца

Иногда полезно иметь доступ к объекту владельца при построении запроса. Владелец передается как параметр в блок. Например, следующая ассоциация найдет все события, которые происходят в день рождения пользователя:

class User < ActiveRecord::Base
  has_many :birthday_events, ->(user) { where(starts_on: user.birthday) }, class_name: 'Event'
end

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

Обработчики событий ассоциации

Аналогично обычным обработчикам событий, которые привязываются к жизненному циклу объекта Active Record, вы также можете определить обработчики событий, которые вызываются при добавлении или удалении объекта из коллекции ассоциации.

class Firm < ActiveRecord::Base
  has_many :clients,
           dependent: :destroy,
           after_add: :congratulate_client,
           after_remove: :log_after_remove

  def congratulate_client(record)
    # ...
  end

  def log_after_remove(record)
    # ...
  end

Можно объединять обработчики событий, передавая их как массив. Пример:

class Firm < ActiveRecord::Base
  has_many :clients,
           dependent: :destroy,
           after_add: [:congratulate_client, -> (firm, record) { firm.log << "after_adding#{record.id}" }],
           after_remove: :log_after_remove
end

Возможные обработчики событий: before_add, after_add, before_remove и after_remove.

Если какой-либо из обработчиков событий before_add вызывает исключение, объект не будет добавлен в коллекцию.

Аналогично, если какой-либо из обработчиков событий before_remove вызывает исключение, объект не будет удален из коллекции.

Примечание: Чтобы вызвать обработчики удаления, необходимо использовать методы destroy / destroy_all. Например:

* <tt>firm.clients.destroy(client)</tt>
* <tt>firm.clients.destroy(*clients)</tt>
* <tt>firm.clients.destroy_all</tt>

Методы delete / delete_all подобного типа не вызывают обработчиков удаления:

* <tt>firm.clients.delete(client)</tt>
* <tt>firm.clients.delete(*clients)</tt>
* <tt>firm.clients.delete_all</tt>

Расширения ассоциаций

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

class Account < ActiveRecord::Base
  has_many :people do
    def find_or_create_by_name(name)
      first_name, last_name = name.split(" ", 2)
      find_or_create_by(first_name: first_name, last_name: last_name)
    end
  end
end

person = Account.first.people.find_or_create_by_name("David Heinemeier Hansson")
person.first_name # => "David"
person.last_name  # => "Heinemeier Hansson"

Если вам нужно использовать одинаковые расширения для многих ассоциаций, вы можете использовать именованный модуль расширений.

module FindOrCreateByNameExtension
  def find_or_create_by_name(name)
    first_name, last_name = name.split(" ", 2)
    find_or_create_by(first_name: first_name, last_name: last_name)
  end
end

class Account < ActiveRecord::Base
  has_many :people, -> { extending FindOrCreateByNameExtension }
end

class Company < ActiveRecord::Base
  has_many :people, -> { extending FindOrCreateByNameExtension }
end

Некоторые расширения могут работать только при наличии знаний о внутренней работе ассоциации. Расширения могут получить доступ к соответствующему состоянию, используя следующие методы (где items - имя ассоциации):

END_OF_DOCUMENT_MARKER
  • record.association(:items).owner - Возвращает объект, частью которого является ассоциация.

  • record.association(:items).reflection - Возвращает объект рефлексии, описывающий ассоциацию.

  • record.association(:items).target - Возвращает связанный объект для belongs_to и has_one или коллекцию связанных объектов для has_many и has_and_belongs_to_many.

Однако, внутри собственного кода расширения у вас не будет доступа к record как указано выше. В этом случае вы можете получить доступ к proxy_association. Например, record.association(:items) и record.items.proxy_association вернут один и тот же объект, что позволит вам делать вызовы, такие как proxy_association.owner внутри расширений ассоциаций.

Модели объединения ассоциаций

Ассоциации Has Many могут быть сконфигурированы с опцией :through для использования явной модели объединения для извлечения данных. Это работает аналогично ассоциации has_and_belongs_to_many. Преимущество состоит в том, что вы можете добавлять валидации, колбэки и дополнительные атрибуты к модели объединения. Рассмотрим следующую схему:

class Author < ActiveRecord::Base
  has_many :authorships
  has_many :books, through: :authorships
end

class Authorship < ActiveRecord::Base
  belongs_to :author
  belongs_to :book
end

@author = Author.first
@author.authorships.collect { |a| a.book } # selects all books that the author's authorships belong to
@author.books                              # selects all books by using the Authorship join model

Вы также можете пройти через ассоциацию has_many в модели объединения:

class Firm < ActiveRecord::Base
  has_many   :clients
  has_many   :invoices, through: :clients
end

class Client < ActiveRecord::Base
  belongs_to :firm
  has_many   :invoices
end

class Invoice < ActiveRecord::Base
  belongs_to :client
end

@firm = Firm.first
@firm.clients.flat_map { |c| c.invoices } # select all invoices for all clients of the firm
@firm.invoices                            # selects all invoices by going through the Client join model

Аналогично, вы можете пройти через ассоциацию has_one в модели объединения:

class Group < ActiveRecord::Base
  has_many   :users
  has_many   :avatars, through: :users
end

class User < ActiveRecord::Base
  belongs_to :group
  has_one    :avatar
end

class Avatar < ActiveRecord::Base
  belongs_to :user
end

@group = Group.first
@group.users.collect { |u| u.avatar }.compact # select all avatars for all users in the group
@group.avatars                                # selects all avatars by going through the User join model.

Важное замечание при прохождении через ассоциации has_one или has_many в модели объединения заключается в том, что эти ассоциации являются только для чтения. Например, следующее не будет работать после предыдущего примера:

@group.avatars << Avatar.new   # this would work if User belonged_to Avatar rather than the other way around
@group.avatars.delete(@group.avatars.last)  # so would this

Установка обратных ссылок

Если вы используете belongs_to в модели объединения, рекомендуется установить опцию :inverse_of для belongs_to, что означает правильную работу следующего примера (где tags — ассоциация has_many :through):

@post = Post.first
@tag = @post.tags.build name: "ruby"
@tag.save

Последняя строка должна сохранить запись через (запись Tagging). Это будет работать только если :inverse_of установлена:

class Tagging < ActiveRecord::Base
  belongs_to :post
  belongs_to :tag, inverse_of: :taggings
end

Если вы не установите запись :inverse_of, ассоциация сделает всё возможное, чтобы сопоставить себя с правильной обратной ссылкой. Автоматическое определение обратной ссылки работает только для ассоциаций has_many, has_one и belongs_to.

Опции :foreign_key и :through для ассоциаций также помешают автоматически найти обратную ссылку, как и пользовательские области в некоторых случаях. Дополнительные сведения см. в руководстве по ассоциациям Active Record .

Автоматическое определение обратной ссылки использует эвристику, основанную на имени класса, поэтому оно может не работать для всех ассоциаций, особенно для тех, у которых нестандартные имена.

Вы можете отключить автоматическое определение обратных ссылок, установив опцию :inverse_of в false, как показано ниже:

class Tagging < ActiveRecord::Base
  belongs_to :tag, inverse_of: false
end

Вложенные ассоциации

Вы можете указать любую ассоциацию с опцией :through, включая ассоциацию, у которой есть сама опция :through. Например:

class Author < ActiveRecord::Base
  has_many :posts
  has_many :comments, through: :posts
  has_many :commenters, through: :comments
end

class Post < ActiveRecord::Base
  has_many :comments
end

class Comment < ActiveRecord::Base
  belongs_to :commenter
end

@author = Author.first
@author.commenters # => People who commented on posts written by the author

Эквивалентный способ настройки этой ассоциации:

class Author < ActiveRecord::Base
  has_many :posts
  has_many :commenters, through: :posts
end

class Post < ActiveRecord::Base
  has_many :comments
  has_many :commenters, through: :comments
end

class Comment < ActiveRecord::Base
  belongs_to :commenter
end

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

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

Полиморфные ассоциации моделей не ограничены типами моделей, с которыми они могут быть связаны. Вместо этого они определяют интерфейс, которому должна соответствовать ассоциация has_many.

class Asset < ActiveRecord::Base
  belongs_to :attachable, polymorphic: true
end

class Post < ActiveRecord::Base
  has_many :assets, as: :attachable         # The :as option specifies the polymorphic interface to use.
end

@asset.attachable = @post

Это работает с использованием столбца типа в дополнение к внешнему ключу для указания связанной записи. В примере с активами вам потребуется столбец с целочисленным типом attachable_id и строковым столбцом attachable_type.

Использование полиморфных ассоциаций в сочетании с наследованием одной таблицы (STI) немного сложно. Для корректной работы ассоциаций убедитесь, что вы сохраняете базу модели для моделей STI в столбце типа полиморфной ассоциации. Чтобы продолжить пример с активами, предположим, что гостевые записи и пользовательские записи используют таблицу записей для STI. В этом случае в таблице записей должен быть столбец type.

Примечание: метод attachable_type= вызывается при назначении attachable. class_name объекта attachable передаётся как String.

class Asset < ActiveRecord::Base
  belongs_to :attachable, polymorphic: true

  def attachable_type=(class_name)
     super(class_name.constantize.base_class.to_s)
  end
end

class Post < ActiveRecord::Base
  # because we store "Post" in attachable_type now dependent: :destroy will work
  has_many :assets, as: :attachable, dependent: :destroy
end

class GuestPost < Post
end

class MemberPost < Post
end

Кэширование

Все методы основаны на простом принципе кэширования, которое сохранит результат последнего запроса, если специально не указано иное. Кэш даже совместно используется между методами, чтобы сделать использование макро-добавляемых методов ещё более дешёвым, не беспокоясь слишком сильно о производительности с первого раза.

project.milestones             # fetches milestones from the database
project.milestones.size        # uses the milestone cache
project.milestones.empty?      # uses the milestone cache
project.milestones.reload.size # fetches milestones from the database
project.milestones             # uses the milestone cache

Жадное загрузка ассоциаций

Жадная загрузка — способ поиска объектов определённого класса и нескольких именованных ассоциаций. Это один из самых простых способов предотвратить проблему N+1, в которой извлечение 100 записей, каждая из которых должна отображать автора, вызывает 101 запрос к базе данных. Благодаря жадной загрузке количество запросов будет уменьшено с 101 до 2.

class Post < ActiveRecord::Base
  belongs_to :author
  has_many   :comments
end

Рассмотрим следующий цикл, используя вышеупомянутый класс:

Post.all.each do |post|
  puts "Post:            " + post.title
  puts "Written by:      " + post.author.name
  puts "Last comment on: " + post.comments.first.created_on
end

Для итерации по этим ста записям мы сгенерируем 201 запрос к базе данных. Давайте сначала оптимизируем его для извлечения автора:

Post.includes(:author).each do |post|

Это ссылается на имя ассоциации belongs_to, которая также использовала символ :author. После загрузки записей find соберет author_id из каждой записи и загрузит все ссылающиеся авторов одним запросом. Это сократит количество запросов с 201 до 102.

Мы можем улучшить ситуацию ещё больше, указав обе ассоциации в методе поиска:

Post.includes(:author, :comments).each do |post|

Это загрузит все комментарии одним запросом. Это уменьшит общее количество запросов до 3. В общем случае количество запросов будет равно 1 плюс количество именованных ассоциаций (если некоторые из ассоциаций являются полиморфными belongs_to — см. ниже).

Для включения глубокой иерархии ассоциаций используйте хэш:

Post.includes(:author, { comments: { author: :gravatar } }).each do |post|

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

Вся эта мощь не должна вводить вас в заблуждение, что вы можете извлечь огромные объёмы данных без штрафа за производительность просто потому, что вы уменьшили количество запросов. Базе данных всё равно нужно отправлять все данные в Active Record, и их всё равно нужно обработать. Поэтому это не панацея от проблем с производительностью, но это отличный способ сократить количество запросов в такой ситуации, как описанная выше.

Поскольку загружается только одна таблица за раз, условия или упорядочивания не могут ссылаться на таблицы, кроме основной. В этом случае Active Record возвращается к ранее использовавшейся стратегии на основе LEFT OUTER JOIN. Например:

Post.includes([:author, :comments]).where(['comments.approved = ?', true])

Это приведёт к одному SQL-запросу со соединениями в духе: LEFT OUTER JOIN comments ON comments.post_id = posts.id и LEFT OUTER JOIN authors ON authors.id = posts.author_id. Обратите внимание, что использование таких условий может иметь непредвиденные последствия. В приведённом выше примере записи, у которых нет одобренных комментариев, вообще не возвращаются, так как условия применяются к SQL-запросу в целом, а не только к ассоциации.

Для разбора ссылок на столбцы для этого отката, например, order: "author.name DESC" будет работать, но order: "name DESC" нет.

Если вы хотите загрузить все записи (включая записи без одобренных комментариев), напишите свой собственный LEFT OUTER JOIN запрос, используя ON:

Post.joins("LEFT OUTER JOIN comments ON comments.post_id = posts.id AND comments.approved = '1'")

В этом случае обычно более естественно включить ассоциацию, имеющую определённые условия:

class Post < ActiveRecord::Base
  has_many :approved_comments, -> { where(approved: true) }, class_name: 'Comment'
end

Post.includes(:approved_comments)

Это загрузит записи и жадно загрузит ассоциацию approved_comments, содержащую только те комментарии, которые были одобрены.

Если вы жадно загружаете ассоциацию со заданной опцией :limit, она будет проигнорирована, и будут возвращены все связанные объекты:

class Picture < ActiveRecord::Base
  has_many :most_recent_comments, -> { order('id DESC').limit(10) }, class_name: 'Comment'
end

Picture.includes(:most_recent_comments).first.most_recent_comments # => returns all associated comments.

Жадная загрузка поддерживается для полиморфных ассоциаций.

class Address < ActiveRecord::Base
  belongs_to :addressable, polymorphic: true
end

Вызов, который пытается жадно загрузить модель адреса

Address.includes(:addressable)

Это выполнит один запрос для загрузки адресов и загрузит addressable одним запросом на тип addressable. Например, если все addressable — это либо тип Person, либо тип Company, то всего будет выполнено 3 запроса. Список типов addressable для загрузки определяется на основе загруженных адресов. Это не поддерживается, если Active Record должен вернуться к предыдущей реализации жадной загрузки, и вызовет ActiveRecord::EagerLoadPolymorphicError. Причина в том, что тип родительской модели — это значение столбца, поэтому соответствующее имя таблицы не может быть включено в FROM/JOIN клаузы этого запроса.

Псевдонимы таблиц

Active Record использует псевдонимы таблиц в случае, если таблица упоминается несколько раз в соединении. Если таблица упоминается только один раз, используется стандартное имя таблицы. Во второй раз таблица алиасится как #{reflection_name}_#{parent_table_name}. Индексы добавляются для любых последующих упоминаний имени таблицы.

Post.joins(:comments)
# => SELECT ... FROM posts INNER JOIN comments ON ...
Post.joins(:special_comments) # STI
# => SELECT ... FROM posts INNER JOIN comments ON ... AND comments.type = 'SpecialComment'
Post.joins(:comments, :special_comments) # special_comments is the reflection name, posts is the parent table name
# => SELECT ... FROM posts INNER JOIN comments ON ... INNER JOIN comments special_comments_posts

Пример древовидной структуры:

TreeMixin.joins(:children)
# => SELECT ... FROM mixins INNER JOIN mixins childrens_mixins ...
TreeMixin.joins(children: :parent)
# => SELECT ... FROM mixins INNER JOIN mixins childrens_mixins ...
                            INNER JOIN parents_mixins ...
TreeMixin.joins(children: {parent: :children})
# => SELECT ... FROM mixins INNER JOIN mixins childrens_mixins ...
                            INNER JOIN parents_mixins ...
                            INNER JOIN mixins childrens_mixins_2

Таблицы Has and Belongs to Many используют ту же идею, но добавляют суффикс _join:

Post.joins(:categories)
# => SELECT ... FROM posts INNER JOIN categories_posts ... INNER JOIN categories ...
Post.joins(categories: :posts)
# => SELECT ... FROM posts INNER JOIN categories_posts ... INNER JOIN categories ...
                           INNER JOIN categories_posts posts_categories_join INNER JOIN posts posts_categories
Post.joins(categories: {posts: :categories})
# => SELECT ... FROM posts INNER JOIN categories_posts ... INNER JOIN categories ...
                           INNER JOIN categories_posts posts_categories_join INNER JOIN posts posts_categories
                           INNER JOIN categories_posts categories_posts_join INNER JOIN categories categories_posts_2

Если вы хотите указать собственные пользовательские соединения с помощью метода ActiveRecord::QueryMethods#joins, эти имена таблиц будут иметь приоритет над жадными ассоциациями:

Post.joins(:comments).joins("inner join comments ...")
# => SELECT ... FROM posts INNER JOIN comments_posts ON ... INNER JOIN comments ...
Post.joins(:comments, :special_comments).joins("inner join comments ...")
# => SELECT ... FROM posts INNER JOIN comments comments_posts ON ...
                           INNER JOIN comments special_comments_posts ...
                           INNER JOIN comments ...

Псевдонимы таблиц автоматически усекаются в соответствии с максимальной длиной идентификаторов таблиц в соответствии со спецификой базы данных.

Модули

По умолчанию ассоциации будут искать объекты в текущем объеме модуля. Рассмотрим:

module MyApplication
  module Business
    class Firm < ActiveRecord::Base
      has_many :clients
    end

    class Client < ActiveRecord::Base; end
  end
end

Когда Firm#clients вызывается, он, в свою очередь, вызовет MyApplication::Business::Client.find_all_by_firm_id(firm.id). Если вы хотите ассоциировать с классом в другом объеме модуля, это можно сделать, указав полное имя класса.

module MyApplication
  module Business
    class Firm < ActiveRecord::Base; end
  end

  module Billing
    class Account < ActiveRecord::Base
      belongs_to :firm, class_name: "MyApplication::Business::Firm"
    end
  end
end

Взаимные ассоциации

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

class Dungeon < ActiveRecord::Base
  has_many :traps
  has_one :evil_wizard
end

class Trap < ActiveRecord::Base
  belongs_to :dungeon
end

class EvilWizard < ActiveRecord::Base
  belongs_to :dungeon
end

Ассоциация traps в модели Dungeon и ассоциация dungeon в модели Trap являются обратными друг другу, и обратной ассоциацией dungeon в модели EvilWizard является ассоциация evil_wizard в модели Dungeon (и наоборот). По умолчанию Active Record может угадать обратную ассоциацию на основе имени класса. Результат следующий:

d = Dungeon.first
t = d.traps.first
d.object_id == t.dungeon.object_id # => true

Экземпляры Dungeon d и t.dungeon в приведенном выше примере ссылаются на один и тот же экземпляр в памяти, поскольку ассоциация соответствует имени класса. Результат был бы таким же, если бы мы добавили :inverse_of к нашим определениям моделей:

class Dungeon < ActiveRecord::Base
  has_many :traps, inverse_of: :dungeon
  has_one :evil_wizard, inverse_of: :dungeon
end

class Trap < ActiveRecord::Base
  belongs_to :dungeon, inverse_of: :traps
end

class EvilWizard < ActiveRecord::Base
  belongs_to :dungeon, inverse_of: :evil_wizard
end

Для получения дополнительной информации см. документацию по параметру :inverse_of.

Удаление из ассоциаций

Зависимые ассоциации

has_many, has_one и belongs_to ассоциации поддерживают параметр :dependent. Это позволяет указать, что связанные записи должны быть удалены при удалении владельца.

Например:

class Author
  has_many :posts, dependent: :destroy
end
Author.find(1).destroy # => Will destroy all of the author's posts, too

Параметр :dependent может иметь разные значения, которые определяют, как выполняется удаление. Для получения дополнительной информации см. документацию по этому параметру для разных типов ассоциаций. При отсутствии параметра поведение заключается в том, чтобы не предпринимать никаких действий со связанными записями при уничтожении записи.

Обратите внимание, что :dependent реализовано с помощью системы обратных вызовов Rails, которая работает путем обработки обратных вызовов в определенном порядке. Следовательно, другие объявленные до или после параметра :dependent обратные вызовы могут повлиять на его работу.

Обратите внимание, что параметр :dependent игнорируется для ассоциаций has_one :through.

Удалить или уничтожить?

has_many и has_and_belongs_to_many ассоциации имеют методы destroy, delete, destroy_all и delete_all.

Для has_and_belongs_to_many, delete и destroy одинаковы: они приводят к удалению записей в таблице соединения.

Для has_many, destroy и destroy_all всегда вызывают метод destroy удаляемой(ых) записи(ей), чтобы обработать обратные вызовы. Однако delete и delete_all либо выполняют удаление в соответствии со стратегией, указанной параметром :dependent, или, если параметр :dependent не задан, следует стратегии по умолчанию. Стратегия по умолчанию - ничего не делать (оставить внешние ключи со значениями родительских id), за исключением has_many :through, где стратегия по умолчанию - delete_all (удалить записи соединения без запуска их обратных вызовов).

Также есть метод clear, который такой же, как delete_all, за исключением того, что он возвращает ассоциацию, а не записи, которые были удалены.

Что удаляется?

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

Ответ заключается в том, что предполагается, что удаление по ассоциации заключается в удалении связи между владельцем и связанным(ыми) объектом(ами), а не обязательно самих связанных объектов. Итак, с has_and_belongs_to_many и has_many :through, записи в таблице соединения будут удалены, но сами связанные записи — нет.

Это имеет смысл, если подумать: если вы вызовете post.tags.delete(Tag.find_by(name: 'food')), вы захотите разорвать связь метки «еда» со статьей, а не удалять саму метку из базы данных.

Однако есть примеры, где эта стратегия не имеет смысла. Например, предположим, что у человека есть много проектов, а каждый проект имеет много задач. Если мы удалили одну из задач человека, мы, вероятно, не захотим удалить проект. В этой ситуации метод удаления фактически не сработает: он может быть использован только в том случае, если ассоциация в модели соединения является belongs_to. В других ситуациях ожидается, что вы будете выполнять операции непосредственно на связанных записях или на ассоциации :through.

В случае обычной has_many нет различия между «связанными записями» и «связью», поэтому есть только один вариант того, что будет удалено.

В случае has_and_belongs_to_many и has_many :through, если вы хотите удалить связанные записи сами, вы всегда можете сделать что-то вроде person.tasks.each(&:destroy).

Безопасность типов с ActiveRecord::AssociationTypeMismatch

Если вы попытаетесь назначить объект ассоциации, которая не соответствует выведенному или указанному :class_name, вы получите ActiveRecord::AssociationTypeMismatch.

Параметры

Все макросы ассоциаций можно специализировать с помощью параметров. Это делает случаи более сложными, чем простые и предсказуемые.

END_OF_DOCUMENT_MARKER

Общедоступные методы экземпляра

belongs_to(name, scope = nil, **options) Показать исходный код
# File activerecord/lib/active_record/associations.rb, line 1789
def belongs_to(name, scope = nil, **options)
  reflection = Builder::BelongsTo.build(self, name, scope, options)
  Reflection.add_reflection self, name, reflection
end

Указывает одно-к-одному ассоциацию с другим классом. Этот метод следует использовать только в том случае, если в этом классе есть внешний ключ. Если внешний ключ находится в другом классе, то следует использовать has_one вместо него. Также см. обзор ActiveRecord::Associations::ClassMethods о том, когда использовать has_one, а когда belongs_to.

Будут добавлены методы для извлечения и запроса единственного связанного объекта, для которого у этого объекта есть идентификатор:

association — это заполнитель для символа, переданного в качестве аргумента name, поэтому belongs_to :author добавит, среди прочего, author.nil?.

association

Возвращает связанный объект. nil возвращается, если ничего не найдено.

association=(associate)

Присваивает объект associate, извлекает первичный ключ и устанавливает его как внешний ключ. Никаких изменений или удалений существующих записей не происходит.

build_association(attributes = {})

Возвращает новый объект связанного типа, который был инициализирован с attributes и связан с этим объектом через внешний ключ, но еще не сохранён.

create_association(attributes = {})

Возвращает новый объект связанного типа, который был инициализирован с attributes, связан с этим объектом через внешний ключ и уже сохранён (если он прошёл проверку).

create_association!(attributes = {})

Делает то же самое, что и create_association, но вызывает ActiveRecord::RecordInvalid, если запись некорректна.

reload_association

Возвращает связанный объект, принуждая к чтению из базы данных.

association_changed?

Возвращает true, если новый связанный объект был назначен, и следующее сохранение обновит внешний ключ.

association_previously_changed?

Возвращает true, если предыдущее сохранение обновило ассоциацию для ссылки на новый связанный объект.

Пример

Класс Post объявляет belongs_to :author, который добавит:

  • Post#author (аналогично Author.find(author_id))

  • Post#author=(author) (аналогично post.author_id = author.id)

  • Post#build_author (аналогично post.author = Author.new)

  • Post#create_author (аналогично post.author = Author.new; post.author.save; post.author)

  • Post#create_author! (аналогично post.author = Author.new; post.author.save!; post.author)

  • Post#reload_author

  • Post#author_changed?

  • Post#author_previously_changed?

Объявление также может включать хэш options, чтобы специализировать поведение ассоциации.

Скопы

Вы можете передать второй аргумент scope в качестве вызываемого объекта (т.е. proc или lambda), чтобы получить конкретную запись или настроить сгенерированный запрос при доступе к связанному объекту.

Примеры скопов:

belongs_to :firm, -> { where(id: 2) }
belongs_to :user, -> { joins(:friends) }
belongs_to :level, ->(game) { where("game_level > ?", game.current_level) }

Параметры

:class_name

Укажите имя класса ассоциации. Используйте его только в том случае, если это имя нельзя вывести из имени ассоциации. Так, belongs_to :author по умолчанию будет связан с классом Author, но если реальное имя класса — Person, вам нужно указать его с помощью этого параметра.

:foreign_key

Укажите внешний ключ, используемый для ассоциации. По умолчанию это определяется по имени ассоциации с суффиксом «_id». Таким образом, класс, который определяет ассоциацию belongs_to :person, будет использовать «person_id» в качестве значения по умолчанию :foreign_key. Аналогично, belongs_to :favorite_person, class_name: "Person" будет использовать внешний ключ «favorite_person_id».

Если вы собираетесь изменять ассоциацию (а не только читать из неё), то рекомендуется установить параметр :inverse_of.

:foreign_type

Укажите столбец, используемый для хранения типа связанного объекта, если это полиморфная ассоциация. По умолчанию это определяется по имени ассоциации с суффиксом «_type». Таким образом, класс, который определяет ассоциацию belongs_to :taggable, polymorphic: true, будет использовать «taggable_type» в качестве значения по умолчанию :foreign_type.

:primary_key

Укажите метод, который возвращает первичный ключ связанного объекта, используемый для ассоциации. По умолчанию это id.

:dependent

Если задано значение :destroy, связанный объект уничтожается при уничтожении этого объекта. Если задано значение :delete, связанный объект удаляется без вызова метода destroy. Если задано :destroy_async, связанный объект запланирован на уничтожение в фоновом задании. Этот параметр не следует указывать, когда belongs_to используется в сочетании с отношением has_many в другом классе из-за потенциального оставления «сирот».

:counter_cache

Кэширует количество принадлежащих объектов в связанном классе с помощью CounterCache::ClassMethods#increment_counter и CounterCache::ClassMethods#decrement_counter. Кэш счётчика увеличивается при создании объекта этого класса и уменьшается при его уничтожении. Для этого требуется столбец под названием #{table_name}_count (например, comments_count для класса Comment) в связанном классе (например, в классе Post) — для этого создается миграция для #{table_name}_count, (так что Post.comments_count вернёт сохранённый счёт). Вы также можете указать столбец счётчика, указав имя столбца вместо значения true/false в этом параметре (например, counter_cache: :my_custom_counter). Примечание: Указание параметра counter_cache добавит его в список только для чтения атрибутов данной модели с помощью attr_readonly.

:polymorphic

Укажите, что эта ассоциация является полиморфной, передав true. Примечание: Если вы включили параметр counter_cache, то, возможно, вам потребуется добавить атрибут counter_cache в список attr_readonly в связанных классах (например, class Post; attr_readonly :comments_count; end).

:validate

Когда установлено значение true, проверяет новые объекты, добавленные в ассоциацию, при сохранении родительского объекта. По умолчанию false. Если вы хотите гарантировать, что связанные объекты перепроверяются при каждом обновлении, используйте validates_associated.

:autosave

Если true, всегда сохраняйте связанный объект или уничтожайте его, если он помечен на уничтожение, при сохранении родительского объекта. Если false, никогда не сохраняйте и не уничтожайте связанный объект. По умолчанию сохраняйте связанный объект только в случае, если это новая запись.

Обратите внимание, что NestedAttributes::ClassMethods#accepts_nested_attributes_for устанавливает :autosave на true.

:touch

Если true, связанный объект будет отмечен (атрибуты updated_at/on будут установлены на текущее время) при сохранении или уничтожении этой записи. Если вы укажете символ, этот атрибут будет обновлен текущим временем в дополнение к атрибуту updated_at/on. Обратите внимание, что при использовании параметра touch не выполняется проверка, и выполняются только обратные вызовы after_touch, after_commit и after_rollback.

:inverse_of

Указывает имя ассоциации has_one или has_many в связанном объекте, являющейся обратной ассоциацией belongs_to. См. обзор ActiveRecord::Associations::ClassMethods по двунаправленным ассоциациям для получения дополнительной информации.

:optional

Если установлено значение true, проверка наличия ассоциации не будет выполняться.

:required

Если установлено значение true, проверка наличия ассоциации также будет выполняться. Эта проверка будет выполняться для самой ассоциации, а не для идентификатора. Можно использовать :inverse_of для избежания дополнительного запроса при проверке. ПРИМЕЧАНИЕ: required устанавливается на true по умолчанию и устарело. Если вы не хотите проверять наличие ассоциации, используйте optional: true.

:default

Укажите вызываемый объект (например, proc или lambda), чтобы указать, что ассоциация должна быть инициализирована определённой записью перед проверкой.

:strict_loading

Принудительно загружает связанную запись каждый раз, когда она загружается через эту ассоциацию.

:ensuring_owner_was

Указывает метод экземпляра, который должен быть вызван у владельца. Метод должен возвращать true, чтобы связанные записи могли быть удалены в фоновом задании.

Примеры параметров:

belongs_to :firm, foreign_key: "client_of"
belongs_to :person, primary_key: "name", foreign_key: "person_name"
belongs_to :author, class_name: "Person", foreign_key: "author_id"
belongs_to :valid_coupon, ->(o) { where "discounts > ?", o.payments_count },
                          class_name: "Coupon", foreign_key: "coupon_id"
belongs_to :attachable, polymorphic: true
belongs_to :project, -> { readonly }
belongs_to :post, counter_cache: true
belongs_to :comment, touch: true
belongs_to :company, touch: :employees_last_updated_at
belongs_to :user, optional: true
belongs_to :account, default: -> { company.account }
belongs_to :account, strict_loading: true
has_and_belongs_to_many(name, scope = nil, **options, &extension) Показать исходный код
# File activerecord/lib/active_record/associations.rb, line 1961
        def has_and_belongs_to_many(name, scope = nil, **options, &extension)
          habtm_reflection = ActiveRecord::Reflection::HasAndBelongsToManyReflection.new(name, scope, options, self)

          builder = Builder::HasAndBelongsToMany.new name, self, options

          join_model = builder.through_model

          const_set join_model.name, join_model
          private_constant join_model.name

          middle_reflection = builder.middle_reflection join_model

          Builder::HasMany.define_callbacks self, middle_reflection
          Reflection.add_reflection self, middle_reflection.name, middle_reflection
          middle_reflection.parent_reflection = habtm_reflection

          include Module.new {
            class_eval <<-RUBY, __FILE__, __LINE__ + 1
              def destroy_associations
                association(:#{middle_reflection.name}).delete_all(:delete_all)
                association(:#{name}).reset
                super
              end
            RUBY
          }

          hm_options = {}
          hm_options[:through] = middle_reflection.name
          hm_options[:source] = join_model.right_reflection.name

          [:before_add, :after_add, :before_remove, :after_remove, :autosave, :validate, :join_table, :class_name, :extend, :strict_loading].each do |k|
            hm_options[k] = options[k] if options.key? k
          end

          has_many name, scope, **hm_options, &extension
          _reflections[name.to_s].parent_reflection = habtm_reflection
        end

Указывает на отношение "многие ко многим" с другим классом. Это связывает два класса через промежуточную таблицу соединения. Если таблица соединения не указана явно в качестве параметра, она определяется по лексикографическому порядку имён классов. Так, связь между Developer и Project даст имя таблицы соединения "developers_projects", так как "D" предшествует "P" в алфавитном порядке. Обратите внимание, что этот порядок рассчитывается с использованием оператора < для String. Это означает, что если строки имеют разную длину, и строки равны при сравнении до минимальной длины, то более длинная строка считается имеющей более высокий лексикографический приоритет, чем более короткая. Например, ожидается, что таблицы "paper_boxes" и "papers" сгенерируют имя таблицы соединения "papers_paper_boxes" из-за длины имени "paper_boxes", но фактически она сгенерирует имя таблицы соединения "paper_boxes_papers". Будьте внимательны к этому нюансу и используйте параметр :join_table если вам нужно другое имя. Если таблицы имеют общий префикс, он будет отображаться только один раз в начале. Например, таблицы "catalog_categories" и "catalog_products" генерируют имя таблицы соединения "catalog_categories_products".

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

class CreateDevelopersProjectsJoinTable < ActiveRecord::Migration[7.0]
  def change
    create_join_table :developers, :projects
  end
end

Также рекомендуется добавить индексы к каждому из этих столбцов для ускорения процесса объединения. Однако в MySQL рекомендуется добавить составной индекс для обоих столбцов, так как MySQL использует только один индекс на таблицу во время поиска.

Добавляет следующие методы для извлечения и запроса:

collection — это заполнитель для символа, переданного в качестве аргумента name, поэтому has_and_belongs_to_many :categories добавит, среди прочего, categories.empty?.

collection

Возвращает Relation всех связанных объектов. Если не найдено ни одного, возвращается пустой Relation.

collection<<(object, …)

Добавляет один или несколько объектов в коллекцию, создавая связи в таблице соединения (collection.push и collection.concat являются псевдонимами для этого метода). Обратите внимание, что эта операция мгновенно выполняет обновление SQL без ожидания вызова сохранения или обновления родительского объекта, если родительский объект не является новым записями.

collection.delete(object, …)

Удаляет один или несколько объектов из коллекции, удаляя их связи из таблицы соединения. Это не уничтожает объекты.

collection.destroy(object, …)

Удаляет один или несколько объектов из коллекции, выполняя destroy для каждой связи в таблице соединения, переопределяя любой параметр dependent. Это не уничтожает объекты.

collection=objects

Заменяет содержимое коллекции, удаляя и добавляя объекты по мере необходимости.

collection_singular_ids

Возвращает массив идентификаторов связанных объектов.

collection_singular_ids=ids

Заменяет коллекцию объектами, идентифицированными первичными ключами в ids.

collection.clear

Удаляет все объекты из коллекции. Это не уничтожает объекты.

collection.empty?

Возвращает true если связанных объектов нет.

collection.size

Возвращает количество связанных объектов.

collection.find(id)

Ищет связанный объект, соответствующий id и удовлетворяющий условию, что он должен быть связан с этим объектом. Использует те же правила, что и ActiveRecord::FinderMethods#find.

collection.exists?(…)

Проверяет, существует ли связанный объект с заданными условиями. Использует те же правила, что и ActiveRecord::FinderMethods#exists?.

collection.build(attributes = {})

Возвращает новый объект типа коллекции, который был инициализирован с attributes и связан с этим объектом через таблицу соединения, но еще не сохранён.

collection.create(attributes = {})

Возвращает новый объект типа коллекции, который был инициализирован с attributes, связан с этим объектом через таблицу соединения и уже сохранён (если он прошёл валидацию).

collection.reload

Возвращает Relation всех связанных объектов, принудительно выполняя чтение из базы данных. Если не найдено ни одного, возвращается пустой Relation.

Пример

Класс Developer объявляет has_and_belongs_to_many :projects, что добавит:

  • Developer#projects

  • Developer#projects<<

  • Developer#projects.delete

  • Developer#projects.destroy

  • Developer#projects=

  • Developer#project_ids

  • Developer#project_ids=

  • Developer#projects.clear

  • Developer#projects.empty?

  • Developer#projects.size

  • Developer#projects.find(id)

  • Developer#projects.exists?(...)

  • Developer#projects.build (аналогично Project.new(developer_id: id))

  • Developer#projects.create (аналогично c = Project.new(developer_id: id); c.save; c)

  • Developer#projects.reload

Объявление может включать хэш options, чтобы специализировать поведение ассоциации.

Схемы

Вы можете передать второй аргумент scope как вызываемый объект (например, proc или lambda), чтобы получить определённый набор записей или настроить генерируемый запрос при доступе к связанной коллекции.

Примеры схем:

has_and_belongs_to_many :projects, -> { includes(:milestones, :manager) }
has_and_belongs_to_many :categories, ->(post) {
  where("default_category = ?", post.default_category)
}

Расширения

Аргумент extension позволяет передавать блок в ассоциацию has_and_belongs_to_many. Это полезно для добавления новых методов поиска, создания и других методов типа фабрики, которые будут использоваться в рамках ассоциации.

Примеры расширений:

has_and_belongs_to_many :contractors do
  def find_or_create_by_name(name)
    first_name, last_name = name.split(" ", 2)
    find_or_create_by(first_name: first_name, last_name: last_name)
  end
end

Параметры

:class_name

Укажите имя класса ассоциации. Используйте его только в том случае, если имя нельзя определить по имени ассоциации. Так, has_and_belongs_to_many :projects по умолчанию будет связан с классом Project, но если фактическое имя класса — SuperProject, вам нужно будет указать его с помощью этого параметра.

:join_table

Укажите имя таблицы соединения, если значение по умолчанию, основанное на лексикографическом порядке, не подходит. ПРЕДУПРЕЖДЕНИЕ: Если вы перезаписываете имя таблицы любого из классов, метод table_name ДОЛЖЕН быть объявлен под любым объявлением has_and_belongs_to_many для корректной работы.

:foreign_key

Укажите внешний ключ, используемый для ассоциации. По умолчанию он предполагается как имя этого класса в нижнем регистре с добавленным "_id". Так, класс Person, который создаёт ассоциацию has_and_belongs_to_many с классом Project, будет использовать "person_id" в качестве значения по умолчанию :foreign_key. Если вы собираетесь изменять ассоциацию (а не только читать из неё), рекомендуется установить параметр :inverse_of.

:association_foreign_key

Укажите внешний ключ, используемый для ассоциации со стороны получающего объекта. По умолчанию он предполагается как имя связанного класса в нижнем регистре с добавленным "_id". Например, если класс Person создает ассоциацию has_and_belongs_to_many с классом Project, ассоциация будет использовать "project_id" в качестве значения по умолчанию :association_foreign_key.

:validate

Если установлено в true, валидирует новые объекты, добавленные в ассоциацию, при сохранении родительского объекта. По умолчанию true. Если требуется гарантировать повторную валидацию связанных объектов при каждом обновлении, используйте validates_associated.

:autosave

Если true, всегда сохраняет связанные объекты или уничтожает их, если они помечены для уничтожения, при сохранении родительского объекта. Если false, никогда не сохраняет или не уничтожает связанные объекты. По умолчанию сохраняются только новые связанные объекты.

Обратите внимание, что NestedAttributes::ClassMethods#accepts_nested_attributes_for устанавливает :autosave в true.

:strict_loading

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

Примеры параметров:

has_and_belongs_to_many :projects
has_and_belongs_to_many :projects, -> { includes(:milestones, :manager) }
has_and_belongs_to_many :nations, class_name: "Country"
has_and_belongs_to_many :categories, join_table: "prods_cats"
has_and_belongs_to_many :categories, -> { readonly }
has_and_belongs_to_many :categories, strict_loading: true
has_many(name, scope = nil, **options, &extension) Показать исходный код
# File activerecord/lib/active_record/associations.rb, line 1465
def has_many(name, scope = nil, **options, &extension)
  reflection = Builder::HasMany.build(self, name, scope, options, &extension)
  Reflection.add_reflection self, name, reflection
end

Указывает ассоциацию «один ко многим». Будут добавлены следующие методы для извлечения и запроса коллекций связанных объектов:

collection является заменителем символа, переданного в качестве аргумента name, поэтому has_many :clients добавит, среди прочего, clients.empty?.

collection

Возвращает Relation всех связанных объектов. Если не найдено ни одного, возвращается пустой Relation.

collection<<(object, …)

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

collection.delete(object, …)

Удаляет один или несколько объектов из коллекции, установив их внешние ключи на NULL. Объекты также будут дополнительно уничтожены, если они связаны с dependent: :destroy, и удалены, если они связаны с dependent: :delete_all.

Если используется опция :through, то записи соединения удаляются по умолчанию (а не обнуляются), но вы можете указать dependent: :destroy или dependent: :nullify для переопределения этого.

collection.destroy(object, …)

Удаляет один или несколько объектов из коллекции, выполняя destroy для каждой записи, независимо от опции зависимостей, гарантируя, что будут выполнены обратные вызовы.

Если используется опция :through, то вместо самих объектов будут уничтожены записи соединения.

collection=objects

Заменяет содержимое коллекций, удаляя и добавляя объекты по мере необходимости. Если опция :through имеет значение true, обратные вызовы в моделях соединения вызываются, за исключением обратных вызовов удаления, поскольку удаление по умолчанию выполняется напрямую. Вы можете указать dependent: :destroy или dependent: :nullify для переопределения этого.

collection_singular_ids

Возвращает массив идентификаторов связанных объектов.

collection_singular_ids=ids

Заменяет коллекцию объектами, идентифицированными первичными ключами в ids. Этот метод загружает модели и вызывает collection=. См. выше.

collection.clear

Удаляет все объекты из коллекции. Это уничтожает связанные объекты, если они связаны с dependent: :destroy, удаляет их напрямую из базы данных, если dependent: :delete_all, в противном случае устанавливает их внешние ключи на NULL. Если опция :through имеет значение true, обратные вызовы удаления не вызываются для моделей соединения. Модели соединения удаляются напрямую.

collection.empty?

Возвращает true, если нет связанных объектов.

collection.size

Возвращает количество связанных объектов.

collection.find(…)

Находит связанный объект по тем же правилам, что и ActiveRecord::FinderMethods#find.

collection.exists?(…)

Проверяет, существует ли связанный объект с заданными условиями. Использует те же правила, что и ActiveRecord::FinderMethods#exists?.

collection.build(attributes = {}, …)

Возвращает один или несколько новых объектов типа коллекции, которые были созданы с attributes и связаны с этим объектом через внешний ключ, но еще не сохранены.

collection.create(attributes = {})

Возвращает новый объект типа коллекции, который был создан с attributes, связан с этим объектом через внешний ключ и уже сохранен (если он прошел валидацию). Примечание: Это работает только в том случае, если базовая модель уже существует в БД, а не если это новая (несохраненная) запись!

collection.create!(attributes = {})

Делает то же самое, что и collection.create, но вызывает ActiveRecord::RecordInvalid, если запись недействительна.

collection.reload

Возвращает Relation всех связанных объектов, принудительно выполнив чтение из базы данных. Если не найдено ни одного, возвращается пустой Relation.

Пример

Класс Firm объявляет has_many :clients, что добавит:

  • Firm#clients (аналогично Client.where(firm_id: id))

  • Firm#clients<<

  • Firm#clients.delete

  • Firm#clients.destroy

  • Firm#clients=

  • Firm#client_ids

  • Firm#client_ids=

  • Firm#clients.clear

  • Firm#clients.empty? (аналогично firm.clients.size == 0)

  • Firm#clients.size (аналогично Client.count "firm_id = #{id}")

  • Firm#clients.find (аналогично Client.where(firm_id: id).find(id))

  • Firm#clients.exists?(name: 'ACME') (аналогично Client.exists?(name: 'ACME', firm_id: firm.id))

  • Firm#clients.build (аналогично Client.new(firm_id: id))

  • Firm#clients.create (аналогично c = Client.new(firm_id: id); c.save; c)

  • Firm#clients.create! (аналогично c = Client.new(firm_id: id); c.save!)

  • Firm#clients.reload

Объявление также может содержать хэш options для специализации поведения ассоциации.

Свойства

Вы можете передать второй аргумент scope в виде вызываемого объекта (например, proc или lambda) для получения набора записей или настройки генерируемого запроса при доступе к связанной коллекции.

Примеры свойств:

has_many :comments, -> { where(author_id: 1) }
has_many :employees, -> { joins(:address) }
has_many :posts, ->(blog) { where("max_post_length > ?", blog.max_post_length) }

Расширения

Аргумент extension позволяет передать блок в ассоциацию has_many. Это полезно для добавления новых методов поиска, создания и других методов типа фабрики, которые можно использовать как часть ассоциации.

Примеры расширений:

has_many :employees do
  def find_or_create_by_name(name)
    first_name, last_name = name.split(" ", 2)
    find_or_create_by(first_name: first_name, last_name: last_name)
  end
end

Параметры

:class_name

Укажите имя класса ассоциации. Используйте его только в том случае, если имя нельзя вывести из имени ассоциации. Так, has_many :products по умолчанию будет связан с классом Product, но если реальное имя класса — SpecialProduct, вам нужно будет указать его с помощью этого параметра.

:foreign_key

Укажите внешний ключ, используемый для ассоциации. По умолчанию он определяется по имени этого класса в нижнем регистре с добавлением суффикса «_id». Так, класс Person, который создает ассоциацию has_many, будет использовать «person_id» в качестве значения по умолчанию :foreign_key.

Если вы собираетесь изменить ассоциацию (а не только читать из неё), рекомендуется установить параметр :inverse_of.

:foreign_type

Укажите столбец, используемый для хранения типа связанного объекта, если это полиморфная ассоциация. По умолчанию он определяется по имени полиморфной ассоциации, указанной в параметре «as», с добавлением суффикса «_type». Так, класс, определяющий ассоциацию has_many :tags, as: :taggable, будет использовать «taggable_type» в качестве значения по умолчанию :foreign_type.

:primary_key

Укажите имя столбца, используемого в качестве первичного ключа для ассоциации. По умолчанию это id.

:dependent

Управляет тем, что происходит со связанными объектами, когда их владелец уничтожается. Обратите внимание, что эти действия реализованы как колбэки, и Rails выполняет колбэки в порядке их определения. Поэтому другие подобные колбэки могут повлиять на поведение :dependent, а поведение :dependent может повлиять на другие колбэки.

  • nil ничего не делает (по умолчанию).

  • :destroy приводит к уничтожению всех связанных объектов.

  • :destroy_async уничтожает все связанные объекты в фоновой задаче. ПРЕДУПРЕЖДЕНИЕ: Не используйте этот параметр, если ассоциация поддерживается ограничениями внешнего ключа в вашей базе данных. Действия ограничений внешнего ключа будут выполняться в рамках той же транзакции, что и удаление владельца.

  • :delete_all приводит к непосредственному удалению всех связанных объектов из базы данных (так что колбэки не будут выполнены).

  • :nullify приводит к установке внешних ключей в NULL. Полиморфный тип также будет сброшен в полиморфных ассоциациях. Callbacks не выполняются.

  • :restrict_with_exception приводит к тому, что если существуют связанные записи, будет поднято исключение ActiveRecord::DeleteRestrictionError.

  • :restrict_with_error добавляет ошибку к владельцу, если существуют связанные объекты.

При использовании с параметром :through, ассоциация на модели соединения должна быть belongs_to, и удаляемыми записями являются записи соединения, а не связанные записи.

При использовании dependent: :destroy для ассоциации с фильтром, уничтожаются только объекты, соответствующие фильтру. Например, если модель Post определяет has_many :comments, -> { where published: true }, dependent: :destroy, и вызывается destroy для поста, уничтожаются только опубликованные комментарии. Это означает, что любые неопубликованные комментарии в базе данных по-прежнему будут содержать внешний ключ, указывающий на теперь удалённый пост.

:counter_cache

Этот параметр можно использовать для настройки настраиваемого :counter_cache.. Вам нужен только этот параметр, если вы изменили имя вашего :counter_cache в ассоциации belongs_to.

:as

Указывает полиморфный интерфейс (см. belongs_to).

:through

Указывает ассоциацию, через которую выполняется запрос. Это может быть любой другой тип ассоциации, включая другие :through ассоциации. Параметры :class_name, :primary_key и :foreign_key игнорируются, так как ассоциация использует отражение источника.

Если ассоциация на модели соединения является belongs_to, коллекция может быть изменена, и записи на модели :through будут автоматически создаваться и удаляться по мере необходимости. В противном случае коллекция является только для чтения, поэтому вы должны напрямую манипулировать ассоциацией :through.

Если вы собираетесь изменить ассоциацию (а не только читать из нее), рекомендуется установить параметр :inverse_of на исходной ассоциации на модели соединения. Это позволяет создавать связанные записи, которые автоматически создадут соответствующие записи модели соединения при их сохранении. (См. раздел «Модели соединения ассоциаций» выше.)

:disable_joins

Указывает, следует ли пропускать соединения для ассоциации. Если установлено в значение true, будет сгенерировано две или более запросов. Обратите внимание, что в некоторых случаях, если применяется сортировка или ограничение, оно будет выполнено в памяти из-за ограничений базы данных. Этот параметр применим только для ассоциаций `has_many :through`, так как `has_many` без `:through` не выполняют соединение.

:source

Указывает имя исходной ассоциации, используемое has_many :through запросами. Используйте его только в том случае, если имя нельзя вывести из ассоциации. has_many :subscribers, through: :subscriptions будет искать либо :subscribers или :subscriber в Subscription, если не задан :source.

:source_type

Указывает тип исходной ассоциации, используемой has_many :through запросами, где исходная ассоциация является полиморфной belongs_to.

:validate

При установке в true, валидирует новые объекты, добавленные в ассоциацию, при сохранении родительского объекта. true по умолчанию. Если необходимо убедиться, что связанные объекты перевалидируются при каждом обновлении, используйте validates_associated.

:autosave

Если true, всегда сохраняет связанные объекты или уничтожает их, если они помечены для уничтожения, при сохранении родительского объекта. Если false, никогда не сохраняет или не уничтожает связанные объекты. По умолчанию, сохраняются только новые связанные объекты. Этот параметр реализован как колбэк before_save. Поскольку колбэки выполняются в определённом порядке, связанные объекты могут потребовать явного сохранения в любых пользовательских колбэках before_save.

Обратите внимание, что NestedAttributes::ClassMethods#accepts_nested_attributes_for устанавливает :autosave в true.

:inverse_of

Указывает имя ассоциации belongs_to связанного объекта, которая является обратной к этой ассоциации has_many. Более подробную информацию см. в обзоре ActiveRecord::Associations::ClassMethods по двунаправленным ассоциациям.

:extend

Указывает модуль или массив модулей, которые будут расширены в объект ассоциации. Полезно для определения методов ассоциаций, особенно когда они должны быть общими для нескольких объектов ассоциаций.

:strict_loading

Когда установлено в true, при каждом загрузке связанной записи через эту ассоциацию выполняется жёсткая загрузка.

:ensuring_owner_was

Указывает метод экземпляра, который будет вызван на владельце. Метод должен вернуть true, чтобы связанные записи были удалены в фоновой задаче.

Примеры параметров:

has_many :comments, -> { order("posted_on") }
has_many :comments, -> { includes(:author) }
has_many :people, -> { where(deleted: false).order("name") }, class_name: "Person"
has_many :tracks, -> { order("position") }, dependent: :destroy
has_many :comments, dependent: :nullify
has_many :tags, as: :taggable
has_many :reports, -> { readonly }
has_many :subscribers, through: :subscriptions, source: :user
has_many :subscribers, through: :subscriptions, disable_joins: true
has_many :comments, strict_loading: true
has_one(name, scope = nil, **options) Показать исходный код
# File activerecord/lib/active_record/associations.rb, line 1629
def has_one(name, scope = nil, **options)
  reflection = Builder::HasOne.build(self, name, scope, options)
  Reflection.add_reflection self, name, reflection
end

Определяет одно-к-одному ассоциацию с другим классом. Этот метод следует использовать только в том случае, если другой класс содержит внешний ключ. Если текущий класс содержит внешний ключ, необходимо использовать belongs_to вместо него. См. также обзор ActiveRecord::Associations::ClassMethods о том, когда использовать has_one, а когда belongs_to.

Будут добавлены следующие методы для получения и запроса одного связанного объекта:

association — это плейсхолдер для символа, переданного в качестве аргумента name, поэтому has_one :manager добавит, среди прочего, manager.nil?.

association

Возвращает связанный объект. nil возвращается, если объект не найден.

association=(associate)

Присваивает объект associate, извлекает первичный ключ, устанавливает его как внешний ключ и сохраняет объект associate. Чтобы избежать несоответствий в базе данных, при присвоении нового объекта постоянно удаляется существующий связанный объект, даже если новый объект не сохранён в базе данных.

build_association(attributes = {})

Возвращает новый объект связанного типа, который был создан с attributes и связан с этим объектом через внешний ключ, но еще не сохранён.

create_association(attributes = {})

Возвращает новый объект связанного типа, который был создан с attributes, связан с этим объектом через внешний ключ и уже сохранён (если прошёл валидацию).

create_association!(attributes = {})

Делает то же самое, что и create_association, но вызывает ActiveRecord::RecordInvalid, если запись некорректна.

reload_association

Возвращает связанный объект, принудительно выполняя чтение из базы данных.

Пример

Класс Account объявляет has_one :beneficiary, который добавит:

  • Account#beneficiary (аналогично Beneficiary.where(account_id: id).first)

  • Account#beneficiary=(beneficiary) (аналогично beneficiary.account_id = account.id; beneficiary.save)

  • Account#build_beneficiary (аналогично Beneficiary.new(account_id: id))

  • Account#create_beneficiary (аналогично b = Beneficiary.new(account_id: id); b.save; b)

  • Account#create_beneficiary! (аналогично b = Beneficiary.new(account_id: id); b.save!; b)

  • Account#reload_beneficiary

Схемы

Можно передать второй аргумент scope как вызываемый объект (т.е. proc или lambda), чтобы получить определённую запись или настроить сгенерированный запрос при обращении к связанному объекту.

Примеры схем:

has_one :author, -> { where(comment_id: 1) }
has_one :employer, -> { joins(:company) }
has_one :latest_post, ->(blog) { where("created_at > ?", blog.enabled_at) }

Параметры

Объявление также может включать options хэш, чтобы специализировать поведение ассоциации.

Параметры:

:class_name

Укажите имя класса ассоциации. Используйте только в том случае, если имя не может быть выведено из имени ассоциации. Так has_one :manager по умолчанию будет связан с классом Manager, но если реальное имя класса — Person, необходимо указать его с помощью этого параметра.

:dependent

Управляет тем, что происходит со связанным объектом при уничтожении его владельца:

  • nil ничего не делать (по умолчанию).

  • :destroy приводит к уничтожению связанного объекта.

  • :destroy_async приводит к уничтожению связанного объекта в фоновом задании. ПРЕДУПРЕЖДЕНИЕ: Не используйте этот параметр, если ассоциация поддерживается ограничениями внешнего ключа в вашей базе данных. Действия ограничения внешнего ключа будут выполняться в рамках той же транзакции, которая удаляет владельца.

  • :delete приводит к прямому удалению связанного объекта из базы данных (так что обратные вызовы не будут выполнены).

  • :nullify приводит к установке внешнего ключа в NULL. Столбец полиморфного типа также обнуляется для полиморфных ассоциаций. Callbacks не выполняются.

  • :restrict_with_exception вызывает исключение ActiveRecord::DeleteRestrictionError, если существует связанная запись.

  • :restrict_with_error добавляет ошибку к владельцу, если существует связанный объект.

Обратите внимание, что параметр :dependent игнорируется при использовании параметра :through.

:foreign_key

Укажите внешний ключ, используемый для ассоциации. По умолчанию он предполагается по имени текущего класса в нижнем регистре и с добавленным суффиксом «_id». Так, класс Person, использующий has_one ассоциацию, будет использовать «person_id» в качестве значения по умолчанию :foreign_key.

Если вы собираетесь изменить ассоциацию (а не только читать из неё), рекомендуется установить параметр :inverse_of.

:foreign_type

Укажите столбец, используемый для хранения типа связанного объекта, если это полиморфная ассоциация. По умолчанию он предполагается по имени полиморфной ассоциации, указанной в параметре «as», с добавленным суффиксом «_type». Так, класс, определяющий has_one :tag, as: :taggable ассоциацию, будет использовать «taggable_type» в качестве значения по умолчанию :foreign_type.

:primary_key

Укажите метод, возвращающий первичный ключ, используемый для ассоциации. По умолчанию это id.

:as

Указывает полиморфный интерфейс (см. belongs_to).

:through

Указывает модель соединения, через которую выполняется запрос. Параметры для :class_name, :primary_key, и :foreign_key игнорируются, так как ассоциация использует отражение источника. Вы можете использовать запрос :through только через ассоциацию has_one или belongs_to в модели соединения.

Если ассоциация в модели соединения является belongs_to, коллекция может быть изменена, и записи в модели :through будут автоматически создаваться и удаляться по мере необходимости. В противном случае коллекция является только для чтения, поэтому вы должны изменять ассоциацию :through напрямую.

Если вы собираетесь изменить ассоциацию (а не только читать из неё), рекомендуется установить параметр :inverse_of в исходной ассоциации модели соединения. Это позволяет создавать связанные записи, которые будут автоматически создавать соответствующие записи модели соединения при их сохранении. (См. раздел «Модели соединения ассоциаций» выше.)

:disable_joins

Указывает, следует ли пропускать соединения для ассоциации. Если установлено в true, будут сгенерированы два или более запросов. Обратите внимание, что в некоторых случаях, если применяются порядок или лимит, он будет выполнен в оперативной памяти из-за ограничений базы данных. Этот параметр применим только к ассоциациям `has_one :through`, так как `has_one` сам по себе не выполняет соединение.

:source

Указывает имя исходной ассоциации, используемой запросами has_one :through. Используйте только в случае, если имя не может быть определено из ассоциации. has_one :favorite, through: :favorites будет искать :favorite в Favorite, если не задан параметр :source.

:source_type

Указывает тип исходной ассоциации, используемой запросами has_one :through , где исходная ассоциация является полиморфной belongs_to.

:validate

Когда установлено в true, проверяет новые объекты, добавленные в ассоциацию, при сохранении родительского объекта. Установлено в true по умолчанию. Если нужно убедиться, что связанные объекты перепроверяются при каждом обновлении, используйте validates_associated.

:autosave

Если true, всегда сохраняет связанный объект или уничтожает его, если он помечен для уничтожения, при сохранении родительского объекта. Если false, никогда не сохраняет или не уничтожает связанный объект. По умолчанию сохраняет связанный объект только если он новая запись.

Обратите внимание, что NestedAttributes::ClassMethods#accepts_nested_attributes_for устанавливает :autosave в true.

:inverse_of

Указывает имя ассоциации belongs_to в связанном объекте, которая является обратной этой ассоциации has_one. Для получения более подробной информации см. обзор ActiveRecord::Associations::ClassMethods по двунаправленным ассоциациям.

:required

Если установлено в true, ассоциация также будет проверять своё наличие. Это будет проверять саму ассоциацию, а не id. Можно использовать :inverse_of для избежания дополнительного запроса во время проверки.

:strict_loading

Принудительно выполняет строгое загрузка каждый раз, когда связанная запись загружается через эту ассоциацию.

:ensuring_owner_was

Указывает метод экземпляра, который должен вызываться на владельце. Метод должен возвращать true, чтобы связанные записи могли быть удалены в фоновом задании.

Примеры параметров:

has_one :credit_card, dependent: :destroy  # destroys the associated credit card
has_one :credit_card, dependent: :nullify  # updates the associated records foreign
                                              # key value to NULL rather than destroying it
has_one :last_comment, -> { order('posted_on') }, class_name: "Comment"
has_one :project_manager, -> { where(role: 'project_manager') }, class_name: "Person"
has_one :attachment, as: :attachable
has_one :boss, -> { readonly }
has_one :club, through: :membership
has_one :club, through: :membership, disable_joins: true
has_one :primary_address, -> { where(primary: true) }, through: :addressables, source: :addressable
has_one :credit_card, required: true
has_one :credit_card, strict_loading: true

© 2004–2021 David Heinemeier Hansson
Licensed under the MIT License.

Spec-Zone.ru

Настройки Оффлайн Что нового Помощь О нас
Spec-Zone .ru
спецификации, руководства, описания, API