Как работают методы объединения рельсов?
Как работают методы объединения рельсов? Рассмотрим этот пример
class User < ActiveRecord::Base
has_many :articles
end
class Article < ActiveRecord::Base
belongs_to :user
end
Теперь я могу сделать что-то вроде
@user = User.find(:first)
@user.articles
Это извлекает мне статьи, принадлежащие этому пользователю. Пока все хорошо.
Теперь я могу продолжить и найти на этих статьях некоторые условия.
@user.articles.find(:all, :conditions => {:sector_id => 3})
Или просто метод declare и ассоциаций вроде
class User < ActiveRecord::Base
has_many :articles do
def of_sector(sector_id)
find(:all, :conditions => {:sector_id => sector_id})
end
end
end
И сделайте
@user.articles.of_sector(3)
Теперь мой вопрос: как этот find
работает с массивом объектов ActiveRecord
, извлеченных с использованием метода ассоциации? Поскольку, если мы реализуем собственный метод экземпляра User
под названием articles
и пишем нашу собственную реализацию, которая дает нам те же результаты, что и метод ассоциации, поиск в массиве выборки объектов ActiveRecord
не работает.
Я предполагаю, что методы ассоциации присоединяют определенные свойства к массиву извлеченных объектов, которые позволяют продолжить запрос с использованием методов find
и других ActiveRecord
. Какова последовательность выполнения кода в этом случае? Как я могу проверить это?
Ответы
Ответ 1
Как это работает, так это то, что объект ассоциации является "прокси-объектом". Конкретный класс AssociationProxy. Если вы посмотрите на строку 52 этого файла, вы увидите:
instance_methods.each { |m| undef_method m unless m =~ /(^__|^nil\?$|^send$|proxy_|^object_id$)/ }
Таким образом, методы, подобные class
, больше не существуют на этом объекте. Поэтому, если вы вызываете class
на этом объекте, вы получите метод. Итак, существует method_missing
для объекта-прокси, который перенаправляет вызов метода на "цель":
def method_missing(method, *args)
if load_target
unless @target.respond_to?(method)
message = "undefined method `#{method.to_s}' for \"#{@target}\":#{@target.class.to_s}"
raise NoMethodError, message
end
if block_given?
@target.send(method, *args) { |*block_args| yield(*block_args) }
else
@target.send(method, *args)
end
end
end
Цель - это массив, поэтому, когда вы вызываете class
на этом объекте, он говорит, что это массив, но это только потому, что целью является массив, фактический класс является ассоциативным прокси, но вы не можете видеть, что больше.
Итак, все методы, которые вы добавляете, например of_sector
, добавляются в прокси-сервер ассоциации, поэтому они вызываются напрямую. Методы типа []
и class
не определены в прокси-соединении, поэтому они отправляются в цель, которая является массивом.
Чтобы помочь вам понять, как это происходит, добавьте его в строку 217 этого файла в локальной копии ассоциации_proxy.rb:
Rails.logger.info "AssociationProxy forwarding call to `#{method.to_s}' method to \"#{@target}\":#{@target.class.to_s}"
Если вы не знаете, где находится этот файл, команда gem which 'active_record/associations/association_proxy'
сообщит вам. Теперь, когда вы вызываете class
на AssociationProxy, вы увидите сообщение в журнале, сообщающее вам, что оно отправляет его цели, что должно сделать более ясным, что происходит. Это все для Rails 2.3.2 и может быть изменено в других версиях.
Ответ 2
Как уже упоминалось, активные ассоциации записей создают метрическую нагрузку удобных методов. Конечно, вы могли бы написать свои собственные методы, чтобы получить все. Но это не путь Rails.
The Rails Way является кульминацией двух девизов. DRY (не повторяйте себя) и "Конвенция по конфигурации". По сути, обозначая вещи таким образом, что имеет смысл, некоторые надежные методы, предоставляемые структурой, могут абстрагировать весь общий код. Код, который вы размещаете в своем вопросе, является прекрасным примером того, что можно заменить одним вызовом метода.
В тех случаях, когда эти удобные методы действительно блестят, это более сложные ситуации. Тип вещи, включающий модели объединений, условия, проверки и т.д.
Чтобы ответить на ваш вопрос, когда вы делаете что-то вроде @user.articles.find(:all, :conditions => ["created_at > ? ", tuesday])
, Rails готовит два SQL-запроса, а затем объединяет их в один. где, когда ваша версия просто возвращает список объектов. Именованные области делают то же самое, но обычно не пересекают границы модели.
Вы можете проверить его, проверив SQL-запросы в development.log, как вы это называете в консоли.
Итак, давайте поговорим о Named Scopes на мгновение, потому что они дают отличный пример того, как рельсы обрабатывают SQL, и я думаю, более простой способ продемонстрировать, что происходит за кулисами, поскольку им не нужны никакие ассоциации моделей, которые хвастаются.
Именованные области могут использоваться для выполнения пользовательских поисков модели. Их можно связать вместе или даже позвонить через ассоциации. Вы можете легко создавать пользовательские поисковые устройства, которые возвращают идентичные списки, но затем вы сталкиваетесь с теми же проблемами, что и в вопросе.
class Article < ActiveRecord::Base
belongs_to :user
has_many :comments
has_many :commentators, :through :comments, :class_name => "user"
named_scope :edited_scope, :conditions => {:edited => true}
named_scope :recent_scope, lambda do
{ :conditions => ["updated_at > ? ", DateTime.now - 7.days]}
def self.edited_method
self.find(:all, :conditions => {:edited => true})
end
def self.recent_method
self.find(:all, :conditions => ["updated_at > ?", DateTime.now - 7 days])
end
end
Article.edited_scope
=> # Array of articles that have been flagged as edited. 1 SQL query.
Article.edited_method
=> # Array of Articles that have been flagged as edited. 1 SQL query.
Array.edited_scope == Array.edited_method
=> true # return identical lists.
Article.recent_scope
=> # Array of articles that have been updated in the past 7 days.
1 SQL query.
Article.recent_method
=> # Array of Articles that have been updated in the past 7 days.
1 SQL query.
Array.recent_scope == Array.recent_method
=> true # return identical lists.
Здесь, где вещи меняются:
Article.edited_scope.recent_scope
=> # Array of articles that have both been edited and updated
in the past 7 days. 1 SQL query.
Article.edited_method.recent_method
=> # no method error recent_scope on Array
# Can't even mix and match.
Article.edited_scope.recent_method
=> # no method error
Article.recent_method.edited_scope
=> # no method error
# works even across associations.
@user.articles.edited.comments
=> # Array of comments belonging to Articles that are flagged as
edited and belong to @user. 1 SQL query.
По сути, каждая именованная область создает фрагмент SQL. Rails будет умело сливаться со всеми остальными фрагментами SQL в цепочке, чтобы создать один запрос, сохраняющий именно то, что вы хотите. Методы, добавленные методами ассоциации, работают одинаково. Именно поэтому они легко интегрируются с named_scopes.
Причина смешивания и совпадения не работает, это то же самое, что метод of_sector, определенный в вопросе, не работает. edit_methods возвращает массив, где, как отредактированный_scope (а также поиск и все другие методы удобства AR, называемые частью цепочки), передают свой SQL-фрагмент дальше к следующей вещи в цепочке. Если он последний в цепочке, он выполняет запрос. Точно так же это не сработает.
@edited = Article.edited_scope
@edited.recent_scope
Вы пытались использовать этот код. Вот правильный способ сделать это:
class User < ActiveRecord::Base
has_many :articles do
def of_sector(sector_id)
find(:all, :conditions => {:sector_id => sector_id})
end
end
end
Для достижения этой функциональности вы хотите сделать это:
class Articles < ActiveRecord::Base
belongs_to :user
named_scope :of_sector, lambda do |*sectors|
{ :conditions => {:sector_id => sectors} }
end
end
class User < ActiveRecord::Base
has_many :articles
end
Затем вы можете делать такие вещи:
@user.articles.of_sector(4)
=> # articles belonging to @user and sector of 4
@user.articles.of_sector(5,6)
=> # articles belonging to @user and either sector 4 or 5
@user.articles.of_sector([1,2,3,])
=> # articles belonging to @user and either sector 1,2, or 3
Ответ 3
Как уже отмечалось, при выполнении
@user.articles.class
=> Array
то, что вы на самом деле получаете, это Array. Это связано с тем, что метод #class был undefined, как упоминалось ранее.
Но как вы получаете фактический класс @user.articles(который должен быть прокси)?
Object.instance_method(:class).bind(@user.articles).call
=> ActiveRecord::Associations::CollectionProxy
И почему вы получили Array в первую очередь? Поскольку метод #class был делегирован экземпляру CollectionProxy @target с помощью метода missin, который на самом деле является массивом.
Вы могли бы заглянуть за сцену, сделав что-то вроде этого:
@user.articles.proxy_association
Ответ 4
Когда вы выполняете ассоциацию (has_one
, has_many
и т.д.), она сообщает модели автоматически включать некоторые методы ActiveRecord. Однако, когда вы решили создать метод экземпляра, возвращающий ассоциацию самостоятельно, вы не сможете использовать эти методы.
Последовательность выглядит примерно так:
- setup
articles
в модели User
, т.е. выполните has_many :articles
- ActiveRecord автоматически включает в себя удобные методы в модели (например,
size
, empty?
, find
, all
, first
и т.д.)
- setup
User
в Article
, т.е. выполните belongs_to :user
- ActiveRecord автоматически включает в себя удобные методы в модели (например,
user=
и т.д.)
Таким образом, ясно, что когда вы объявляете ассоциацию, методы автоматически добавляются ActiveRecord, что является красавицей, поскольку она обрабатывает огромную работу, что нужно будет сделать вручную иначе =)
вы можете прочитать об этом здесь: http://guides.rubyonrails.org/association_basics.html#detailed-association-reference
надеюсь, что это поможет =)