I want to DRY up my Rails code by making a common method that will be reused. In order to do so, I have to make some field/attributes and the class name that is used in the code variables, so it can work for the three models (and their fields) with the same code. I tried to learn from this question and this one, but I haven't been able to get it to work.
In my model, I have this:
def self.update_percentages
update_percentages_2(User, "rank", "top_percent")
end
def self.update_percentages_2(klass, rank_field, percent_field)
rank_class = (klass.name).constantize
total_ranks = rank_class.maximum(rank_field)
top_5 = (total_ranks * 0.05).ceil
rank_class.find_each do |f|
if f.send("#{rank_field}") <= top_5
f.send("#{percent_field}", 5)
f.save
end
end
end
With this code, I get ArgumentError: wrong number of arguments (1 for 0). When I start commenting lines out to narrow down the problem, it seems that the f.send("#{percent_field}", 5) causes the error.
And if I add:
percent_field = (percent_field).constantize
I get: Name Error: wrong constant name top_percent.
Can someone please help me determine what I'm doing wrong?
If you want to assign to an attribute, you need the method name with the equal sign:
f.send("#{percent_field}=", 5)
Also, this:
rank_class = (klass.name).constantize
is equivalent to this:
rank_class = klass
I would rewrite your method to update all qualifying records in on transaction.
def self.update_percentages_2(klass, rank_field, percent_field)
top_5 = ( klass.maximum(rank_field) * 0.05).ceil
klass.where("#{rank_field} <= ?", top_5).update_all(percent_field => 5)
end
BTW
Here is an answer to your original question.
Related
Given this model:
class User < ActiveRecord::Base
has_many :things
end
Then we can do this::
#user = User.find(123)
#user.things.find_each{ |t| print t.name }
#user.thing_ids.each{ |id| print id }
There are a large number of #user.things and I want to iterate through only their ids in batches, like with find_each. Is there a handy way to do this?
The goal is to:
not load the entire thing_ids array into memory at once
still only load arrays of thing_ids, and not instantiate a Thing for each id
Rails 5 introduced in_batches method, which yields a relation and uses pluck(primary_key) internally. And we can make use of the where_values_hash method of the relation in order to retrieve already-plucked ids:
#user.things.in_batches { |batch_rel| p batch_rel.where_values_hash['id'] }
Note that in_batches has order and limit restrictions similar to find_each.
This approach is a bit hacky since it depends on the internal implementation of in_batches and will fail if in_batches stops plucking ids in the future. A non-hacky method would be batch_rel.pluck(:id), but this runs the same pluck query twice.
You can try something like below, the each slice will take 4 elements at a time and them you can loop around the 4
#user.thing_ids.each_slice(4) do |batch|
batch.each do |id|
puts id
end
end
It is, unfortunately, not a one-liner or helper that will allow you to do this, so instead:
limit = 1000
offset = 0
loop do
batch = #user.things.limit(limit).offset(offset).pluck(:id)
batch.each { |id| puts id }
break if batch.count < limit
offset += limit
end
UPDATE Final EDIT:
I have updated my answer after reviewing your updated question (not sure why you would downvote after I backed up my answer with source code to prove it...but I don't hold grudges :)
Here is my solution, tested and working, so you can accept this as the answer if it pleases you.
Below, I have extended ActiveRecord::Relation, overriding the find_in_batches method to accept one additional option, :relation. When set to true, it will return the activerecord relation to your block, so you can then use your desired method 'pluck' to get only the ids of the target query.
#put this file in your lib directory:
#active_record_extension.rb
module ARAExtension
extend ActiveSupport::Concern
def find_in_batches(options = {})
options.assert_valid_keys(:start, :batch_size, :relation)
relation = self
start = options[:start]
batch_size = options[:batch_size] || 1000
unless block_given?
return to_enum(:find_in_batches, options) do
total = start ? where(table[primary_key].gteq(start)).size : size
(total - 1).div(batch_size) + 1
end
end
if logger && (arel.orders.present? || arel.taken.present?)
logger.warn("Scoped order and limit are ignored, it's forced to be batch order and batch size")
end
relation = relation.reorder(batch_order).limit(batch_size)
records = start ? relation.where(table[primary_key].gteq(start)) : relation
records = records.to_a unless options[:relation]
while records.any?
records_size = records.size
primary_key_offset = records.last.id
raise "Primary key not included in the custom select clause" unless primary_key_offset
yield records
break if records_size < batch_size
records = relation.where(table[primary_key].gt(primary_key_offset))
records = records.to_a unless options[:relation]
end
end
end
ActiveRecord::Relation.send(:include, ARAExtension)
here is the initializer
#put this file in config/initializers directory:
#extensions.rb
require "active_record_extension"
Originally, this method forced a conversion of the relation to an array of activrecord objects and returned it to you. Now, I optionally allow you to return the query before the conversion to the array happens. Here is an example of how to use it:
#user.things.find_in_batches(:batch_size=>10, :relation=>true).each do |batch_query|
# do any kind of further querying/filtering/mapping that you want
# show that this is actually an activerecord relation, not an array of AR objects
puts batch_query.to_sql
# add more conditions to this query, this is just an example
batch_query = batch_query.where(:color=>"blue")
# pluck just the ids
puts batch_query.pluck(:id)
end
Ultimately, if you don't like any of the answers given on an SO post, you can roll-your-own solution. Consider only downvoting when an answer is either way off topic or not helpful in any way. We are all just trying to help. Downvoting an answer that has source code to prove it will only deter others from trying to help you.
Previous EDIT
In response to your comment (because my comment would not fit):
calling
thing_ids
internally uses
pluck
pluck internally uses
select_all
...which instantiates an activerecord Result
Previous 2nd EDIT:
This line of code within pluck returns an activerecord Result:
....
result = klass.connection.select_all(relation.arel, nil, bound_attributes)
...
I just stepped through the source code for you. Using select_all will save you some memory, but in the end, an activerecord Result was still created and mapped over even when you are using the pluck method.
I would use something like this:
User.things.find_each(batch_size: 1000).map(&:id)
This will give you an array of the ids.
I'm getting an error with the following example when running the program.
The error reads as such:
burger.rb:8:in `initialize': wrong number of arguments (1 for 0) (ArgumentError)
from burger.rb:19:in `new'
from burger.rb:19:in `<main>'
Any help is appreciated for this noob, the code is below:
$toppings = false
class Burger
AVAILABLE_TOPPINGS = ["lettuce", "tomato", "onion", "cheese"]
attr_reader :options
def initialize
#toppings = []
end
def order
print "how many burgers would you like? "
number = gets.chomp
puts "#{number} burgers coming right up boss!"
end
end
burger = Burger.new("lettuce")
burger.order
The error is telling you that the method initialize expects 0 argument, while you give it 1 ("lettuce" in Burger.new("lettuce")).
You need to make initialize expecting one argument:
def initialize(options)
#toppings = []
#options = options
end
As others have said your initializer is expecting no arguments but you're giving it lettuce. If you're using ruby 2.1 or later I would suggest using keyword arguments:
class Burger
TOPPINGS = %i[lettuce tomato onion cheese]
attr_reader :toppings
def initialize(toppings: [])
#toppings = TOPPINGS & toppings
end
end
This allows you do to Burger.new(toppings: [:lettuce]) which I feel is a lot more readable.
$toppings = false
is code smell. Globals are generally not necessary, and should only be used when you're absolutely sure they're needed. When you're first learning an OO language I think it's better to avoid them and learn about variable scoping.
In this case, you don't use it in your sample code, but you do use:
#toppings = []
(which is again not used elsewhere). It isn't a good idea to name a global variable the same as an instance variable because it's too easy to use one when you mean the other, and introduce a bug.
I have several calculated values as part of my risk.rb model
before_save :calculate_risk
def calculate_risk
self.risk1_total = self.component1 * self.component2 * self.component3
self.risk2_total = self.component4 * self.component5 * self.component6
...
end
I want to be able to create a risk without filling out the form completely thus each of those components would be nil. So this method creates an error because * is not a valid method for a nil. What is the best way to handle this? I have considered
def calculate_risk
if self.component1.nil? || self.component2.nil? || self.component3.nil?
self.risk1_total = self.component1 * self.component 2 * self.component3
elsif ...
end
However, this is obviously inefficient and repetitive. I also considered initializing all of these values, though I do not know the most efficient way of doing this.
You can do something like the following:
before_save :calculate_risk
def calculate_risk
self.risk1_total = [self.component1,self.component2,self.component3].compact.inject(:*)
self.risk2_total = [self.component4,self.component5,self.component6].compact.inject(:*)
...
end
This is assuming you want nil values to just be dropped from the calculation. This will give a result of nil if all values are nil. You could replace the nils with zeroes if you prefer. You may also be interested in the :reject method or other cools tools in the Ruby Array and Enumerable classes.
I hope that helps.
I am trying to find a record based on two associated attributes. The Record should be selected, if its association contains those two records.
So far, I tried following - Which seemed to me a very bad practice and I want to avoid using it.
#size = Spree::OptionValue.find(params[:size])
#color = Spree::OptionValue.find(params[:color])
vari = Spree::Variant.all
vari.each do |va|
if va.option_values.include?(#size && #color)
#variant = va
end
end
So far, I also tried
#variant = Spree::Variant.all(:include => :option_values, :conditions => ['option_value.id = ?', params[:color])
This seems to be the way to go, but I can't seem to figure out the right way to get the result.
The return error I keep on getting is following:
ActiveRecord::StatementInvalid: PG::Error: ERROR: missing FROM-clause entry for table "option_values"
LINE 1: ..._option_values_variants"."option_value_id" WHERE (option_val...
EDIT:
I got it working due to the great help given in the accepted answer:
Spree::Variant.joins(:option_values).where("spree_option_values.id in (?)", [size, color])
First off, your code is probably broken. I doubt that .include?(#size && #color) does what you think it does; you're effectively only checking if option_values includes #color. This is equivalent to doing (true && #color). If you want to include both values, you need .include?(#size) && .include?(#color).
So your code should probably look like this:
vari = Spree::Variant.all
vari.each do |va|
if va.option_values.include?(#size) && va.option_values.include?(#color)
#variant = va
end
end
Next, you can make your code much more Ruby-esque:
#variant = Spree::Variant.all.select do |v|
v.option_values.include?(#size) && v.option_values.include?(#color)
end
But it's far better to actually evaluate the condition at the database level rather than load the entire table into your application. You seem to be looking for all records where the associated OptionValues includes the two you've selected into #size and #color.
The query you're looking for probably looks something like this:
Spree::Variant.joins(:option_values).where("option_values.id in (?)", [#size, #color])
In a rails application, I have a number of attributes for a model called Record. I want to design a method that when called on an attribute, returns the name of the attribute (which is essentially a method on the Record object). This name is then passed to an Hash, which returns a number (for the sake of this example, say the number is a percentage which is then multiplied by the original attribute value to get a new value).
For example, say my Record has four attributes: teachers, students, principals, and parents. The method would then look like the following:
def name
**something here**
end
and the corresponding new_value method and PRECENTAGE hash would look like this:
def new_value
self * PERCENTAGE[self.name]
end
PERCENTAGE = {
"teachers" => 0.40,
"students" => 0.53,
"principals" => 0.21,
"parents" => 0.87
}
Then, to execute this whole thing, I would do Record.students.new_value, which would return new number of students according to the percentage obtained in the hash.
I know that to get the name of a method that is currently executing, you can do something like this: (found on http://ryat.la/7RDk)
def this_method
__method__
end
but that won't work for me, because I need the name of the previously executed method.
If you have any suggestions as to an alternative approach to accomplishing my goal, I'd be happy to try something else.
Ryan, I'm struggling to understand your question, but I think this is what you want, for record.teachers_percent, for example:
["teachers", "students", "principals", "parents"].each do |attrib|
Record.class_eval <<-RUBY
def #{attrib}_percent
#{attrib} * PERCENTAGE[#{attrib.inspect}]
end
RUBY
end
Although this is probably a cleaner solution, giving record.percent(:teachers) or record.percent("teachers"):
class Record
def percent(attrib)
self.send(attrib) * PERCENTAGE[attrib.to_s]
end
end