Spec-Zone.ru › Ruby on Rails 5.0

модуль 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 были бы плохим выбором имён ассоциаций.

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

См. также методы Instance Public ниже для получения более подробной информации.

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

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

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

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

Ассоциации 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 при работе со схемами legacy или когда вы никогда не работаете непосредственно с самим отношением.

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

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

Последняя строка должна сохранить запись через (a 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. Тип 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

Леничная загрузка ассоциаций

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

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

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

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

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

Однако существуют случаи, когда эта стратегия не имеет смысла. Например, предположим, что у человека много проектов, а каждый проект имеет много задач. Если мы удалили одну из задач человека, мы, вероятно, не захотим удалять проект. В этом сценарии метод удаления фактически не сработает: он может быть использован только если ассоциация в модели связи является 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 1644
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.

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

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

ассоциация

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

ассоциация=(ассоциировать)

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

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

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

belongs_to :firm, -> { 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

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

:polymorphic

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

:validate

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

:autosave

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

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

:touch

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

:inverse_of

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

:optional

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

:required

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

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

belongs_to :firm, foreign_key: "client_of"
belongs_to :person, primary_key: "name", foreign_key: "person_name"
belongs_to :author, class_name: "Person", foreign_key: "author_id"
belongs_to :valid_coupon, ->(o) { where "discounts > ?", o.payments_count },
                          class_name: "Coupon", foreign_key: "coupon_id"
belongs_to :attachable, polymorphic: true
belongs_to :project, -> { readonly }
belongs_to :post, counter_cache: true
belongs_to :comment, touch: true
belongs_to :company, touch: :employees_last_updated_at
belongs_to :user, optional: true
has_and_belongs_to_many(name, scope = nil, options = {}, &extension) Показать исходный код
# File activerecord/lib/active_record/associations.rb, line 1810
      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

        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 "          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 = 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[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 для каждой ассоциации в таблице присоединения, переопределяя любой параметр dependent. Это не уничтожает объекты.

collection=objects

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

collection_singular_ids

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

collection_singular_ids=ids

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

collection.clear

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

collection.empty?

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

collection.size

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

collection.find(id)

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

collection.exists?(…)

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

collection.build(attributes = {})

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

collection.create(attributes = {})

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

collection.reload

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

Пример

Класс Developer объявляет has_and_belongs_to_many :projects, который добавит:

  • Developer#projects

  • Developer#projects<<

  • Developer#projects.delete

  • Developer#projects.destroy

  • Developer#projects=

  • Developer#project_ids

  • Developer#project_ids=

  • Developer#projects.clear

  • Developer#projects.empty?

  • Developer#projects.size

  • Developer#projects.find(id)

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

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

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

  • Developer#projects.reload

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

Области

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

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

has_and_belongs_to_many :projects, -> { includes(:milestones, :manager) }
has_and_belongs_to_many :categories, ->(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.

: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 1370
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 для каждой записи, независимо от опции dependent, гарантируя выполнение обратных вызовов.

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

collection=objects

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

collection_singular_ids

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

collection_singular_ids=ids

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

collection.clear

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

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

При значении 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. Не работает в сочетании с параметрами :through или :as. Подробнее см. обзор двунаправленных ассоциаций в 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 1504
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 :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 добавляет ошибку к владельцу, если связанный объект существует.

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

: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

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

:autosave

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

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

:inverse_of

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

:required

При установке в true, к ассоциации также будет применена проверка на присутствие. Это будет проверять саму ассоциацию, а не идентификатор. Вы можете использовать :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