Rails model class method for collection of objects - ruby-on-rails

I'm having trouble writing class methods to use on collections of ActiveRecord objects. I've run into this issue twice in the last couple of hours, and it seems like a simple problem, so I know I'm missing something, but I haven't been able to find answers elsewhere.
Example:
class Order < ActiveRecord::Base
belongs_to :customer
scope :month, -> { where('order_date > ?', DateTime.now.beginning_of_month.utc) }
def self.first_order_count
map(&:first_for_customer?).count(true)
end
def first_for_customer?
self == customer.orders.first
# this self == bit seems awkward, but that's a separate question...
end
end
If I call Order.month.first_order_count, I get
NoMethodError: undefined method 'map' for #<Class:...
As far as I know, that's because map can't be called directly on Order, but needs an Enumerable object instead. If I call Order.year.map(&:first_for_customer?).count(true), I get the desired result.
What's the right way to write methods to use on a collection of ActiveRecord objects, but not on the class directly?

In your case, you can use a trick in this case.
def self.first_order_count
all.map(&:first_for_customer?).count(true)
end
Will do the trick, without any other problems, this way if you concatenate this method on where clause you still get results from that where, this way you get what you need if you call this method directly on Order.

ActiveRecord collections are usually manipulated using scopes, with the benefits of being able to chain them and let the database do the heavy lifting. If you must manage it in Ruby, you can start with all.
def self.first_order_count
all.map(&:first_for_customer?).count(true)
end
What are you trying to achieve with your code though?

Related

How to define rescue_from-type handler for ActiveRecord models?

Developing in Rails 5.2.2.1. I want to define a "global" rescue handler for my model, so that I can catch NoMethodError and take appropriate action. I find that controllers can do this with rescue_from, but models cannot. Knowing that the Rails Developers are smart people ;) I figure there must be some Good Reason for this, but I'm still frustrated. Googling around, and I can't even find any examples of people asking how to do this, and other people either telling them how, or why they can't, or why they shouldn't want to. Maybe it's because rescue handlers can't return a value to the original caller?
Here's what I'm trying to do: I need to refactor my app so that what used to be a single model is now split into two (let's call them Orig and New). Briefly, I want to make it so that when an attribute getter method (say) is called against an Orig object, if that attribute has moved to New, then I can catch this error and call new.getter instead (understanding that Orig now belongs_to a New). This solution is inspired by my experience doing just this sort of thing with Perl5's AUTOLOAD feature.
Any ideas of how to get this done are much appreciated. Maybe I just have to define getters/setters for all the moved attributes individually.
Overide method_missing :) !?
You could try overriding the method_missing method. This could potentially cause confusing bugs, but overriding that method is definitely used to great effect in at least one gem that i know of.
I didn't want to call the class new because it is a reserved keyword and can be confusing. So I changed the class name to Upgraded.
This should get you started.
class Upgraded
def getter
puts "Congrats, it gets!"
end
end
class Original
def initialize
#new_instance = Upgraded.new
end
def method_missing(message, *args, &block)
if message == :attribute_getter
#new_instance.send(:getter, *args, &block)
else
super
end
end
def respond_to_missing?(method_name, *args)
method_name == :attribute_getter or super
end
end
c = Original.new
c.attribute_getter
You will have to change names of the getter and setter methods. Because you have a belongs_to association you can just use that.
Or you could try just using delegate_to
like #mu_is_too_short suggests, you could try something like this?
class Original < ApplicationRecord
belongs_to :upgraded
delegate :getter_method, :to => :upgraded
end
class Upgraded < ApplicationRecord
def getter_method
end
end
Apparently what I needed to know is the word "delegation". It seems there are a variety of ways to do this kind of thing in Ruby, and Rails, and I should have expected that Ruby's way of doing it would be cleaner, more elegant, and more evolved than Perl5. In particular, recent versions of Rails provide "delegate_missing_to", which appears to be precisely what I need for this use case.

Rails - apply an array of scopes

