Как работают методы рельсового объединения? - PullRequest
20 голосов
/ 07 октября 2009

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

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 ]

21 голосов
/ 12 октября 2009

На самом деле работает то, что объект ассоциации является «прокси-объектом». Конкретный класс AssociationProxy . Если вы посмотрите на строку 52 этого файла, вы увидите:

instance_methods.each { |m| undef_method m unless m =~ /(^__|^nil\?$|^send$|proxy_|^object_id$)/ }

При этом такие методы, как class, больше не существуют для этого объекта. Поэтому, если вы вызовете class для этого объекта, вы получите метод, отсутствующий. Итак, для прокси-объекта реализовано method_missing, которое перенаправляет вызов метода в «target»:

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

9 голосов
/ 12 октября 2009

Как уже упоминалось, активные ассоциации записей создают метрическую прикладную загрузку удобных методов. Конечно, вы можете написать свои собственные методы, чтобы получить все. Но это не Rails Way.

Железнодорожный путь - кульминация двух девизов. СУХОЙ (не повторяйте себя) и «Соглашение о конфигурации». По сути, называя вещи так, чтобы это имело смысл, некоторые надежные методы, предоставляемые каркасом, могут абстрагировать весь общий код. Код, который вы помещаете в свой вопрос, является идеальным примером того, что можно заменить одним вызовом метода.

Где эти удобные методы действительно сияют, так это более сложные ситуации. Такие вещи, как модели соединений, условия, проверки и т. Д.

Чтобы ответить на ваш вопрос, когда вы делаете что-то вроде @user.articles.find(:all, :conditions => ["created_at > ? ", tuesday]), Rails готовит два запроса SQL, а затем объединяет их в один. где как ваша версия просто возвращает список объектов. Именованные области действия делают то же самое, но обычно не пересекают границы модели.

Вы можете проверить это, проверив SQL-запросы в файле development.log, когда вы вызываете эти вещи в консоли.

Итак, давайте на минутку поговорим о Именованных областях , потому что они дают отличный пример того, как рельсы обрабатывают 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.

Причина, по которой метод mix & match не работает, заключается в том, что метод of_sector, определенный в вопросе, не работает. edited_methods возвращает Array, где 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
1 голос
/ 16 августа 2012

Как упоминалось ранее, при выполнении

@user.articles.class
=> Array

что вы на самом деле получаете, так это массив. Это потому, что метод #class был неопределен, как также упоминалось ранее.

Но как получить фактический класс @ user.articles (который должен быть прокси)?

Object.instance_method(:class).bind(@user.articles).call
=> ActiveRecord::Associations::CollectionProxy

А с чего ты взял Array? Потому что метод #class был делегирован экземпляру CollectionProxy @target через метод missin, который на самом деле является массивом. Вы можете заглянуть за кулисы, выполнив что-то вроде этого:

@user.articles.proxy_association
0 голосов
/ 07 октября 2009

Когда вы делаете ассоциацию (has_one, has_many и т. Д.), Она указывает модели автоматически включать некоторые методы с помощью ActiveRecord. Однако, когда вы решили создать метод экземпляра, возвращающий ассоциацию самостоятельно, вы не сможете использовать эти методы.

Последовательность примерно такая

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

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

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

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

...