How to add a method to an activerecord collection?

前端 未结 3 1405
时光取名叫无心
时光取名叫无心 2021-02-18 23:37

I would like to add a method to all collections for a specific model. Let\'s say I want to add the method my_complicated_averaging_method to the WeatherData collect

3条回答
  •  野的像风
    2021-02-19 00:17

    There a lot of ways how to do it, and Yours is completely valid (though I personally prefer to wrap class methods into separate block check this out ), but as people are adding more business logic to their models and blindly follow "skinny controllers, fat models" concept, models turn into complete mess.

    To avoid this mess it's a good idea to introduce service objects, in Your case it would be like 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's 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
    

    Now You can call this class directly by passing Your collection to it. But You can also proxy the call like this:

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

    I encourage You to lear more of this approach by reading this blog: http://blog.codeclimate.com/blog/2012/10/17/7-ways-to-decompose-fat-activerecord-models/

    UPD

    You are right using instance variable is a possible way to mess up object internals(plus it's not a public interface and it might change in future). My proposal here is to use method scoped. Basically replace @relation with scoped.

    Check this example. I have used a model from my own project to show that it actually works

    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

    In Rails 4 scoped is deprecated, so it's correct to use all.

    all.map(&:id)
    

提交回复
热议问题