I have a class that takes an array of scopes and applies them iteratively:
class AssignableLearningObjectives::Collector
def initialize(user:, only_self_assignable: false, scopes: [])
#user = user
#only_self_assignable = only_self_assignable
#scopes = scopes
end
.....
def available_objectives
objectives = assignable_objectives.or(manager_assigned_objectives).or(global_objectives).distinct
return objectives unless scopes.any?
scopes.each{ |scope| objectives = objectives.send(scope) }
objectives
end
My issue is with
scopes.each{ |scope| objectives = objectives.send(scope) }
objectives
Is there a better way of doing this? I was hoping for a rails method apply_scopes or something like that, however can't find anything like that.
My concern is the scopes are sent from the controller, and it is possible for the user to submit a request with a scope of 'destroy_all' or something equally fun.
Is there an easy way for me to let rails handle this? Or will I need to manually check each scope before I apply it to the collection?
Thanks in advance
EDIT:
I'm happy to validate each scope individually if I have to, but even that's causing issues. There is a method in rails which was dropped in 3.0.9 which I could use, Model.scopes :
https://apidock.com/rails/v3.0.9/ActiveRecord/NamedScope/ClassMethods/scopes
however that's deprecated. Is there any method I can call on a class to list its scopes? I can't believe the feature was there in rails 3 and removed completely...
From the fine guide:
14 Scopes
[...]
To define a simple scope, we use the scope method inside the class, passing the query that we'd like to run when this scope is called:
class Article < ApplicationRecord
scope :published, -> { where(published: true) }
end
This is exactly the same as defining a class method, and which you use is a matter of personal preference:
class Article < ApplicationRecord
def self.published
where(published: true)
end
end
So scope is mostly just a fancy way of creating a class method that is supposed to have certain behavior (i.e. return a relation) and any class method method that returns a relation is a scope. scope used to be something special but now they're just class methods and all the class methods are copied to relations to support chaining.
There is no way to know if method Model.m is a "real" scope that will return a relation or some random class method without running it and checking what it returns or manually examining its source code. The scopes method you seek is gone and will never come back.
You could try to blacklist every class method that you know is bad; this way lies bugs and madness.
The only sane option is to whitelist every class method that you know is good and is something that you want users to be able to call. Then you should filter the scopes array up in the controller and inside AssignableLearningObjectives::Collector. I'd check in both places because you could have different criteria for what is allowed depending on what information is available and what path you're taking through the code; slightly less DRY I suppose but efficiency and robustness aren't friends.
You could apply the scope whitelist in the AssignableLearningObjectives::Collector constructor or in available_objectives.
If you want something prettier than:
scopes.each{ |scope| objectives = objectives.send(scope) }
objectives
then you could use inject:
def available_objectives
objectives = assignable_objectives....
scopes.inject(objectives) { |objectives, scope| objectives.send(scope) }
end

Where to put method that returns a collection

I have a Shop model.
In my class, I have created several methods, that allow me to make my code easier to read.
Things like :
# Returns True if the shop has an owner
def is_claimed
return self.owner_id != nil
end
Now I want to create a method that would return a collection of Shops.
Something like :
def get_all_open
return Shop.where("shops.closed != 1")
end
I am simply wondering where I should place it, so it is available from anywhere where I can potentially use
Shop.all
in my code.
If I place it in the Model, it will be available to instances of shops, which would sound weird.
This method also belongs to Shop, so I don't really like placing it in the ApplicationController.
I also thought about placing it in the shops_controller, but then it won't be available easily to other controllers.
I have looked on the internet, but failed to find something I like so far. What would be considered a good practice in rails ?
Thanks
You can keep it in the Model, but instead of defining it as an instance method, define it as a class method:
def self.all_open
where("shops.closed != 1")
end
This way you can use it like this:
Shop.all_open
And even nest methods, like this:
Shop.all_open.where(field: "value")
UPDATE:
Like others pointed out, you could also use scopes (this is the preferred way):
class Shop < ActiveRecord::Base
scope :all_open, -> { where("shops.closed != 1") }
end
The usage would be the same:
Shop.all_open
This is what Scopes are usually used for.
class Shop < ActiveRecord::Base
scope :open, ->{ where("shops.closed != 1") }
# ->{ where.not(closed: true) } # <- Either will work, but I prefer this
# ...
end
Then:
Shop.open.all # => (a collection of Shops)
The advantage of scopes is that you can chain them, so, for example, if your Shop model had a type attribute, you could get all open Shops with a certain type like so:
Shop.open.where(type: "pharmacy").all
Define it as a class method in Shop.
class Shop
def self.all_open
where("shops.closed != 1")
end
end
open_shops = Shop.all_open
Note:
0) The self in def self.all_open is what makes this a class method, defined on the class Shop itself, and callable on the class itself as Shop.all_open, instead of an ordinary instance method which needs to be called on a particular instance.
1) Calling the method all_open instead of get_all_open leads to more natural and idiomatic code when you call Shop.all_open. I might in fact just call it open, leading to open_shops = Shop.open.
2) Note the suggested implementation omits the return, just for readability. Note it also omits the Shop.where, and just does where. This mostly only matters if you end up making sub-classes of Shop, which you probably won't, but is good style, and shorter code. Just where is the same as self.where, and since this is a class method, self is the class itself, Shop (unless you have sub-classes inheriting this code, in which case self could be a sub-class).
3) You can in fact chain this with toher stuff, no problem. Check it out: Shop.all_open.where(:city => "Baltimore").order(:updated_at). Pretty neat, it just works!
4) Some people will tell you to use ActiveRecord 'scopes' for this. You don't really need to for this case, you can just write a plain old method, just like that. The Rails docs acknowledge this about using a scope for this sort of thing: "This is exactly the same as defining a class method, and which you use is a matter of personal preference." I'd just use a plain old method here, just like you started out with (except defined as a class method on the model) -- it's more straightforward, it does what you expect, you don't need to learn about 'scopes', it's just a method. Using a scope to do this instead is kind of leftover from before Rails supported using an ordinary class method in this way, an ordinary class method is just fine.

