Связи Active Record

Это руководство раскрывает особенности связей Active Record.

После его прочтения, вы узнаете:

  • Как объявлять связи между моделями Active Record
  • Как понимать различные типы связей Active Record
  • Как использовать методы, добавленные в ваши модели при создании связей

1. Зачем нужны связи?

В Rails связь - это соединение между двумя моделями Active Record. Зачем нам нужны связи между моделями? Затем, что они позволяют сделать код для обычных операций проще и легче. Например, рассмотрим простое приложение на Rails, которое включает модель для авторов и модель для книг. Каждый автор может иметь много книг. Без связей объявление модели будет выглядеть так:

class Author < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
end

class Book < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
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  # ActiveRecord::Base до Rails 5.0
  has_many :books, dependent: :destroy
end

class Book < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  belongs_to :author
end

С этими изменениями создание новой книги для определенного автора проще:

@book = @author.books.create(published_at: Time.now)

Удаление автора и всех его книг намного проще:

@author.destroy

Чтобы узнать больше о различных типах связей, читайте следующий раздел руководства. Затем следуют некоторые полезные советы по работе со связями, а затем полное описание методов и опций для связей в Rails.

2. Типы связей

Rails поддерживает шесть типов связей:

  • belongs_to
  • has_one
  • has_many
  • has_many :through
  • has_one :through
  • has_and_belongs_to_many

Связи реализуются с использованием макро-вызовов (macro-style calls), и, таким образом, вы можете декларативно добавлять возможности для своих моделей. Например, объявляя, что одна модель принадлежит (belongs_to) другой, вы указываете Rails сохранять информацию о первичном-внешнем ключах между экземплярами двух моделей, а также получаете несколько полезных методов, добавленных в модель.

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

2.1. Связь belongs_to

Связь belongs_to устанавливает соединение один-к-одному с другой моделью, когда один экземпляр объявляющей модели "принадлежит" одному экземпляру другой модели. Например, если в приложении есть авторы и книги, и одна книга может быть связана только с одним автором, нужно объявить модель book следующим образом:

class Book < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  belongs_to :author
end

Диаграмма для связи belongs_to

связи belongs_to обязаны использовать единственное число. Если использовать множественное число в вышеприведенном примере для связи author в модели Book, вам будет сообщено "uninitialized constant Book::Authors". Это так, потому что Rails автоматически получает имя класса из имени связи. Если в имени связи неправильно использовано число, то получаемый класс также будет неправильного числа.

Соответствующая миграция может выглядеть так:

class CreateOrders < ActiveRecord::Migration[5.0]
  def change
    create_table :authors do |t|
      t.string :name
      t.timestamps null: false
    end

    create_table :books do |t|
      t.belongs_to :author, index: true
      t.datetime :published_at
      t.timestamps null: false
    end
  end
end

2.2. Связь has_one

Связь has_one также устанавливает соединение один-к-одному с другой моделью, но в несколько ином смысле (и с другими последствиями). Эта связь показывает, что каждый экземпляр модели содержит или обладает одним экземпляром другой модели. Например, если каждый поставщик имеет только один аккаунт, можете объявить модель supplier подобно этому:

class Supplier < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  has_one :account
end

Диаграмма для связи has_one

Соответствующая миграция может выглядеть так:

class CreateSuppliers < ActiveRecord::Migration[5.0]
  def change
    create_table :suppliers do |t|
      t.string :name
      t.timestamps null: false
    end

    create_table :accounts do |t|
      t.belongs_to :supplier, index: true
      t.string :account_number
      t.timestamps null: false
    end
  end
end

В зависимости от применения, возможно потребуется создать индекс уникальности и/или ограничение внешнего ключа на указанный столбец таблицы accounts. В этом случае определение столбца может выглядеть так:

create_table :accounts do |t|
  t.belongs_to :supplier, index: true, unique: true, foreign_key: true
  # ...
end

2.3. Связь has_many

Связь has_many указывает на соединение один-ко-многим с другой моделью. Эта связь часто бывает на "другой стороне" связи belongs_to. Эта связь указывает на то, что каждый экземпляр модели имеет ноль или более экземпляров другой модели. Например, в приложении, содержащем авторов и книги, модель author может быть объявлена следующим образом:

class Author < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  has_many :books
end

Имя другой модели указывается во множественном числе при объявлении связи has_many.

Диаграмма для связи has_many

Соответствующая миграция может выглядеть так:

class CreateAuthors < ActiveRecord::Migration[5.0]
  def change
    create_table :authors do |t|
      t.string :name
      t.timestamps null: false
    end

    create_table :books do |t|
      t.belongs_to :author, index: true
      t.datetime :published_at
      t.timestamps null: false
    end
  end
end

2.4. Связь has_many :through

Связь has_many :through часто используется для настройки соединения многие-ко-многим с другой моделью. Эта связь указывает, что объявляющая модель может соответствовать нулю или более экземплярам другой модели через третью модель. Например, рассмотрим поликлинику, где пациентам (patients) дают направления (appointments) к врачам (physicians). Соответствующие объявления связей будут выглядеть следующим образом:

class Physician < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  has_many :appointments
  has_many :patients, through: :appointments
end

class Appointment < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  belongs_to :physician
  belongs_to :patient
end

class Patient < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  has_many :appointments
  has_many :physicians, through: :appointments
end

Диаграмма для связи has_many :through

Соответствующая миграция может выглядеть так:

class CreateAppointments < ActiveRecord::Migration[5.0]
  def change
    create_table :physicians do |t|
      t.string :name
      t.timestamps null: false
    end

    create_table :patients do |t|
      t.string :name
      t.timestamps null: false
    end

    create_table :appointments do |t|
      t.belongs_to :physician, index: true
      t.belongs_to :patient, index: true
      t.datetime :appointment_date
      t.timestamps null: false
    end
  end
end

Коллекция соединительных моделей может управляться с помощью методов связи has_many. Например, если вы присвоите:

physician.patients = patients

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

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

Связь has_many :through также полезна для настройки "ярлыков" через вложенные связи has_many. Например, если документ имеет много секций, а секция имеет много параграфов, иногда хочется получить просто коллекцию всех параграфов в документе. Это можно настроить следующим образом:

class Document < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  has_many :sections
  has_many :paragraphs, through: :sections
end

class Section < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  belongs_to :document
  has_many :paragraphs
end

class Paragraph < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  belongs_to :section
end

С определенным through: :sections Rails теперь понимает:

@document.paragraphs

2.5. Связь has_one :through

Связь has_one :through настраивает соединение один-к-одному с другой моделью. Эта связь показывает, что объявляющая модель может быть связана с одним экземпляром другой модели через третью модель. Например, если каждый поставщик имеет один аккаунт, и каждый аккаунт связан с одной историей аккаунта, тогда модели могут выглядеть так:

