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 collections:
WeatherData.all.limit(3).my_complicated_averaging_method()
Station.first.weatherdata.my_complicated_averaging_method()
What is the best way to do this? At the moment the only way I've found is like this:
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
Is this a good way for adding a method to a collection? Is there a better / supported way to do this?
Active Record allows you to validate the state of a model before it gets written into the database. There are several methods that you can use to check your models and validate that an attribute value is not empty, is unique and not already in the database, follows a specific format, and many more.
ActiveRecord::Base indicates that the ActiveRecord class or module has a static inner class called Base that you're extending.
ActiveRecord is an ORM. It's a layer of Ruby code that runs between your database and your logic code. When you need to make changes to the database, you'll write Ruby code, and then run "migrations" which makes the actual changes to the database.
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)
On Rails >= 4 you can to use where(nil)
inplace of scoped
class Foo < ActiveRecord::Base
def self.bar
where(nil).pluck(:id)
end
end
Foo.where(id: [1, 2, 3]).order(:id).bar
And further, you can use #scope
, for example:
class Foo < ActiveRecord::Base
scope :bar, -> {where(nil).pluck(:id)}
end
Finally, You can write code like Foo.all.bar
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With