Rails Model: includes method on self

I'm trying to optimise database queries so have been adding Model.includes(:related_model) where appropriate.
What is the appropriate way use this within methods inside my model? For example if I have a method in my model like:
def some_method
self.child_models.each do |child_model|
total_score += child_model.attribute
end
end
How do I use includes in instances like this? It seems natural to do it like this but it doesn't work:
def some_method
self.includes(:child_model).child_models.each do |child_model|
total_score += child_model.attribute
end
end
Most times when I produce an n+1 query it seems I'm referencing the model self but I can't seem to find any examples of this.
Thanks!
You are using self in an instance method so self is the instance of your class but includes is a class method. You need to use your original sample code to use includes Model.includes(:related_model). I think what you really want is:
def some_method
self.child_models.sum('attribute')
end
I would use includes when I am building conditions in a relation not looking at the children of an instance.

Ruby / Rails: create a class method that operates on instances of its children?

In my app, Photo has_and_belong_to_many :land_uses
I have this helper method in the Photo model:
def land_use_list
land_uses.map(&:name).join(', ')
end
This strikes me as a code smell (demeter), but I haven't been able to figure out how to move it to the LandUse model. What I'd like to do is something like:
class LandUse < ActiveRecord::Base
...
def self.list
self.map(&:name).join(', ')
end
...
end
So that instead of calling photo.land_use_list I could call photo.land_uses.list.
But that doesn't work, because it gets called against the class instead of being called against the scoped instances belonging to a particular photo.
Is there a way to do what I'm thinking of? And, more generally, how do you approach issues like this in your app? Is moving the list code to the LandUse model the right approach, or would you recommend something different?
First, I don't think this is violating the Law of Demeter per se. You have a method on an object that calls one method on an attribute to create a temporary variable, and then act on the temporary variable.
It would be a violation of the Law of Demeter, if you were doing this from a different class entirely. eg
class User
def names_of_lands_ive_known
photos.map(:land_uses).map(:name).join ', '
end
end
As it is, it's just good information hiding. But, if you wanted to be able to write photo.land_uses.names, you could add an extension to the association to do what you want.
class Photo
has_and_belong_to_many :land_uses do
def names_as_list_string
all.map(:name).join ', '
end
end
end
For more information on association extensions, check out the docs.
The best way to conform to the law of demeter is to do more or less what you are doing though, because by adding your method on Photo, it means that the methods that interact with Photo, don't also need to know about the LandUse class, just that photo has a method that returns a string of the names of land uses.
I am not in front of a rails app but I believe
photo.land_uses
with return an Array of LandUse objects
So you just need to move your map down to that array like:
photo.land_uses.map(&:name).join(', ')
which is what you had originally - just in your other model. I think you may be right and it means that Photo knows too much about LandUse therefore I would move it out.
You can use :
class LandUse
def self.list_for_photo(id)
LandUse.find_by_photo_id(id).join(', ')
end
def to_s
self.name
end
end
Hope it helps !

Resources