class Supplier < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  has_one :account
  has_one :account_history, through: :account
end

class Account < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  belongs_to :supplier
  has_one :account_history
end

class AccountHistory < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  belongs_to :account
end

Диаграмма для связи has_one :through

Соответствующая миграция может выглядеть так:

class CreateAccountHistories < ActiveRecord::Migration[5.0]
  def change
    create_table :suppliers do |t|
      t.string :name
      t.timestamps null: false
    end

    create_table :accounts do |t|
      t.belongs_to :supplier, index: true
      t.string :account_number
      t.timestamps null: false
    end

    create_table :account_histories do |t|
      t.belongs_to :account, index: true
      t.integer :credit_rating
      t.timestamps null: false
    end
  end
end

2.6. Связь has_and_belongs_to_many

Связь has_and_belongs_to_many создает прямое соединение многие-ко-многим с другой моделью, без промежуточной модели. Например, если ваше приложение включает узлы (assemblies) и детали (parts), где каждый узел имеет много деталей, и каждая деталь встречается во многих узлах, модели можно объявить таким образом:

class Assembly < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  has_and_belongs_to_many :parts
end

class Part < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  has_and_belongs_to_many :assemblies
end

Диаграмма для связи has_and_belongs_to_many

Соответствующая миграция может выглядеть так:

class CreateAssembliesAndParts < ActiveRecord::Migration[5.0]
  def change
    create_table :assemblies do |t|
      t.string :name
      t.timestamps null: false
    end

    create_table :parts do |t|
      t.string :part_number
      t.timestamps null: false
    end

    create_table :assemblies_parts, id: false do |t|
      t.belongs_to :assembly, index: true
      t.belongs_to :part, index: true
    end
  end
end

2.7. Выбор между belongs_to и has_one

Если хотите настроить отношение один-к-одному между двумя моделями, необходимо добавить belongs_to к одной и has_one к другой. Как узнать что к какой?

Различие в том, где помещен внешний ключ (он должен быть в таблице для класса, объявляющего связь belongs_to), но вы также должны думать о реальном значении данных. Отношение has_one говорит, что что-то принадлежит вам - то есть что что-то указывает на вас. Например, больше смысла в том, что поставщик владеет аккаунтом, чем в том, что аккаунт владеет поставщиком. Это означает, что правильные отношения подобны этому:

class Supplier < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  has_one :account
end

class Account < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  belongs_to :supplier
end

Соответствующая миграция может выглядеть так:

class CreateSuppliers < ActiveRecord::Migration[5.0]
  def change
    create_table :suppliers do |t|
      t.string  :name
      t.timestamps null: false
    end

    create_table :accounts do |t|
      t.integer :supplier_id
      t.string  :account_number
      t.timestamps null: false
    end

    add_index :accounts, :supplier_id
  end
end

Использование t.integer :supplier_id указывает имя внешнего ключа очевидно и явно. В современных версиях Rails можно абстрагироваться от деталей реализации используя t.references :supplier.

2.8. Выбор между has_many :through и has_and_belongs_to_many

Rails предлагает два разных способа объявления отношения многие-ко-многим между моделями. Простейший способ - использовать has_and_belongs_to_many, который позволяет создать связь напрямую:

class Assembly < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  has_and_belongs_to_many :parts
end

class Part < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  has_and_belongs_to_many :assemblies
end

Второй способ объявить отношение многие-ко-многим - использование has_many :through. Это осуществляет связь не напрямую, а через соединяющую модель:

class Assembly < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  has_many :manifests
  has_many :parts, through: :manifests
end

class Manifest < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  belongs_to :assembly
  belongs_to :part
end

class Part < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  has_many :manifests
  has_many :assemblies, through: :manifests
end

Простейший признак того, что нужно настраивать отношение has_many :through - если необходимо работать с моделью отношений как с независимым объектом. Если вам не нужно ничего делать с моделью отношений, проще настроить связь has_and_belongs_to_many (хотя нужно не забыть создать соединяющую таблицу в базе данных).

Вы должны использовать has_many :through, если нужны валидации, колбэки или дополнительные атрибуты для соединительной модели.

2.9. Полиморфные связи

Полиморфные связи - это немного более "навороченный" вид связей. С полиморфными связями модель может принадлежать более чем одной модели, на одиночной связи. Например, имеется модель изображения, которая принадлежит или модели работника, или модели продукта. Вот как это объявляется:

class Picture < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  belongs_to :imageable, polymorphic: true
end

class Employee < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  has_many :pictures, as: :imageable
end

class Product < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  has_many :pictures, as: :imageable
end

Можете считать полиморфное объявление belongs_to как настройку интерфейса, который может использовать любая другая модель. Из экземпляра модели Employee можно получить коллекцию изображений: @employee.pictures.

Подобным образом можно получить @product.pictures.

Если имеется экземпляр модели Picture, можно получить его родителя посредством @picture.imageable. Чтобы это работало, необходимо объявить столбец внешнего ключа и столбец типа в модели, объявляющей полиморфный интерфейс:

class CreatePictures < ActiveRecord::Migration[5.0]
  def change
    create_table :pictures do |t|
      t.string  :name
      t.integer :imageable_id
      t.string  :imageable_type
      t.timestamps null: false
    end

    add_index :pictures, [:imageable_type, :imageable_id]
  end
end

Эта миграция может быть упрощена при использовании формы t.references:

class CreatePictures < ActiveRecord::Migration[5.0]
  def change
    create_table :pictures do |t|
      t.string :name
      t.references :imageable, polymorphic: true, index: true
      t.timestamps null: false
    end
  end
end

Диаграмма для полиморфной связи

2.10. Присоединение к себе

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

class Employee < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  has_many :subordinates, class_name: "Employee",
                          foreign_key: "manager_id"

  belongs_to :manager, class_name: "Employee"
end

С такой настройкой, вы можете получить @employee.subordinates и @employee.manager.

В миграциях/схеме следует добавить столбец ссылки модели на саму себя.

class CreateEmployees < ActiveRecord::Migration[5.0]
  def change
    create_table :employees do |t|
      t.references :manager, index: true
      t.timestamps null: false
    end
  end
end

3. Полезные советы и предупреждения

Вот некоторые вещи, которые необходимо знать для эффективного использования связей Active Record в вашем приложении на Rails:

  • Управление кэшированием
  • Предотвращение коллизий имен
  • Обновление схемы
  • Управление областью видимости связей
  • Двусторонние связи

3.1. Управление кэшированием

Все методы связи построены вокруг кэширования, которое хранит результаты последних запросов доступными для будущих операций. Кэш является общим для разных методов. Например:

author.books                 # получаем книги из базы данных
author.books.size            # используем кэшированную копию книг
author.books.empty?          # используем кэшированную копию книг

Но что если вы хотите перезагрузить кэш, так как данные могли быть изменены другой частью приложения? Всего лишь передайте true в вызов связи:

