Creating a variable number of new ActiveRecord records at once Rails - ruby-on-rails

I have a concept called Transactions. Each transaction can consist of a specific number_of_stocks- say 10.
What I'm now looking at doing, is creating a new record (Share) for each of that number_of_stocks.
Example: If #transaction.number_of_stocks = 10 then we should create 10 new records of Share.
The code below works if I just create one at a time, but I haven't found a way to create a variable number of new records depending on the #transaction.number_of_stocks variable.
In addition, and to make things trickier - each Share has an attribute share_number. What I would like to do is that for each new Share record created, the share_number should increment, but it should also check if there are any existing share_numbers belonging to the cap-table already, and "start there".
Just for context, I'll probably wrap this functionality into a TransactionsHelper somewhere so I can use it elsewhere.
#number_of_share_numbers_to_create = #transaction.number_of_stocks # Example: 10
Share.create( #TODO: Now we need to create 10 records here
owner_id: params[:buying_shareholder_id],
captable_id: #transaction.captable.id,
company_id: #transaction.company.id,
share_number: #TODO: Increment based on the latest share_number linked to this captable and then for each new record in this transaction
)
The model
class Share < ApplicationRecord
belongs_to :captable
belongs_to :company
end

You'll want to look at the times method on Integer. Something, perhaps, like:
#transaction.number_of_stocks.times do |i|
Share.create( #TODO: Now we need to create 10 records here
owner_id: params[:buying_shareholder_id],
captable_id: #transaction.captable.id,
company_id: #transaction.company.id,
share_number: #TODO: Increment based on the latest share_number linked to this captable and then for each new record in this transaction
)
end
If number_of_stocks is really a decimal and not an integer, as you state in your comment above, then you'll need to fiddle in order to make times work and to manage fractional shares. But, that's a different issue.
As an aside, will you always be transacting only stocks? What about other asset forms (e.g., bonds), derivatives (e.g., options), or other transactable assets/instruments? If at some point you will be transacting other assets/instruments, you might want to think about that now. Just a thought.

I solved the share_number piece with the following
A quick method to get the latest share_number
def get_latest_share_number_from_captable(captable)
#captable = Captable.find(captable.id)
#shares = #captable.shares
#share_numbers = []
#shares.each do |s|
#share_numbers.push(s.share_number)
end
#latest_share = #share_numbers.max
return #latest_share
end
Then I use that method and the .times solution suggested in order to create new records.
# Add share numbering for this transaction
#number_of_share_numbers_to_create = #transaction.number_of_stocks.to_i
# Get last sharenumber in captable
#latest_share = get_latest_share_number_from_captable(#transaction.captable)
#number_of_share_numbers_to_create.times do |i|
Share.create(
owner_id: params[:buying_shareholder_id],
captable_id: #transaction.captable.id,
company_id: #transaction.company.id,
share_number: #latest_share += 1 #Increment for each new record
)
end

Related

Updating Lots of Records at Once in Rails

