I have method in my rails model which returns anonymous class:
def today_earnings
Class.new do
def initialize(user)
#user = user
end
def all
#user.store_credits.where(created_at: Time.current.beginning_of_day..Time.current.end_of_day)
end
def unused
all.map { |el| el.amount - el.amount_used }.instance_eval do
def to_f
reduce(:+)
end
end
end
def used
all.map(&:amount_used).instance_eval do
def to_f
reduce(:+)
end
end
end
end.new(self)
end
I want to achieve possibility to chain result in that way user.today_earning.unused.to_f and I have some problems with instance eval because when I call to_f on result it's undefined method, I guess it is due to ruby copying returned value so the instance gets changed, is it true? And if I'm correct how can I change the code to make it work. Also I'm wondering if making new model can be better solution than anomyous class thus I need advice if anonymous class is elegant in that case and if so how can I add to_f method to returned values
Yes, Anonymous class makes the code much complex. I would suggest a seperate class. It will solve 2 problems here.
defining some anonymous class again and again when we call the today_earnings method.
Readability of the code.
Now coming to actual question, you can try something similar to hash_with_indifferent_access. The code looks as follows.
class NumericArray < Array
def to_f
reduce(:+)
end
end
Array.class_eval do
def with_numeric_operations
NumericArray.new(self)
end
end
Usage will be:
Class Earnings
def initialize(user)
#user = user
end
def all
#user.store_credits.where(created_at: Time.current.beginning_of_day..Time.current.end_of_day)
end
def unused
all.map { |el| el.amount - el.amount_used }.with_numeric_operations
end
def used
all.map(&:amount_used).with_numeric_operations
end
end
This looks like a "clever" but ridiculously over-complicated way to do something that can be simply and efficiently done in the database.
User.joins(:store_credits)
.select(
'users.*',
'SUM(store_credits.amount_used) AS amount_used',
'SUM(store_credits.amount) - amount_used AS unused',
)
.where(store_credits: { created_at: Time.current.beginning_of_day..Time.current.end_of_day })
.group(:id)
Related
So I got introduced to using PORO instead of AR object for abstraction and size reduction.
But I have so many AR tables didn't make sense to put so much time to build a PORO class for each and every. Would take like a hour or two!! So instead I spent many hours thinking about how can I make this simpler.
And this is what I ended up making:
class BasePORO
def initialize(obj, immutable = true)
self.class::ATTRIBUTES.each do |attr|
instance_variable_set("##{attr}".to_sym, obj.attributes[attr.to_s])
instance_eval("undef #{attr}=") if immutable
end
end
end
class UserPORO < BasePORO
# or plug your own attributes
ATTRIBUTES = User.new.attributes.keys.map(&:to_sym).freeze
attr_accessor(*ATTRIBUTES)
end
But I can't somehow move the attr_accessor into the Base class or even ATTRIBUTES when not given explicitly. Not sure if its possible even.
Can I somehow move attr_accessor and default ATTRIBUTES into the main BasePORO class?
Any pointers or feedback is welcome.
As suggested in the comments, OpenStruct can do most of the heavy lifting for you. One thing to note is that if you don't freeze it, then after it's initialization you'll be able to add more attributes to it throughout its lifetime, e.g.:
struct = OpenStruct.new(name: "Joe", age: 20)
struct.email = "joe#example.com" # this works
p struct.email # => "joe#example.com"
(so essentially it works like a Hash with object-like interface)
This behavior may be undesired. And if you do freeze the struct, it won't allow any more attributes definition, but then you'd also lose the ability to override existing values (which I think you want to do in cases when someone sets immutable to false).
For the immutable flag to work as I understand you to expect it, I'd create a class that uses OpenStruct under its hood, for example like this:
class BasePORO
def initialize(obj, immutable = true)
#immutable = immutable
#data = OpenStruct.new(obj.attributes)
obj.attributes.keys.each do |attr|
self.class.define_method(attr.to_sym) do
#data.send(attr.to_sym)
end
self.class.define_method("#{attr}=".to_sym) do |new_value|
if #immutable
raise StandardError.new("#{self} is immutable")
else
#data.send("#{attr}=".to_sym, new_value)
end
end
end
end
end
class UserPORO < BasePORO
end
BTW, if you insisted on having a solution similar to the one shown in the question, then you could achieve this with something like that:
class BasePORO
def initialize(obj, immutable = true)
#immutable = immutable
attributes.each do |attr|
instance_variable_set("##{attr}".to_sym, obj.attributes[attr.to_s])
self.class.define_method(attr.to_sym) do
instance_variable_get("##{attr}".to_sym)
end
self.class.define_method("#{attr}=".to_sym) do |new_value|
if #immutable
raise StandardError.new("#{self} is immutable")
else
instance_variable_set("##{attr}".to_sym, new_value)
end
end
end
end
private
# default attributes
def attributes
[:id]
end
end
class UserPORO < BasePORO
private
# overriding default attributes from BasePORO
def attributes
User.new.attributes.keys.map(&:to_sym).freeze
end
end
So this is what actually ended up with:
class BaseStruct < OpenStruct
def initialize(model, immutable: true, only: [], includes: [])
if only.empty?
hash = model.attributes
else
hash = model.attributes.slice(*only.map!(&:to_s))
end
includes.each do |i|
relation = model.public_send(i)
if relation.respond_to?(:each)
hash[i.to_s] = relation.map{|r| OpenStruct.new(r.attributes).freeze}
else
hash[i.to_s] = OpenStruct.new(relation.attributes).freeze
end
end
super(hash)
self.freeze if immutable
end
end
Feel free to critique or suggest improvements.
Situation
I have a model User:
def User
has_many :cars
def cars_count
cars.count
end
def as_json options = {}
super options.merge(methods: [:cars_count])
end
end
Problem
When I need to render to json a collection of users, I end up being exposed to the N+1 query problem. It is my understanding that including cars doesn't solve the problem for me.
Attempted Fix
What I would like to do is add a method to User:
def User
...
def self.as_json options = {}
cars_counts = Car.group(:user_id).count
self.map do |user|
user.define_singleton_method(:cars_count) do
cars_counts[user.id]
end
user.as_json options
end
end
end
That way all cars counts would be queried in a single query.
Remaining Issue
ActiveRecord::Relation already has a as_json method and therefore doesn't pick the class defined one. How can I make ActiveRecord::Relation use the as_json method from the class when it is defined? Is there a better way to do this?
Edits
1. Caching
I can cache my cars_count method:
def cars_count
Rails.cache.fetch("#{cache_key}/cars_count") do
cars.count
end
end
This is nice once the cache is warm, but if a lot of users are updated at the same time, it can cause request timeouts because a lot of queries have to be updated in a single request.
2. Dedicated method
Instead of calling my method as_json, I can call it my_dedicated_as_json_method and each time I need to render a collection of users, instead of
render json: users
write
render json: users.my_dedicated_as_json_method
However, I don't like this way of doing. I may forget to call this method somewhere, someone else might forget to call it, and I'm losing clarity of the code. Monkey patching seems a better route for these reasons.
Have you considered using a counter_cache for cars_count? It's a good fit for what you're wanting to do.
This blog article also offers up some other alternatives, e.g. if you want to manually build a hash.
If you really wanted to continue down the monkey patching route, then ensure that you are patching ActiveRecord::Relation rather than User, and override the instance method rather than creating a class method. Note that this will then affect every ActiveRecord::Relation, but you can use #klass to add a condition that only runs your logic for User
# Just an illustrative example - don't actually monkey patch this way
# use `ActiveSupport::Concern` instead and include the extension
class ActiveRecord::Relation
def as_json(options = nil)
puts #klass
end
end
Option 1
In your user model:
def get_cars_count
self.cars.count
end
And in your controller:
User.all.as_json(method: :get_cars_count)
Option 2
You can create a method which will get all the users and their car count. And then you can call the as_json method on that.
It would roughly look like:
#In Users Model:
def self.users_with_cars
User.left_outer_joins(:cars).group(users: {:id, :name}).select('users.id, users.name, COUNT(cars.id) as cars_count')
# OR may be something like this
User.all(:joins => :cars, :select => "users.*, count(cars.id) as cars_count", :group => "users.id")
end
And in your controller you can call as_json:
User.users_with_cars.as_json
Here is my solution in case someone else is interested.
# config/application.rb
config.autoload_paths += %W(#{config.root}/lib)
# config/initializers/core_extensions.rb
require 'core_extensions/active_record/relation/serialization'
ActiveRecord::Relation.include CoreExtensions::ActiveRecord::Relation::Serialization
# lib/core_extensions/active_record/relation/serialization.rb
require 'active_support/concern'
module CoreExtensions
module ActiveRecord
module Relation
module Serialization
extend ActiveSupport::Concern
included do
old_as_json = instance_method(:as_json)
define_method(:as_json) do |options = {}|
if #klass.respond_to? :collection_as_json
scoping do
#klass.collection_as_json options
end
else
old_as_json.bind(self).(options)
end
end
end
end
end
end
end
# app/models/user.rb
def User
...
def self.collection_as_json options = {}
cars_counts = Car.group(:user_id).count
self.map do |user|
user.define_singleton_method(:cars_count) do
cars_counts[user.id]
end
user.as_json options
end
end
end
Thanks #gwcodes for pointing me at ActiveSupport::Concern.
I am rather new to Rails, and would greatly appreciate any bit of help. I have created the following method:
def name_fix
name = self.split
mod_name = []
name.each do |n|
n.split("")
if n[0]
n.upcase
else
n.downcase
end
mod_name.push(n)
end
mod_name.join
end
I would like to use this method in my Controller as such:
def create
#patient = Patient.new(params[:patient])
#patient.name = params[:params][:name].name_fix
if #patient.save
redirect_to patients_path
else
render :new
end
end
How can I go about accomplishing this? Will this method reside within my Model or Controller? Previously, I've run into an undefined method error.
Note: I'm sure that there is a way to better write my code. I am grateful for help with that as well.
#app/models/patient.rb
class Patient < ActiveRecord::Base
protected
def name=(value)
mod_name = []
value.split.each do |n|
n.split("")
type = n[0] ? "up" : "down"
n.send("#{type}case")
mod_name.push(n)
end
#name = mod_name.join
end
end
#app/controllers/patients_controller.rb
class PatientsController < ApplicationController
def create
#patient = Patient.new patient_params
#patient.save ? redirect_to(patients_path) : render(:new)
end
private
def patient_params
params.require(:patient).permit(:name)
end
end
What you're doing is trying to override the setter method, which can be done using the above code. Much more efficient and out of the way.
I have created the following method
Since you're new, let me explain something else.
It is important to note where you're using this method.
You've currently put it in the model, which means you'll have to call it to manipulate some attribute / functionality of any object created with said model.
--
Models - in Rails - build the objects which populate your app. Ruby is an object orientated language, which means that every element of your program should revolve around data objects in some degree.
As you can see above, the method of building objects in your system is really about invoking classes. These classes contain methods which can be called, either at class level (IE invoking the class through the method), or at instance level (IE calling a method on an already invoked object).
This is where you get "class" methods (Model.method) and "instance" methods (#model.method) from:
#app/models/patient.rb
class Patient < ActiveRecord::Base
def explode
#this is an instance method
puts "Instance Explode"
end
def self.explode
#this is a class method
puts "Exploded"
end
end
Thus you can call the following:
#patient = Patient.find params[:id]
#patient.explode #-> "Instance explode"
Patient.explode #-> "Exploded"
--
This is important because it gives you a strict framework of where you should, and shouldn't use methods in your models.
It explains why you have controllers & helpers, and allows you to formulate the best way to structure your application as to get the most out of the least code.
For example...
Your use of #patient.name = params[:params][:name].name_fix is incorrect.
It's wrong because you're calling the instance method .name_fix on a piece of data totally unrelated to your model. If you wanted to use .name_fix in a general sense like this, you'd probably use a helper:
#app/helpers/patients_helper.rb
class PatientsHelper
def name_fix value
# stuff here
end
end
#app/controllers/patients_controller.rb
class PatientsController < ApplicationController
def create
#patient.name = name_fix params[:patient][:name]
end
end
Since you're using the method to populate the .name attribute of your model, it makes sense to override the name= setter. This will not only provide added functionality, but is much smoother and efficient than any other way.
Methods that are called directly are best put in the Controller (or in ApplicationController if you think more than one controller might want to use it).
These are methods like
# app/controllers/my_controller.rb
def foo(bar)
# do something here
end
def create
id = params[:id]
value = foo(id)
end
If you want a chained method that acts as a property method of whatever you're calling it on. Those are characteristic of how Models work - you have your main model and you call attributes or methods on the instance of that model.
# app/models/my_model.rb
def full_name
first_name + " " + last_name
end
# app/controller/my_controller.rb
def create
id = params[:id]
model = MyModel.find(id)
full_name = model.full_name
end
In your case, you want to call name_fix ON whatever is returned by params[:params][:name], which is (I'm guessing) a String.
You have two options
Modify the String class to define a method named name_fix. I highly recommend against this. It's call "monkeypatching" and shouldn't be done without good reason. Just letting you know you can do it in some cases.
Use a direct method in your controller or ApplicationController like the first example above.
#patient.name = name_fix(params[:params][:name])
Edit: As for your request about a better way to write your code... that's difficult to teach or convey in one answer. I'd say read some open source projects out there to see how people write Ruby and some common idioms used to clean up the code. To get you started, here's how I'd re-write your code
def create
#patient = Patient.new(params[:patient])
# 1. Be descriptive with your method names. `name_fix` is vague
# 2. Why is `:name` nested under another `[:params]` hash?
#patient.name = capitalize_name(params[:name])
if #patient.save
# 1. I think `patient_path` has to be singular
# 2. It needs a `Patient` object to know how to construct the URL
# e.g. `/patients/:id`
redirect_to patient_path(#patient)
else
render :new
end
end
def capitalize_name(full_name)
# Example: julio jones
#
# 1. `split` produces an array => ["julio", "jones"]
# 2. `map` applies a function (`capitalize`) to each element
# => ["Julio", "Jones"]
# 3. `join(" ")` rejoins it => "Julio Jones"
full_name.split.map(&:capitalize).join(" ")
end
Assuming your goal with the name_fix method is just to capitalize the first letter of each name, you could just pass name as an argument and store it as a private method on the Controller:
# app/controllers/patient_controller.rb
private
def name_fix(name)
name.split.map(&:capitalize).join(" ")
end
Then you could do
#patient.name = name_fix(params[:params][:name])
in the create method.
OR, you could store this method in the model:
# app/models/patient.rb
def self.name_fix(name)
name.split.map(&:capitalize).join(" ")
end
Then you could do this instead, in the controller:
#patient.name = Patient.name_fix(params[:params][:name])
I would also suggest renaming your name_fix method to something like capitalize_name.
update your create method as below
def create
#patient = Patient.new(params[:patient])
#patient.name = params[:params][:name]
#patient = #patient.name_fix
if #patient.save
redirect_to patients_path
else
render :new
end
end
It should work.
I'm not 100% sure about why ActiveModel::Dirty has its name. I'm guessing it is because it is considered as dirty to use it.
But in some cases, it is not possible to avoid watching on specific fields.
Ex:
if self.name_changed?
self.slug = self.name.parameterize
end
Without ActiveModel::Dirty, the code would look like:
if old_name != self.name
self.slug = self.name.parameterize
end
which implies having stored old_name before, and it is not readable, so IMHO, it is dirtier than using ActiveModel::Dirty. It becomes even worse if old_number is a number and equals params[:user]['old_number'] as it needs to be correctly formated (parsed as int), whereas ActiveRecord does this automagically.
So I would find clean to define watchable fields at Model level:
class User < ActiveRecord::Base
include ActiveModel::Dirty
watchable_fields :name
before_save :generate_slug, if: name_changed?
def generate_slug
self.slug = self.name.parameterize
end
end
Or (even better?) at controller level, before assigning new values:
def update
#user = current_user
#user.watch_fields(:name)
#user.assign_attributes(params[:user])
#user.generate_slug if #user.name_changed?
#user.save # etc.
end
The good thing here is that it removes the memory overload produced by using ActiveModel::Dirty.
So my question is:
Can I do that using ActiveRecord pre-built tools, or should I write a custom library to this?
Thanks
If ActiveModel::Dirty solves your problem, feel free to use it. The name comes from the term "dirty objects" and is not meant to imply that it's a dirty/hackish module.
See this answer for more details on dirty objects: What is meant by the term "dirty object"?
Here's what I have ended up doing. I like it:
class User < ActiveRecord::Base
attr_accessor :watched
def field_watch(field_name)
self.watched ||= {}
self.watched[field_name] = self.send(field_name)
return self
end
def field_changed?(field_name)
self.send(field_name) != self.watched(field_name)
end
end
And in the controller
def update
#user = current_user.field_watch(:name)
#user.assign_attributes(params[:user])
#user.generate_slug if #user.field_changed?(:name)
#user.save
end
I'll report here if I take the time to wrap this code in a gem or something.
I have a table with data that needs to be updated at run-time by additional data from an external service. What I'd like to do is something like this:
MyModel.some_custom_scope.some_other_scope.enhance_with_external_data.each do |object|
puts object.some_attribute_from_external_data_source
end
Even if I can't use this exact syntax, I would like the end result to respect any scopes I may use. I've tried this:
def self.enhance_with_external_data
external_data = get_external_data
Enumerator.new do |yielder|
# mimick some stuff I saw in ActiveRecord and don't quite understand:
relation.to_a.each do |obj|
update_obj_with_external_data(obj)
yielder.yield(obj)
end
end
end
This mostly works, except it doesn't respect any previous scopes that were applied, so if I do this:
MyModel.some_custom_scope.some_other_scope.enhance_with_external_data
It gives back ALL MyModels, not just the ones scoped by some_custom_scope and some_other_scope.
Hopefully what I'm trying to do makes sense. Anyone know how to do it, or am I trying to put a square peg in a round hole?
I figured out a way to do this. Kind of ugly, but seems to work:
def self.merge_with_extra_info
the_scope = scoped
class << the_scope
alias :base_to_a :to_a
def to_a
MyModel.enhance(base_to_a)
end
end
the_scope
end
def self.enhance(items)
items.each do |item|
item = add_extra_item_info(item)
end
items
end
What this does is add a class method to my model - which for reasons unknown to me seems to also make it available to ActiveRecord::Relation instances. It overrides, just for the current scope object, the to_a method that gets called to get the records. That lets me add extra info to each record before returning. So now I get all the chainability and everything like:
MyModel.where(:some_attribute => some_value).merge_with_extra_info.limit(10).all
I'd have liked to be able to get at it as it enumerates versus after it's put into an array like here, but couldn't figure out how to get that deep into AR/Arel.
I achieved something similar to this by extending the relation:
class EnhancedModel < DelegateClass(Model)
def initialize(model, extra_data)
super(model)
#extra_data = extra_data
end
def use_extra_data
#extra_data.whatever
end
end
module EnhanceResults
def to_a
extra_data = get_data_from_external_source(...)
super.to_a.map do |model_obj|
EnhancedModel.new(model_obj, extra_data)
end
end
end
models = Model.where('condition')
models.extend(EnhanceResults)
models.each do |enhanced_model|
enhanced_model.use_extra_data
end