Это руководство раскрывает особенности связей Active Record.
После его прочтения, вы узнаете, как:
В Rails связь - это соединение между двумя моделями Active Record. Зачем нам нужны связи между моделями? Затем, что они позволяют сделать код для обычных операций проще и легче.
Например, рассмотрим простое приложение на Rails, которое включает модель для авторов и модель для книг.
Каждый автор может иметь много книг. Без связей объявление модели будет выглядеть так:
class Author < ApplicationRecord
end
class Book < ApplicationRecord
end
Теперь, допустим, мы хотим добавить новую книгу для существующего автора. Нам нужно сделать так:
@book = Book.create(published_at: Time.now, author_id: @author.id)
Или, допустим, удалим автора и убедимся, что все его книги также будут удалены:
@books = Book.where(author_id: @author.id)
@books.each do |book|
book.destroy
end
@author.destroy
Со связями Active Record можно упростить эти и другие операции, декларативно сказав Rails, что имеется соединение между двумя моделями. Вот пересмотренный код для создания авторов и книг:
class Author < ApplicationRecord
has_many :books, dependent: :destroy
end
class Book < ApplicationRecord
belongs_to :author
end
С этими изменениями создание новой книги для определенного автора проще:
@book = @author.books.create(published_at: Time.now)
Удаление автора и всех его книг намного проще:
@author.destroy
Чтобы узнать больше о различных типах связей, читайте следующий раздел руководства. Затем следуют некоторые полезные советы по работе со связями, а затем полное описание методов и опций для связей в Rails.
Rails поддерживает шесть типов связей, каждый подразумевает конкретный вариант использования.
Вот список всех поддерживаемых типов со ссылкой на их документацию API с подробностями, как их использовать, параметрами их методов, и т.д.
Связи реализуются с использованием макро-вызовов (macro-style calls), и, таким образом, вы можете декларативно добавлять возможности для своих моделей. Например, объявляя, что одна модель принадлежит (belongs_to
) другой, вы указываете Rails сохранять информацию о первичном-внешнем ключах между экземплярами двух моделей, а также получаете несколько полезных методов, добавленных в модель.
После прочтения всего этого руководства, вы научитесь объявлять и использовать различные формы связей. Но сначала следует быстро ознакомиться с ситуациями, когда применим каждый тип связи.
belongs_to
Связь belongs_to
устанавливает соединение один-к-одному с другой моделью, когда один экземпляр объявляющей модели "принадлежит" одному экземпляру другой модели. Например, если в приложении есть авторы и книги, и одна книга может быть связана только с одним автором, нужно объявить модель book следующим образом:
class Book < ApplicationRecord
belongs_to :author
end
Связи belongs_to
обязаны использовать единственное число. Если использовать множественное число в вышеприведенном примере для связи author
в модели Book
и создать экземпляр с помощью Book.create(authors: @author)
, будет сообщено "uninitialized constant Book::Authors". Это так, потому что Rails автоматически получает имя класса из имени связи. Если в имени связи неправильно использовано число, то получаемый класс также будет неправильного числа.
Соответствующая миграция может выглядеть так:
class CreateOrders < ActiveRecord::Migration[7.2]
def change
create_table :authors do |t|
t.string :name
t.timestamps
end
create_table :books do |t|
t.belongs_to :author
t.datetime :published_at
t.timestamps
end
end
end
При одиночном использовании, belongs_to
создает однонаправленное соединение один-к-одному. Следовательно, в вышеприведенном примере каждая книга "знает" своего автора, но авторы не знают о своих книгах. Чтобы настроить двунаправленную связь - используйте belongs_to
в сочетании с has_one
или has_many
на другой модели, в данном случае модели Author.
belongs_to
не гарантирует ссылочной целостности, если optional
установлен true, поэтому, в зависимости от использования, возможно, необходимо добавить ограничение внешнего ключа на столбце ссылки на уровне базы данных, подобным образом:
create_table :books do |t|
t.belongs_to :author, foreign_key: true
# ...
end
has_one
Связь has_one
показывает, что у другой модели есть ссылка на эту модель. Та модель может быть извлечена с помощью этой связи.
Например, если каждый поставщик имеет только один аккаунт, можете объявить модель supplier подобно этому:
class Supplier < ApplicationRecord
has_one :account
end
Главным отличием от belongs_to
является то, что связующий столбец supplier_id
расположен в другой таблице:
Соответствующая миграция может выглядеть так:
class CreateSuppliers < ActiveRecord::Migration[7.2]
def change
create_table :suppliers do |t|
t.string :name
t.timestamps
end
create_table :accounts do |t|
t.belongs_to :supplier
t.string :account_number
t.timestamps
end
end
end
В зависимости от применения, возможно, потребуется создать индекс уникальности и/или ограничение внешнего ключа на указанный столбец таблицы accounts. В этом случае определение столбца может выглядеть так:
create_table :accounts do |t|
t.belongs_to :supplier, index: { unique: true }, foreign_key: true
# ...
end
Эта связь может быть двунаправленной при использовании в сочетании с belongs_to
на другой модели.
has_many
Связь has_many
указывает на соединение один-ко-многим с другой моделью. Эта связь часто бывает на "другой стороне" связи belongs_to
. Эта связь указывает на то, что каждый экземпляр модели имеет ноль или более экземпляров другой модели. Например, в приложении, содержащем авторов и книги, модель author может быть объявлена следующим образом:
class Author < ApplicationRecord
has_many :books
end
Имя другой модели указывается во множественном числе при объявлении связи has_many
.
Соответствующая миграция может выглядеть так:
class CreateAuthors < ActiveRecord::Migration[7.2]
def change
create_table :authors do |t|
t.string :name
t.timestamps
end
create_table :books do |t|
t.belongs_to :author
t.datetime :published_at
t.timestamps
end
end
end
В зависимости от применения, обычно неплохо было бы создать неуникальный индекс и опционально ограничение внешнего ключа на столбец автора для таблицы books:
create_table :books do |t|
t.belongs_to :author, index: true, foreign_key: true
# ...
end
has_many :through
Связь has_many :through
часто используется для настройки соединения многие-ко-многим с другой моделью. Эта связь указывает, что объявляющая модель может соответствовать нулю или более экземплярам другой модели через третью модель. Например, рассмотрим поликлинику, где пациентам (patients) дают направления (appointments) к врачам (physicians). Соответствующие объявления связей будут выглядеть следующим образом:
class Physician < ApplicationRecord
has_many :appointments
has_many :patients, through: :appointments
end
class Appointment < ApplicationRecord
belongs_to :physician
belongs_to :patient
end
class Patient < ApplicationRecord
has_many :appointments
has_many :physicians, through: :appointments
end
Соответствующая миграция может выглядеть так:
class CreateAppointments < ActiveRecord::Migration[7.2]
def change
create_table :physicians do |t|
t.string :name
t.timestamps
end
create_table :patients do |t|
t.string :name
t.timestamps
end
create_table :appointments do |t|
t.belongs_to :physician
t.belongs_to :patient
t.datetime :appointment_date
t.timestamps
end
end
end
Коллекция соединительных моделей может управляться с помощью методов связи has_many
. Например, если вы присвоите:
physician.patients = patients
Тогда будут автоматически созданы новые соединительные модели для вновь связанных объектов. Если некоторые из ранее существующих сейчас отсутствуют, их соединительные строки автоматически удаляются.
Автоматическое удаление соединительных моделей прямое, ни один из колбэков на уничтожение не включается.
Связь has_many :through
также полезна для настройки "ярлыков" через вложенные связи has_many
. Например, если документ имеет много секций, а секция имеет много параграфов, иногда хочется получить просто коллекцию всех параграфов в документе. Это можно настроить следующим образом:
class Document < ApplicationRecord
has_many :sections
has_many :paragraphs, through: :sections
end
class Section < ApplicationRecord
belongs_to :document
has_many :paragraphs
end
class Paragraph < ApplicationRecord
belongs_to :section
end
С определенным through: :sections
Rails теперь понимает:
@document.paragraphs
has_one :through
Связь has_one :through
настраивает соединение один-к-одному с другой моделью. Эта связь показывает, что объявляющая модель может быть связана с одним экземпляром другой модели через третью модель. Например, если каждый поставщик имеет один аккаунт, и каждый аккаунт связан с одной историей аккаунта, тогда модели могут выглядеть так:
class Supplier < ApplicationRecord
has_one :account
has_one :account_history, through: :account
end
class Account < ApplicationRecord
belongs_to :supplier
has_one :account_history
end
class AccountHistory < ApplicationRecord
belongs_to :account
end
Соответствующая миграция может выглядеть так:
class CreateAccountHistories < ActiveRecord::Migration[7.2]
def change
create_table :suppliers do |t|
t.string :name
t.timestamps
end
create_table :accounts do |t|
t.belongs_to :supplier
t.string :account_number
t.timestamps
end
create_table :account_histories do |t|
t.belongs_to :account
t.integer :credit_rating
t.timestamps
end
end
end
has_and_belongs_to_many
Связь has_and_belongs_to_many
создает прямое соединение многие-ко-многим с другой моделью, без промежуточной модели. Эта связь показывает, что каждый экземпляр объявляющей модели ссылается на ноль или более записей другой модели. Например, если ваше приложение включает сборки (assemblies) и детали (parts), где каждый узел имеет много деталей, и каждая деталь встречается во многих сборках, модели можно объявить таким образом:
class Assembly < ApplicationRecord
has_and_belongs_to_many :parts
end
class Part < ApplicationRecord
has_and_belongs_to_many :assemblies
end
Соответствующая миграция может выглядеть так:
class CreateAssembliesAndParts < ActiveRecord::Migration[7.2]
def change
create_table :assemblies do |t|
t.string :name
t.timestamps
end
create_table :parts do |t|
t.string :part_number
t.timestamps
end
create_table :assemblies_parts, id: false do |t|
t.belongs_to :assembly
t.belongs_to :part
end
end
end
belongs_to
и has_one
Если хотите настроить отношение один-к-одному между двумя моделями, необходимо добавить belongs_to
к одной и has_one
к другой. Как узнать что к какой?
Различие в том, где помещен внешний ключ (он должен быть в таблице для класса, объявляющего связь belongs_to
), но вы также должны думать о реальном значении данных. Отношение has_one
говорит, что что-то принадлежит вам - то есть что что-то указывает на вас. Например, больше смысла в том, что поставщик владеет аккаунтом, чем в том, что аккаунт владеет поставщиком. Это означает, что правильные отношения подобны этому:
class Supplier < ApplicationRecord
has_one :account
end
class Account < ApplicationRecord
belongs_to :supplier
end
Соответствующая миграция может выглядеть так:
class CreateSuppliers < ActiveRecord::Migration[7.2]
def change
create_table :suppliers do |t|
t.string :name
t.timestamps
end
create_table :accounts do |t|
t.bigint :supplier_id
t.string :account_number
t.timestamps
end
add_index :accounts, :supplier_id
end
end
Использование t.bigint :supplier_id
указывает имя внешнего ключа очевидно и явно. В современных версиях Rails можно абстрагироваться от деталей реализации используя t.references :supplier
.
has_many :through
и has_and_belongs_to_many
Rails предлагает два разных способа объявления отношения многие-ко-многим между моделями. Первый способ - использовать has_and_belongs_to_many
, который позволяет создать связь напрямую:
class Assembly < ApplicationRecord
has_and_belongs_to_many :parts
end
class Part < ApplicationRecord
has_and_belongs_to_many :assemblies
end
Второй способ объявить отношение многие-ко-многим - использование has_many :through
. Это осуществляет связь не напрямую, а через соединяющую модель:
class Assembly < ApplicationRecord
has_many :manifests
has_many :parts, through: :manifests
end
class Manifest < ApplicationRecord
belongs_to :assembly
belongs_to :part
end
class Part < ApplicationRecord
has_many :manifests
has_many :assemblies, through: :manifests
end
Простейший признак того, что нужно настраивать отношение has_many :through
- если необходимо работать с моделью отношений как с независимым объектом. Если вам не нужно ничего делать с моделью отношений, проще настроить связь has_and_belongs_to_many
(хотя нужно не забыть создать соединяющую таблицу в базе данных).
Вы должны использовать has_many :through
, если нужны валидации, колбэки или дополнительные атрибуты для соединительной модели.
Хотя has_and_belongs_to_many
предлагает создать соединительную таблицу без первичного ключа с помощью id: false
, рассмотрите использование составного первичного ключа для соединительной таблицы в связях has_many :through
. Например, рекомендуется использовать create_table :manifests, primary_key: [:assembly_id, :part_id]
в вышеуказанном примере.
Полиморфные связи - это немного более "навороченный" вид связей. С полиморфными связями модель может принадлежать более чем одной модели, на одиночной связи. Например, имеется модель изображения, которая принадлежит или модели работника, или модели продукта. Вот как это объявляется:
class Picture < ApplicationRecord
belongs_to :imageable, polymorphic: true
end
class Employee < ApplicationRecord
has_many :pictures, as: :imageable
end
class Product < ApplicationRecord
has_many :pictures, as: :imageable
end
Можно считать полиморфное объявление belongs_to
как настройку интерфейса, которую может использовать любая другая модель. Из экземпляра модели Employee
можно получить коллекцию изображений: @employee.pictures
.
Подобным образом можно получить @product.pictures
.
Если имеется экземпляр модели Picture
, можно получить его родителя посредством @picture.imageable
. Чтобы это работало, необходимо объявить столбец внешнего ключа и столбец типа в модели, объявляющей полиморфный интерфейс:
class CreatePictures < ActiveRecord::Migration[7.2]
def change
create_table :pictures do |t|
t.string :name
t.bigint :imageable_id
t.string :imageable_type
t.timestamps
end
add_index :pictures, [:imageable_type, :imageable_id]
end
end
Эта миграция может быть упрощена при использовании формы t.references
:
class CreatePictures < ActiveRecord::Migration[7.2]
def change
create_table :pictures do |t|
t.string :name
t.references :imageable, polymorphic: true
t.timestamps
end
end
end
Rails часто способен вывести информацию о первичном/внешнем ключах между связанными моделями со составными первичными ключами, без необходимости дополнительной информации. Возьмем следующий пример:
class Order < ApplicationRecord
self.primary_key = [:shop_id, :id]
has_many :books
end
class Book < ApplicationRecord
belongs_to :order
end
Тут Rails предполагает, что столбец :id
должен быть использован в качестве первичного ключа для связи между заказом и его книгами, как и при обычных связях has_many
/ belongs_to
. Он выведет, что столбец внешнего ключа на таблице books
- :order_id
. Получение заказа книги:
order = Order.create!(id: [1, 2], status: "pending")
book = order.books.create!(title: "A Cool Book")
book.reload.order
сгенерирует следующий SQL для доступа к заказу:
SELECT * FROM orders WHERE id = 2
Это работает, только если составной первичный ключ модели содержит столбец :id
и столбец, уникальный для всех записей. Чтобы использовать полный составной первичный ключ в связях, установите опцию query_constraints
связи. Эта опция указывает составной внешний ключ на связи: будут использованы все столбцы во внешнем ключе при запросе связанной записи(ей). Например:
class Author < ApplicationRecord
self.primary_key = [:first_name, :last_name]
has_many :books, query_constraints: [:first_name, :last_name]
end
class Book < ApplicationRecord
belongs_to :author, query_constraints: [:author_first_name, :author_last_name]
end
Доступ к автору книги:
author = Author.create!(first_name: "Jane", last_name: "Doe")
book = author.books.create!(title: "A Cool Book")
book.reload.author
использует :first_name
и :last_name
в запросе SQL:
SELECT * FROM authors WHERE first_name = 'Jane' AND last_name = 'Doe'
При разработке модели данных иногда находится модель, которая может иметь отношение сама к себе. Например, мы хотим хранить всех работников в одной модели базы данных, но нам нужно отслеживать отношения начальник-подчиненный. Эта ситуация может быть смоделирована с помощью связей, присоединяемых к себе:
class Employee < ApplicationRecord
has_many :subordinates, class_name: "Employee",
foreign_key: "manager_id"
belongs_to :manager, class_name: "Employee", optional: true
end
С такой настройкой, вы можете получить @employee.subordinates
и @employee.manager
.
В миграциях/схеме следует добавить столбец ссылки модели на саму себя.
class CreateEmployees < ActiveRecord::Migration[7.2]
def change
create_table :employees do |t|
t.references :manager, foreign_key: { to_table: :employees }
t.timestamps
end
end
end
Опция to_table
, передаваемая в foreign_key
, подробнее объяснена в SchemaStatements#add_reference
.
Вот некоторые вещи, которые необходимо знать для эффективного использования связей Active Record в вашем приложении на Rails:
Все методы связи построены вокруг кэширования, которое хранит результаты последних запросов доступными для будущих операций. Кэш является общим для разных методов. Например:
# получаем книги из базы данных
author.books
# используем кэшированную копию книг
author.books.size
# используем кэшированную копию книг
author.books.empty?
Но что если вы хотите перезагрузить кэш, так как данные могли быть изменены другой частью приложения? Всего лишь вызовите reload
на связи:
# получаем книги из базы данных
author.books
# используем кэшированную копию книг
author.books.size
# отказываемся от кэшированной копии книг и снова обращаемся к базе данных
author.books.reload.empty?
Вы не свободны в выборе любого имени для своих связей. Поскольку создание связи добавляет метод с таким именем в модель, будет плохой идеей дать связи имя, уже используемое как метод экземпляра ActiveRecord::Base
. Метод связи тогда переопределит базовый метод, и что-нибудь перестанет работать. Например, attributes
или connection
плохие имена для связей.
Связи очень полезные, но не волшебные. Вы ответственны за содержание вашей схемы базы данных в соответствии со связями. На практике это означает две вещи, в зависимости от того, какой тип связей создаете. Для связей belongs_to
нужно создать внешние ключи, а для связей has_and_belongs_to_many
нужно создать подходящую соединительную таблицу.
belongs_to
Когда объявляете связь belongs_to
, нужно создать внешние ключи, при необходимости. Например, рассмотрим эту модель:
class Book < ApplicationRecord
belongs_to :author
end
Это объявление нуждается в поддержке соответствующим столбцом внешнего ключа в таблице books. Для совершенно новой таблицы миграция может выглядеть примерно так:
class CreateBooks < ActiveRecord::Migration[7.2]
def change
create_table :books do |t|
t.datetime :published_at
t.string :book_number
t.references :author
end
end
end
В то время как для существующей таблицы, это может выглядеть следующим образом:
class AddAuthorToBooks < ActiveRecord::Migration[7.2]
def change
add_reference :books, :author
end
end
Если необходимо принудительно использовать ссылочную целостность на уровне базы данных, добавьте опцию foreign_key: true
в вышеприведенное объявление 'reference' столбца.
has_and_belongs_to_many
Если вы создали связь has_and_belongs_to_many
, необходимо обязательно создать соединительную таблицу. Если имя соединительной таблицы явно не указано с использованием опции :join_table
, Active Record создает имя, используя алфавитный порядок имен классов. Поэтому соединение между моделями author и book по умолчанию даст значение имени таблицы "authors_books", так как "a" идет перед "b" в алфавитном порядке.
Приоритет между именами модели рассчитывается с использованием оператора <=>
для String
. Это означает, что если строки имеют разную длину и в своей короткой части они равны, тогда более длинная строка рассматривается как с более высоким лексическим приоритетом, по сравнению с короткой. Например, кто-то ожидает, что таблицы "paper_boxes" и "papers" создадут соединительную таблицу "papers_paper_boxes" поскольку имя "paper_boxes" длиннее, но фактически будет сгенерирована таблица с именем "paper_boxes_papers" (поскольку знак подчеркивания "_" лексикографически меньше, чем "s" в обычной кодировке).
Какое бы ни было имя, вы должны вручную сгенерировать соединительную таблицу в соответствующей миграции. Например, рассмотрим эти связи:
class Assembly < ApplicationRecord
has_and_belongs_to_many :parts
end
class Part < ApplicationRecord
has_and_belongs_to_many :assemblies
end
Теперь нужно написать миграцию для создания таблицы assemblies_parts
. Эта таблица должна быть создана без первичного ключа:
class CreateAssembliesPartsJoinTable < ActiveRecord::Migration[7.2]
def change
create_table :assemblies_parts, id: false do |t|
t.bigint :assembly_id
t.bigint :part_id
end
add_index :assemblies_parts, :assembly_id
add_index :assemblies_parts, :part_id
end
end
Мы передаем id: false
в create_table
, так как эта таблица не представляет модель. Это необходимо, чтобы связь работала правильно. Если вы видите странное поведение в связи has_and_belongs_to_many
, например, искаженные ID моделей, или исключения в связи с конфликтом ID, скорее всего вы забыли убрать первичный ключ.
Для простоты, также можно использовать метод create_join_table
class CreateAssembliesPartsJoinTable < ActiveRecord::Migration[7.2]
def change
create_join_table :assemblies, :parts do |t|
t.index :assembly_id
t.index :part_id
end
end
end
По умолчанию связи ищут объекты только в пределах области видимости текущего модуля. Это важно, когда вы объявляете модели Active Record внутри модуля. Например:
module MyApplication
module Business
class Supplier < ApplicationRecord
has_one :account
end
class Account < ApplicationRecord
belongs_to :supplier
end
end
end
Это будет работать, так как оба класса Supplier
и Account
определены в пределах одной области видимости. Но нижеследующее не будет работать, потому что Supplier
и Account
определены в разных областях видимости:
module MyApplication
module Business
class Supplier < ApplicationRecord
has_one :account
end
end
module Billing
class Account < ApplicationRecord
belongs_to :supplier
end
end
end
Для связи модели с моделью в другом пространстве имен, необходимо указать полное имя класса в объявлении связи:
module MyApplication
module Business
class Supplier < ApplicationRecord
has_one :account,
class_name: "MyApplication::Billing::Account"
end
end
module Billing
class Account < ApplicationRecord
belongs_to :supplier,
class_name: "MyApplication::Business::Supplier"
end
end
end
Для связей нормально работать в двух направлениях, затребовав объявление в двух различных моделях:
class Author < ApplicationRecord
has_many :books
end
class Book < ApplicationRecord
belongs_to :author
end
Active Record попытается автоматически определить, что эти две модели образуют двунаправленную связь, основываясь на имени связи. Эта информация позволяет Active Record:
Предотвращать необходимость запросов для уже загруженных данных:
irb> author = Author.first
irb> author.books.all? do |book|
irb> book.author.equal?(author) # Тут не выполняются дополнительные запросы
irb> end
=> true
Предотвращать несогласованные данные (как только есть только одна загруженная копия объекта Author
):
irb> author = Author.first
irb> book = author.books.first
irb> author.name == book.author.name
=> true
irb> author.name = "Changed Name"
irb> author.name == book.author.name
=> true
Автоматически сохраняет связи в большинстве случаев:
irb> author = Author.new
irb> book = author.books.new
irb> book.save!
irb> book.persisted?
=> true
irb> author.persisted?
=> true
Проверяет наличие и отсутствие связей в большинстве случаев:
irb> book = Book.new
irb> book.valid?
=> false
irb> book.errors.full_messages
=> ["Author must exist"]
irb> author = Author.new
irb> book = author.books.new
irb> book.valid?
=> true
Active Record поддерживает автоматическое определение для большинства связей со стандартными именами. Однако, двунаправленные связи, содержащие опции :through
или :foreign_key
, не будут автоматически определены.
Пользовательские скоупы на противоположных связях также предотвращают автоматическое определение, как и пользовательские скоупы на самой связи, за исключением когда config.active_record.automatic_scope_inversing
установлена true (по умолчанию для новых приложений).
Например, рассмотрим следующие объявления моделей:
class Author < ApplicationRecord
has_many :books
end
class Book < ApplicationRecord
belongs_to :writer, class_name: 'Author', foreign_key: 'author_id'
end
Из-за опции :foreign_key
, Active Record больше не будет автоматически распознавать двунаправленную связь. Это может вызвать, что ваше приложение:
Выполняет необходимые запросы для тех же данных (в этом примере вызывая N+1 запрос):
irb> author = Author.first
irb> author.books.any? do |book|
irb> book.author.equal?(author) # Это выполняет запрос для каждой книги
irb> end
=> false
Ссылается на несколько копий модели с несогласованными данными:
irb> author = Author.first
irb> book = author.books.first
irb> author.name == book.author.name
=> true
irb> author.name = "Changed Name"
irb> author.name == book.author.name
=> false
Не в состоянии автоматически сохранять связи:
irb> author = Author.new
irb> book = author.books.new
irb> book.save!
irb> book.persisted?
=> true
irb> author.persisted?
=> false
Не в состоянии проверять наличие или отсутствие:
irb> author = Author.new
irb> book = author.books.new
irb> book.valid?
=> false
irb> book.errors.full_messages
=> ["Author must exist"]
Active Record представляет опцию :inverse_of
, таким образом можно явно объявить двунаправленные связи:
class Author < ApplicationRecord
has_many :books, inverse_of: 'writer'
end
class Book < ApplicationRecord
belongs_to :writer, class_name: 'Author', foreign_key: 'author_id'
end
Включив опцию :inverse_of
в объявлении связи has_many
, Active Record будет распознавать двунаправленную связь и будет вести себя как в изначальных примерах выше:
belongs_to
В терминах базы данных связь belongs_to
сообщает, что таблица этой модели содержит столбец, представляющий ссылку на другую таблицу. Она может быть использована для настройки отношений один-к-одному или один-ко-многим, в зависимости от настроек. Если таблица другого класса содержит ссылку в отношении один-к-одному, вместо этого следует использовать has_one
.
belongs_to
Когда объявляете связь belongs_to
, объявляющий класс автоматически получает 8 методов, относящихся к связи:
association
association=(associate)
build_association(attributes = {})
create_association(attributes = {})
create_association!(attributes = {})
reload_association
reset_association
association_changed?
association_previously_changed?
Во всех четырех методах association
заменяется символом, переданным как первый аргумент в belongs_to
. Например, имеем объявление:
class Book < ApplicationRecord
belongs_to :author
end
Каждый экземпляр модели Book
будет иметь эти методы:
author
author=
build_author
create_author
create_author!
reload_author
reset_author
author_changed?
author_previously_changed?
Когда устанавливаете новую связь has_one
или belongs_to
, следует использовать префикс build_
для построения связи, в отличие от метода association.build
, используемый для связей has_many
или has_and_belongs_to_many
. Чтобы создать связь, используйте префикс create_
.
association
Метод association
возвращает связанный объект, если он есть. Если объекта нет, возвращает nil
.
@author = @book.author
Если связанный объект уже был получен из базы данных для этого объекта, возвращается кэшированная версия. Чтобы переопределить это поведение (и заставить прочитать из базы данных), вызовите #reload_association
на родительском объекте.
@author = @book.reload_author
Чтобы выгрузить кэшированную версию связанного объекта при следующем доступе, если таковая имеется, и прочитать ее из базы данных, вызовите #reset_association
на родительском объекте.
@book.reset_author
association=(associate)
Метод association=
привязывает связанный объект к этому объекту. Фактически это означает извлечение первичного ключа из связанного объекта и присвоение его значения внешнему ключу.
@book.author = @author
build_association(attributes = {})
Метод build_association
возвращает новый объект связанного типа. Этот объект будет экземпляром с переданными атрибутами, будет установлена связь с внешним ключом этого объекта, но связанный объект пока не будет сохранен.
@author = @book.build_author(author_number: 123,
author_name: "John Doe")
create_association(attributes = {})
Метод create_association
возвращает новый объект связанного типа. Этот объект будет экземпляром с переданными атрибутами, будет установлена связь с внешним ключом этого объекта, и, если он пройдет валидации, определенные в связанной модели, связанный объект будет сохранен.
@author = @book.create_author(author_number: 123,
author_name: "John Doe")
create_association!(attributes = {})
Работает так же, как и вышеприведенный create_association
, но вызывает ActiveRecord::RecordInvalid
, если запись невалидна.
association_changed?
Метод association_changed?
возвращает true, если был назначен новый связанный объект, и внешний ключ будет обновлен при следующем сохранении.
@book.author # => #<Author author_number: 123, author_name: "John Doe">
@book.author_changed? # => false
@book.author = Author.second # => #<Author author_number: 456, author_name: "Jane Smith">
@book.author_changed? # => true
@book.save!
@book.author_changed? # => false
association_previously_changed?
Метод association_previously_changed?
возвращает true, если предыдущее сохранение обновило связь, ссылающуюся на новый связанный объект.
@book.author # => #<Author author_number: 123, author_name: "John Doe">
@book.author_previously_changed? # => false
@book.author = Author.second # => #<Author author_number: 456, author_name: "Jane Smith">
@book.save!
@book.author_previously_changed? # => true
belongs_to
Хотя Rails использует разумные значения по умолчанию, работающие во многих ситуациях, бывают случаи, когда хочется изменить поведение связи belongs_to
. Такая настройка легко выполнима с помощью передачи опций и блоков со скоупом при создании связи. Например, эта связь использует две такие опции:
class Book < ApplicationRecord
belongs_to :author, touch: :books_updated_at,
counter_cache: true
end
Связь belongs_to
поддерживает эти опции:
:autosave
:class_name
:counter_cache
:default
:dependent
:ensuring_owner_was
:foreign_key
:foreign_type
:primary_key
:inverse_of
:optional
:polymorphic
:required
:strict_loading
:touch
:validate
:autosave
Если установить опцию :autosave
в true
, Rails сохранит любые загруженные связанные члены и уничтожит члены, помеченные для уничтожения, всякий раз, когда сохраняется родительский объект. Но установить :autosave
в false
- не то же самое, что не устанавливать опцию :autosave
. Если опция :autosave
отсутствует, то новые связанные объекты будут сохранены, но обновленные связанные объекты сохранены не будут.
:class_name
Если имя другой модели не может быть получено из имени связи, можете использовать опцию :class_name
для предоставления имени модели. Например, если книга принадлежит автору, но фактическое имя модели, содержащей авторов, Patron
, можете установить это следующим образом:
class Book < ApplicationRecord
belongs_to :author, class_name: "Patron"
end
:counter_cache
Опция :counter_cache
может быть использована, чтобы сделать поиск количества принадлежащих объектов более эффективным. Рассмотрим эти модели:
class Book < ApplicationRecord
belongs_to :author
end
class Author < ApplicationRecord
has_many :books
end
С этими объявлениями запрос значения @author.books.size
требует обращения к базе данных для выполнения запроса COUNT(*)
. Чтобы этого избежать, можете добавить кэш счетчика в принадлежащую модель:
class Book < ApplicationRecord
belongs_to :author, counter_cache: true
end
class Author < ApplicationRecord
has_many :books
end
С этим объявлением, Rails будет хранить в кэше актуальное значение и затем возвращать это значение в отклик на метод size
.
Хотя опция :counter_cache
определяется в модели, включающей определение belongs_to
, фактический столбец должен быть добавлен в связанную (has_many
) модель. В вышеописанном случае, необходимо добавить столбец, названный books_count
в модель Author
.
Имя столбца по умолчанию можно переопределить, указав произвольное имя столбца в объявлении counter_cache
вместо true
. Например, для использования count_of_books
вместо books_count
:
class Book < ApplicationRecord
belongs_to :author, counter_cache: :count_of_books
end
class Author < ApplicationRecord
has_many :books
end
Опцию :counter_cache
необходимо указывать только на стороне belongs_to
связи.
Столбцы кэша счетчика добавляются в список атрибутов модели только для чтения посредством attr_readonly
.
Если по какой-то причине вы изменяете значение первичного ключа модели, но не обновляете внешние ключи посчитанных моделей, то кэш счетчика может иметь устаревшие данные. Другими словами, любые "осиротевшие" модели все еще будут считать в отношении счетчика. Чтобы починить кэш счетчика, используйте reset_counters
.
:default
Когда установлен true
, у связи не будет проверки ее наличия.
:dependent
Если установить опцию :dependent
в:
:destroy
, когда объект уничтожается, destroy
вызывается на его связанных объектах.
:delete
, когда объект уничтожается, все его связанные объекты удаляются прямо из базы данных без вызова метода destroy
.
:destroy_async
: когда объект уничтожается, задание ActiveRecord::DestroyAssociationAsyncJob
ставится в очередь, которое вызовет destroy на его связанных объектах. Чтобы это работало, должен быть настроен Active Job. Не используйте эту опцию, если связь обеспечена ограничением внешнего ключу в вашей базе данных. Действия ограничения внешнего ключа произойдут в той же транзакции, которая удаляет владельца.
Не следует определять эту опцию в связи belongs_to
, которая соединена со связью has_many
в другом классе. Это приведет к "битым" связям в записях вашей базы данных.
:ensuring_owner_was
Указывает метод экземпляра, вызываемого на владельце. Этот метод должен возвратить true, чтобы связанные записи удалялись в фоновом задании.
:foreign_key
По соглашению Rails предполагает, что столбец, используемый для хранения внешнего ключа в этой модели, имеет имя модели с добавленным суффиксом _id
. Опция :foreign_key
позволяет установить имя внешнего ключа явно:
class Book < ApplicationRecord
belongs_to :author, class_name: "Patron",
foreign_key: "patron_id"
end
В любом случае, Rails не создаст столбцы внешнего ключа за вас. Вам необходимо явно определить их в своих миграциях.
:foreign_type
Указывает столбец, используемый для хранения типа связанного объекта, если это полиморфная связь. По умолчанию это угадывается как имя связи с суффиксом “_type
”. Таким образом, класс, определяющий связь belongs_to :taggable, polymorphic: true
, будет использовать “taggable_type
” как :foreign_type
по умолчанию.
:primary_key
По соглашению Rails предполагает, что для первичного ключа используется столбец id
в таблице. Опция :primary_key
позволяет указать иной столбец.
Например, имеется таблица users
с guid
в качестве первичного ключа. Если мы хотим отдельную таблицу todos
, содержащую внешний ключ user_id
из столбца guid
, для этого можно использовать primary_key
следующим образом:
class User < ApplicationRecord
self.primary_key = 'guid' # primary key is guid and not id
end
class Todo < ApplicationRecord
belongs_to :user, primary_key: 'guid'
end
При выполнении @user.todos.create
, у записи @todo
будет значение user_id
таким же, как значение guid
у @user
.
:inverse_of
Опция :inverse_of
определяет имя связи has_many
или has_one
, являющейся противоположностью для этой связи. Подробности смотрите в разделе Двунаправленная связь.
class Author < ApplicationRecord
has_many :books, inverse_of: :author
end
class Book < ApplicationRecord
belongs_to :author, inverse_of: :books
end
:optional
Если установить опции :optional
true
, не будет проверяться наличие связанного объекта. По умолчанию этой опции установлено false
.
:polymorphic
Передача true
для опции :polymorphic
показывает, что это полиморфная связь. Полиморфные связи подробно рассматривались ранее.
:required
Когда установлена true
, у связи проверяется ее наличие. Будет проверяться сама связь, а не ее id. Можно использовать :inverse_of
, чтобы избежать дополнительного запроса во время валидации.
Установлена true
по умолчанию и устарела. Если не хотите, чтобы проверялось наличие связи, используйте optional: true
.
:strict_loading
Обеспечивает строгую загрузку всякий раз, когда связанная запись загружается через связь.
:touch
Если установите опцию :touch
в true
, то временные метки updated_at
или updated_on
на связанном объекте будут установлены в текущее время всякий раз, когда этот объект будет сохранен или уничтожен:
class Book < ApplicationRecord
belongs_to :author, touch: true
end
class Author < ApplicationRecord
has_many :books
end
В этом случае, сохранение или уничтожение книги обновит временную метку на связанном авторе. Также можно определить конкретный атрибут временной метки для обновления:
class Book < ApplicationRecord
belongs_to :author, touch: :books_updated_at
end
:validate
Если установите опцию :validate
в true
, тогда связанные объекты будут проходить валидацию всякий раз, когда вы сохраняете этот объект. По умолчанию она равна false
: связанные объекты не проходят валидацию, когда этот объект сохраняется.
belongs_to
Иногда хочется настроить запрос, используемый belongs_to
. Такая настройка может быть достигнута с помощью блока скоупа. Например:
class Book < ApplicationRecord
belongs_to :author, -> { where active: true }
end
Внутри блока скоупа можно использовать любые стандартные методы запросов. Далее обсудим следующие из них:
where
includes
readonly
select
where
Метод where
позволяет определить условия, которым должен отвечать связанный объект.
class Book < ApplicationRecord
belongs_to :author, -> { where active: true }
end
includes
Метод includes
можно использовать для определения связей второго порядка, которые должны быть лениво загружены при использовании этой связи. Например, рассмотрим эти модели:
class Chapter < ApplicationRecord
belongs_to :book
end
class Book < ApplicationRecord
belongs_to :author
has_many :chapters
end
class Author < ApplicationRecord
has_many :books
end
Если вы часто получаете авторов непосредственно из глав (@chapter.book.author
), то можно улучшить эффективность кода, включив авторов в связь между книгой и ее главами:
class Chapter < ApplicationRecord
belongs_to :book, -> { includes :author }
end
class Book < ApplicationRecord
belongs_to :author
has_many :chapters
end
class Author < ApplicationRecord
has_many :books
end
Нет необходимости в использовании includes
для ближайших связей - то есть, если есть Book belongs_to :author
, то author автоматически лениво загружается при необходимости.
readonly
При использовании readonly
, связанный объект будет только для чтения при получении через связь.
select
Метод select
позволяет переопределить SQL выражение SELECT
, используемое для получения данных о связанном объекте. По умолчанию Rails получает все столбцы.
При использовании метода select
на связи belongs_to
, следует также установить опцию :foreign_key
для гарантии правильных результатов.
Можно увидеть, существует ли какой-либо связанный объект, при использовании метода association.nil?
:
if @book.author.nil?
@msg = "No author found for this book"
end
Присвоение связи belongs_to
не приводит к автоматическому сохранению ни самого объекта, ни связанного объекта.
has_one
Связь has_one
создает соответствие один-к-одному с другой моделью. В терминах базы данных эта связь сообщает, что другой класс содержит внешний ключ. Если этот класс содержит внешний ключ, следует использовать belongs_to
.
has_one
Когда объявляете связь has_one
, объявляющий класс автоматически получает 6 методов, относящихся к связи:
association
association=(associate)
build_association(attributes = {})
create_association(attributes = {})
create_association!(attributes = {})
reload_association
reset_association
Во всех этих методах association
заменяется на символ, переданный как первый аргумент в has_one
. Например, имеем объявление:
class Supplier < ApplicationRecord
has_one :account
end
Каждый экземпляр модели Supplier
будет иметь эти методы:
account
account=
build_account
create_account
create_account!
reload_account
reset_account
При установлении новой связи has_one
или belongs_to
, следует использовать префикс build_
для построения связи, в отличие от метода association.build
, используемого для связей has_many
или has_and_belongs_to_many
. Чтобы создать связь, используйте префикс create_
.
association
Метод association
возвращает связанный объект, если таковой имеется. Если связанный объект не найден, возвращает nil
.
@account = @supplier.account
Если связанный объект уже был получен из базы данных для этого объекта, возвращается кэшированная версия. Чтобы переопределить это поведение (и заставить прочитать из базы данных), вызовите #reload_association
на родительском объекте.
@account = @supplier.reload_account
Чтобы выгрузить кэшированную версию связанного объекта при следующем доступе, если таковая имеется, и прочитать ее из базы данных, вызовите #reset_association
на родительском объекте.
@book.reset_author
association=(associate)
Метод association=
привязывает связанный объект к этому объекту. Фактически это означает извлечение первичного ключа этого объекта и присвоение его значения внешнему ключу связанного объекта.
@supplier.account = @account
build_association(attributes = {})
Метод build_association
возвращает новый объект связанного типа. Этот объект будет экземпляром с переданными атрибутами, и будет установлена связь через внешний ключ, но связанный объект пока не будет сохранен.
@account = @supplier.build_account(terms: "Net 30")
create_association(attributes = {})
Метод create_association
возвращает новый объект связанного типа. Этот объект будет экземпляром с переданными атрибутами, будет установлена связь через внешний ключ, и, если он пройдет валидации, определенные в связанной модели, связанный объект будет сохранен
@account = @supplier.create_account(terms: "Net 30")
create_association!(attributes = {})
Работает так же, как и вышеприведенный create_association
, но вызывает ActiveRecord::RecordInvalid
, если запись невалидна.
has_one
Хотя Rails использует разумные значения по умолчанию, работающие во многих ситуациях, бывают случаи, когда хочется изменить поведение связи has_one
. Такая настройка легко выполнима с помощью передачи опции при создании связи. Например, эта связь использует две такие опции:
class Supplier < ApplicationRecord
has_one :account, class_name: "Billing", dependent: :nullify
end
Связь has_one
поддерживает эти опции:
:as
:autosave
:class_name
:dependent
:disable_joins
:ensuring_owner_was
:foreign_key
:inverse_of
:primary_key
:query_constraints
:required
:source
:source_type
:strict_loading
:through
:touch
:validate
:as
Установка опции :as
показывает, что это полиморфная связь. Полиморфные связи подробно рассматривались ранее.
:autosave
Если установить опцию :autosave
в true
, Rails сохранит любые загруженные связанные члены и уничтожит члены, помеченные для уничтожения, всякий раз, когда сохраняется родительский объект. Но установить :autosave
в false
- не то же самое, что не устанавливать опцию :autosave
. Если опция :autosave
отсутствует, то новые связанные объекты будут сохранены, но обновленные связанные объекты сохранены не будут.
:class_name
Если имя другой модели не может быть образовано из имени связи, можете использовать опцию :class_name
для предоставления имени модели. Например, если поставщик имеет аккаунт, но фактическое имя модели, содержащей аккаунты, это Billing
, можете установить это следующим образом:
class Supplier < ApplicationRecord
has_one :account, class_name: "Billing"
end
:dependent
Управляет тем, что произойдет со связанным объектом, когда его владелец будет уничтожен:
:destroy
приведет к тому, что связанный объект также будет уничтожен
:delete
приведет к тому, что связанный объект будет удален из базы данных напрямую (таким образом не будут выполнены колбэки)
:destroy_async
: когда объект уничтожается, задание ActiveRecord::DestroyAssociationAsyncJob
ставится в очередь, которое вызовет destroy на его связанных объектах. Чтобы это работало, должен быть настроен Active Job. Не используйте эту опцию, если связь обеспечена ограничением внешнего ключу в вашей базе данных. Действия ограничения внешнего ключа произойдут в той же транзакции, которая удаляет владельца.
:nullify
приведет к тому, что внешний ключ будет установлен NULL
. Столбцы полиморфного типа на полиморфных связях также обнуляются. Колбэки не выполняются.
:restrict_with_exception
приведет к вызову исключения ActiveRecord::DeleteRestrictionError
, если есть связанный объект
:restrict_with_error
приведет к ошибке, добавляемой к владельцу, если есть связанный объект
Нельзя устанавливать или оставлять опцию :nullify
для связей, имеющих ограничение NOT NULL
. Если не установить dependent
для уничтожения таких связей, вы не сможете изменить связанный объект, так как внешнему ключу изначально связанного объекта будет назначено недопустимое значение NULL
.
:disable_joins
Указывает, должны ли опускаться join для связи. Если установлено true
, будут сгенерированы два или более запроса. Отметьте, что в некоторых случаях, если применено упорядочивание или лимит, это будет выполнено в памяти из-за ограничений базы данных. Эта опция применима только на связях has_one :through
, так как одиночный has_one
не выполняет join.
:foreign_key
По соглашению Rails предполагает, что столбец, используемый для хранения внешнего ключа в этой модели, имеет имя модели с добавленным суффиксом _id
. Опция :foreign_key
позволяет установить имя внешнего ключа явно:
class Supplier < ApplicationRecord
has_one :account, foreign_key: "supp_id"
end
В любом случае, Rails не создаст столбцы внешнего ключа за вас. Вам необходимо явно определить их в своих миграциях.
:inverse_of
Опция :inverse_of
определяет имя связи belongs_to
, являющейся обратной для этой связи. Подробности смотрите в разделе Двунаправленная связь.
class Supplier < ApplicationRecord
has_one :account, inverse_of: :supplier
end
class Account < ApplicationRecord
belongs_to :supplier, inverse_of: :account
end
:primary_key
По соглашению, Rails предполагает, что столбец, используемый для хранения первичного ключа, это id
. Вы можете переопределить это и явно определить первичный ключ с помощью опции :primary_key
.
:query_constraints
Служит в качестве составного внешнего ключа. Определяет список столбцов для использования при запросе связанного объекта. Это необязательная опция. По умолчанию Rails попытается вывести значение автоматически. Когда есть значение, размер массива должен соответствовать размеру первичного ключа связанной модели или размеру query_constraints
.
:required
Когда установлено true
, у связи также будет проверено ее наличие. Это проверит саму связь, а не id. Можно использовать :inverse_of
, чтобы избежать дополнительного запроса во время валидации.
:source
Опция :source
определяет имя источника связи для связи has_one :through
.
:source_type
Опция :source_type
определяет тип источника связи для связи has_one :through
, который действует при полиморфной связи.
class Author < ApplicationRecord
has_one :book
has_one :hardback, through: :book, source: :format, source_type: "Hardback"
has_one :dust_jacket, through: :hardback
end
class Book < ApplicationRecord
belongs_to :format, polymorphic: true
end
class Paperback < ApplicationRecord; end
class Hardback < ApplicationRecord
has_one :dust_jacket
end
class DustJacket < ApplicationRecord; end
:strict_loading
Обеспечивает строгую загрузку всякий раз, когда связанная запись загружается через связь.
:through
Опция :through
определяет соединительную модель, через которую выполняется запрос. Связи has_one :through
подробно рассматривались ранее.
:touch
Если опция :touch
установлена true
, тогда временные метки updated_at
или updated_on
у связанного объекта будут установлены в текущее время всякий раз, когда этот объект будет сохранен или уничтожен:
class Supplier < ApplicationRecord
has_one :account, touch: true
end
class Account < ApplicationRecord
belongs_to :supplier
end
В этом случае, сохранение или удаление поставщика обновит временную метку у связанного счета. Также можно указать конкретный атрибут временной метки для обновления:
class Supplier < ApplicationRecord
has_one :account, touch: :suppliers_updated_at
end
:validate
Если установите опцию :validate
в true
, тогда новые связанные объекты будут проходить валидацию всякий раз, когда вы сохраняете этот объект. По умолчанию она равна false
: новые связанные объекты не проходят валидацию, когда этот объект сохраняется.
has_one
Иногда хочется настроить запрос, используемый has_one
. Такая настройка может быть достигнута с помощью блока скоупа. Например:
class Supplier < ApplicationRecord
has_one :account, -> { where active: true }
end
Внутри блока скоупа можно использовать любые стандартные методы запросов. Далее обсудим следующие из них:
where
includes
readonly
select
where
Метод where
позволяет определить условия, которым должен отвечать связанный объект.
class Supplier < ApplicationRecord
has_one :account, -> { where "confirmed = 1" }
end
includes
Метод includes
позволяет определить связи второго порядка, которые должны быть лениво загружены при использовании этой связи. Например, рассмотрим эти модели:
class Supplier < ApplicationRecord
has_one :account
end
class Account < ApplicationRecord
belongs_to :supplier
belongs_to :representative
end
class Representative < ApplicationRecord
has_many :accounts
end
Если вы часто получаете representatives непосредственно из suppliers (@supplier.account.representative
), то можно улучшить эффективность кода, включив representatives в связь между suppliers и accounts:
class Supplier < ApplicationRecord
has_one :account, -> { includes :representative }
end
class Account < ApplicationRecord
belongs_to :supplier
belongs_to :representative
end
class Representative < ApplicationRecord
has_many :accounts
end
readonly
При использовании readonly
, связанный объект будет только для чтения при получении через связь.
select
Метод select
позволяет переопределить SQL выражение SELECT
, используемое для получения данных о связанном объекте. По умолчанию Rails получает все столбцы.
Можно увидеть, существует ли какой-либо связанный объект, при использовании метода association.nil?
:
if @supplier.account.nil?
@msg = "No account found for this supplier"
end
Когда вы назначаете объект связью has_one
, этот объект автоматически сохраняется (для того, чтобы обновить его внешний ключ). Кроме того, любой заменяемый объект также автоматически сохраняется, поскольку его внешний ключ также изменяется.
Если одно из этих сохранений проваливается из-за ошибок валидации, тогда выражение назначения возвращает false
, и само назначение отменяется.
Если родительский объект (который объявляет связь has_one
) является несохраненным (то есть new_record?
возвращает true
), тогда дочерние объекты не сохраняются. Они сохранятся автоматически, когда сохранится родительский объект.
Если вы хотите назначить объект связью has_one
без сохранения объекта, используйте метод build_association
.
has_many
Связь has_many
создает отношение один-ко-многим с другой моделью. В терминах базы данных эта связь говорит, что другой класс будет иметь внешний ключ, относящийся к экземплярам этого класса.
has_many
Когда объявляете связь has_many
, объявляющий класс автоматически получает 17 методов, относящихся к связи:
collection
collection<<(object, ...)
collection.delete(object, ...)
collection.destroy(object, ...)
collection=(objects)
collection_singular_ids
collection_singular_ids=(ids)
collection.clear
collection.empty?
collection.size
collection.find(...)
collection.where(...)
collection.exists?(...)
collection.build(attributes = {})
collection.create(attributes = {})
collection.create!(attributes = {})
collection.reload
Во всех этих методах collection
заменяется символом, переданным как первый аргумент в has_many
, и collection_singular
заменяется версией в единственном числе этого символа. Например, имеем объявление:
class Author < ApplicationRecord
has_many :books
end
Каждый экземпляр модели Author
будет иметь эти методы:
books
books<<(object, ...)
books.delete(object, ...)
books.destroy(object, ...)
books=(objects)
book_ids
book_ids=(ids)
books.clear
books.empty?
books.size
books.find(...)
books.where(...)
books.exists?(...)
books.build(attributes = {}, ...)
books.create(attributes = {})
books.create!(attributes = {})
books.reload
collection
Метод collection
возвращает Relation всех связанных объектов. Если нет связанных объектов, он возвращает пустой Relation.
@books = @author.books
collection<<(object, ...)
Метод collection<<
добавляет один или более объектов в коллекцию, устанавливая их внешние ключи равными первичному ключу вызывающей модели.
@author.books << @book1
collection.delete(object, ...)
Метод collection.delete
убирает один или более объектов из коллекции, установив их внешние ключи в NULL
.
@author.books.delete(@book1)
Объекты будут в дополнение уничтожены, если связаны с dependent: :destroy
, и удалены, если они связаны с dependent: :delete_all
.
collection.destroy(object, ...)
Метод collection.destroy
убирает один или более объектов из коллекции, выполняя destroy
для каждого объекта.
@author.books.destroy(@book1)
Объекты будут всегда удаляться из базы данных, игнорируя опцию :dependent
.
collection=(objects)
Метод collection=
делает коллекцию содержащей только представленные объекты, добавляя и удаляя по мере необходимости. Изменения будут персистентными в базе данных.
collection_singular_ids
Метод collection_singular_ids
возвращает массив id объектов в коллекции.
@book_ids = @author.book_ids
collection_singular_ids=(ids)
Метод collection_singular_ids=
делает коллекцию содержащей только объекты, идентифицированные представленными значениями первичного ключа, добавляя и удаляя по мере необходимости. Изменения будут персистентными в базе данных.
collection.clear
Метод collection.clear
убирает каждый объект из коллекции в соответствии со стратегией, определенной опцией dependent
. Если опция не указана, он следует стратегии по умолчанию. Стратегия по умолчанию для has_many :through
это delete_all
, а для связей has_many
— установить их внешние ключи в NULL
.
@author.books.clear
Объекты будут удалены, если они связаны с помощью dependent: :destroy
или dependent: :destroy_async
, как и с помощью dependent: :delete_all
.
collection.empty?
Метод collection.empty?
возвращает true
, если коллекция не содержит каких-либо связанных объектов.
<% if @author.books.empty? %>
No Books Found
<% end %>
collection.size
Метод collection.size
возвращает количество объектов в коллекции.
@book_count = @author.books.size
collection.find(...)
Метод collection.find
ищет объекты в таблице коллекции.
@available_book = @author.books.find(1)
collection.where(...)
Метод collection.where
ищет объекты в коллекции, основываясь на переданных условиях, но объекты загружаются лениво, что означает, что база данных запрашивается только когда происходит доступ к объекту(-там).
@available_books = @author.books.where(available: true) # Пока нет запроса
@available_book = @available_books.first # Теперь база данных будет запрошена
collection.exists?(...)
Метод collection.exists?
проверяет, существует ли в таблице коллекции объект, отвечающий представленным условиям.
collection.build(attributes = {})
Метод collection.build
возвращает один или массив объектов связанного типа. Объект(ы) будут экземплярами с переданными атрибутами, будет создана ссылка через их внешние ключи, но связанные объекты не будут пока сохранены.
@book = @author.books.build(published_at: Time.now,
book_number: "A12345")
@books = @author.books.build([
{ published_at: Time.now, book_number: "A12346" },
{ published_at: Time.now, book_number: "A12347" }])
collection.create(attributes = {})
Метод collection.create
возвращает один или массив новых объектов связанного типа. Объект(ы) будут экземплярами с переданными атрибутами, будет создана ссылка через его внешний ключ, и, если он пройдет валидации, определенные в связанной модели, связанный объект будет сохранен
@book = @author.books.create(published_at: Time.now,
book_number: "A12345")
@books = @author.books.create([
{ published_at: Time.now, book_number: "A12346" },
{ published_at: Time.now, book_number: "A12347" }])
collection.create!(attributes = {})
Работает так же, как вышеприведенный collection.create
, но вызывает ActiveRecord::RecordInvalid
, если запись невалидна.
collection.reload
Метод collection.reload
возвращает Relation всех связанных объектов, принудительно читая базу данных. Если нет связанных объектов, он возвращает пустой Relation.
@books = @author.books.reload
has_many
Хотя Rails использует разумные значения по умолчанию, работающие во многих ситуациях, бывают случаи, когда хочется изменить поведение связи has_many
. Такая настройка легко выполнима с помощью передачи опций при создании связи. Например, эта связь использует две такие опции:
class Author < ApplicationRecord
has_many :books, dependent: :delete_all, validate: false
end
Связь has_many
поддерживает эти опции:
:as
:autosave
:class_name
:counter_cache
:dependent
:disable_joins
:ensuring_owner_was
:extend
:foreign_key
:foreign_type
:inverse_of
:primary_key
:query_constraints
:source
:source_type
:strict_loading
:through
:validate
:as
Установка опции :as
показывает, что это полиморфная связь. Полиморфные связи подробно рассматривались ранее.
:autosave
Если установить опцию :autosave
в true
, Rails сохранит любые загруженные связанные члены и уничтожит члены, помеченные для уничтожения, всякий раз, когда сохраняется родительский объект. Но установить :autosave
в false
- не то же самое, что не устанавливать опцию :autosave
. Если опция :autosave
отсутствует, то новые связанные объекты будут сохранены, но обновленные связанные объекты сохранены не будут.
:class_name
Если имя другой модели не может быть произведено из имени связи, можете использовать опцию :class_name
для предоставления имени модели. Например, если автор имеет много книг, но фактическое имя модели, содержащей книги, это Transaction
, можете установить это следующим образом:
class Author < ApplicationRecord
has_many :books, class_name: "Transaction"
end
:counter_cache
Эта опция используется для настройки произвольно названного :counter_cache
. Эту опцию нужно использовать, только если вы изменили имя вашего :counter_cache
у связи belongs_to.
:dependent
Управляет тем, что произойдет со связанными объектами, когда его владелец будет уничтожен:
:destroy
приведет к тому, что связанные объекты также будут уничтожены
:delete_all
приведет к тому, что связанные объекты будут удалены из базы данных напрямую (таким образом не будут выполнены колбэки)
:destroy_async
: когда объект уничтожается, задание ActiveRecord::DestroyAssociationAsyncJob
ставится в очередь, которое вызовет destroy на его связанных объектах. Чтобы это работало, должен быть настроен Active Job.
:nullify
приведет к тому, что внешние ключи будет установлен NULL
. Столбцы полиморфного типа на полиморфных связях также обнуляются. Колбэки не выполняются.
:restrict_with_exception
приведет к вызову исключения ActiveRecord::DeleteRestrictionError
, если есть какой-нибудь связанный объект
:restrict_with_error
приведет к ошибке, добавляемой к владельцу, если есть какой-нибудь связанный объект
Опции :destroy
и :delete_all
также влияют на семантику методов collection.delete
и collection=
, вынуждая их удалять связанные объекты при удалении из коллекции.
:disable_joins
Указывает, должны ли опускаться join для связи. Если установлено true
, будут сгенерированы два или более запроса. Отметьте, что в некоторых случаях, если применено упорядочивание или лимит, это будет выполнено в памяти из-за ограничений базы данных. Эта опция применима только на связях has_many :through
, так как одиночный has_many
не выполняет join.
:ensuring_owner_was
Указывает метод экземпляра, вызываемого на владельце. Этот метод должен возвратить true, чтобы связанные записи удалялись в фоновом задании.
:extend
Указывает модуль или массив модулей, расширяющих возвращаемый связанный объект. Полезно для определения методов на связях, в особенности когда они должны быть общими у объектов нескольких связей.
:foreign_key
По соглашению Rails предполагает, что столбец, используемый для хранения внешнего ключа в этой модели, имеет имя модели с добавленным суффиксом _id
. Опция :foreign_key
позволяет установить имя внешнего ключа явно:
class Author < ActiveRecord::Base
has_many :books, foreign_key: "cust_id"
end
В любом случае, Rails не создаст столбцы внешнего ключа за вас. Вам необходимо явно определить их в своих миграциях.
:foreign_type
Указывает столбец, используемый для хранения типа связанного объекта, если это полиморфная связь. По умолчанию это угадывается как имя полиморфной связи, указанное на опции “as
” с суффиксом “_type
”. Таким образом, класс, определяющий связь has_many :tags, as: :taggable
, будет использовать “taggable_type
” как :foreign_type
по умолчанию.
:inverse_of
Опция :inverse_of
определяет имя связи belongs_to
, являющейся обратной для этой связи. Подробности смотрите в разделе Двунаправленная связь.
class Author < ApplicationRecord
has_many :books, inverse_of: :author
end
class Book < ApplicationRecord
belongs_to :author, inverse_of: :books
end
:primary_key
По соглашению, Rails предполагает, что столбец, используемый для хранения первичного ключа, это id
. Вы можете переопределить это и явно определить первичный ключ с помощью опции :primary_key
.
Допустим, в таблице users
есть id
в качестве primary_key, но также имеется столбец guid
. Имеется требование, что таблица todos
должна содержать значение столбца guid
, а не значение id
. Это достигается следующим образом:
class User < ApplicationRecord
has_many :todos, primary_key: :guid
end
Теперь, если выполнить @todo = @user.todos.create
, то в запись @todo
значение user_id
будет таким же, как значение guid
в @user
.
:query_constraints
Служит в качестве составного внешнего ключа. Определяет список столбцов для использования при запросе связанного объекта. Это необязательная опция. По умолчанию Rails попытается вывести значение автоматически. Когда есть значение, размер массива должен соответствовать размеру первичного ключа связанной модели или размеру query_constraints
.
:source
Опция :source
определяет имя источника связи для связи has_many :through
. Эту опцию нужно использовать, только если имя источника связи не может быть автоматически выведено из имени связи.
class Author < ApplicationRecord
has_many :books
has_many :paperbacks, through: :books, source: :format, source_type: "Paperback"
end
class Book < ApplicationRecord
belongs_to :format, polymorphic: true
end
class Hardback < ApplicationRecord; end
class Paperback < ApplicationRecord; end
:source_type
Опция :source_type
определяет тип источника связи для связи has_many :through
, который действует при полиморфной связи.
:strict_loading
Когда установлено true, обеспечивает строгую загрузку всякий раз, когда связанная запись загружается через связь.
:through
Опция :through
определяет соединительную модель, через которую выполняется запрос. Связи has_many :through
предоставляют способ осуществления отношений многие-ко-многим, как обсуждалось ранее в этом руководстве.
:validate
Если установите опцию :validate
в false
, тогда новые связанные объекты не будут проходить валидацию всякий раз, когда вы сохраняете этот объект. По умолчанию она равна true
: новые связанные объекты проходят валидацию, когда этот объект сохраняется.
has_many
Иногда хочется настроить запрос, используемый has_many
. Такая настройка может быть достигнута с помощью блока скоупа. Например:
class Author < ApplicationRecord
has_many :books, -> { where processed: true }
end
Внутри блока скоупа можно использовать любые стандартные методы запросов. Далее обсудим следующие из них:
where
extending
group
includes
limit
offset
order
readonly
select
distinct
where
Метод where
позволяет определить условия, которым должен отвечать связанный объект.
class Author < ApplicationRecord
has_many :confirmed_books, -> { where "confirmed = 1" },
class_name: "Book"
end
Также можно задать условия хэшем:
class Author < ApplicationRecord
has_many :confirmed_books, -> { where confirmed: true },
class_name: "Book"
end
При использовании опции where
хэшем, при создание записи через эту связь будет автоматически применен скоуп с использованием хэша. В этом случае при использовании @author.confirmed_books.create
или @author.confirmed_books.build
будут созданы книги, в которых столбец confirmed будет иметь значение true
.
extending
Метод extending
определяет именованный модуль для расширения прокси связи. Расширения связей подробно обсуждаются позже в этом руководстве.
group
Метод group
предоставляет имя атрибута, по которому группируется результирующий набор, используя выражение GROUP BY
в поисковом SQL.
class Author < ApplicationRecord
has_many :chapters, -> { group 'books.id' },
through: :books
end
includes
Можете использовать метод includes
для определения связей второго порядка, которые должны быть нетерпеливо загружены, когда эта связь используется. Например, рассмотрим эти модели:
class Author < ApplicationRecord
has_many :books
end
class Book < ApplicationRecord
belongs_to :author
has_many :chapters
end
class Chapter < ApplicationRecord
belongs_to :book
end
Если вы часто получаете главы прямо из авторов (@author.books.chapters
), тогда можете сделать свой код более эффективным, включив главы в связь от авторов к книгам:
class Author < ApplicationRecord
has_many :books, -> { includes :chapters }
end
class Book < ApplicationRecord
belongs_to :author
has_many :chapters
end
class Chapter < ApplicationRecord
belongs_to :book
end
limit
Метод limit
позволяет ограничить общее количество объектов, которые будут выбраны через связь.
class Author < ApplicationRecord
has_many :recent_books,
-> { order('published_at desc').limit(100) },
class_name: "Book"
end
offset
Метод offset
позволяет определить начальное смещение для выбора объектов через связь. Например, -> { offset(11) }
пропустит первые 11 записей.
order
Метод order
предписывает порядок, в котором связанные объекты будут получены (в синтаксисе SQL, используемом в условии ORDER BY
).
class Author < ApplicationRecord
has_many :books, -> { order "date_confirmed DESC" }
end
readonly
При использовании метода readonly
, связанные объекты будут доступны только для чтения, когда получены посредством связи.
select
Метод select
позволяет переопределить SQL условие SELECT
, которое используется для получения данных о связанном объекте. По умолчанию Rails получает все столбцы.
Если укажете свой собственный select
, не забудьте включить столбцы первичного ключа и внешнего ключа в связанной модели. Если так не сделать, Rails выдаст ошибку.
distinct
Используйте метод distinct
, чтобы убирать дубликаты из коллекции. Это полезно в сочетании с опцией :through
.
class Person < ApplicationRecord
has_many :readings
has_many :articles, through: :readings
end
irb> person = Person.create(name: 'John')
irb> article = Article.create(name: 'a1')
irb> person.articles << article
irb> person.articles << article
irb> person.articles.to_a
=> [#<Article id: 5, name: "a1">, #<Article id: 5, name: "a1">]
irb> Reading.all.to_a
=> [#<Reading id: 12, person_id: 5, article_id: 5>, #<Reading id: 13, person_id: 5, article_id: 5>]
В вышеописанной задаче два reading, и person.articles
выявляет их оба, даже если эти записи указывают на одну и ту же статью.
Давайте установим distinct
:
class Person
has_many :readings
has_many :articles, -> { distinct }, through: :readings
end
irb> person = Person.create(name: 'Honda')
irb> article = Article.create(name: 'a1')
irb> person.articles << article
irb> person.articles << article
irb> person.articles.to_a
=> [#<Article id: 7, name: "a1">]
irb> Reading.all.to_a
=> [#<Reading id: 16, person_id: 7, article_id: 7>, #<Reading id: 17, person_id: 7, article_id: 7>]
В вышеописанной задаче все еще два reading. Однако person.articles
показывает только одну статью, поскольку коллекция загружает только уникальные записи.
Если вы хотите быть уверенными, что после вставки все записи персистентной связи различны (и, таким образом, убедиться, что при просмотре связи никогда не будет дублирующихся записей), следует добавить уникальный индекс для самой таблицы. Например, если таблица называется readings
, и вы хотите убедиться, что все публикации могут быть добавлены к персоне один раз, следует добавить в миграцию:
add_index :readings, [:person_id, :article_id], unique: true
Как только у вас появится этот индекс уникальности, попытка добавить статью к персоне дважды вызовет ошибку ActiveRecord::RecordNotUnique
:
irb> person = Person.create(name: 'Honda')
irb> article = Article.create(name: 'a1')
irb> person.articles << article
irb> person.articles << article
ActiveRecord::RecordNotUnique
Отметьте, что проверка уникальности при использовании чего-то, наподобие include?
, подвержено состояниям гонки. Не пытайтесь использовать include?
для соблюдения уникальности в связи. Используя вышеприведенный пример со статьёй, нижеследующий код вызовет гонку, поскольку несколько пользователей могут использовать его одновременно:
person.articles << article unless person.articles.include?(post)
Когда вы назначаете объект связью has_many
, этот объект автоматически сохраняется (для того, чтобы обновить его внешний ключ). Если назначаете несколько объектов в одном выражении, они все будут сохранены.
Если одно из этих сохранений проваливается из-за ошибок валидации, тогда выражение назначения возвращает false
, и само назначение отменяется.
Если родительский объект (который объявляет связь has_many
) является несохраненным (то есть new_record?
возвращает true
), тогда дочерние объекты не сохраняются при добавлении. Все несохраненные члены связи сохранятся автоматически, когда сохранится родительский объект.
Если вы хотите назначить объект связью has_many
без сохранения объекта, используйте метод collection.build
.
has_and_belongs_to_many
Связь has_and_belongs_to_many
создает отношение многие-ко-многим с другой моделью. В терминах базы данных это связывает два класса через промежуточную соединительную таблицу, которая включает внешние ключи, относящиеся к каждому классу.
has_and_belongs_to_many
Когда объявляете связь has_and_belongs_to_many
, объявляющий класс автоматически получает несколько методов, относящихся к связи:
collection
collection<<(object, ...)
collection.delete(object, ...)
collection.destroy(object, ...)
collection=(objects)
collection_singular_ids
collection_singular_ids=(ids)
collection.clear
collection.empty?
collection.size
collection.find(...)
collection.where(...)
collection.exists?(...)
collection.build(attributes = {})
collection.create(attributes = {})
collection.create!(attributes = {})
collection.reload
Во всех этих методах collection
заменяется символом, переданным как первый аргумент в has_and_belongs_to_many
, а collection_singular
заменяется версией в единственном числе этого символа. Например, имеем объявление:
class Part < ApplicationRecord
has_and_belongs_to_many :assemblies
end
Каждый экземпляр модели Part
будет иметь следующие методы:
assemblies
assemblies<<(object, ...)
assemblies.delete(object, ...)
assemblies.destroy(object, ...)
assemblies=(objects)
assembly_ids
assembly_ids=(ids)
assemblies.clear
assemblies.empty?
assemblies.size
assemblies.find(...)
assemblies.where(...)
assemblies.exists?(...)
assemblies.build(attributes = {}, ...)
assemblies.create(attributes = {})
assemblies.create!(attributes = {})
assemblies.reload
Если соединительная таблица для связи has_and_belongs_to_many
имеет дополнительные столбцы, кроме двух внешних ключей, эти столбцы будут добавлены как атрибуты к записям, получаемым посредством связи. Записи, возвращаемые с дополнительными атрибутами, будут всегда только для чтения, поскольку Rails не может сохранить значения этих атрибутов.
Использование дополнительных атрибутов в соединительной таблице в связи has_and_belongs_to_many устарело. Если требуется этот тип сложного поведения таблицы, соединяющей две модели в отношениях многие-ко-многим, следует использовать связь has_many :through
вместо has_and_belongs_to_many
.
collection
Метод collection
возвращает Relation всех связанных объектов. Если нет связанных объектов, он возвращает пустой Relation.
@assemblies = @part.assemblies
collection<<(object, ...)
Метод collection<<
добавляет один или более объектов в коллекцию, создавая записи в соединительной таблице.
@part.assemblies << @assembly1
Этот метод - просто псевдоним к collection.concat
и collection.push
.
collection.delete(object, ...)
Метод collection.delete
убирает один или более объектов из коллекции, удаляя записи в соединительной таблице. Это не уничтожает объекты.
@part.assemblies.delete(@assembly1)
collection.destroy(object, ...)
Метод collection.destroy
убирает один или более объектов из коллекции, удаляя записи в соединительной таблице. Это не уничтожает объекты.
@part.assemblies.destroy(@assembly1)
collection=(objects)
Метод collection=
делает коллекцию содержащей только представленные объекты, добавляя и удаляя по мере необходимости. Изменения будут персистентными в базе данных.
collection_singular_ids
Метод collection_singular_ids
возвращает массив id объектов в коллекции.
@assembly_ids = @part.assembly_ids
collection_singular_ids=(ids)
Метод collection_singular_ids=
делает коллекцию содержащей только объекты, идентифицированные представленными значениями первичного ключа, добавляя и удаляя по мере необходимости. Изменения будут персистентными в базе данных.
collection.clear
Метод collection.clear
убирает каждый объект из коллекции, удаляя строки из соединительной таблицы. Это не уничтожает связанные объекты.
collection.empty?
Метод collection.empty?
возвращает true
, если коллекция не содержит каких-либо связанных объектов.
<% if @part.assemblies.empty? %>
This part is not used in any assemblies
<% end %>
collection.size
Метод collection.size
возвращает количество объектов в коллекции.
@assembly_count = @part.assemblies.size
collection.find(...)
Метод collection.find
ищет объекты в таблице коллекции.
@assembly = @part.assemblies.find(1)
collection.where(...)
Метод collection.where
ищет объекты в коллекции, основываясь на переданных условиях, но объекты загружаются лениво, что означает, что база данных запрашивается только когда происходит доступ к объекту(-там).
@new_assemblies = @part.assemblies.where("created_at > ?", 2.days.ago)
collection.exists?(...)
Метод collection.exists?
проверяет, существует ли в таблице коллекции объект, отвечающий представленным условиям.
collection.build(attributes = {})
Метод collection.build
возвращает один или более объектов связанного типа. Эти объекты будут экземплярами с переданными атрибутами, и будет создана связь через соединительную таблицу, но связанный объект пока не будет сохранен.
@assembly = @part.assemblies.build({ assembly_name: "Transmission housing" })
collection.create(attributes = {})
Метод collection.create
возвращает один или более объектов связанного типа. Эти объекты будут экземплярами с переданными атрибутами, будет создана связь через соединительную таблицу, и, если он пройдет валидации, определенные в связанной модели, связанный объект будет сохранен.
@assembly = @part.assemblies.create({ assembly_name: "Transmission housing" })
collection.create!(attributes = {})
Работает так же, как вышеприведенный collection.create
, но вызывает ActiveRecord::RecordInvalid
, если запись невалидна.
collection.reload
Метод collection.reload
возвращает Relation всех связанных объектов, принудительно читая базу данных. Если нет связанных объектов, он возвращает пустой Relation.
@assemblies = @part.assemblies.reload
has_and_belongs_to_many
Хотя Rails использует разумные значения по умолчанию, работающие во многих ситуациях, бывают случаи, когда хочется изменить поведение связи has_and_belongs_to_many
. Такая настройка легко выполнима с помощью передачи опции при создании связи. Например, эта связь использует две такие опции:
class Parts < ApplicationRecord
has_and_belongs_to_many :assemblies, -> { readonly },
autosave: true
end
Связь has_and_belongs_to_many
поддерживает эти опции:
:association_foreign_key
:autosave
:class_name
:foreign_key
:join_table
:strict_loading
:validate
:association_foreign_key
По соглашению Rails предполагает, что столбец в соединительной таблице, используемый для хранения внешнего ключа, указываемого на другую модель, является именем этой модели с добавленным суффиксом _id
. Опция :association_foreign_key
позволяет установить имя внешнего ключа явно:
Опции :foreign_key
и :association_foreign_key
полезны при настройке присоединения к себе многие-ко-многим. Например:
class User < ApplicationRecord
has_and_belongs_to_many :friends,
class_name: "User",
foreign_key: "this_user_id",
association_foreign_key: "other_user_id"
end
:autosave
Если установить опцию :autosave
в true
, Rails сохранит любые загруженные связанные члены и уничтожит члены, помеченные для уничтожения, всякий раз, когда сохраняется родительский объект. Но установить :autosave
в false
- не то же самое, что не устанавливать опцию :autosave
. Если опция :autosave
отсутствует, то новые связанные объекты будут сохранены, но обновленные связанные объекты сохранены не будут.
:class_name
Если имя другой модели не может быть произведено из имени связи, можете использовать опцию :class_name
для предоставления имени модели. Например, если часть имеет много сборок, но фактическое имя модели, содержащей сборки - это Gadget
, можете установить это следующим образом:
class Parts < ApplicationRecord
has_and_belongs_to_many :assemblies, class_name: "Gadget"
end
:foreign_key
По соглашению Rails предполагает, что столбец в соединительной таблице, используемый для хранения внешнего ключа, указываемого на эту модель, имеет имя модели с добавленным суффиксом _id
. Опция :foreign_key
позволяет установить имя внешнего ключа явно:
class User < ApplicationRecord
has_and_belongs_to_many :friends,
class_name: "User",
foreign_key: "this_user_id",
association_foreign_key: "other_user_id"
end
:join_table
Если имя соединительной таблицы по умолчанию, основанное на алфавитном порядке, - это не то, что вам нужно, используйте опцию :join_table
, чтобы переопределить его.
:strict_loading
Обеспечивает строгую загрузку всякий раз, когда связанная запись загружается через связь.
:validate
Если установите опцию :validate
в false
, тогда новые связанные объекты не будут проходить валидацию всякий раз, когда вы сохраняете этот объект. По умолчанию она равна true
: новые связанные объекты проходят валидацию, когда этот объект сохраняется.
has_and_belongs_to_many
Иногда хочется настроить запрос, используемый has_many
. Такая настройка может быть достигнута с помощью блока скоупа. Например:
class Parts < ApplicationRecord
has_and_belongs_to_many :assemblies, -> { where active: true }
end
Внутри блока скоупа можно использовать любые стандартные методы запросов. Далее обсудим следующие из них:
where
extending
group
includes
limit
offset
order
readonly
select
distinct
where
Метод where
позволяет определить условия, которым должен отвечать связанный объект.
class Parts < ApplicationRecord
has_and_belongs_to_many :assemblies,
-> { where "factory = 'Seattle'" }
end
Также можно задать условия хэшем:
class Parts < ApplicationRecord
has_and_belongs_to_many :assemblies,
-> { where factory: 'Seattle' }
end
При использовании опции where
хэшем, при создание записи через эту связь будет автоматически применен скоуп с использованием хэша. В этом случае при использовании @parts.assemblies.create
или @parts.assemblies.build
будут созданы сборки, в которых столбец factory
будет иметь значение Seattle
.
extending
Метод extending
определяет именованный модуль для расширения прокси связи. Расширения связей подробно обсуждаются позже в этом руководстве.
group
Метод group
предоставляет имя атрибута, по которому группируется результирующий набор, используя выражение GROUP BY
в поисковом запросе SQL.
class Parts < ApplicationRecord
has_and_belongs_to_many :assemblies, -> { group "factory" }
end
includes
Можете использовать метод includes
для определения связей второго порядка, которые должны быть нетерпеливо загружены, когда эта связь используется.
limit
Метод limit
позволяет ограничить общее количество объектов, которые будут выбраны через связь.
class Customer < ApplicationRecord
has_and_belongs_to_many :assemblies,
-> { order("created_at DESC").limit(50) }
end
offset
Метод offset
позволяет определить начальное смещение для выбора объектов через связь. Например, -> { offset(11) }
пропустит первые 11 записей.
order
Метод order
предписывает порядок, в котором связанные объекты будут получены (в синтаксисе SQL, используемом в условии ORDER BY
).
class Customer < ApplicationRecord
has_and_belongs_to_many :assemblies,
-> { order "assembly_name ASC" }
end
readonly
При использовании метода :readonly
, связанные объекты будут доступны только для чтения, когда получены посредством связи.
select
Метод select
позволяет переопределить SQL условие SELECT
, которое используется для получения данных о связанном объекте. По умолчанию Rails получает все столбцы.
distinct
Используйте метод distinct
, чтобы убирать дубликаты из коллекции.
Когда вы назначаете объект связью has_and_belongs_to_many
, этот объект автоматически сохраняется (в порядке обновления соединительной таблицы). Если назначаете несколько объектов в одном выражении, они все будут сохранены.
Если одно из этих сохранений проваливается из-за ошибок валидации, тогда выражение назначения возвращает false
, a само назначение отменяется.
Если родительский объект (который объявляет связь has_and_belongs_to_many
) является несохраненным (то есть new_record?
возвращает true
), тогда дочерние объекты не сохраняются при добавлении. Все несохраненные члены связи сохранятся автоматически, когда сохранится родительский объект.
Если вы хотите назначить объект связью has_and_belongs_to_many
без сохранения объекта, используйте метод collection.build
.
Обычно колбэки прицепляются к жизненному циклу объектов Active Record, позволяя вам работать с этими объектами в различных точках. Например, можете использовать колбэк :before_save
, чтобы вызвать что-то перед тем, как объект будет сохранен.
Колбэки связи похожи на обычные колбэки, но они включаются событиями в жизненном цикле коллекции. Доступны четыре колбэка связи:
before_add
after_add
before_remove
after_remove
Колбэки связи объявляются с помощью добавления опций в объявление связи. Например:
class Author < ApplicationRecord
has_many :books, before_add: :check_credit_limit
def check_credit_limit(book)
# ...
end
end
Подробнее о колбэках связи читайте в Руководстве о колбэках Active Record
Вы не ограничены функциональностью, которую Rails автоматически встраивает в выданные по связи объекты. Можно расширить эти объекты с помощью анонимных модулей, добавив новые методы поиска, методы создания и иные методы. Например:
class Author < ApplicationRecord
has_many :books do
def find_by_book_prefix(book_number)
find_by(category_id: book_number[0..2])
end
end
end
Если имеется расширение, которое должно быть общим для нескольких связей, можно использовать именованный модуль расширения. Например:
module FindRecentExtension
def find_recent
where("created_at > ?", 5.days.ago)
end
end
class Author < ApplicationRecord
has_many :books, -> { extending FindRecentExtension }
end
class Supplier < ApplicationRecord
has_many :deliveries, -> { extending FindRecentExtension }
end
Расширения могут ссылаться на внутренние методы выданных по связи объектов, используя следующие три атрибута акцессора proxy_association
:
proxy_association.owner
возвращает объект, в котором объявлена связь.
proxy_association.reflection
возвращает объект reflection, описывающий связь.
proxy_association.target
возвращает связанный объект для belongs_to
или has_one
, или коллекцию связанных объектов для has_many
или has_and_belongs_to_many
.
Владелец связи может быть передан в качестве единственного аргумента в блок скоупа в ситуации, когда вам необходим больший контроль над ограничением связи. Однако, предупреждаем, что предварительная загрузка связи не будет больше работать.
class Supplier < ApplicationRecord
has_one :account, ->(supplier) { where active: supplier.active? }
end
Иногда можно делиться полями и поведением между различными моделями. Скажем, у нас есть модели Car, Motorcycle и Bicycle. Мы хотим совместно использовать поля color
и price
и некоторые методы всеми из них, но иметь некоторое специфичное поведение для каждого, а также различные контроллеры.
Сначала нужно сгенерировать базовую модель Vehicle:
$ bin/rails generate model vehicle type:string color:string price:decimal{10.2}
Вы заметили, что мы добавили поле "type"? Так как все модели будут сохранены в одну таблицу базы данных, Rails сохранит в этот столбец имя модели, которая сохраняется. В нашем примере это может быть "Car", "Motorcycle" или "Bicycle." STI не работает без поля "type" в таблице.
Затем мы сгенерируем модель Car, унаследованную от Vehicle. Для этого можно использовать опцию --parent=PARENT
, которая сгенерирует модель, унаследованную от указанного родителя и без эквивалентной миграции (так как таблица уже существует).
Например, чтобы сгенерировать модель Car:
$ bin/rails generate model car --parent=Vehicle
Сгенерированная модель будет выглядеть так:
class Car < Vehicle
end
Это означает, что все поведение, такое как связи, публичные методы и так далее, добавленное в Vehicle, доступно также для Car.
Создание автомобиля сохранит его в таблице vehicles
со значением "Car" в поле type
:
Car.create(color: 'Red', price: 10000)
сгенерирует следующий SQL:
INSERT INTO "vehicles" ("type", "color", "price") VALUES ('Car', 'Red', 10000)
Запрос записей автомобилей будет искать только среди транспортных средств, которые являются автомобилями:
Car.all
запустит подобный запрос:
SELECT "vehicles".* FROM "vehicles" WHERE "vehicles"."type" IN ('Car')
Наследование с единой таблицей (STI)
хорошо работает, когда между подклассами и их атрибутами небольшая разница, но для включения всех атрибутов всех подклассов нужно создать единую таблицу.
Недостаток такого подхода в том, что он приводит к раздуванию этой таблицы. Поскольку ей нужно включать атрибуты, специфичные только для подкласса, но не используемые где-либо еще.
В следующем примере есть две модели Active Record, наследуемые от одного класса "Entry", включающего атрибут subject
.
# Schema: entries[ id, type, subject, created_at, updated_at]
class Entry < ApplicationRecord
end
class Comment < Entry
end
class Message < Entry
end
Делегируемые типы решают эту проблему с помощью delegated_type
.
Чтобы использовать делегируемые типы, нужно смоделировать данные определенным образом. Требования следующие:
Это устраняет необходимость определять атрибуты в единой таблице, которые непреднамеренно доступны всем подклассам.
Чтобы применить это в вышеприведенном примере, нужно перегенерировать модели. Сначала сгенерируем базовую модель Entry
, которая будет выступать в роли суперкласса:
$ bin/rails generate model entry entryable_type:string entryable_id:integer
Затем сгенерируем новые модели Message
и Comment
для делегации:
$ bin/rails generate model message subject:string body:string
$ bin/rails generate model comment content:string
После запуска генераторов, должны получиться такие модели:
# Schema: entries[ id, entryable_type, entryable_id, created_at, updated_at ]
class Entry < ApplicationRecord
end
# Schema: messages[ id, subject, body, created_at, updated_at ]
class Message < ApplicationRecord
end
# Schema: comments[ id, content, created_at, updated_at ]
class Comment < ApplicationRecord
end
delegated_type
Сначала объявляем delegated_type
в суперклассе Entry
.
class Entry < ApplicationRecord
delegated_type :entryable, types: %w[ Message Comment ], dependent: :destroy
end
Параметр entryable
указывает поле, используемое для делегации, и включает типы Message
и Comment
в качестве делегируемых классов.
У класса Entry
есть поля entryable_type
и entryable_id
. Это поля с добавленными суффиксами _type
, _id
, добавленными к имени entryable
из определения delegated_type
. entryable_type
хранит им подкласса делегации, и entryable_id
хранит id записи подкласса делегации.
Затем нужно определить модуль для реализации этих делегируемых типов, объявляя параметр as: :entryable
у связи has_one
.
module Entryable
extend ActiveSupport::Concern
included do
has_one :entry, as: :entryable, touch: true
end
end
А затем включить созданный модуль в подкласс.
class Message < ApplicationRecord
include Entryable
end
class Comment < ApplicationRecord
include Entryable
end
По завершение определения, делегатор Entry
будет предоставлять следующие методы:
Method | Return |
---|---|
Entry#entryable_class |
Message или Comment |
Entry#entryable_name |
"message" или "comment" |
Entry.messages |
Entry.where(entryable_type: "Message") |
Entry#message? |
Возвращает true, когда entryable_type == "Message" |
Entry#message |
Возвращает запись сообщения, когда entryable_type == "Message" , иначе nil |
Entry#message_id |
Возвращает entryable_id , когда entryable_type == "Message" , иначе nil |
Entry.comments |
Entry.where(entryable_type: "Comment") |
Entry#comment? |
Возвращает true, когда entryable_type == "Comment" |
Entry#comment |
Возвращает запись комментария, когда entryable_type == "Comment" , иначе nil |
Entry#comment_id |
Возвращает entryable_id , когда entryable_type == "Comment" , иначе nil |
При создании нового объекта Entry
в то же время можно указать подкласс entryable
.
Entry.create! entryable: Message.new(subject: "hello!")
Можно расширить делегатор Entry
и усовершенствовать его, определив delegate
и используя полиморфизм на подклассах. Например, чтобы делегировать метод title
из Entry
в его подклассы:
class Entry < ApplicationRecord
delegated_type :entryable, types: %w[ Message Comment ]
delegate :title, to: :entryable
end
class Message < ApplicationRecord
include Entryable
def title
subject
end
end
class Comment < ApplicationRecord
include Entryable
def title
content.truncate(20)
end
end