Why do a singleton method and a delegated method behave differently? - ruby-on-rails

Okay, this is kind of a followup to this question: Is overriding an ActiveRecord relation's count() method okay? Basically I have a relation I want to paginate on, and counting it is slow, so I'm overriding count() with a cached counter attribute.
I have:
class CountDelegator < SimpleDelegator
def initialize(obj, total_count)
super(obj)
#total_count = total_count
end
def count
#total_count
end
end
class Parent
has_many :kids do
def chatty_with_singleton
resultset = where(:chatty => true)
def resultset.count
proxy_association.owner.chatty_kids_count
end
resultset
end
def chatty_with_delegation
resultset = where(:chatty => true)
CountDelegator.new(resultset, proxy_association.owner.chatty_kids_count)
end
end
end
p = Parent.first
Now, when I do either p.kids.chatty_with_singleton.count or p.kids.chatty_with_delegation.count, I use the cached count. Great! However, the following behave differently:
# Uses the cached count
p.kids.chatty_with_singleton(:order => "id desc").count
# Does not use the cached count
p.kids.chatty_with_delegation(:order => "id desc").count
I'm totally confused — I don't know why these two cases would behave differently in practice. (Yes, I'm aware that p.kids.chatty_with_singleton(:id => 0).count returns the wrong value and I am okay with that.)
Why does defining the method on the singleton resultset cause that definition to dominate, while the delegator doesn't?

You're re-implementing the built-in counter_cache option provided by belongs_to. You simply specify the column of your cache as well, if you're not using the rails default column.
Using the counter_cache will automatically read the cached value on table instead of executing a COUNT.

Related

class << self in ruby and its methods

I have a model in ruby on rails with the below code, which uses a singelton class definition. Also, som metaprogramming logic. But, I don't understand when this code will invoke.Is it when an attribute below specified is editing?
class Product < ApplicationRecord
class << self
['cat_no', 'effort', 'impact', 'effect', 'feedback'].each do |attr|
define_method "update_#{attr}" do |pr, count, user_id|
pr.order=pr.cat_no
pr.idea=pr.description
pr.update("#{attr}"=>count,:last_modified_by=>user_id)
end
end
end
end
Please help.
Thanks
This code generates five methods, one for each attribute name in the list. All these generated methods take three arguments and will basically look like this (I use the impact attribute name as an example):
def self.update_impact(pr, count, user_id)
pr.order = pr.cat_no
pr.idea = pr.description
pr.update("impact" => count, :last_modified_by => user_id)
end
That means there are five methods generated that update the passed in pr with some data from itself and with a count and a user_id.
Note that this method only deals with a specific pr therefore it is certainly better to use an instance instead of a class method as Stefan already suggested in his comment. And IMO there is not really a benefit in meta-programming here. I would change the logic to
def update_count(type, count, user_id) # or any another name that makes sense in the domain
if type.in?(%i[cat_no effort impact effect feedback])
update(
:order => cat_no,
:idea => description,
:last_modified_by => user_id,
type => count
)
else
raise ArgumentError, "unsupported type '#type'"
end
end
and call it instead of
Model.update_impact(pr, count, user_id)
like this
pr.update_count(:impact, count, user_id)

How to query Rails / ActiveRecord model based on custom model method?

Disclaimer: I'm relatively new to rails.
I have a custom method in my model that I'd like to query on. The method, called 'active?', returns a boolean. What I'd really like to do is create an ActiveRecord query of the following form:
Users.where(:active => true)
Naturally, I get a "column does not exist" when I run the above as-is, so my question is as follows:
How do I do the equivalent of the above, but for a custom method on the model rather than an actual DB column?
Instead of using the active? method, you would have a scope to help find items that match.
Something like this...
def self.active
joins(:parent_table).where(:archived => false).where("? BETWEEN parent_table.start_date AND parent_table.end_date ", Time.now)
end
And, you should be able to do this
def active?
User.active.exists?(self)
end
If you would like to reuse this scope for the instance test.
An easy way to do this would be by using the select method with your exiting model method.
Users.select{|u| u.active}
This will return an array so you won't be able to use Active Record Query methods on it. To return the results as an ActiveRecord_Relation object, you can use the where function to query instances that have matching ids:
class User < ActiveRecord::Base
def self.active
active_array = self.select{|r| r.active?}
active_relation = self.where(id: active_array.map(&:id))
return active_relation
end
end

How to return results filtered on relations count to a view in RAILS?

Basically, I defined a property on my model which returns true or false depending on values in another table.
What I want is to have my Index action in the controller to return only those results that meet this condition.
I tried this:
#What I've tried on my Controller:
def index
#projects = Project.where(:is_available?)
end
#What I've on my Controller:
def index
#projects = Project.all
end
#What I've on my Model:
def is_available?
workers.count<2 ? true : false
end
Thanks.
Why your code doesn't work?
Project.where(:is_available?)
Here in the where method, you have to pass a hash of arguments OR a string of (SQL) conditions. What you are trying to do here is to select all projects where the method is_available? returns true. The problem is that the method is_available? is a Ruby method (defined in your model). Since it is a Ruby function, you can't call it inside SQL. The where method is expecting SQL conditions, not ruby code.
(Thanks to #benzado for the comment)
To fix your problem:
This is what you are looking for, computed only at the db level:
Project.joins(:workers)
.select('projects.*')
.group('projects.id')
.having('COUNT(workers.*) > 2')
This should returns all project having at least 2 workers associated with.
How to improve this?
You can make a scope of this query, to use it everywhere easily:
#in your model
class Project < ActiveRecord::Base
scope :having_more_than_x_workers, lambda do |workers_count|
joins(:workers).select('projects.*').group('projects.id').having("COUNT(workers.*) > #{workers_count || 0}")
end
end
To use it, in your controller for example:
#in your controller
def index
#projects = Project.having_more_than_x_workers(2)
end
The Rails query methods (like where) work by creating a database query; in other words, you can't use an attribute in where unless it actually exists on your data model. Otherwise, the database doesn't know about it and can't perform the filtering for you.
In your case, you should define a method on the Project class which performs the "is available?" query, so you can use your method in place of where. You can do it like this:
class Project < ActiveRecord::Base
def self.available_projects
where('workers_count > 2')
end
end
See MrYoshiji's answer for specifics on how to write the query or how to define it as a named scope.

Rails: Avoiding caching on assocation count

I have the following piece of Rails code:
class Shop < ActiveRecord::Base
# ...
def validate_books_have_authors
self.books.each do |book|
# Urghh...caching book.authors unless we call directly
# puts book.authors
errors[:books] << t('book.no_authors', :book => book.name) unless book.authors.any?
end
end
end
On first run, the validator will process correctly...but if I run the same method again, the value for book.authors.any? returns a cached value unless I uncomment that puts book.authors line
So, simple question really: how do I ensure the value of book.authors.any? isn't cached?
You should be able to either use exists? or call count and check that the result is greater than zero.
Counting is usually a little slower as it requires more work from the database to establish a precise count.
For reference: I fixed this by changing .any? to .present?

Rails: call method within model

Can't figure this one out.
In rails model, I want to call a method within the same model to manipulate data returned by a find method. This 'filter' method will be called from many custom find method within this model, so I want it to be separate. (and I cannot filter from the SQL it is too complicated)
Here is an example:
#controller
#data = Model.find_current
#model
class Model
def self.find_current
#rows = find(:all)
filter_my_rows
return #rows
end
def filter_my_rows
#do stuff here on #rows
for row in #rows
#basically I remove rows that do not meet certain conditions
end
end
end
The result of this is: undefined method `filter_my_rows'
Thank you for any help!
Part of the problem is you're defining a class method called find_current and an instance method called filter_my_rows. Generally you define them both within the same scope for them to work together.
Another thing is you can do a lot of the filtering you need with a simple Array#reject call. For example:
#models = all.reject do |m|
# This block is used to remove entries that do not qualify
# by having this evaluate to true.
!m.current
end
You can modularize this somewhat by plugging in functions as required, too, but that can get wildly complicated to manage if you're not careful.
# Define reusable blocks that are organized into a Hash
CONDITION_FILTERS = {
:current => lambda { |m| m.current }
}
# Array#select is the inverse of Array#reject
#models = all.select(CONDITION_FILTERS[:current])
While you stated in your question that this was only required because of concerns about not being able to determine the relevance of a particular record before all the records are loaded from the database, this is generally bad form since you will probably be rejecting a large amount of data that you've gone through the trouble of retrieving and instantiating as models only to immediately discard them.
If possible, you should at least cache the retrieved rows for the duration of the request so you don't have to keep fetching them over and over.
function of class and function of instance is your problem.
You can't call a instance function in your class function that way.
Use self.filter_my_rows to define your function (note the self) and everything will go right.
use a named_scope instead
named_scope :current, :conditions => {:active => true} # this is normal find criteria
then in your controller
#date = Model.current
you can also make the named_scopes lambda functions
What's wrong with your solutions? What are you looking for exactly?
If I understood your point of view, the main problem of your implementation is that
This 'filter' method will be called
from many custom find method within
this model, so I want it to be
separate.
... that you can't use named_scopes or with_scope, the first solution that comes to my mind is to create a custom wrapper to act as a filter.
class Model
def self.find_current
filtered do
all
end
end
def self.other_method
filtered do
all :conditions => { :foo => "bar" }
end
end
def self.filtered(&block)
records = yield
# do something with records
records
end
end

Resources