I've got a background job that I run about 5,000 of them every 10 minutes. Each job makes a request to an external API and then either adds new or updates existing records in my database. Each API request returns around 100 items, so every 10 minutes I am making 50,000 CREATE or UPDATE sql queries.
The way I handle this now is, each API item returned has a unique ID. I search my database for a post that has this id, and if it exists, it updates the model. If it doesn't exist, it creates a new one.
Imagine the api response looks like this:
[
{
external_id: '123',
text: 'blah blah',
count: 450
},
{
external_id: 'abc',
text: 'something else',
count: 393
}
]
which is set to the variable collection
Then I run this code in my parent model:
class ParentModel < ApplicationRecord
def update
collection.each do |attrs|
child = ChildModel.find_or_initialize_by(external_id: attrs[:external_id], parent_model_id: self.id)
child.assign_attributes attrs
child.save if child.changed?
end
end
end
Each of these individual calls is extremely quick, but when I am doing 50,000 in a short period of time it really adds up and can slow things down.
I'm wondering if there's a more efficient way I can handle this, I was thinking of doing something instead like:
class ParentModel < ApplicationRecord
def update
eager_loaded_children = ChildModel.where(parent_model_id: self.id).limit(100)
collection.each do |attrs|
cached_child = eager_loaded_children.select {|child| child.external_id == attrs[:external_id] }.first
if cached_child
cached_child.update_attributes attrs
else
ChildModel.create attrs
end
end
end
end
Essentially I would be saving the lookups and instead doing a bigger query up front (this is also quite fast) but making a tradeoff in memory. But this doesn't seem like it would be that much time, maybe slightly speeding up the lookup part, but I'd still have to do 100 updates and creates.
Is there some kind of way I can do batch updates that I'm not thinking of? Anything else obvious that could make this go faster, or reduce the amount of queries I am doing?
You can do something like this:
collection2 = collection.map { |c| [c[:external_id], c.except(:external_id)]}.to_h
def update
ChildModel.where(external_id: collection2.keys).each |cm| do
ext_id = cm.external_id
cm.assign_attributes collection2[ext_id]
cm.save if cm.changed?
collection2.delete(ext_id)
end
if collection2.present?
new_ids = collection2.keys
new = collection.select { |c| new_ids.include? c[:external_id] }
ChildModel.create(new)
end
end
Better because
fetches all required records all at once
creates all new records at once
You can use update_columns if you don't need callbacks/validations
Only drawback, more ruby code manipulation which I think is a good tradeoff for db queries..

How can I build out child associations on my object in Rails without saving the parent?

I'm trying to initialize an object, but I don't want to create the object to the database until the user has click saved. I've gotten this to work with the parent object only, but I can't seem to get my child objects to associate to the parent because I don't have an id yet for the parent object. What am I doing wrong in the code below? Thanks.
def new
# Build a new report object with the default type of "draft" and assign it to the player
report_options = { player_id: #player.id,
author_id: current_user.id,
position: #player.position,
type: "draft",
submitted: false }
#report = Report.new(report_options)
#skills.where('disabled = (?)',FALSE).each do |skill|
# On the line below I get an evaluation record with a proper skill id,
# but report_id is `nil` because I'm guessing the report hasn't been
# created yet. Is there a way to reserve an id for it without actually
# committing the record to the database until the user saves the record?
#report.evaluations.build(skill_id: skill.id)
end
end
The model of evaluations is not quite clear to me, but basically if you do something like
#report.evaluations.build
#skills.where('disabled = (?)', FALSE).each do |skill|
#report.evaluations << Evaluation.new(skill_id: skill.id)
end
it will add objects to evaluations without saving and won't require the report.id to exist at the moment of adding.

Infinite loop during Rails model method

