Spec-Zone.ru › Ruby on Rails 5.2

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

Включенные модули:

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

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#portfolio.nil?

  • Project#project_manager, Project#project_manager=(project_manager), Project#project_manager.nil?,

  • 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

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

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

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

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

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

class Project
  has_and_belongs_to_many :developers, after_add: :evaluate_velocity

  def evaluate_velocity(developer)
    ...
  end
end

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

class Project
  has_and_belongs_to_many :developers,
                          after_add: [:evaluate_velocity, Proc.new { |p, d| p.shipping_date = Time.now}]
end

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

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

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

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

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

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 внутри расширений ассоциаций.

Ассоциации с присоединяемыми моделями

Ассоциации «многие ко многим» могут быть настроены с опцией :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.

Дополнительные опции для ассоциаций, определённые в константе AssociationReflection::INVALID_AUTOMATIC_INVERSE_OPTIONS, также помешают автоматически находить обратную ссылку ассоциации.

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

Вы можете отключить автоматическое обнаружение обратных ссылок, установив опцию :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. Тип attachable передаётся как строка.

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)

Это выполнит один запрос для загрузки адресов и загрузит адресные данные с одним запросом на каждый тип адресных данных. Например, если все адресные данные являются либо класса Person, либо Company, тогда всего будет выполнено 3 запроса. Список типов адресных данных для загрузки определяется на основе загруженных адресов. Это не поддерживается, если 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

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

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

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

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

Пример

Класс 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

Объявление также может включать хеш 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. Этот параметр не следует указывать, когда belongs_to используется совместно с has_many отношением в другом классе из-за возможности оставить незавершенные записи.

:counter_cache

Кэширует количество принадлежащих объектов в классе associate с помощью ActiveRecord::CounterCache::ClassMethods#increment_counter и ActiveRecord::CounterCache::ClassMethods#decrement_counter. Кэш счетчика увеличивается при создании объекта этого класса и уменьшается при его уничтожении. Это требует, чтобы в классе associate использовался столбец с именем #{table_name}_count (например, comments_count для класса Comment, принадлежащего к классу), - то есть миграция для #{table_name}_count создается в классе associate (так, что 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, никогда не сохраняет или не уничтожает связанный объект. По умолчанию, сохраняется только связанный объект, если это новая запись.

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

:touch

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

:inverse_of

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

:optional

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

:required

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

:default

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

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

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

Указывает на отношение «многие ко многим» с другим классом. Это связывает два класса через промежуточную таблицу присоединения. Если таблица присоединения не указана явно в качестве параметра, она определяется по лексикографическому порядку имён классов. Таким образом, связь между Разработчиком и Проектом даст по умолчанию имя таблицы присоединения «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[5.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 для каждой ассоциации в таблице присоединения, переопределяя любой параметр зависимостей. Это не уничтожает объекты.

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 в качестве вызываемого объекта (т.е. блока или лямбда-функции) для получения набора записей или для настройки генерируемого запроса при обращении к связанной коллекции.

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

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

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

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

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_many(name, scope = nil, **options, &extension) Показать исходный код
# File activerecord/lib/active_record/associations.rb, line 1368
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 может повлиять на другие обратные вызовы.

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

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

  • :nullify приводит к установке внешних ключей в NULL. Обратные вызовы не выполняются.

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

  • :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 на ассоциации источника в модели соединения. Это позволяет создавать связанные записи, которые автоматически создадут соответствующие записи модели соединения при сохранении. (См. раздел «Модели соединения ассоциации» выше.)

: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 обратных вызовах.

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

:inverse_of

Указывает имя ассоциации belongs_to на связанном объекте, являющейся обратной ассоциацией has_many. Подробнее см. обзор би-направленных ассоциаций в ActiveRecord::Associations::ClassMethods.

:extend

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

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

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

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

Пример

Класс 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

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

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

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

  • :nullify приводит к установке внешнего ключа в NULL. Обратные вызовы не выполняются.

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

  • :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 ассоциацию в модели-посреднике.

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

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

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

:inverse_of

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

:required

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

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

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 :primary_address, -> { where(primary: true) }, through: :addressables, source: :addressable
has_one :credit_card, required: true

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

Spec-Zone.ru

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