Как работают методы ассоциации 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. Однако, когда вы решили создать метод экземпляра, возвращающий ассоциацию самостоятельно, вы не сможете использовать эти методы.

последовательность-это что-то вроде этого

  1. настройка articles на User модель, то есть сделать has_many :articles
  2. ActiveRecord автоматически включает удобные методы в модель (например,size, empty?, find, all, first, etc)
  3. настройка user на Article, то есть сделать belongs_to :user
  4. ActiveRecord автоматически включает удобные методы в модель (например,user=, etc)

таким образом, ясно, что когда вы объявляете ассоциацию, методы добавляются автоматически ActiveRecord, который является красотой, поскольку он обработал огромное количество работы, которая должна быть выполнена вручную в противном случае =)

вы можете прочитать больше об этом здесь: http://guides.rubyonrails.org/association_basics.html#detailed-association-reference

надеюсь, что это помогает =)