author.books                 # получаем книги из базы данных
author.books.size            # используем кэшированную копию книг
author.books(true).empty?    # отказываемся от кэшированной копии книг
                             # и снова обращаемся к базе данных

3.2. Предотвращение коллизий имен

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

3.3. Обновление схемы

Связи очень полезные, но не волшебные. Вы ответственны за содержание вашей схемы базы данных в соответствии со связями. На практике это означает две вещи, в зависимости от того, какой тип связей создаете. Для связей belongs_to нужно создать внешние ключи, а для связей has_and_belongs_to_many нужно создать подходящую соединительную таблицу.

3.3.1. Создание внешних ключей для связей belongs_to

Когда объявляете связь belongs_to, нужно создать внешние ключи, при необходимости. Например, рассмотрим эту модель:

class Book < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  belongs_to :author
end

Это объявление нуждается в создании подходящего внешнего ключа в таблице books:

class CreateBooks < ActiveRecord::Migration[5.0]
  def change
    create_table :books do |t|
      t.datetime :published_at
      t.string   :book_number
      t.integer  :author_id
    end

    add_index :books, :author_id
  end
end

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

3.3.2. Создание соединительных таблиц для связей 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  # ActiveRecord::Base до Rails 5.0
  has_and_belongs_to_many :parts
end

class Part < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  has_and_belongs_to_many :assemblies
end

Теперь нужно написать миграцию для создания таблицы assemblies_parts. Эта таблица должна быть создана без первичного ключа:

class CreateAssembliesPartsJoinTable < ActiveRecord::Migration[5.0]
  def change
    create_table :assemblies_parts, id: false do |t|
      t.integer :assembly_id
      t.integer :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[5.0]
  def change
    create_join_table :assemblies, :parts do |t|
      t.index :assembly_id
      t.index :part_id
    end
  end
end

3.4. Управление областью видимости связей

По умолчанию связи ищут объекты только в пределах области видимости текущего модуля. Это важно, когда вы объявляете модели Active Record внутри модуля. Например:

module MyApplication
  module Business
    class Supplier < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
       has_one :account
    end

    class Account < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
       belongs_to :supplier
    end
  end
end

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

module MyApplication
  module Business
    class Supplier < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
       has_one :account
    end
  end

  module Billing
    class Account < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
       belongs_to :supplier
    end
  end
end

Для связи модели с моделью в другом пространстве имен, необходимо указать полное имя класса в объявлении связи:

module MyApplication
  module Business
    class Supplier < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
       has_one :account,
        class_name: "MyApplication::Billing::Account"
    end
  end

  module Billing
    class Account < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
       belongs_to :supplier,
        class_name: "MyApplication::Business::Supplier"
    end
  end
end

3.5. Двусторонние связи

Для связей нормально работать в двух направлениях, затребовав объявление в двух различных моделях:

class Author < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  has_many :books
end

class Book < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  belongs_to :author
end

По умолчанию, Active Record не знает о зависимости между этими двумя связями. Это может привести к двум несинхронизированным копиям объекта:

a = Author.first
b = a.books.first
a.first_name == b.author.first_name # => true
a.first_name = 'Manny'
a.first_name == b.author.first_name # => false

Это произошло потому, что a и b.author это два разных представления в памяти одних и тех же данных, и ни одно из них автоматически не обновляется при изменении другого. Active Record предоставляет опцию :inverse_of, чтобы вы могли его проинформировать об этих зависимостях:

class Author < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  has_many :books, inverse_of: :author
end

class Book < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  belongs_to :author, inverse_of: :books
end

С этими изменениями Active Record загрузит только одну копию объекта author, предотвратив несоответствия и сделав приложение более эффективным:

a = Author.first
b = a.books.first
a.first_name == b.author.first_name # => true
a.first_name = 'Manny'
a.first_name == b.author.first_name # => true

Имеется несколько ограничений в поддержке inverse_of:

  • Они не работают со связями :through.
  • Они не работают со связями :polymorphic.
  • Они не работают со связями :as.
  • Для связей belongs_to противоположные связи has_many игнорируются.

Каждая связь попытается автоматически найти противоположную связь и установить опцию :inverse_of эвристически (основываясь на имени связи). Поддерживается большинство связей со стандартными именами. Однако, связям, содержащим следующие опции, противоположности не будут установлены автоматически:

  • :conditions
  • :through
  • :polymorphic
  • :foreign_key

4. Подробная информация по связи belongs_to

Связь belongs_to создает соответствие один-к-одному с другой моделью. В терминах базы данных эта связь сообщает, что этот класс содержит внешний ключ. Если внешний ключ содержит другой класс, вместо этого следует использовать has_one.

4.1. Методы, добавляемые belongs_to

Когда объявляете связь belongs_to, объявляющий класс автоматически получает пять методов, относящихся к связи:

  • association
  • association=(associate)
  • build_association(attributes = {})
  • create_association(attributes = {})
  • create_association!(attributes = {})

Во всех четырех методах association заменяется символом, переданным как первый аргумент в belongs_to. Например, имеем объявление:

class Book < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  belongs_to :author
end

Каждый экземпляр модели Book будет иметь эти методы:

author
author=
build_author
create_author
create_author!

Когда устанавливаете новую связь has_one или belongs_to, следует использовать префикс build_ для построения связи, в отличие от метода association.build, используемый для связей has_many или has_and_belongs_to_many. Чтобы создать связь, используйте префикс create_.

4.1.1. association

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

@author = @book.author

Если связанный объект уже был получен из базы данных для этого объекта, возвращается кэшированная версия. Чтобы переопределить это поведение (и заставить прочитать из базы данных), вызовите #reload на родительском объекте.

@author = @book.reload.author

4.1.2. association=(associate)

Метод association= привязывает связанный объект к этому объекту. Фактически это означает извлечение первичного ключа из связанного объекта и присвоение его значения внешнему ключу.

@book.author = @author

4.1.3. build_association(attributes = {})

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

@author = @book.build_author(author_number: 123,
                                  author_name: "John Doe")

4.1.4. create_association(attributes = {})

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

@author = @book.create_author(author_number: 123,
                                   author_name: "John Doe")

4.1.5. create_association!(attributes = {})

Работает так же, как и вышеприведенный create_association, но вызывает ActiveRecord::RecordInvalid, если запись невалидна.

4.2. Опции для belongs_to

Хотя Rails использует разумные значения по умолчанию, работающие во многих ситуациях, бывают случаи, когда хочется изменить поведение связи belongs_to. Такая настройка легко выполнима с помощью передачи опций и блоков со скоупом при создании связи. Например, эта связь использует две такие опции:

class Book < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  belongs_to :author, dependent: :destroy,
    counter_cache: true
end

