Rails - incrementing integers from db with concurrency - ruby-on-rails

In our database, we have a table for comments and blogs.
There's a field comments.comment_blog_index that increments for each comment in the blog.
...so if we have 3 comments for a particular blog, the value for comment_blog_index for each comment is: 1, 2, 3 (respectively)
The code that sets comment_blog_index looks like this:
#comment = Comment.new
#comment.comment_blog_index = #blog.comments.count + 1
The problem happens when two users trigger this code simultaneously. It will calculate the same value for both users, and the comment_blog_index is duplicated.
I've seen code for Item.increment_counter( :total_bids, item.id ), but that requires you to already have a record in the database in a table that stores a summation. In our case, the record is being created inside the `commments`` table.
How do we prevent this?

As you've already seen your current method is not safe. You could add validations and callbacks and all manner of things to try and make it safe but I would suggest that kind of work should happen at the database level.
Either implement a propert auto-incrementing field or offload that work to a gem.

It will help
Generate an auto increment field in rails
It shows how auto increment can be implemented in rails

Related

Rails - how to save existing record and apply update to a copy?

I have a webpage that tracks budgets containing a LOT of variables, stored in 40+ columns. Over time, adjustments are made to these budgets, but I need to be able to track changes over time and year to year. I tried adding a private method to my model that should create a duplicate of the existing record triggered by a :before_update callback. However, it's not working. The update changes the existing record, and the original is not preserved at all.
Model:
class Budget < ActiveRecord::Base
before_update :copy_budget
private
def copy_budget
#budget = Budget.find(params[:id])
#budget.dup
#budget.save
end
end
I'm still learning rails, (this is in Rails 4) and I think this would have been the best way to do this. If not, is there a better way to set the form to ALWAYS post a new record instead of routing to update if a record already exists?
Currently the form_for line looks like this:
<%= form_for(#budget) do |f| %>
Everything works as it should, with the exception of the duplication not happening. What am I missing? Is it possible the .dup function is also duplicating the :id? This is assigned by auto-increment in the MySQL db I an using, so if .dup is copying EVERYTHING, is there a way to copy all of the data except the :id into a new record?
Thanks in advance for any suggestions.
the dup method returns the new object without an id, it doesn't update it in place. Since your copy_budget method is already an instance method on Budget, you also would not need to (and you wouldn't even be able to, since params aren't accessible in models) look up the budget by id and instead could just use the current instance (self). So the following changed would fix the copy_budget method for you, but you are still copying an already modified object, just before it gets saved to the database
def copy_budget
copy_of_budget = self.dup
copy_of_budget.save
end
it would work the way you're expecting it to work. However, you aren't linking the copy in anyway to the current version of the Budget (no way to tell Budget id = 1 is an older version of Budget id = 2). I'd recommend taking a look at a gem such as PaperTrail (I'm sure there are lots of others if that one doesn't suit your needs) which has already thought through a lot of the problems and features with keeping a history of record changed.

Add auto increment with scope to existing column in migration-file rails

I have posts and organisations in my database. Posts belongs_to organisation and organisation has_many posts.
I have an existing post_id column in my post table which I by now increment manually when I create a new post.
How can I add auto increment to that column scoped to the organisation_id?
Currently I use mysql as my database, but I plan to switch to PostgreSQL, so the solution should work for both if possible :)
Thanks a lot!
#richard-huxton has the correct answer and is thread safe.
Use a transaction block and use SELECT FOR UPDATE inside that transaction block. Here is my rails implementation. Use 'transaction' on a ruby class to start a transaction block. Use 'lock' on the row you want to lock, essentially blocking all other concurrent access to that row, which is what you want for ensuring unique sequence number.
class OrderFactory
def self.create_with_seq(order_attributes)
order_attributes.symbolize_keys!
raise "merchant_id required" unless order_attributes.has_key?(:merchant_id)
merchant_id = order_attributes[:merchant_id]
SequentialNumber.transaction do
seq = SequentialNumber.lock.where(merchant_id: merchant_id, type: 'SequentialNumberOrder').first
seq.number += 1
seq.save!
order_attributes[:sb_order_seq] = seq.number
Order.create(order_attributes)
end
end
end
We run sidekiq for background jobs, so I tested this method by creating 1000 background jobs to create orders using 8 workers with 8 threads each. Without the lock or the transaction block, duplicate sequence number occur as expected. With the lock and the transaction block, all sequence numbers appear to be unique.
OK - I'll be blunt. I can't see the value in this. If you really want it though, this is what you'll have to do.
Firstly, create a table org_max_post (org_id, post_id). Populate it when you add a new organisation (I'd use a database trigger).
Then, when adding a new post you will need to:
BEGIN a transaction
SELECT FOR UPDATE that organisation's row to lock it
Increment the post_id by one, update the row.
Use that value to create your post.
COMMIT the transaction to complete your updates and release locks.
You want all of this to happen within a single transaction of course, and with a lock on the relevant row in org_max_post. You want to make sure that a new post_id gets allocated to one and only one post and also that if the post fails to commit that you don't waste post_id's.
If you want to get clever and reduce the SQL in your application code you can do one of:
Wrap the hole lot above in a custom insert_post() function.
Insert via a view that lacks the post_id and provides it via a rule/trigger.
Add a trigger that overwrites whatever is provided in the post_id column with a correctly updated value.
Deleting a post obviously doesn't affect your org_max_post table, so won't break your numbering.
Prevent any updates to the posts at the database level with a trigger. Check for any changes in the OLD vs NEW post_id and throw an exception if there is one.
Then delete your existing redundant id column in your posts table and use (org_id,post_id) as your primary key. If you're going to this trouble you might as well use it as your pkey.
Oh - and post_num or post_index is probably better than post_id since it's not an identifier.
I've no idea how much of this will play nicely with rails I'm afraid - the last time I looked at it, the database handling was ridiculously primitive.
Its good to know how to implement it. I would prefer to use a gem myself.
https://github.com/austinylin/sequential (based on sequenced)
https://github.com/djreimer/sequenced
https://github.com/felipediesel/auto_increment
First, I must say this is not a good practice, but I will only focus on a solution for your problem:
You can always get the organisation's posts count by doing on your PostsController:
def create
post = Post.new(...)
...
post.post_id = Organization.find(organization_id).posts.count + 1
post.save
...
end
You should not alter the database yourself. Let ActiveRecord take care of it.

Rails 3 - modifying an attribute of other model after model.save

Unfortunately I'm probably still too much a Rails beginner, so, even though I thought about and tried different approaches, I didn't get to work what I want and now have to ask for help again.
I have a REST comment vote mechanism with thumbs up and down for each comment. That works fine, each handled with counter_cache to count. Now, based on these thumbs up and down votes, I want to calculate a plusminus value for each comment, thumbs_up-votes - thumbs_down-votes. Although I'm not sure if it's the most efficient way to deal with that, I am planning to have the plus-minus value as an extra integer attribute of the comment model (whereas the thumbs up and down are own models). So, what I basically want is, that when a thumbs_up is saved, the comment's plusminus attr automatically should be += 1, and respectively for the thumbs_down.save a -= 1.
How can I issue such an action from within the thumbs_up controller? Do I need to modify my form_for or is my approach completely wrong?
Is there an after_save callback to deal with an attribute of a different model?
From what you've given, it's hard to tell. But I'd say that if you need to show a comment's "thumbs up" and "thumbs down" independently, store them as fields for your Comment model. Then, just making a helper method in your Comment model to get a comment's rating:
def rating
thumbs_up - thumbs_down
end
Edit:
With your new comment, I'd still say make a helper method rather than a field.
#models/comment.rb
def rating
thumbs_up.all.length - thumbs_down.all.length #or whatever way you want to do this
end
if you don't want to mix two different models with helper methods that don't actually belong to neither of those models, you can use Observers http://api.rubyonrails.org/classes/ActiveRecord/Observer.html
your observer will watch one model and do something

Scaffolding user ID resetting

in the application i am currently creating in ruby on rails. I am trying to do some tests in rails console where i have to destroy data in the database and the database is connected to a server. I am importing an XML and parsing it and putting it into a database with scaffolding.
Now what i need: Basically what i am attempting to do is to destroy the data and replace it with a new one every week..but the problem i am getting, the userid is gone up to 700+ and there are only 50 records :S cause it doesnt reset...
To delete all records i am currently using "whatever.destroy_all" does the trick
Any help?
Btw i am using SQLITE
The ID column created in the table usually is set as unique and to increment by 1 for each new record, which is why each time you destroy and add new data the ID keeps getting higher.
The fact that the ID # is getting larger and larger is not an issue at all.
If you really want to start back at zero, I would think you could drop the table and recreate it, but that seems like overkill for a trivial issue.
Regarding the connection to the other scaffold, how are you connecting the two and what do they both represent?
Ideally the data population for testing should be done through fixtures (or easy tools such as factorygirl etc..)
The main advantage of having a fix data set is you can run your tests in any environment. But as per your requirement you can do something like this,
When you populate the date through the active records pass the id parameter as well
Ex: User.new(:id => 1, :name => "sameera").create
By this way you can have constant id's But make sure you increment the id accordingly.

Modifying Database IDs from Rails Console?

I have a small database and have been adding entries through a Rails page. I "destroyed" one of the entries and now my sequence of IDs are skipping by one. For example, I now have 42 then 44, instead of the obvious: 42, 43, 44.
I was wondering if there was a way to edit the ID number of a new object through the console. I have tried:
record.id = 43
record.save
and
record = record.new
record.attributes = { :id => 43 }
but both didn't work. I'm fairly certain there has to be a console method for this, but I can't seem to find much specific on Google and I probably read the Rails API incorrectly... Would I possibly have to do this through direct SQL in sqlite?
Thanks
The best way to do it is to execute the SQL directly, and solve this temporal glitch in the sequence.
Try accessing the console (ruby script/console) and type:
>> sql = "update records set id=43 where id=44"
>> ActiveRecord::Base.connection.execute(sql)
Where 44 is the newly created object's id, and 43 is the one you were missing in your table
Good luck!
Actually, you can set the id manually for new objects:
record = Record.new
record.id = 1234
record.save
However, what you're trying to do is update the id of an existing object. When you set record.id = 43 and then call save what happens is that ActiveRecord will try to generate SQL like this:
update records set id = 43 where id = 43
Notice that the id it's looking for to update is the same as the one you're trying to change. That's why it doesn't work.
So yes, you would have to use SQL to change this. Whether or not that's a good idea is another issue, but sometimes it needs to be done.
Would I possibly have to do this through direct SQL in sqlite?
Yes.
The whole point of ActiveRecord is that is abstracts DB functions and just returns collections of data. You shouldn't be worrying about the ID of a record, that is something specific to the DB. Off the top of my head I can't think of any reasons to reference the model's ID.
If your app depends on having a sequenced number then you should add another field to the model which has this. For instance, if I have a store with products (a Product model) and I give the ID number the DB provides to other vendors. Two weeks later, my boss asks me to have a unique, but similar ID for two variations of products: "45a" and "45b". Nuts. The ID field should only be used by to the database and ActiveRecord, not you or your users, to identify the record.
There is a small chance that there might be an obscure method which force sets the ID if the DB allows it, but it is obscure for a reason. Don't try and find it :)
All that being said, type ruby script/dbconsole to quickly pull up the sqlite interface without having to type your password.
Also, if you delete the sqlite database that will reset the counter and start at 0. With great power comes great responsibility.
EDIT
If I remember correctly Dave Thomas wrote about this somewhere. Perhaps here?

Resources