Spec-Zone.ru › Ruby on Rails 7.1

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

Ассоциации Active Record

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

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

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

project = Project.first
project.portfolio
project.portfolio = Portfolio.first
project.reload_portfolio

project.project_manager
project.project_manager = ProjectManager.first
project.reload_project_manager

project.milestones.empty?
project.milestones.size
project.milestones
project.milestones << Milestone.first
project.milestones.delete(Milestone.first)
project.milestones.destroy(Milestone.first)
project.milestones.find(Milestone.first.id)
project.milestones.build
project.milestones.create

project.categories.empty?
project.categories.size
project.categories
project.categories << Category.first
project.categories.delete(category1)
project.categories.destroy(category1)

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

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

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

См. также «Общедоступные методы экземпляра» ниже (из belongs_to) для получения более подробной информации.

Одноэлементные ассоциации (один к одному)

                                  |            |  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

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

Мощность и ассоциации

Ассоциации Active Record могут использоваться для описания отношений один-к-одному, один-ко-многим и многие-ко-многим между моделями. Каждая модель использует ассоциацию для описания своей роли в отношении. Ассоциация 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

Примечание: Объединение или жадное загрузка таких ассоциаций невозможны, потому что эти операции выполняются до создания экземпляра. Такие ассоциации могут быть предварительно загружены, но это выполнит N+1 запросов, так как для каждого записей будет другой область видимости (аналогично предварительной загрузке полиморфных областей видимости).

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

Аналогично обычным обработчикам событий, которые прикрепляются к жизненному циклу объекта 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
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. Например:

  • firm.clients.destroy(client)

  • firm.clients.destroy(*clients)

  • firm.clients.destroy_all

Методы delete / delete_all , подобные следующим, не запускают обработчики удаления:

  • firm.clients.delete(client)

  • firm.clients.delete(*clients)

  • firm.clients.delete_all

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

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

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 — имя ассоциации):

  • 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

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

Использование полиморфных ассоциаций в сочетании с наследованием таблиц (STI) немного сложно. Для корректной работы ассоциаций убедитесь, что вы храните базовый тип модели STI в столбце типа полиморфной ассоциации. Для продолжения примера с asset, предположим, что гостевые посты и посты пользователей используют таблицу постов для 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

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

Address.includes(:addressable)

Это выполнит один запрос для загрузки адресов и загрузит addressables одним запросом на каждый тип addressable. Например, если все addressables являются либо класса 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 и руководство по ассоциациям Active Record Active Record Associations guide.

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

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

Ассоциации 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 не указан, будут следовать умолчанию. Умолчание — ничего не делать (оставить внешние ключи со значениями родительских идентификаторов), за исключением 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')), вы захотите отсоединить тег «еда» от записи, а не удалить сам тег из базы данных.

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

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

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

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

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

Параметры

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

Методы публичного экземпляра

belongs_to(name, scope = nil, **options) Показать исходный код
# File activerecord/lib/active_record/associations.rb, line 1886
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)

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

build_association(attributes = {})

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

create_association(attributes = {})

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

create_association!(attributes = {})

Делает то же, что и create_association, но генерирует исключение ActiveRecord::RecordInvalid, если запись некорректна.

reload_association

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

reset_association

Разгружает связанный объект. Следующий доступ запросит его из базы данных.

association_changed?

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

association_previously_changed?

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

Пример

class Post < ActiveRecord::Base
  belongs_to :author
end

Объявление belongs_to :author добавляет следующие методы (и другие):

post = Post.find(7)
author = Author.find(19)

post.author           # similar to Author.find(post.author_id)
post.author = author  # similar to post.author_id = author.id
post.build_author     # similar to post.author = Author.new
post.create_author    # similar to post.author = Author.new; post.author.save; post.author
post.create_author!   # similar to post.author = Author.new; post.author.save!; post.author
post.reload_author
post.reset_author
post.author_changed?
post.author_previously_changed?

Ограничения

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

Примеры ограничений:

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

Параметры

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

:class_name

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

:foreign_key

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

Указание параметра :foreign_key предотвращает автоматическое обнаружение обратной ассоциации, поэтому обычно рекомендуется установить параметр :inverse_of.

:foreign_type

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

:primary_key

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

:dependent

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

: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). Примечание: Указание счётчика добавит его в список только для чтения атрибутов этой модели с использованием attr_readonly.

:polymorphic

Укажите, что эта ассоциация является полиморфной, передав true. Примечание: если вы включили счётчик, то, возможно, вы захотите добавить атрибут счётчика к списку 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 / updated_on установлены в текущее время), когда эта запись сохраняется или уничтожается. Если вы укажете символ, этот атрибут будет обновлён текущим временем в дополнение к атрибуту updated_at / updated_on. Обратите внимание, что при изменении не будет выполняться проверка, и будут выполнены только обратные вызовы 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, чтобы связанные записи были удалены в фоновом задании.