Связь belongs_to поддерживает эти опции:

  • :autosave
  • :class_name
  • :counter_cache
  • :dependent
  • :foreign_key
  • :primary_key
  • :inverse_of
  • :polymorphic
  • :touch
  • :validate
  • :optional
4.2.1. :autosave

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

4.2.2. :class_name

Если имя другой модели не может быть получено из имени связи, можете использовать опцию :class_name для предоставления имени модели. Например, если книга принадлежит автору, но фактическое имя модели, содержащей авторов, Patron, можете установить это следующим образом:

class Book < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  belongs_to :author, class_name: "Patron"
end

4.2.3. :counter_cache

Опция :counter_cache может быть использована, чтобы сделать поиск количества принадлежащих объектов более эффективным. Рассмотрим эти модели:

class Book < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  belongs_to :author
end
class Author < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  has_many :books
end

С этими объявлениями запрос значения @author.books.size требует обращения к базе данных для выполнения запроса COUNT(*). Чтобы этого избежать, можете добавить кэш счетчика в принадлежащую модель:

class Book < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  belongs_to :author, counter_cache: true
end
class Author < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  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  # ActiveRecord::Base до Rails 5.0
  belongs_to :author, counter_cache: :count_of_books
end
class Author < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  has_many :books
end

Опцию :counter_cache необходимо указывать только на стороне belongs_to связи.

Столбцы кэша счетчика добавляются в список атрибутов модели только для чтения посредством attr_readonly.

4.2.4. :dependent

Если установить опцию :dependent как:

  • :destroy, то, когда объект уничтожен, метод destroy будет вызван на его связанных объектах.
  • :delete_all, то, когда объект уничтожен, все его связанные объекты будут удалены непосредственно из базы данных без вызова их методов destroy.
  • :nullify, вызывает установление внешнему ключу NULL. Колбэки не запускаются.
  • :restrict_with_exception, вызывает исключение, если имеется связанная запись
  • :restrict_with_error, вызывает ошибку, добавляемую владеющей модели, если есть связанный объект

Не следует определять эту опцию в связи belongs_to, которая соединена со связью has_many в другом классе. Это приведет к "битым" связям в записях вашей базы данных.

4.2.5. :foreign_key

По соглашению Rails предполагает, что столбец, используемый для хранения внешнего ключа в этой модели, имеет имя модели с добавленным суффиксом _id. Опция :foreign_key позволяет установить имя внешнего ключа явно:

class Book < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  belongs_to :author, class_name: "Patron",
                        foreign_key: "patron_id"
end

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

4.2.5.1. :primary_key

По соглашению Rails предполагает, что для первичного ключа используется столбец id в таблице. Опция :primary_key позволяет указать иной столбец.

Например, имеется таблица users с guid в качестве первичного ключа. Если мы хотим отдельную таблицу todos, содержащую внешний ключ user_id из столбца guid, для этого можно использовать primary_key следующим образом:

class User < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  self.primary_key = 'guid' # primary key is guid and not id
end

class Todo < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  belongs_to :user, primary_key: 'guid'
end

При выполнении @user.todos.create, у записи @todo будет значение user_id таким же, как значение guid у @user.

4.2.6. :inverse_of

Опция :inverse_of определяет имя связи has_many или has_one, являющейся противоположностью для этой связи. Не работает в комбинации с опциями :polymorphic.

class Author < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  has_many :books, inverse_of: :author
end

class Book < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  belongs_to :author, inverse_of: :books
end

4.2.7. :polymorphic

Передача true для опции :polymorphic показывает, что это полиморфная связь. Полиморфные связи подробно рассматривались ранее.

4.2.8. :touch

Если установите опцию :touch в true, то временные метки updated_at или updated_on на связанном объекте будут установлены в текущее время всякий раз, когда этот объект будет сохранен или уничтожен:

class Book < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  belongs_to :author, touch: true
end

class Author < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  has_many :books
end

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

class Book < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  belongs_to :author, touch: :books_updated_at
end

4.2.9. :validate

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

4.2.9.1. :optional

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

4.3. Скоупы для belongs_to

Иногда хочется настроить запрос, используемый belongs_to. Такая настройка может быть достигнута с помощью блока скоупа. Например:

class Book < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  belongs_to :author, -> { where active: true },
                        dependent: :destroy
end

Внутри блока скоупа можно использовать любые стандартные методы запросов. Далее обсудим следующие из них:

  • where
  • includes
  • readonly
  • select
4.3.1. where

Метод where позволяет определить условия, которым должен отвечать связанный объект.

class Book < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  belongs_to :author, -> { where active: true }
end

4.3.2. includes

Метод includes можно использовать для определения связей второго порядка, которые должны быть лениво загружены при использовании этой связи. Например, рассмотрим эти модели:

class LineItem < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  belongs_to :book
end

class Book < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  belongs_to :author
  has_many :line_items
end

class Author < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  has_many :books
end

Если вы часто получаете авторов непосредственно из элементов (@line_item.book.author), то можно улучшить эффективность кода, включив авторов в связь между книгой и ее элементами:

class LineItem < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  belongs_to :book, -> { includes :author }
end

class Book < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  belongs_to :author
  has_many :line_items
end

class Author < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  has_many :books
end

Нет необходимости в использовании includes для ближайших связей - то есть, если есть Book belongs_to :author, то author автоматически лениво загружается при необходимости.

4.3.3. readonly

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

4.3.4. select

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

При использовании метода select на связи belongs_to, следует также установить опцию :foreign_key для гарантии правильных результатов.

4.4. Существуют ли связанные объекты?

Можно увидеть, существует ли какой-либо связанный объект, при использовании метода association.nil?:

if @book.author.nil?
  @msg = "No author found for this book"
end

4.5. Когда сохраняются объекты?

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

5. Подробная информация по связи has_one

Связь has_one создает соответствие один-к-одному с другой моделью. В терминах базы данных эта связь сообщает, что другой класс содержит внешний ключ. Если этот класс содержит внешний ключ, следует использовать belongs_to.

5.1. Методы, добавляемые has_one

Когда объявляете связь has_one, объявляющий класс автоматически получает пять методов, относящихся к связи:

  • association
  • association=(associate)
  • build_association(attributes = {})
  • create_association(attributes = {})
  • create_association!(attributes = {})

Во всех этих методах association заменяется на символ, переданный как первый аргумент в has_one. Например, имеем объявление:

class Supplier < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  has_one :account
end

Каждый экземпляр модели Supplier будет иметь эти методы:

account
account=
build_account
create_account
create_account!

При установлении новой связи has_one или belongs_to, следует использовать префикс build_ для построения связи, в отличие от метода association.build, используемого для связей has_many или has_and_belongs_to_many. Чтобы создать связь, используйте префикс create_.

5.1.1. association

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

@account = @supplier.account

Если связанный объект уже был получен из базы данных для этого объекта, возвращается кэшированная версия. Чтобы переопределить это поведение (и заставить прочитать из базы данных), вызовите #reload на родительском объекте.

