Подтвердить что ты не робот

Как работают методы объединения рельсов?

Как работают методы объединения рельсов? Рассмотрим этот пример

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. Какова последовательность выполнения кода в этом случае? Как я могу проверить это?

4b9b3361

Ответ 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

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