:query_constraints

Служит составным внешним ключом. Определяет список столбцов, используемых для запроса связанного объекта. Это необязательный параметр. По умолчанию Rails попытается получить значение автоматически. Если значение задано, размер Array должен соответствовать первичному ключу модели-партнёра или размеру query_constraints.

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

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
belong_to  :note, query_constraints: [:organization_id, :note_id]
has_and_belongs_to_many(name, scope = nil, **options, &extension) Показать исходный код
# File activerecord/lib/active_record/associations.rb, line 2067
        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.1]
  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.

Пример

class Developer < ActiveRecord::Base
  has_and_belongs_to_many :projects
end

Объявление has_and_belongs_to_many :projects добавляет следующие методы (и больше):

developer = Developer.find(11)
project   = Project.find(9)

developer.projects
developer.projects << project
developer.projects.delete(project)
developer.projects.destroy(project)
developer.projects = [project]
developer.project_ids
developer.project_ids = [9]
developer.projects.clear
developer.projects.empty?
developer.projects.size
developer.projects.find(9)
developer.projects.exists?(9)
developer.projects.build  # similar to Project.new(developer_id: 11)
developer.projects.create # similar to Project.create(developer_id: 11)
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.

Установка параметра :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 1522
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.

Пример

class Firm < ActiveRecord::Base
  has_many :clients
end

Объявление has_many :clients добавляет следующие методы (и другие):

firm = Firm.find(2)
client = Client.find(6)

firm.clients                       # similar to Client.where(firm_id: 2)
firm.clients << client
firm.clients.delete(client)
firm.clients.destroy(client)
firm.clients = [client]
firm.client_ids
firm.client_ids = [6]
firm.clients.clear
firm.clients.empty?                # similar to firm.clients.size == 0
firm.clients.size                  # similar to Client.count "firm_id = 2"
firm.clients.find                  # similar to Client.where(firm_id: 2).find(6)
firm.clients.exists?(name: 'ACME') # similar to Client.exists?(name: 'ACME', firm_id: 2)
firm.clients.build                 # similar to Client.new(firm_id: 2)
firm.clients.create                # similar to Client.create(firm_id: 2)
firm.clients.create!               # similar to Client.create!(firm_id: 2)
firm.clients.reload

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

Скопы

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

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

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.

Установка параметра :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 самостоятельно не выполняют соединение.

: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, чтобы связанные записи были удалены в фоновой задаче.

:query_constraints

Выступает в качестве составного внешнего ключа. Определяет список столбцов, которые будут использоваться для запроса связанного объекта. Это необязательный параметр. По умолчанию Rails попытается определить значение автоматически. При установке значения размер Array должен совпадать с первичным ключом связанной модели или размером query_constraints.

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

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_many :comments, query_constraints: [:blog_id, :post_id]
has_one(name, scope = nil, **options) Показать исходный код
# File activerecord/lib/active_record/associations.rb, line 1708
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)

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

build_association(attributes = {})

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

create_association(attributes = {})

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

create_association!(attributes = {})

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

reload_association

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

reset_association

Раскладывает связанный объект. Следующий доступ запросит его из базы данных.

Пример

class Account < ActiveRecord::Base
  has_one :beneficiary
end

Объявление has_one :beneficiary добавляет следующие методы (и другие):

account = Account.find(5)
beneficiary = Beneficiary.find(8)

account.beneficiary               # similar to Beneficiary.find_by(account_id: 5)
account.beneficiary = beneficiary # similar to beneficiary.update(account_id: 5)
account.build_beneficiary         # similar to Beneficiary.new(account_id: 5)
account.create_beneficiary        # similar to Beneficiary.create(account_id: 5)
account.create_beneficiary!       # similar to Beneficiary.create!(account_id: 5)
account.reload_beneficiary
account.reset_beneficiary

Скопы

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

Примеры использования скопов:

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.

Установка параметра :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, валидируются новые объекты, добавляемые в ассоциацию, при сохранении родительского объекта. По умолчанию false. Если вы хотите гарантировать повторную валидацию связанных объектов при каждом обновлении, используйте validates_associated.

:autosave

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

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

:touch

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

:inverse_of

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

:required

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

:strict_loading

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

:ensuring_owner_was

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

:query_constraints

Служит составным внешним ключом. Определяет список столбцов, используемых для запроса связанного объекта. Этот параметр необязателен. По умолчанию Rails попытается вывести значение автоматически. При установке значения размер Array должен соответствовать первичному ключу связанной модели или размеру query_constraints.

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

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
has_one :employment_record_book, query_constraints: [:organization_id, :employee_id]

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

Spec-Zone.ru

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