@account = @supplier.reload.account

5.1.2. association=(associate)

Метод association= привязывает связанный объект к этому объекту. Фактически это означает извлечение первичного ключа этого объекта и присвоение его значения внешнему ключу связанного объекта.

@supplier.account = @account

5.1.3. build_association(attributes = {})

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

@account = @supplier.build_account(terms: "Net 30")

5.1.4. create_association(attributes = {})

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

@account = @supplier.create_account(terms: "Net 30")

5.1.5. create_association!(attributes = {})

Работает так же, как и вышеприведенный create_association, но вызывает ActiveRecord::RecordInvalid, если запись невалидна.

5.2. Опции для has_one

Хотя Rails использует разумные значения по умолчанию, работающие во многих ситуациях, бывают случаи, когда хочется изменить поведение связи has_one. Такая настройка легко выполнима с помощью передачи опции при создании связи. Например, эта связь использует две такие опции:

class Supplier < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  has_one :account, class_name: "Billing", dependent: :nullify
end

Связь has_one поддерживает эти опции:

  • :as
  • :autosave
  • :class_name
  • :dependent
  • :foreign_key
  • :inverse_of
  • :primary_key
  • :source
  • :source_type
  • :through
  • :validate
5.2.1. :as

Установка опции :as показывает, что это полиморфная связь. Полиморфные связи подробно рассматривались ранее.

5.2.2. :autosave

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

5.2.3. :class_name

Если имя другой модели не может быть образовано из имени связи, можете использовать опцию :class_name для предоставления имени модели. Например, если поставщик имеет аккаунт, но фактическое имя модели, содержащей аккаунты, это Billing, можете установить это следующим образом:

class Supplier < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  has_one :account, class_name: "Billing"
end

5.2.4. :dependent

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

  • :destroy приведет к тому, что связанный объект также будет уничтожен
  • :delete приведет к тому, что связанный объект будет удален из базы данных напрямую (таким образом не будут выполнены колбэки)
  • :nullify приведет к тому, что внешний ключ будет установлен NULL. Колбэки не запускаются.
  • :restrict_with_exception приведет к вызову исключения, если есть связанный объект
  • :restrict_with_error приведет к ошибке, добавляемой к владельцу, если есть связанный объект

Нельзя устанавливать или уставлять опцию :nullify для связей, имеющих ограничение NOT NULL. Если не установить dependent для уничтожения таких связей, вы не сможете изменить связанный объект, акт как внешнему ключу изначально связанного объекта будет назначено недопустимое значение NULL.

5.2.5. :foreign_key

По соглашению Rails предполагает, что столбец, используемый для хранения внешнего ключа в этой модели, имеет имя модели с добавленным суффиксом _id. Опция :foreign_key позволяет установить имя внешнего ключа явно:

class Supplier < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  has_one :account, foreign_key: "supp_id"
end

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

5.2.6. :inverse_of

Опция :inverse_of определяет имя связи belongs_to, являющейся обратной для этой связи. Не работает в комбинации с опциями :through или :as.

class Supplier < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  has_one :account, inverse_of: :supplier
end

class Account < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  belongs_to :supplier, inverse_of: :account
end

5.2.7. :primary_key

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

5.2.8. :source

Опция :source определяет имя источника связи для связи has_one :through.

5.2.9. :source_type

Опция :source_type определяет тип источника связи для связи has_one :through, который действует при полиморфной связи.

5.2.10. :through

Опция :through определяет соединительную модель, через которую выполняется запрос. Связи has_one :through подробно рассматривались ранее.

5.2.11. :validate

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

5.3. Скоупы для has_one

Иногда хочется настроить запрос, используемый has_one. Такая настройка может быть достигнута с помощью блока скоупа. Например:

class Supplier < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  has_one :account, -> { where active: true }
end

Внутри блока скоупа можно использовать любые стандартные методы запросов. Далее обсудим следующие из них:

  • where
  • includes
  • readonly
  • select
5.3.1. where

Метод where позволяет определить условия, которым должен отвечать связанный объект.

class Supplier < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  has_one :account, -> { where "confirmed = 1" }
end

5.3.2. includes

Метод includes позволяет определить связи второго порядка, которые должны быть лениво загружены при использовании этой связи. Например, рассмотрим эти модели:

class Supplier < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  has_one :account
end

class Account < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  belongs_to :supplier
  belongs_to :representative
end

class Representative < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  has_many :accounts
end

Если вы часто получаете representatives непосредственно из suppliers (@supplier.account.representative), то можно улучшить эффективность кода, включив representatives в связь между suppliers и accounts:

class Supplier < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  has_one :account, -> { includes :representative }
end

class Account < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  belongs_to :supplier
  belongs_to :representative
end

class Representative < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  has_many :accounts
end

5.3.3. readonly

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

5.3.4. select

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

5.4. Существуют ли связанные объекты?

Можно увидеть, существует ли какой-либо связанный объект, при использовании метода association.nil?:

if @supplier.account.nil?
  @msg = "No account found for this supplier"
end

5.5. Когда сохраняются объекты?

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

Если одно из этих сохранений проваливается из-за ошибок валидации, тогда выражение назначения возвращает false, и само назначение отменяется.

Если родительский объект (который объявляет связь has_one) является несохраненным (то есть new_record? возвращает true), тогда дочерние объекты не сохраняются. Они сохранятся автоматически, когда сохранится родительский объект.

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

6. Подробная информация по связи has_many

Связь has_many создает отношение один-ко-многим с другой моделью. В терминах базы данных эта связь говорит, что другой класс будет иметь внешний ключ, относящийся к экземплярам этого класса.

6.1. Добавляемые методы

Когда объявляете связь has_many, объявляющий класс автоматически получает 16 методов, относящихся к связи:

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

class Author < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  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 = {})

6.1.1. collection

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

@books = @author.books

6.1.2. collection<<(object, ...)

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

@author.books << @book1

6.1.3. collection.delete(object, ...)

Метод collection.delete убирает один или более объектов из коллекции, установив их внешние ключи в NULL.

@author.books.delete(@book1)

Объекты будут в дополнение уничтожены, если связаны с dependent: :destroy, и удалены, если они связаны с dependent: :delete_all.

6.1.4. collection.destroy(object, ...)

Метод collection.destroy убирает один или более объектов из коллекции, выполняя destroy для каждого объекта.

@author.books.destroy(@book1)

Объекты будут всегда удаляться из базы данных, игнорируя опцию :dependent.

6.1.5. collection=(objects)

Метод collection= делает коллекцию содержащей только представленные объекты, добавляя и удаляя по мере необходимости.

6.1.6. collection_singular_ids

Метод collection_singular_ids возвращает массив id объектов в коллекции.

@book_ids = @author.book_ids

