Как работают методы ассоциации rails?
как работают методы ассоциации rails? Рассмотрим этот пример
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})
или просто объявить и ассоциации метод, как
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
методы. Какова последовательность выполнения кода в данном случае? Как я могу это подтвердить?
4 ответов
как это на самом деле работает, так это то, что объект ассоциации является "прокси-объектом". Конкретный класс -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
в target-это массив, поэтому при вызове class
на этом объекте он говорит, что это массив, но это просто потому, что цель-массив, фактический класс-AssociationProxy, но вы больше не можете этого видеть.
Итак, все методы, которые вы добавляете, такие как of_sector
, добавляется к прокси-серверу ассоциации, поэтому они вызываются напрямую. Такие методы, как []
и class
не определены на прокси-сервере ассоциации, поэтому они отправляются в цель, которая является массивом.
, чтобы помочь вы видите, как это происходит, добавьте это в строку 217 этого файла в локальной копии association_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 и может измениться в других версиях.
как уже упоминалось, ассоциации активных записей создают метрическую загрузку методов удобства. Конечно, вы можете написать свои собственные методы, чтобы получить все. Но это не рельсы.
путь рельсов является кульминацией двух девизов. Сухой (не повторяйте) и"соглашение по конфигурации". По сути, называя вещи таким образом, который имеет смысл, некоторые надежные методы, предоставляемые платформой, могут абстрагироваться от всего общего кода. Код, который вы вводите ваш вопрос-идеальный пример того, что можно заменить одним вызовом метода.
где эти методы действительно являются более сложными ситуациями. Тип вещи, включающей модели соединения, условия, проверки и т. д.
чтобы ответить на ваш вопрос, когда вы делаете что-то вроде @user.articles.find(:all, :conditions => ["created_at > ? ", tuesday])
, Rails готовит два SQL-запроса, а затем объединяет их в один. где как ваша версия просто возвращает список объектов. Именованные области делают то же самое, но обычно не пересекают границы модели.
вы можете проверить его, проверив SQL-запросы в разработке.войдите как вы называете эти вещи в консоли.
Итак, давайте поговорим о Именованные Группы на мгновение, потому что они дают отличный пример того, как rails обрабатывает 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 определенными в вопрос doeso не работать. edited_methods возвращает массив, в котором как edited_scope (а также find и все другие методы удобства 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
как было сказано ранее, при выполнении
@user.articles.class
=> Array
на самом деле вы получаете массив. Это потому, что метод #class не был определен, как также упоминалось ранее.
но как вы получаете фактический класс @user.статьи (которые должны быть прокси)?
Object.instance_method(:class).bind(@user.articles).call
=> ActiveRecord::Associations::CollectionProxy
и почему вы получили Array в первую очередь? Поскольку метод #class был делегирован экземпляру CollectionProxy @target через метод missin, который на самом деле является массивом. Вы могли бы заглянуть за сцены, делая что-то вроде этого:
@user.articles.proxy_association
когда вы делаете ассоциация (has_one
, has_many
, etc.), он сообщает модели автоматически включать некоторые методы с помощью ActiveRecord. Однако, когда вы решили создать метод экземпляра, возвращающий ассоциацию самостоятельно, вы не сможете использовать эти методы.
последовательность-это что-то вроде этого
- настройка
articles
наUser
модель, то есть сделатьhas_many :articles
- ActiveRecord автоматически включает удобные методы в модель (например,
size
,empty?
,find
,all
,first
, etc) - настройка
user
наArticle
, то есть сделатьbelongs_to :user
- ActiveRecord автоматически включает удобные методы в модель (например,
user=
, etc)
таким образом, ясно, что когда вы объявляете ассоциацию, методы добавляются автоматически ActiveRecord, который является красотой, поскольку он обработал огромное количество работы, которая должна быть выполнена вручную в противном случае =)
вы можете прочитать больше об этом здесь: http://guides.rubyonrails.org/association_basics.html#detailed-association-reference
надеюсь, что это помогает =)