I'm working on a gem.
Here is the homepage of it:
https://github.com/scaryguy/vakit
If you check out the source code, you can see that I'm parsing an external HTML page to filter some data from it.
The issue is, eventhough I fetch all data I want with one request, each time I call Vakit.sabah or Vakit.oglen a new request is done.
require "vakit/version"
require 'vakit/connect'
require 'Nokogiri'
require 'open-uri'
module Vakit
def self.today
Vakit::Connect.shaber
end
def self.imsak
Vakit::Connect.shaber[:imsak]
end
def self.sabah
Vakit::Connect.shaber[:sabah]
end
def self.oglen
Vakit::Connect.shaber[:oglen]
end
def self.ikindi
Vakit::Connect.shaber[:ikindi]
end
def self.aksam
Vakit::Connect.shaber[:aksam]
end
def self.yatsi
Vakit::Connect.shaber[:yatsi]
end
end
I don't think that it's an efficient way.
I should be able to access attributes of my hash without new request, shouldn't I?
module Vakit
class Connect
def initialize(opt={})
#path = opt[:path]
end
def self.shaber
doc = Nokogiri::HTML(open('http://www.samanyoluhaber.com/'))
x = doc.css('#hnmzT')
times = []
x.each do |vakit|
data = vakit.children.first.children.last.content
data_add = data.slice(0..data.length-2)
times.push(data_add)
end
times
vakit = {
imsak: times[0],
sabah: times[1],
oglen: times[2],
ikindi: times[3],
aksam: times[4],
yatsi: times[5]
}
end
end
end
I need some enlightment.
Every time you use shaber you explicitly open and reparse the content. You're not making an attempt to locally store the content or parsed DOM and check to see if you already have it.
Instead of doc = use ##doc ||= and change the occurrences of doc to ##doc.
The ||= operator will only assign when ##doc is empty. Once it's assigned to a non-nil or non-false value it won't trigger again so it's a poor-mans "memoize".
Because you are using a class-method I recommended using a class variable. ##doc could be an instance variable #doc instead if you have multiple instances of the class that are looking at different pages. As is, that won't make a difference because you've hard-coded only one page, but for future code-growth it might be useful.
The code you've written to access the page isn't very idiomatic Ruby. I'd write it more like the following, which doesn't work because the URL isn't returning a page that has enough time values:
require 'nokogiri'
require 'open-uri'
module Vakit
URL = 'http://www.samanyoluhaber.com/'
class Connect
def initialize(opt={})
#path = opt[:path]
#url = opt[:url] || URL
end
def shaber(url=nil)
doc = Nokogiri::HTML(open(url || #url))
doc.at_css('#hnmzT').to_html # => "<li id=\"hnmzT\" name=\"imsak\"><a id=\"at\"><span>\u0130msak:</span>4:22\u00A0</a></li>"
x = doc.at_css('li#hnmzT a')
x.to_html # => "<a id=\"at\"><span>\u0130msak:</span>4:22\u00A0</a>"
times = x.text.scan(/\d+:\d+/)
Hash[[:imsak, :sabah, :oglen, :ikindi, :aksam, :yatsi].zip(times)]
end
end
end
connection = Vakit::Connect.new
connection.shaber # => {:imsak=>"4:22", :sabah=>nil, :oglen=>nil, :ikindi=>nil, :aksam=>nil, :yatsi=>nil}
Connect isn't a good name for a class. A class is an object, a thing. Connect is a verb, something that occurs, or happens, to a thing. connect would be a good name for a method.
This line
doc = Nokogiri::HTML(open('http://www.samanyoluhaber.com/')) is what's making the requested call several times.
I tested this with your gem and this seems to work.
if #doc.nil?
#doc = Nokogiri::HTML(open('http://www.samanyoluhaber.com/'))
end
In addition in vakit.rb change
require 'Nokogiri' to require 'nokogiri' (simple n)
I will make the following suggestions:
Vakit::Connect should be an instance method inside a class. so it can be instantiated & saved in memory. this way each request will be different instances of the same object. if class doesn't exist. Do create it.
use memorization to cache costly operations.
If the operation takes long time, say greater than 3sec. convert it into a background job & use workers to process them.however, since you are authoring a gem. I think this responsibility should be delagated to developers using the gem.
so I would do it something like this:
the connect class:
module Vakit
class Connect
attr_accessor :path, :doc
def initialize(opt={}, url)
#path = opt[:path]
#doc = Nokogiri::HTML(open(url))
end
def shaber
x = #doc.css('#hnmzT')
#no changes here
end
end
the Vakit module
module Vakit
class Main #name it something more meaningful
def initialize(opt={})
#connected = Connect.new(opt[:path], 'http://www.samanyoluhaber.com/')
end
def today
#connected.shaber
end
def imsak
today[:imsak]
end
def sabah
today[:sabah]
end
def oglen
today[:oglen]
end
def ikindi
today[:ikindi]
end
def aksam
today[:aksam]
end
def yatsi
today[:yatsi]
end
end
then in code, you can use it like this:
#v = Vakit::Main.new
#v.aksam
This may not be 100% perfect because I really don't understand the purpose of your code, O understand what its doing but not why. But this will not make new request everytime you access your hash.
Related
So I'm wanting to parse a table on about 10 websites, so I want to create a new thread for each site. However, I'm not exactly sure how to return the data from this type of request.
Here's one class:
class TestRequest
def initialize
end
def start
urls = ['site1','site2','site3']
existing_data = Data.pluck(:symbol, :page)
data = GetData.pool(size: 10)
urls.each do |url|
data.async.perform_requests(url, existing_data)
end
end
end
and then GetData class looks like this:
require 'celluloid/current'
class GetData
include Celluloid
def perform_requests(url, existing_data)
# perform HTTP request
# parse HTTP response
# return returned data ???
end
end
What I'd ultimately like to do is have an instance variable in TestRequest class and simply add the returned value from GetData into that instance variable from the TestRequest class. After the threads are finished, I want to perform another action using the data in the instance variable.
I tried playing around with attr_reader, but it doesn't seem to play in my favor.
I tried this:
class TestRequest
def initialize
end
def start
#returned_data = []
urls = ['site1','site2','site3']
existing_data = Data.pluck(:symbol, :page)
data = GetData.pool(size: 10)
urls.each do |url|
data.async.perform_requests(url, existing_data)
end
end
attr_reader :returned_data
end
and then
require 'celluloid/current'
class GetData
include Celluloid
def perform_requests(tr, existing_data)
# perform HTTP request
# parse HTTP response
t = TestData.new
t.returned_data << "value"
end
end
but this doesn't work either.
Multi-threading and Ruby on Rails don't mix very well.
However, you should consider using the ActiveJob documentation (http://guides.rubyonrails.org/active_job_basics.html).
With ActiveJob, you can enqueue jobs and have them executed in the background. There are hooks method defined as well to notify you when a job is about to start, is running or have finished.
I use a gem to manage certain attributes of a gmail api integration, and I'm pretty happy with the way it works.
I want to add some local methods to act on the Gmail::Message class that is used in that gem.
i.e. I want to do something like this.
models/GmailMessage.rb
class GmailMessage < Gmail::Message
def initialize(gmail)
#create a Gmail::Message instance as a GmailMessage instance
self = gmail
end
def something_clever
#do something clever utilising the Gmail::Message methods
end
end
I don't want to persist it. But obviously I can't define self in that way.
To clarify, I want to take an instance of Gmail::Message and create a GmailMessage instance which is a straight copy of that other message.
I can then run methods like #gmail.subject and #gmail.html, but also run #gmail.something_clever... and save local attributes if necessary.
Am I completely crazy?
You can use concept of mixin, wherein you include a Module in another class to enhance it with additional functions.
Here is how to do it. To create a complete working example, I have created modules that resemble what you may have in your code base.
# Assumed to be present in 3rd party gem, dummy implementation used for demonstration
module Gmail
class Message
def initialize
#some_var = "there"
end
def subject
"Hi"
end
end
end
# Your code
module GmailMessage
# You can code this method assuming as if it is an instance method
# of Gmail::Message. Once we include this module in that class, it
# will be able to call instance methods and access instance variables.
def something_clever
puts "Subject is #{subject} and #some_var = #{#some_var}"
end
end
# Enhance 3rd party class with your code by including your module
Gmail::Message.include(GmailMessage)
# Below gmail object will actually be obtained by reading the user inbox
# Lets create it explicitly for demonstration purposes.
gmail = Gmail::Message.new
# Method can access methods and instance variables of gmail object
p gmail.something_clever
#=> Subject is Hi and #some_var = there
# You can call the methods of original class as well on same object
p gmail.subject
#=> "Hi"
Following should work:
class GmailMessage < Gmail::Message
def initialize(extra)
super
# some additional stuff
#extra = extra
end
def something_clever
#do something clever utilising the Gmail::Message methods
end
end
GmailMessage.new # => will call first the initializer of Gmail::Message class..
Building upon what the other posters have said, you can use built-in class SimpleDelegator in ruby to wrap an existing message:
require 'delegate'
class MyMessage < SimpleDelegator
def my_clever_method
some_method_on_the_original_message + "woohoo"
end
end
class OriginalMessage
def some_method_on_the_original_message
"hey"
end
def another_original_method
"zoink"
end
end
original = OriginalMessage.new
wrapper = MyMessage.new(original)
puts wrapper.my_clever_method
# => "heywoohoo"
puts wrapper.another_original_method
# => "zoink"
As you can see, the wrapper automatically forwards method calls to the wrapped object.
I'm not sure why you can't just have a simple wrapper class...
class GmailMessage
def initialize(message)
#message = message
end
def something_clever
# do something clever here
end
def method_missing(m, *args, &block)
if #message.class.instance_methods.include?(m)
#message.send(m, *args, &block)
else
super
end
end
end
Then you can do...
#my_message = GmailMessage.new(#original_message)
#my_message will correctly respond to all the methods that were supported with #original_message and you can add your own methods to the class.
EDIT - changed thanks to #jeeper's observations in the comments
It's not the prettiest, but it works...
class GmailMessage < Gmail::Message
def initialize(message)
message.instance_variables.each do |variable|
self.instance_variable_set(
variable,
message.instance_variable_get(variable)
)
end
end
def something_clever
# do something clever here
end
end
Thanks for all your help guys.
A user can import his data from other websites. All he needs to do is type in his username on the foreign website and we'll grab all pictures and save it into his own gallery. Some of the pictures needs to be transformed with rMagick (rotating,watermarking), that depends on the importer (depends on which website the user chooses to import data from)
We are discussing the sexiest and most flexible way to do so. We are using carrierwave, but we will change to paperclip in case it fits us more.
Importer Structure
The current structure does looks like (its roughly pseudocode)
module Importer
class Website1
def grab_pictures
end
end
class Website2
def grab_pictures
end
end
end
class ImporterJob
def perform(user, type, foreign_username)
pictures = Importer::type.grab_pictures(foreign_username)
pictures.each do |picture|
user.pictures.create picture
end
end
end
We struggle with the decision, whats the best return of the importer.
Solution1:
The Importer is returning an array of strings with URLs ["http://...", "http://...", "http://..."].
That array we can easily loop and tell carrierwave/paperclip to remote_download the images. After that, we'll run a processor to transform the pictures, if we need to.
def get_picture_urls username
pictures = []
page = get_html(username)
page.scan(/\/p\/\d{4}-\d{2}\/#{username}\/[\w\d]{32}-thumb.jpg/).each do |path|
pictures << path
end
pictures.uniq.collect{|x| "http://www.somewebsite.com/#{x.gsub(/medium|thumb/, "big")}"}
end
this actually returns an array ["url_to_image", "url_to_image", "url_to_image"]
Then in the Picture.after_create, we call something to remove the Watermark on that Image.
Solution2:
grab_pictures is downloading each picture to an tempfile and transform it. it will return an array of tempfiles [tempfile, tempfile, tempfile]
code for that is:
def read_pictures username
pictures = []
page = get_html(username)
page.scan(/\/p\/\d{4}-\d{2}\/#{username}\/[a-z0-9]{32}-thumb.jpg/).each do |path|
pictures << path
end
pictures.uniq.map { |pic_url| remove_logo(pic_url) }
end
def remove_logo pic_url
big = Magick::Image.from_blob(#agent.get(pic_url.gsub(/medium.jpg|thumb.jpg/, 'big.jpg')).body).first
# ... do some transformation and watermarking
file = Tempfile.new(['tempfile', '.jpg'])
result.write(file.path)
file
end
This actually returns an array of [Tempfile, Tempfile, Tempfile]
Summary
The result will be the same for the user - but internally we are discovering 2 different ways of data handling.
We want to keep logic where it belongs and work as generic as possible.
Can you guys help us with choosing the right way? Longterm we want to have around 15 differnt Importers.
I've had a similar situation to this recently - I recommend an array of strings for several reasons:
Familiarity: How often are you working with tempfiles? What about the other developers on your team? How easy is it to manipulate strings vs manipulating tempfiles?
Flexibility: Now you want to just process the picture, but maybe in the future you'll need to keep track of the picture id for each picture from the external site. That's trivial with an array of strings. With an array of tempfiles, it's more difficult (just how much depends, but the fact is it will be more difficult). That of course goes for other as-yet-unknown objectives as well.
Speed: It's faster and uses less disk space to process an array of strings than a group of files. That's perhaps a small issue, but if you get flooded with a lot of photos at the same time, it could be a consideration depending on your environment.
Ultimately, the best thing I can say is start with strings, make a few importers, and then see how it looks and feels. Pretend you're a project manager or a client - start making strange, potentially unreasonable demands of the data you've collected. How easy will it be for you to meet those demands with your current implementation? Would it be easier if you were using tempfiles?
I am doing this for a similar project, where I have to browse and get information on different websites. On each of those websites I have to reach for same goal by performing roughly the same actions, and they are off-course all structured differently.
The solution is inspired from the basic principles of OOP:
Main class: handle the high level operations, handle database operations, handle images operation, manage errors
class MainClass
def import
# Main method, prepare the download and loop through each images
log_in
go_to_images_page
images = get_list_of_images
images.each do |url|
begin
image_record = download_image url
transform_image image_record
rescue
manage_error
end
end
display_logs
send_emails
end
def download_image(url)
# Once the specific class returned the images url, this common method
# Is responsible for downloading and creating database record
record = Image.new picture: url
record.save!
record
end
def transform_image(record)
# Transformation is common so this method sits in the main class
record.watermark!
end
# ... the same for all commom methods (manage_error, display_logs, ...)
end
Specific classes (one per targeted website) : handle low lovel operations and return data to the main class. The only interraction this class must have is with the website, meaning no database access and no error management as much as possible (don't get stuck by your design ;))
Note: In my design I simply inherit from the MainClass, but you can use module inclusion if you prefer.
class Target1Site < MainClass
def log_in
# Perform specific action in website to log the use in
visit '/log_in'
fill_in :user_name, with: ENV['user_name']
...
end
def go_to_images_page
# Go to specific url
visit '/account/gallery'
end
def get_list_of_images
# Use specific css paths
images = all :css, 'div#image-listing img'
images.collect{|i| i['src']}
end
# ...
end
I solved a similar problem... I had to import from a xls file, different resource types using:
The Importer class (ResourcesGroupsImporter).
A base mapper class (ResourceMapper) It acts as interface for specific mappers. It has common methods for all resources and raises NotImplementedError encouraging you to implement those methods when you adds a new resource type.
One mapper by resource type (DetentionsPollMapper, FrontCycleMapper). Each one, implements specific logic for an specific resource.
Implementation example:
The importer...
class ResourcesGroupsImporter
attr_reader :group
attr_reader :mappers
def initialize(_source, _resources_group)
#group = _resources_group
#source = _source
#xls = Roo::Spreadsheet.open(#source.path, extension: :xlsx)
#mappers = Resource::RESOURCEABLE_CLASSES.map { |klass| resource_mapper(klass) }
end
def import
ActiveRecord::Base.transaction do
self.mappers.each { |mapper| create_resource(mapper) }
relate_source_with_group unless self.has_errors?
raise ActiveRecord::Rollback if self.has_errors?
end
end
def has_errors?
!self.mappers.select { |mapper| mapper.has_errors? }.empty?
end
private
def resource_mapper(_class)
"#{_class}Mapper".constantize.new(#xls, #group)
end
def create_resource(_mapper)
return unless _mapper.resource
_mapper.load_resource_attributes
_mapper.resource.complete
_mapper.resource.force_validation = true
if _mapper.resource.save
create_resource_items(_mapper)
else
_mapper.load_general_errors
end
end
def create_resource_items(_mapper)
_mapper.set_items_sheet
columns = _mapper.get_items_columns
#xls.each_with_index(columns) do |data, index|
next if data == columns
break if data.values.compact.size.zero?
item = _mapper.build_resource_item(data)
_mapper.add_detail_errors(index, item.errors.messages) unless item.save
end
end
def relate_source_with_group
#group.reload
#group.source = #source
#group.save!
end
end
The interface...
class ResourceMapper
attr_reader :general_errors
attr_reader :detailed_errors
attr_reader :resource
def initialize(_xls, _resource_group)
#xls = _xls
#resource = _resource_group.resourceable_by_class_type(resource_class)
end
def resource_class
raise_implementation_error
end
def items_sheet_number
raise_implementation_error
end
def load_resource_attributes
raise_implementation_error
end
def get_items_columns
raise_implementation_error
end
def build_resource_item(_xls_item_data)
resource_items.build(_xls_item_data)
end
def raise_implementation_error
raise NotImplementedError.new("#{caller[0]} method not implemented on inherited class")
end
def has_errors?
!self.general_errors.nil? || !self.detailed_errors.nil?
end
def resource_items
self.resource.items
end
def human_resource_name
resource_class.model_name.human
end
def human_resource_attr(_attr)
resource_class.human_attribute_name(_attr)
end
def human_resource_item_attr(_attr)
"#{resource_class}Item".constantize.human_attribute_name(_attr)
end
def load_general_errors
#general_errors = self.resource.errors.messages
end
def add_detail_errors(_xls_row_idx, _error)
#detailed_errors ||= []
#detailed_errors << [ _xls_row_idx+1, _error ]
end
def set_items_sheet
#xls.default_sheet = items_sheet
end
def general_sheet
sheet(0)
end
def items_sheet
sheet(self.items_sheet_number)
end
def sheet(_idx)
#xls.sheets[_idx]
end
def general_cell(_col, _row)
#xls.cell(_col, _row, general_sheet)
end
end
Specific mapper types...
class DetentionsPollMapper < ResourceMapper
def items_sheet_number
6
end
def resource_class
DetentionsPoll
end
def load_resource_attributes
self.resource.crew = general_cell("N", 3)
self.resource.supervisor = general_cell("N", 4)
end
def get_items_columns
{
issue: "Problema identificado",
creation_date: "Fecha",
workers_count: "N° Trabajadores esperando",
detention_hours_string: "HH Detención",
lost_hours: "HH perdidas",
observations: "Observación"
}
end
def build_resource_item(_xls_item_data)
activity = self.resource.activity_by_name(_xls_item_data[:issue])
data = {
creation_date: _xls_item_data[:creation_date],
workers_count: _xls_item_data[:workers_count],
detention_hours_string: _xls_item_data[:detention_hours_string],
lost_hours: _xls_item_data[:lost_hours],
observations: _xls_item_data[:observations],
activity_id: !!activity ? activity.id : nil
}
resource_items.build(data)
end
end
class FrontCycleMapper < ResourceMapper
def items_sheet_number
8
end
def resource_class
FrontCycle
end
def load_resource_attributes
self.resource.front = general_cell("S", 3)
end
def get_items_columns
{
task: "Tarea",
start_time_string: "Hora",
task_type: "Tipo de Tarea",
description: "Descripción"
}
end
def build_resource_item(_xls_item_data)
activity = self.resource.activity_by_name_and_category(
_xls_item_data[:task], _xls_item_data[:task_type])
data = {
description: _xls_item_data[:description],
start_time_string: _xls_item_data[:start_time_string],
activity_id: !!activity ? activity.id : nil
}
resource_items.build(data)
end
end
A helper have to provide a way to access pict as you prefer.
However saving "http://...", "http://...", "http://..." this kind of strings, is a lack of security.
I 'd preferd hash like this: domain_name = {"name_on_url.jpg" =>path_on_disk, ...}
To ensure flexibility of access.
I want to initialize some attributes in retrieved objects with values received from an external API. after_find and after_initialize callbacks won't work for me as this way I have to call the API for each received object, which is is quite slow. I want something like the following:
class Server < ActiveRecord::Base
attr_accessor :dns_names
...
after_find_collection do |servers|
all_dns_names = ForeignLibrary.get_all_dns_entries
servers.each do |s|
s.dns_names = all_dns_names.select{|r| r.ip == s.ip}.map{|r| r.fqdn}
end
end
end
Please note that caching is not a solution, as I need to always have current data, and the data may be changed outside the application.
You'd want to have a class method that enhances each server found with your data. so, something like:
def index
servers = Server.where(condition: params[:condition]).where(second: params[:second])
#servers = Server.with_domains_names(servers)
end
class Server
def self.with_domain_names(servers)
all_dns_names = ForeignLibrary.get_all_dns_entries
servers.each do |s|
s.dns_names = all_dns_names.select{|r| r.ip == s.ip}.map{|r| r.fqdn}
end
end
end
This way, the ForeignLibrary.get_all_dns_entries only gets run once, and you can enhance your servers with that extra information.
If you wanted to do this every time you initialize a server object, I'd simply delegate rather than use after_initialize. So you'd effectively store the all dns entries in a global variable, and then cache it for a period of time. ForeignLibrary.get_all_dns_entries call. So, it would be something like:
class Server
def dns_names
ForeignLibrary.dns_for_server(self)
end
end
class ForeignLibrary
def self.reset
##all_dns_names = nil
end
def self.dns_for_server(server)
all_dns_names.select{|r| r.ip == server.ip}.map{|r| r.fqdn}
end
def self.all_dns_names
Mutex.new.synchronize do
##all_dns_names ||= call_the_library_expensively
end
end
end
(I also used a mutex here since we are doing ||= with class variables)
to use it, you would:
class ApplicationController
before_filter do
ForeignLibrary.reset #ensure every page load has the absolute latest data
end
end
I have an expensive (time-consuming) external request to another web service I need to make, and I'd like to cache it. So I attempted to use this idiom, by putting the following in the application controller:
def get_listings
cache(:get_listings!)
end
def get_listings!
return Hpricot.XML(open(xml_feed))
end
When I call get_listings! in my controller everything is cool, but when I call get_listings Rails complains that no block was given. And when I look up that method I see that it does indeed expect a block, and additionally it looks like that method is only for use in views? So I'm guessing that although it wasn't stated, that the example is just pseudocode.
So my question is, how do I cache something like this? I tried various other ways but couldn't figure it out. Thanks!
an in-code approach could look something like this:
def get_listings
#listings ||= get_listings!
end
def get_listings!
Hpricot.XML(open(xml_feed))
end
which will cache the result on a per-request basis (new controller instance per request), though you may like to look at the 'memoize' helpers as an api option.
If you want to share across requests don't save data on the class objects, as your app will not be threadsafe, unless you're good at concurrent programming & make sure the threads don't interfere with each other's data access to the shared variable.
The "rails way" to cache across requests is the Rails.cache store. Memcached gets used a lot, but you might find the file or memory stores fit your needs. It really depends on how you're deploying and whether you want to prioritise cache hits, response time, storage (RAM), or use a hosted solution e.g. a heroku addon.
As nruth suggests, Rails' built-in cache store is probably what you want.
Try:
def get_listings
Rails.cache.fetch(:listings) { get_listings! }
end
def get_listings!
Hpricot.XML(open(xml_feed))
end
fetch() retrieves the cached value for the specified key, or writes the result of the block to the cache if it doesn't exist.
By default, the Rails cache uses file store, but in a production environment, memcached is the preferred option.
See section 2 of http://guides.rubyonrails.org/caching_with_rails.html for more details.
You can use the cache_method gem:
gem install cache_method
require 'cache_method'
In your code:
def get_listings
Hpricot.XML(open(xml_feed))
end
cache_method :get_listings
You might notice I got rid of get_listings!. If you need a way to refresh the data manually, I suggest:
def refresh
clear_method_cache :get_listings
end
Here's another tidbit:
def get_listings
Hpricot.XML(open(xml_feed))
end
cache_method :get_listings, (60*60) # automatically expire cache after an hour
You can also use cachethod gem (https://github.com/reneklacan/cachethod)
gem 'cachethod'
Then it is deadly simple to cache method's result
class Dog
cache_method :some_method, expires_in: 1.minutes
def some_method arg1
..
end
end
It also supports argument level caching
There was suggested cache_method gem, though it's pretty heavy. If you need to call method without arguments, solution is very simple:
Object.class_eval do
def self.cache_method(method_name)
original_method_name = "_original_#{method_name}"
alias_method original_method_name, method_name
define_method method_name do
#cache ||= {}
#cache[method_name] = send original_method_name unless #cache.key?(method_name)
#cache[method_name]
end
end
end
then you can use it in any class:
def get_listings
Hpricot.XML(open(xml_feed))
end
cache_method :get_listings
Note - this will also cache nil, which is the only reason to use it instead of #cached_value ||=
Late to the party, but in case someone arrives here searching.
I use to carry this little module around from project to project, I find it convenient and extensible enough, without adding an extra gem. It uses the Rails.cache backend, so please use it only if you have one.
# lib/active_record/cache_method.rb
module ActiveRecord
module CacheMethod
extend ActiveSupport::Concern
module ClassMethods
# To be used with a block
def cache_method(args = {})
#caller = caller
caller_method_name = args.fetch(:method_name) { #caller[0][/`.*'/][1..-2] }
expires_in = args.fetch(:expires_in) { 24.hours }
cache_key = args.fetch(:cache_key) { "#{self.name.underscore}/methods/#{caller_method_name}" }
Rails.cache.fetch(cache_key, expires_in: expires_in) do
yield
end
end
end
# To be used with a block
def cache_method(args = {})
#caller = caller
caller_method_name = args.fetch(:method_name) { #caller[0][/`.*'/][1..-2] }
expires_in = args.fetch(:expires_in) { 24.hours }
cache_key = args.fetch(:cache_key) { "#{self.class.name.underscore}-#{id}-#{updated_at.to_i}/methods/#{caller_method_name}" }
Rails.cache.fetch(cache_key, expires_in: expires_in) do
yield
end
end
end
end
Then in an initializer:
# config/initializers/active_record.rb
require 'active_record/cache_method'
ActiveRecord::Base.send :include, ActiveRecord::CacheMethod
And then in a model:
# app/models/user.rb
class User < AR
def self.my_slow_class_method
cache_method do
# some slow things here
end
end
def this_is_also_slow(var)
custom_key_depending_on_var = ...
cache_method(key_name: custom_key_depending_on_var, expires_in: 10.seconds) do
# other slow things depending on var
end
end
end
At this point it only works with models, but can be easily generalized.
Other answers are excellent but if you want a simple hand-rolled approach you can do this. Define a method like the below one in your class...
def use_cache_if_available(method_name,&hard_way)
#cached_retvals ||= {} # or initialize in constructor
return #cached_retvals[method_name] if #cached_retvals.has_key?(method_name)
#cached_retvals[method_name] = hard_way.call
end
Thereafter, for each method you want to cache you can put wrap the method body in something like this...
def some_expensive_method(arg1, arg2, arg3)
use_cache_if_available(__method__) {
calculate_it_the_hard_way_here
}
end
One thing that this does better than the simplest method listed above is that it will cache a nil. It has the convenience that it doesn't require creating duplicate methods. Probably the gem approach is cleaner, though.
I'd like to suggest my own gem https://github.com/igorkasyanchuk/rails_cached_method
For example:
class A
def A.get_listings
....
end
end
Just call:
A.cached.get_listings