Как добавить метод в коллекцию activerecord?

Я хотел бы добавить метод для всех коллекций для конкретной модели. Скажем, я хочу добавить метод my_complicated_averaging_method в коллекции WeatherData:

WeatherData.all.limit(3).my_complicated_averaging_method()
Station.first.weatherdata.my_complicated_averaging_method()

Каков наилучший способ сделать это? В настоящий момент единственный способ, который я нашел, выглядит следующим образом:

class WeatherData < ActiveRecord::Base
  def self.my_complicated_averaging_method
    weighted_average = 0
    @relation.each do |post|
      # do something complicated
      # weighted_average = 
    end
    return weighted_average
  end
end

Является ли это хорошим способом добавления метода в коллекцию? Есть ли лучший способ для этого?

Ответы

Ответ 1

Есть много способов, как это сделать, и ваш полностью действителен (хотя лично я предпочитаю обертывать методы класса в отдельную проверку блока this), но поскольку люди добавляют к своей модели больше бизнес-логики и слепо следуют концепции "тощих контроллеров, толстых моделей", модели превращаются в полный беспорядок.

Чтобы избежать этого беспорядка, рекомендуется ввести объекты обслуживания, в вашем случае это будет так:

class AverageWeatherData
  class << self
    def data(collection)
      new(collection).data
    end
  end

  def initialize(collection)
    @collection = collection
  end

  def data
    @collection.reduce do |avg, post|
      # reduce goes through every post, each next iteration receives in avg a value of the last line of iteration
      # do something with avg and post 
    end
    # no need for explicit return, every line of Ruby code returns it value
    # so this method would return result of the reduce
    # more on reduce: http://ruby-doc.org/core-2.0.0/Enumerable.html#method-i-reduce
  end
end

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

def self.my_complicated_averaging_method
  AverageWeatherData.data(@relation)
end

Я призываю вас изучить этот подход, прочитав этот блог: http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/

UPD

Вы правы, используя переменную экземпляра - это возможный способ испортить внутренние объекты объекта (плюс это не открытый интерфейс, и это может измениться в будущем). Мое предложение здесь - использовать метод scoped. В основном замените @relation на scoped.

Проверьте этот пример. Я использовал модель из своего собственного проекта, чтобы показать, что она действительно работает

2.0.0p247 :001 > Tracking # just asking console to load this class before modifying it
# => Tracking(id: integer, action: string, cookie_id: string, ext_object_id: integer, created_at: datetime, updated_at: datetime)
2.0.0p247 :002 > class Tracking
2.0.0p247 :003?>     def self.fetch_ids
2.0.0p247 :004?>         scoped.map(&:id)
2.0.0p247 :005?>       end
2.0.0p247 :006?>   end
# => nil
2.0.0p247 :007 >
2.0.0p247 :008 >   Tracking.where(id: (1..100)).fetch_ids
#  Tracking Load (2.0ms)  SELECT "trackings".* FROM "trackings" WHERE ("trackings"."id" BETWEEN 1 AND 100)
# => [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16]

UPD

В Rails 4 scoped устарел, поэтому правильно использовать all.

all.map(&:id)

Ответ 2

В Rails >= 4 вы можете использовать where(nil) inplace scoped

class Foo < ActiveRecord::Base  
  def self.bar
    where(nil).pluck(:id)
  end
end

Foo.where(id: [1, 2, 3]).order(:id).bar

Кроме того, вы можете использовать #scope, например:

class Foo < ActiveRecord::Base
  scope :bar, -> {where(nil).pluck(:id)}
end

Наконец, вы можете написать код типа Foo.all.bar

Ответ 3

Похоже, что все работает, но для большей утонченности я уверен, что есть что-то намного лучше. По общему признанию, вы не были слишком конкретны в описании того, что вы хотели достичь с этим, поэтому я могу только дать вам это широкое предложение

Вы можете посмотреть в " классы наблюдателей"


Я написал сообщение о них здесь

Классы наблюдателей в основном контролируют определенную функцию модели и расширяют ее. Я думаю, что они предназначены только для функций before_filter и т.д., Но я не понимаю, почему вы не можете расширять отдельные функции, которые вы создаете.

Вам нужно будет использовать камень rails-observers в rails 4.0+, чтобы заставить их работать, поскольку они были обесценены из ядро рельсов