6.1.7. collection_singular_ids=(ids)

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

6.1.8. collection.clear

Метод collection.clear убирает каждый объект из коллекции в соответствии со стратегией, определенной опцией dependent. Если опция не указана, он следует стратегии по умолчанию. Стратегия по умолчанию для has_many :through это delete_all, а для связей has_many — установить их внешние ключи в NULL.

@author.books.clear

Объекты будут удалены, если они связаны с помощью dependent: :destroy, как и с помощью dependent: :delete_all.

6.1.9. collection.empty?

Метод collection.empty? возвращает true, если коллекция не содержит каких-либо связанных объектов.

<% if @author.books.empty? %>
  No Books Found
<% end %>

6.1.10. collection.size

Метод collection.size возвращает количество объектов в коллекции.

@book_count = @author.books.size

6.1.11. collection.find(...)

Метод collection.find ищет объекты в коллекции. Он использует тот же синтаксис и опции, что и ActiveRecord::Base.find.

@available_books = @author.books.find(1)

6.1.12. collection.where(...)

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

@available_books = @author.books.where(available: true) # Пока нет запроса
@available_book = @available_books.first # Теперь база данных будет запрошена

6.1.13. collection.exists?(...)

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

6.1.14. 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" }])

6.1.15. 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" }])

6.1.16. collection.create!(attributes = {})

Работает так же, как вышеприведенный collection.create, но вызывает ActiveRecord::RecordInvalid, если запись невалидна.

6.2. Опции для has_many

Хотя Rails использует разумные значения по умолчанию, работающие во многих ситуациях, бывают случаи, когда хочется изменить поведение связи has_many. Такая настройка легко выполнима с помощью передачи опций при создании связи. Например, эта связь использует две такие опции:

class Author < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  has_many :books, dependent: :delete_all, validate: false
end

Связь has_many поддерживает эти опции:

  • :as
  • :autosave
  • :class_name
  • :counter_cache
  • :dependent
  • :foreign_key
  • :inverse_of
  • :primary_key
  • :source
  • :source_type
  • :through
  • :validate
6.2.1. :as

Установка опции :as показывает, что это полиморфная связь. Полиморфные связи подробно рассматривались ранее.

6.2.2. :autosave

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

6.2.3. :class_name

Если имя другой модели не может быть произведено из имени связи, можете использовать опцию :class_name для предоставления имени модели. Например, если автор имеет много книг, но фактическое имя модели, содержащей книги, это Transaction, можете установить это следующим образом:

class Author < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  has_many :books, class_name: "Transaction"
end

6.2.3.1. :counter_cache

Эта опция используется для настройки произвольно названного :counter_cache. Эту опцию нужно использовать только если вы изменили имя вашего :counter_cache у связи belongs_to.

6.2.4. :dependent

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

  • :destroy приведет к тому, что связанные объекты также будут уничтожены
  • :delete_all приведет к тому, что связанные объекты будут удалены из базы данных напрямую (таким образом не будут выполнены колбэки)
  • :nullify приведет к тому, что внешние ключи будет установлен NULL. Колбэки не запускаются.
  • :restrict_with_exception приведет к вызову исключения, если есть какой-нибудь связанный объект
  • :restrict_with_error приведет к ошибке, добавляемой к владельцу, если есть какой-нибудь связанный объект
6.2.5. :foreign_key

По соглашению Rails предполагает, что столбец, используемый для хранения внешнего ключа в этой модели, имеет имя модели с добавленным суффиксом _id. Опция :foreign_key позволяет установить имя внешнего ключа явно:

class Author < ActiveRecord::Base
  has_many :books, foreign_key: "cust_id"
end

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

6.2.6. :inverse_of

Опция :inverse_of определяет имя связи belongs_to, являющейся обратной для этой связи. Не работает в комбинации с опциями :through или :as.

class Author < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  has_many :books, inverse_of: :author
end

class Book < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  belongs_to :author, inverse_of: :books
end

6.2.7. :primary_key

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

Допустим, в таблице users есть id в качестве primary_key, но также имеется столбец guid. Имеется требование, что таблица todos должна содержать значение столбца guid, а не значение id. Это достигается следующим образом:

class User < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  has_many :todos, primary_key: :guid
end

Теперь, если выполнить @todo = @user.todos.create, то в запись @todo значение user_id будет таким же, как значение guid в @user.

6.2.8. :source

Опция :source определяет имя источника связи для связи has_many :through. Эту опцию нужно использовать, только если имя источника связи не может быть автоматически выведено из имени связи.

6.2.9. :source_type

Опция :source_type определяет тип источника связи для связи has_many :through, который действует при полиморфной связи.

6.2.10. :through

Опция :through определяет соединительную модель, через которую выполняется запрос. Связи has_many :through предоставляют способ осуществления отношений многие-ко-многим, как обсуждалось ранее в этом руководстве.

6.2.11. :validate

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

6.3. Скоупы для has_many

Иногда хочется настроить запрос, используемый has_many. Такая настройка может быть достигнута с помощью блока скоупа. Например:

class Author < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  has_many :books, -> { where processed: true }
end

Внутри блока скоупа можно использовать любые стандартные методы запросов. Далее обсудим следующие из них:

  • where
  • extending
  • group
  • includes
  • limit
  • offset
  • order
  • readonly
  • select
  • distinct
6.3.1. where

Метод where позволяет определить условия, которым должен отвечать связанный объект.

class Author < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  has_many :confirmed_books, -> { where "confirmed = 1" },
    class_name: "Book"
end

Также можно задать условия хэшем:

class Author < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  has_many :confirmed_books, -> { where confirmed: true },
                              class_name: "Book"
end

При использовании опции where хэшем, при создание записи через эту связь будет автоматически применен скоуп с использованием хэша. В этом случае при использовании @author.confirmed_books.create или @author.confirmed_books.build будут созданы книги, в которых столбец confirmed будет иметь значение true.

6.3.2. extending

Метод extending определяет именованный модуль для расширения прокси связи. Расширения связей подробно обсуждаются позже в этом руководстве.

6.3.3. group

Метод group доставляет имя атрибута, по которому группируется результирующий набор, используя выражение GROUP BY в поисковом SQL.

class Author < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  has_many :line_items, -> { group 'books.id' },
                        through: :books
end

6.3.4. includes

Можете использовать метод includes для определения связей второго порядка, которые должны быть нетерпеливо загружены, когда эта связь используется. Например, рассмотрим эти модели:

class Author < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  has_many :books
end

class Book < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  belongs_to :author
  has_many :line_items
end

class LineItem < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  belongs_to :book
end

Если вы часто получаете элементы прямо из авторов (@author.books.line_items), тогда можете сделать свой код более эффективным, включив элементы в связь от авторов к книгам:

class Author < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  has_many :books, -> { includes :line_items }
end

class Book < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  belongs_to :author
  has_many :line_items