The Use Case
If users haven't filled their box with products up to their credit limit (6 by default), a method is called on the box model which fills it for them.
Code guide
The number of credits in the box is given by box_credits, which loops through all products in the box and returns the total value of them. This seems to work.
The boolean method box_filled? checks if the box_credits method is equal to or greater than the number of credits available (the subscription credits).
The fill_once method should add products to the box until the box is filled (box_filled? returns true). This will happen when box_credits equals the number of credits available.
The Code
def fill_once
unless self.box_filled?
# Get a random product from the user's recommendations
product = self.subscription.user.recommended_product_records[rand(self.subscription.user.recommended_product_records.length - 1)]
# Make sure the product hasn't already been included in the box
unless self.added_product_ids.include? product.id
# If fresh, add the product to the box, size-dependently
unless product.sample_price_credits.nil?
product.add_to_box_credits(self.subscription, "sample")
else
unless product.full_price_credits.nil?
product.add_to_box_credits(self.subscription, "full")
end
end
self.save!
end
self.fill_once # Here's the recursion
end
end
The box_filled? method looks like this:
def box_filled?
subscription = self.subscription
if self.box_credits >= subscription.credits
return true
else
return false
end
end
box_credits are determined by this method:
def box_credits
count = 0
unless self.added_product_hashes.nil?
# Takes product hashes in the form {id, size, method}
self.added_product_hashes.each do |product_hash|
# Add credits to the count accordingly
if product_hash["method"] == "credits"
# Depending on the product size, add the corresponding amount of credits
if product_hash["size"] == "sample"
# Get the credit cost for a product sample
cost = Product.find(product_hash["id"].to_i).sample_price_credits
count += cost
elsif product_hash["size"] == "full"
# Get the credit cost for a full product
cost = Product.find(product_hash["id"].to_i).full_price_credits
count += cost
else
next
end
else
next
end
end
end
return count
end
The Problem
fill_once runs forever: it seems to ignore the unless self.box_filled? conditional.
Attempted solutions
I tried removing the recursive call to fill_once from the fill_once method, and split it into an until loop (until box_filled? ... fill_once ...), but no joy.
Update
Multiple identical products are being added, too. I believe the issue is that the updated record isn't being operated on – only the original instance. E.g. unless self.added_product_ids.include? product.id checks against the original box instance, not the updated record, sees no products in the added_product_ids, and chucks in every product it finds.
Solution
OK, this is solved. As suspected, the updated record wasn't being passed into the iterator. Here's how I solved it:
# Add one random user recommended product to the box
def fill_once(box=self)
unless box.box_filled?
# Get a random product from the user's recommendations
product = box.subscription.user.recommended_product_records[rand(box.subscription.user.recommended_product_records.length - 1)]
# Make sure the product hasn't already been included in the box
unless box.added_product_ids.include? product.id
# If fresh, add the product to the box, size-dependently
unless product.sample_price_credits.nil?
box = product.add_to_box_credits(box.subscription, "sample")
else
unless product.full_price_credits.nil?
box = product.add_to_box_credits(box.subscription, "full")
end
end
end
fill_once(box)
end
end
Using Ruby's default arguments with a default of self, but the option to use the updated record instead, allows me to pass the record through the flow as many times as needed.
unless self.added_product_ids.include? product.id means no duplicate product will be add to box. So if all products recommend is add to box but the total credits is less than box_credits , may cause infinite loop. I'm not sure, but it could be the reason.
You could add
puts "Box credits #{self.box_credits} vs. credits: #{self.subscription.credits} "
before
self.fill_once # Here's the recursion
to see if this happens.
Solution
OK, this is solved. As suspected, the updated record wasn't being passed into the iterator. Here's how I solved it:
# Add one random user recommended product to the box
def fill_once(box=self)
unless box.box_filled?
# Get a random product from the user's recommendations
product = box.subscription.user.recommended_product_records[rand(box.subscription.user.recommended_product_records.length - 1)]
# Make sure the product hasn't already been included in the box
unless box.added_product_ids.include? product.id
# If fresh, add the product to the box, size-dependently
unless product.sample_price_credits.nil?
box = product.add_to_box_credits(box.subscription, "sample")
else
unless product.full_price_credits.nil?
box = product.add_to_box_credits(box.subscription, "full")
end
end
end
fill_once(box)
end
end
Using Ruby's default arguments with a default of self, but the option to use the updated record instead, allows me to pass the record through the flow as many times as needed.

Rails model optimization

