Spec-Zone.ru › Ruby on Rails 4.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. Поскольку ассоциация добавляет метод с этим именем в модель, она переопределит унаследованный метод и сломает все. Например, attributes и connection были бы плохими вариантами для имён ассоциаций.

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

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

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

                                  |            |  belongs_to  |
generated methods                 | belongs_to | :polymorphic | has_one
----------------------------------+------------+--------------+---------
other(force_reload=false)         |     X      |      X       |    X
other=(other)                     |     X      |      X       |    X
build_other(attributes={})        |     X      |              |    X
create_other(attributes={})       |     X      |              |    X
create_other!(attributes={})      |     X      |              |    X

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

                                  |       |          | has_many
generated methods                 | habtm | has_many | :through
----------------------------------+-------+----------+----------
others(force_reload=false)        |   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.uniq                       |   X   |    X     |    X
others.reset                      |   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

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

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

Ассоциации 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 int(11) NOT NULL auto_increment,
  account_id int(11) default NULL,
  name varchar default NULL,
  PRIMARY KEY  (id)
)

CREATE TABLE accounts (
  id int(11) 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

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

class Taggable < 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 Taggable < 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 передается как строка.

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(true).size  # fetches milestones from the database
project.milestones             # uses the milestone cache

Загрузка ассоциаций с помощью eager loading

Загрузка ассоциаций с помощью eager loading — это способ поиска объектов определенного класса и нескольких именованных ассоциаций. Это один из самых простых способов предотвратить проблему N+1, при которой извлечение 100 сообщений, каждое из которых должно отобразить своего автора, вызывает 101 базу данных. Благодаря eager loading количество запросов уменьшится с 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.

Загрузка ассоциаций с помощью eager loading поддерживается для полиморфных ассоциаций.

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

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

Address.includes(:addressable)

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

Если вы хотите указать свои собственные пользовательские соединения, используя метод joins, эти имена таблиц будут иметь приоритет над eager associations.

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.level == t.dungeon.level # => true
d.level = 10
d.level == t.dungeon.level # => false

Объекты Dungeon d и t.dungeon в примере выше относятся к одним и тем же данным из базы данных, но фактически являются различными копии данных в памяти. Указание опции :inverse_of в ассоциациях позволяет Active Record узнать об обратных взаимосвязях, и она будет оптимизировать загрузку объектов. Например, если мы изменим определения моделей на:

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

Тогда, в нашем фрагменте кода выше, d и t.dungeon — это фактически один и тот же экземпляр в памяти, и наш итоговый d.level == t.dungeon.level вернет true.

Есть ограничения по поддержке :inverse_of.

  • не работает с :through ассоциациями.

  • не работает с :polymorphic ассоциациями.

  • для belongs_to ассоциаций has_many обратные ассоциации игнорируются.

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

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

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, могут повлиять на ее действие.

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

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

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

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

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

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

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

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

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

Однако существуют примеры, где эта стратегия не имеет смысла. Например, предположим, что у человека есть много проектов, а каждый проект имеет много задач. Если мы удалили одну из задач человека, то, вероятно, нам не захочется удалять проект. В этом случае метод 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 1514
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(force_reload = false)

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

association=(associate)

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

build_association(attributes = {})

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

create_association(attributes = {})

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

create_association!(attributes = {})

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

Пример

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

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

Скопы

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

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

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

Параметры

: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».

: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 с помощью increment_counter и 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

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

:autosave

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

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

:touch

Если true, связанный объект будет touched (атрибуты updated_at/on установлены на текущее время) при сохранении или удалении этой записи. Если вы укажете символ, этот атрибут будет обновлён текущим временем в дополнение к атрибуту updated_at/on.

:inverse_of

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

:required

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

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

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 :company, touch: true
belongs_to :company, touch: :employees_last_updated_at
belongs_to :company, required: true
has_and_belongs_to_many(name, scope = nil, options = {}, &extension) Показать исходный код
# File activerecord/lib/active_record/associations.rb, line 1679
      def has_and_belongs_to_many(name, scope = nil, options = {}, &extension)
        if scope.is_a?(Hash)
          options = scope
          scope   = nil
        end

        habtm_reflection = ActiveRecord::Reflection::HasAndBelongsToManyReflection.new(name, scope, options, self)

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

        join_model = builder.through_model

        # FIXME: we should move this to the internal constants. Also people
        # should never directly access this constant so I'm not happy about
        # setting it.
        const_set join_model.name, join_model

        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 = [name.to_s, habtm_reflection]

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

        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
        self._reflections[name.to_s].parent_reflection = [name.to_s, 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
  def change
    create_table :developers_projects, id: false do |t|
      t.integer :developer_id
      t.integer :project_id
    end
  end
end

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

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

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

collection(force_reload = false)

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

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::Base.find.

collection.exists?(…)

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

collection.build(attributes = {})

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

collection.create(attributes = {})

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

Пример

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

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

Области

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

Примеры областей:

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

Расширения

Аргумент 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.

:association_foreign_key

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

:readonly

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

:validate

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

:autosave

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

Обратите внимание, что 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 1258
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(force_reload = false)

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

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

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::Base.find.

collection.exists?(…)

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

collection.build(attributes = {}, …)

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

collection.create(attributes = {})

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

collection.create!(attributes = {})

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

Пример

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

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

Ограничения

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

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

has_many :comments, -> { where(author_id: 1) }
has_many :employees, -> { joins(:address) }
has_many :posts, ->(post) { where("max_post_length > ?", 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_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, а удаляемыми записями являются записи соединения, а не связанные записи.

: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

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

:autosave

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

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

:inverse_of

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

Примеры опций:

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 1385
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(force_reload = false)

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

association=(associate)

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

build_association(attributes = {})

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

create_association(attributes = {})

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

create_association!(attributes = {})

Выполняет то же, что и create_association, но вызывает ActiveRecord::RecordInvalid если запись не валидна.

Пример

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

Области видимости

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

Примеры областей видимости:

has_one :author, -> { where(comment_id: 1) }
has_one :employer, -> { joins(:company) }
has_one :dob, ->(dob) { where("Date.new(2000, 01, 01) > ?", dob) }

Параметры

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

Параметры:

:class_name

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

:dependent

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

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

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

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

  • :restrict_with_exception приводит к ошибке, если существует связанная запись

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

:foreign_key

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

: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 вспомогательной модели.

:source

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

:source_type

Определяет тип ассоциации источника, используемой запросами has_one :through где ассоциация источника — полиморфная belongs_to.

:validate

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

:autosave

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

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

:inverse_of

Определяет имя ассоциации belongs_to связанного объекта, которая является обратной для этой ассоциации has_one. Не работает в сочетании с параметрами :through или :as. См. обзор двунаправленных ассоциаций в 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