end

class LineItem < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  belongs_to :book
end

6.3.5. limit

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

class Author < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  has_many :recent_books,
    -> { order('published_at desc').limit(100) },
    class_name: "Book",
end

6.3.6. offset

Метод offset позволяет определить начальное смещение для выбора объектов через связь. Например, -> { offset(11) } пропустит первые 11 записей.

6.3.7. order

Метод order предписывает порядок, в котором связанные объекты будут получены (в синтаксисе SQL, используемом в условии ORDER BY).

class Author < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  has_many :books, -> { order "date_confirmed DESC" }
end

6.3.8. readonly

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

6.3.9. select

Метод select позволяет переопределить SQL условие SELECT, которое используется для получения данных о связанном объекте. По умолчанию Rails получает все столбцы.

Если укажете свой собственный select, не забудьте включить столбцы первичного ключа и внешнего ключа в связанной модели. Если так не сделать, Rails выдаст ошибку.

6.3.10. distinct

Используйте метод distinct, чтобы убирать дубликаты из коллекции. Это полезно в сочетании с опцией :through.

class Person < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  has_many :readings
  has_many :articles, through: :readings
end

article   = Article.create(name: 'a1')
person.articles << article
person.articles << article
person.articles.inspect # => [#<Article id: 5, name: "a1">, #<Article id: 5, name: "a1">]
Reading.all.inspect  # => [#<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

person = Person.create(name: 'Honda')
article   = Article.create(name: 'a1')
person.articles << article
person.articles << article
person.articles.inspect # => [#<Article id: 7, name: "a1">]
Reading.all.inspect  # => [#<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:

person = Person.create(name: 'Honda')
article = Article.create(name: 'a1')
person.articles << article
person.articles << article # => ActiveRecord::RecordNotUnique

Отметьте, что проверка уникальности при использовании чего-то, наподобие include?, это субъект гонки условий. Не пытайтесь использовать include? для соблюдения уникальности в связи. Используя вышеприведенный пример со статьёй, нижеследующий код вызовет гонку, поскольку несколько пользователей могут использовать его одновременно:

person.articles << article unless person.articles.include?(post)

6.4. Когда сохраняются объекты?

Когда вы назначаете объект связью has_many, этот объект автоматически сохраняется (для того, чтобы обновить его внешний ключ). Если назначаете несколько объектов в одном выражении, они все будут сохранены.

Если одно из этих сохранений проваливается из-за ошибок валидации, тогда выражение назначения возвращает false, и само назначение отменяется.

Если родительский объект (который объявляет связь has_many) является несохраненным (то есть new_record? возвращает true) тогда дочерние объекты не сохраняются при добавлении. Все несохраненные члены связи сохранятся автоматически, когда сохранится родительский объект.

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

7. Подробная информация по связи has_and_belongs_to_many

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

7.1. Добавляемые методы

Когда объявляете связь has_and_belongs_to_many, объявляющий класс автоматически получает 16 методов, относящихся к связи:

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

class Part < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  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 = {})

7.1.1. Дополнительные методы столбцов

Если соединительная таблица для связи has_and_belongs_to_many имеет дополнительные столбцы, кроме двух внешних ключей, эти столбцы будут добавлены как атрибуты к записям, получаемым посредством связи. Записи, возвращаемые с дополнительными атрибутами, будут всегда только для чтения, поскольку Rails не может сохранить значения этих атрибутов.

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

7.1.2. collection

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

@assemblies = @part.assemblies

7.1.3. collection<<(object, ...)

Метод collection<< добавляет один или более объектов в коллекцию, создавая записи в соединительной таблице.

@part.assemblies << @assembly1

Этот метод - просто синоним к collection.concat и collection.push.

7.1.4. collection.delete(object, ...)

Метод collection.delete убирает один или более объектов из коллекции, удаляя записи в соединительной таблице. Это не уничтожает объекты.

@part.assemblies.delete(@assembly1)

Это не запустит колбэки на соединительных записях.

7.1.4.1. collection.destroy(object, ...)

Метод collection.destroy убирает один или более объектов из коллекции. запуская destroy на каждой записи в соединительной таблице, включая запуск колбэков. Это не уничтожает объекты.

@part.assemblies.destroy(@assembly1)

7.1.5. collection=(objects)

Метод collection= делает коллекцию содержащей только представленные объекты, добавляя и удаляя по мере необходимости.

7.1.6. collection_singular_ids

Метод collection_singular_ids возвращает массив id объектов в коллекции.

@assembly_ids = @part.assembly_ids

7.1.7. collection_singular_ids=(ids)

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

7.1.8. collection.clear

Метод collection.clear убирает каждый объект из коллекции, удаляя строки из соединительной таблицы. Это не уничтожает связанные объекты.

7.1.9. collection.empty?

Метод collection.empty? возвращает true, если коллекция не содержит каких-либо связанных объектов.

<% if @part.assemblies.empty? %>
  This part is not used in any assemblies
<% end %>

7.1.10. collection.size

Метод collection.size возвращает количество объектов в коллекции.

@assembly_count = @part.assemblies.size

7.1.11. collection.find(...)

Метод collection.find ищет объекты в коллекции. Он использует тот же синтаксис и опции, что и ActiveRecord::Base.find. Он также добавляет дополнительное условие, что объект должен быть в коллекции.

@assembly = @part.assemblies.find(1)

7.1.12. collection.where(...)

Метод collection.where ищет объекты в коллекции, основываясь на переданных условиях, но объекты загружаются лениво, что означает, что база данных запрашивается только когда происходит доступ к объекту(-там). Он также добавляет дополнительное условие, что объект должен быть в коллекции.

@new_assemblies = @part.assemblies.where("created_at > ?", 2.days.ago)

7.1.13. collection.exists?(...)

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

7.1.14. collection.build(attributes = {})

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

@assembly = @part.assemblies.build({assembly_name: "Transmission housing"})

7.1.15. collection.create(attributes = {})

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

@assembly = @part.assemblies.create({assembly_name: "Transmission housing"})

7.1.16. collection.create!(attributes = {})

Работает так же, как вышеприведенный collection.create, но вызывает ActiveRecord::RecordInvalid, если запись невалидна.

7.2. Опции для has_and_belongs_to_many

Хотя Rails использует разумные значения по умолчанию, работающие во многих ситуациях, бывают случаи, когда хочется изменить поведение связи has_and_belongs_to_many. Такая настройка легко выполнима с помощью передачи опции при создании связи. Например, эта связь использует две такие опции:

class Parts < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  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
  • :validate
7.2.1. :association_foreign_key

По соглашению Rails предполагает, что столбец в соединительной таблице, используемый для хранения внешнего ключа, указываемого на другую модель, является именем этой модели с добавленным суффиксом _id. Опция :association_foreign_key позволяет установить имя внешнего ключа явно:

Опции :foreign_key и :association_foreign_key полезны при настройке присоединения к себе многие-ко-многим. Например:

class User < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  has_and_belongs_to_many :friends,
      class_name: "User",
      foreign_key: "this_user_id",
      association_foreign_key: "other_user_id"
end

7.2.2. :autosave

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

7.2.3. :class_name

Если имя другой модели не может быть произведено из имени связи, можете использовать опцию :class_name для предоставления имени модели. Например, если часть имеет много узлов, но фактическое имя модели, содержащей узлы - это Gadget, можете установить это следующим образом:

class Parts < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  has_and_belongs_to_many :assemblies, class_name: "Gadget"
end

7.2.4. :foreign_key

По соглашению Rails предполагает, что столбец в соединительной таблице, используемый для хранения внешнего ключа, указываемого на эту модель, имеет имя модели с добавленным суффиксом _id. Опция :foreign_key позволяет установить имя внешнего ключа явно:

class User < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  has_and_belongs_to_many :friends,
      class_name: "User",
      foreign_key: "this_user_id",
      association_foreign_key: "other_user_id"
end

7.2.5. :join_table

Если имя соединительной таблицы по умолчанию, основанное на алфавитном порядке, - это не то, что вам нужно, используйте опцию :join_table, чтобы переопределить его.

7.2.6. :validate

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

7.3. Скоупы для has_and_belongs_to_many

Иногда хочется настроить запрос, используемый has_many. Такая настройка может быть достигнута с помощью блока скоупа. Например:

class Parts < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  has_and_belongs_to_many :assemblies, -> { where active: true }
end

Внутри блока скоупа можно использовать любые стандартные методы запросов. Далее обсудим следующие из них:

  • where
  • extending
  • group
  • includes
  • limit
  • offset
  • order
  • readonly
  • select
  • distinct
7.3.1. where

Метод where позволяет определить условия, которым должен отвечать связанный объект.

class Parts < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  has_and_belongs_to_many :assemblies,
    -> { where "factory = 'Seattle'" }
end

Также можно задать условия хэшем:

class Parts < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  has_and_belongs_to_many :assemblies,
    -> { where factory: 'Seattle' }
end

При использовании опции where хэшем, при создание записи через эту связь будет автоматически применен скоуп с использованием хэша. В этом случае при использовании @parts.assemblies.create или @parts.assemblies.build будут созданы заказы, в которых столбец factory будет иметь значение Seattle.

7.3.2. extending

Метод extending определяет именованный модуль для расширения прокси связи. Расширения связей подробно обсуждаются позже в этом руководстве.

7.3.3. group

Метод group доставляет имя атрибута, по которому группируется результирующий набор, используя выражение GROUP BY в поисковом SQL.

class Parts < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  has_and_belongs_to_many :assemblies, -> { group "factory" }
end

7.3.4. includes

Можете использовать метод includes для определения связей второго порядка, которые должны быть нетерпеливо загружены, когда эта связь используется.

7.3.5. limit

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

class Customer < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  has_and_belongs_to_many :assemblies,
    -> { order("created_at DESC").limit(50) }
end

7.3.6. offset

Метод offset позволяет определить начальное смещение для выбора объектов через связь. Например, -> { offset(11) } пропустит первые 11 записей.

7.3.7. order

Метод order предписывает порядок, в котором связанные объекты будут получены (в синтаксисе SQL, используемом в условии ORDER BY).

class Customer < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  has_and_belongs_to_many :assemblies,
    -> { order "assembly_name ASC" }
end

7.3.8. readonly

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

7.3.9. select

Метод select позволяет переопределить SQL условие SELECT, которое используется для получения данных о связанном объекте. По умолчанию Rails получает все столбцы.

7.3.10. distinct

Используйте метод distinct, чтобы убирать дубликаты из коллекции.

7.4. Когда сохраняются объекты?

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

Если одно из этих сохранений проваливается из-за ошибок валидации, тогда выражение назначения возвращает false, a само назначение отменяется.

Если родительский объект (который объявляет связь has_and_belongs_to_many) является несохраненным (то есть new_record? возвращает true) тогда дочерние объекты не сохраняются при добавлении. Все несохраненные члены связи сохранятся автоматически, когда сохранится родительский объект.

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

8. Подробная информация по колбэкам и расширениям связи

8.1. Колбэки связи

Обычно колбэки прицепляются к жизненному циклу объектов Active Record, позволяя вам работать с этими объектами в различных точках. Например, можете использовать колбэк :before_save, чтобы вызвать что-то перед тем, как объект будет сохранен.

Колбэки связи похожи на обычные колбэки, но они включаются событиями в жизненном цикле коллекции. Доступны четыре колбэка связи:

  • before_add
  • after_add
  • before_remove
  • after_remove

Колбэки связи объявляются с помощью добавления опций в объявление связи. Например:

class Author < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  has_many :books, before_add: :check_credit_limit

  def check_credit_limit(book)
    ...
  end
end

Rails передает добавляемый или удаляемый объект в колбэк.

Можете помещать колбэки в очередь на отдельное событие, передав их как массив:

class Author < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  has_many :books,
    before_add: [:check_credit_limit, :calculate_shipping_charges]

  def check_credit_limit(book)
    ...
  end

  def calculate_shipping_charges(book)
    ...
  end
end

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

8.2. Расширения связи

Вы не ограничены функциональностью, которую Rails автоматически встраивает в выданные по связи объекты. Можете расширять эти объекты через анонимные модули, добавления новых методов поиска, создания и иных методов. Например:

class Author < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  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  # ActiveRecord::Base до Rails 5.0
  has_many :books, -> { extending FindRecentExtension }
end

class Supplier < ApplicationRecord  # ActiveRecord::Base до Rails 5.0
  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.

9. Наследование с одной таблицей (STI)

Иногда хочется совместно использовать поля и поведения различными моделями. Скажем, у нас есть модели Car, Motorcycle и Bicycle. Мы хотим совместно использовать поля color и price и некоторые методы всеми из них, но иметь некоторое специфичное поведение для каждого, а также различные контроллеры.

Rails позволяет сделать это достаточно просто. Сначала нужно сгенерировать базовую модель Vehicle:

$ rails generate model vehicle type:string color:string price:decimal{10.2}

Вы заметили, что мы добавили поле "type"? Так как все модели будут сохранены в одну таблицу базы данных, Rails сохранит в этот столбец имя модели, которая сохраняется. В нашем примере это может быть "Car", "Motorcycle" или "Bicycle." STI не работает без поля "type" в таблице.

Затем мы сгенерируем три модели, унаследованные от Vehicle. Для этого можно использовать опцию --parent=PARENT, которая сгенерирует модель, унаследованную от указанного родителя и без эквивалентной миграции (так как таблица уже существует).

Например, чтобы сгенерировать модель Car:

$ 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')