I have a List model below, it has a has_and_belongs_to_many association with recipients. The purpose of the method make_recipient_lists is to save a parsed csv of numbers(initial parameter) in this format [[num1],[num2],[num3]...].
add_recipients work by finding existing recipients then adding them to the list or creating new recipients.
This whole process works well for small amount, 20k of numbers in 28minutes. However, the greater the number, the longer it takes exponentially, 70k took 14hours. Probably because it was checking for duplicates to a cached current_lists.
Question is, is there any way to make this faster? I am probably approaching this problem wrong. Thanks!
class List < ActiveRecord::Base
#other methods above
def make_recipient_lists(numbers,options)
rejected_numbers = []
account = self.user.account
#caching recipients
current_recipients = self.recipients
numbers.each do |num|
add_recipient(num[0], current_recipients)
end
end
def add_recipient(num, current_recipients)
account = self.user.account
recipient = current_recipients.where(number:num, account_id: account.id).first
recipient ||= current_recipients.create!(number:num, account_id: account.id)
recipient
end
end
You could do something like this. I have not tested this, but you get the idea.
def make_recipient_lists(numbers, options)
rejected_numbers = []
account = self.user.account
existing_numbers = self.recipients.where(number: numbers, account_id: account.id).map(&:number)
new_records = (numbers - existing_numbers).map {|n| {number: n, account_id: account.id, list_id: self.id} }
Recipient.create new_records
end
I think, you should use rails active_record query interface. you can use method find_or_create method for this: It will make your queries faster. change your method like this, and check the time difference:
def make_recipient_lists(numbers,options)
rejected_numbers = []
account = self.user.account
#caching recipients
current_recipients = self.recipients
numbers.each do |num|
self.recipients.find_or_create_by(number: num, account_id: account.id)
end
end
Hope it will help. Thanks.

In Rails, what is the best way to update a record or create a new one if it doesn't exist?

I have a create statement for some models, but it’s creating a record within a join table regardless of whether the record already exists.
Here is what my code looks like:
#user = User.find(current_user)
#event = Event.find(params[:id])
for interest in #event.interests
#user.choices.create(:interest => interest, :score => 4)
end
The problem is that it creates records no matter what. I would like it to create a record only if no record already exists; if a record does exist, I would like it to take the attribute of the found record and add or subtract 1.
I’ve been looking around have seen something called find_or_create_by. What does this do when it finds a record? I would like it to take the current :score attribute and add 1.
Is it possible to find or create by id? I’m not sure what attribute I would find by, since the model I’m looking at is a join model which only has id foreign keys and the score attribute.
I tried
#user.choices.find_or_create_by_user(:user => #user.id, :interest => interest, :score => 4)
but got
undefined method find_by_user
What should I do?
my_class = ClassName.find_or_initialize_by_name(name)
my_class.update_attributes({
:street_address => self.street_address,
:city_name => self.city_name,
:zip_code => self.zip_code
})
Assuming that the Choice model has a user_id (to associate with a user) and an interest_id (to associate with an interest), something like this should do the trick:
#user = User.find(current_user)
#event = Event.find(params[:id])
#event.interests.each do |interest|
choice = #user.choices.find_or_initialize_by_interest_id(interest.id) do |c|
c.score = 0 # Or whatever you want the initial value to be - 1
end
choice.score += 1
choice.save!
end
Some notes:
You don't need to include the user_id column in the find_or_*_by_*, as you've already instructed Rails to only fetch choices belonging to #user.
I'm using find_or_initialize_by_*, which is essentially the same as find_or_create_by_*, with the one key difference being that initialize doesn't actually create the record. This would be similar to Model.new as opposed to Model.create.
The block that sets c.score = 0 is only executed if the record does not exist.
choice.score += 1 will update the score value for the record, regardless if it exists or not. Hence, the default score c.score = 0 should be the initial value minus one.
Finally, choice.save! will either update the record (if it already existed) or create the initiated record (if it didn't).
find_or_create_by_user_id sounds better
Also, in Rails 3 you can do:
#user.choices.where(:user => #user.id, :interest => interest, :score => 4).first_or_create
If you're using rails 4 I don't think it creates the finder methods like it used to, so find_or_create_by_user isn't created for you. Instead you'd do it like this:
#user = User.find(current_user)
#event = Event.find(params[:id])
for interest in #event.interests
#user.choices.find_or_create_by(:interest => interest) do |c|
c.score ||= 0
c.score += 1
end
end
In Rails 4
You can use find_or_create_by to get an object(if not exist,it will create), then use update to save or update the record, the update method will persist record if it is not exist, otherwise update record.
For example
#edu = current_user.member_edu_basics.find_or_create_by(params.require(:member).permit(:school))
if #edu.update(params.require(:member).permit(:school, :majoy, :started, :ended))

Resources