One of the models in a Rails 3.1 application I'm working on has a "code" attribute that is generated automatically when the record is created and that must be unique. The application should check the database to see if the generated code exists and, if it does, it should generate a new code and repeat the process.
I can ensure the field's uniqueness at the database level with add_index :credits, :code, :unique => true (which I am doing) and also in the model with validates_uniqueness_of, but both of these will simply return an error if the generated code exists. I need to just try again in the case of a duplicate. The generated codes are sufficiently long that duplicates are unlikely but I need to be 100% certain.
This code generation is handled transparently to the end user and so they should never see an error. Once the code is generated, what's the best way to check if it exists and to repeat the process until a unique value is found?
Here's a quick example, there is still technically a race condition here, though unless your seeing hundreds or thousands of creates per second it really shouldnt be a worry, worst case is your user gets a uniquness error if two creates are run in such a way that they both execute the find and return nil with the same Url
class Credit < ActiveRecord::Base
before_validation :create_code, :if => 'self.new_record?'
validates :code, :uniqueness => true
def create_code
self.code = code_generator
self.code = code_generator until Credit.find_by_code(code).nil?
end
end
If you absolutely needed to remove the race condition case where two creates are running in tandem and both trigger the find with the same code and return nil you could wrap the find with a table lock which requires DB specific SQL, or you could create a table that has a row used for locking on via pessimistic locking, but I wouldn't go that far unless your expecting hundreds of creates per second and you absolutely require that the user never ever sees an error, it's doable, just kind of overkill in most cases.
I am not sure if there is a built in way. I have always used a before_create.
Here is an example in the context of a UrlShortener.
class UrlShortener < Activerecord::Base
before_create :create_short_url
def create_short_url
self.short_url = RandomString.generate(6)
until UrlShortener.find_by_short_url(self.short_url).nil?
self.short_url = RandomString.generate(6)
end
end
end
Related
I have two tables, Users and Responses, where there are many responses per user. I'm looking to do some computation on the set of responses for each user. Something like the code below:
all_users = User.all
all_rs = Responses.all
all_users.map { |u| all_rs.where(user: u).count }
As written this will make a call to the database for each user. Is there a way to pre-cache the all_rs data, so that each subsequent where is done in memory?
The logic above could probably be easily written in sql, but imagine that the code in the map block contained a lot more work.
What you need is counter cache (see section 4.1.2.3 of Rails Guide).
To enable counter cache, first, add or change your migration file:
db/migrate/add_counter_cache_to_users.rb
class AddCounterCacheToUsers < ActiveRecord::Migration
def change
# Counter cache field
add_column :users, :responses_count, :integer, null: false, default: 0
# Optionally add index to the column if you want to `order by` it.
add_index :users, :responses_cache
end
end
Then modify your model classes
app/models/response.rb
class Response < ActiveRecord::Base
belongs_to :user, counter_cache: true
end
Run rake db:migrate. From now on, whenever a Response is created, the value of users.responses_count column will automatically increment by 1, and whenever a Response is destroyed, that column's value will decrement by 1.
If you want the count of someone's responses, just call responses_count on that user.
user = User.first
user.responses_count #=> the responses count of the user
# or
user.responses.size
Your original requirement can be fulfilled with
User.select(:responses_count).to_a
UPDATE
I can't think how can I have missed such a bloody easy solution
Response.group(:user_id).count
You could do something like
responses_by_user = Response.all.group_by(&:user_ud)
(assuming that Response has a user_id attribute)
You could then do responses_by_user[user.id] to get the responses for a user without any further queries. Do be careful of the overhead of creating all these extra ActiveRecord objects. As you hint, the very specific example you give can be handled by an sql group/count, which would probably be a lot faster.
something like this maybe ? assuming users has_many responses
all_users = User.includes(:responses)
user_responses_count = all_users.map { |u| u.responses.count }
this will not query for responses 1000 times, in cases you have 1000 users. Let me know if this works for your use-case.
Note that this query level caching. No model changes are required..
I'm writing a web app using Rails, part of which includes giving users the ability to leave reviews for things. I wanted to put a validation in the review model to ensure that one user can't leave multiple reviews of the same item, so I wrote this:
class NoDuplicateReviewValidator < ActiveModel::Validator
def validate(record)
dup_reviews = Review.where({user_id: record.user,
work_id: record.work})
unless dup_reviews.length < 1
record.errors[:duplicate] << "No duplicate reviews!"
end
end
end
This validator has the desired behavior, i.e. it guarantees that a user can't review a work twice. However, it has the undesired side-effect that a user can't update an already existing review that he/she left. I'm using a really simple
def update
#review.update(review_params)
respond_with(#work)
end
in the reviews controller. How can I change either the validator or the update method so that duplicate reviews are prevented but updates are allowed?
I'm very new to Rails and web development, so I'm sure I've done something goofy here. I didn't use one of the built-in unique validators because what is unique is the user/work pair; there can more than one review by the same user of different works, and there can be more than one review of the same work by different users.
You can use validates_uniqueness_of on multiple attributes, like this:
validates_uniqueness_of :user_id, :scope => :work_id
Then a user would not be allowed to review a already reviewed work.
#Sharvy Ahmed's answer is definitely the best, as long as the case is simple enough – the OP's case seems like one of them.
However, if the conditions are more complex, you may need/want to write your custom validation. For that purpose, here's an example (checked with Rails 6.0).
class NoDuplicateReviewValidator < ActiveModel::Validator
def validate(record)
dup_reviews = Review.where(user_id: record.user,
work_id: record.work)
dup_reviews = dup_reviews.where.not(id: record.id) unless record.new_record?
if dup_reviews.count > 0
record.errors[:duplicate] << "No duplicate reviews!"
end
end
end
The idea is,
In create, all the relevant DB records retrieved with where can and should be used to judge the uniqueness. In the example new_record? is used to check it out, but it is actually redundant (because nil id matches no records).
In update, the DB row of the record to update must be excluded from the unique comparison. Otherwise, the update would always fail in the validation.
The count method is slightly more efficient in terms of DB transaction.
I know that before_create is called before the object gets commuted to the database and after_create gets called after.
The only time when before_create will get called and after_create while not is if the object fails to meet data base constants (unique key, etc.). Other that that I can place all the logic from after_create in before_create
Am I missing something?
In order to understand these two callbacks, firstly you need to know when these two are invoked. Below is the ActiveRecord callback ordering:
(-) save
(-) valid
(1) before_validation
(-) validate
(2) after_validation
(3) before_save
(4) before_create
(-) create
(5) after_create
(6) after_save
(7) after_commit
you can see that before_create is called after after_validation, to put it in simple context, this callback is called after your ActiveRecord has met validation. This before_create is normally used to set some extra attributes after validation.
now move on to after_create, you can see this is created after the record is stored persistently onto DB. People normally use this to do things like sending notification, logging.
And for the question, when should you use it? The answer is 'you should not use it at all'. ActiveRecord callbacks are anti-pattern and seasoned Rails developer consider it code-smell, you can achieve all of that by using Service object to wrap around. Here is one simple example:
class Car < ActiveRecord::Base
before_create :set_mileage_to_zero
after_create :send_quality_report_to_qa_team
end
can be rewritten in
# app/services/car_creation.rb
class CarCreation
attr_reader :car
def initialize(params = {})
#car = Car.new(params)
#car.mileage = 0
end
def create_car
if car.save
send_report_to_qa_team
end
end
private
def send_report_to_qa_team
end
end
If you have simple app, then callback is okay, but as your app grows, you will be scratching your head not sure what has set this or that attribute and testing will be very hard.
On second thought, I still think you should extensively use callback and experience the pain refactoring it then you'll learn to avoid it ;) goodluck
The before_create callback can be used to set attributes on the object before it is saved to the database. For example, generating a unique identifier for a record. Putting this in an after_create would require another database call.
before_create:
will be called before saving new object in db. When this method will return false it will prevent the creation by rolling back.
So when you need to do something like check something before saving which is not appropriate in validations you can use them in before_create.
For example: before creation of new Worker ask Master for permission.
before_create :notify_master
def notify_master
# notify_master via ipc and
# if response is true then return true and create this successfully
# else return false and rollback
end
Another use is as Trung Lê suggested you want to format some attribute before saving
like capitalizing name etc.
after_create:
Called after saving object in database for first time. Just when you don't want to interrupt creation and just take a note of creation or trigger something after creation this is useful.
for example: After creating new user with role mod we want to notify other mods
after_create :notify_mod, :is_mod?
def notify_mod
# send notification to all other mods
end
EDIT: for below comment
Q: What's the advantage of putting notify_mod in after_create instead of before_create?
A: Sometimes while saving the object in database it can rollback due to database side validations or due to other issues.
Now if you have written notify_mod in before create then it will be processed even if the creation is not done. No doubt it will rollback but it generates overhead. so it's time consuming
If you have placed it in after_create then notify_mod will only execute if the record is created successfully. Thus decreasing the overhead if the rollback takes places.
Another reason is that it's logical that notification must be sent after user is created not before.
I have some STI setup like this:
class Document < ActiveRecord::Base
attr_accessible :name, description
# Basic stuff omitted
end
class OriginalDocument < Document
has_many :linked_documents, foreign_key: :original_document_id, dependent: :destroy
end
class LinkedDocument < Document
belongs_to :original_document
# Delegation, because it has the same attributes, except the name
delegate :description, to: :original_document
end
Now I want to dup the LinkedDocument and store it as an OriginalDocument, with its own name and keep the attribute values on duplication. However, my approachs are failing because somewhere, the duplicate still wants to access its delegated methods in the after_* callbacks.
class LinkedDocument < Document
def unlink_from_parent
original = self.original_document
copy = self.becomes OriginalDocument
copy.original_document_id = nil
copy.description = original.description
copy.save
end
end
This throws a RuntimeError: LinkedDocument#description delegated to original_document.description, but original_document is nil.
Doing an additional copy.type = 'OriginalDocument' to enforce things won't work, since the save query involves the type; UPDATE documents SET [...] WHERE documents.type IN('OriginalDocument') [...]. This fails, because at the time of the transaction, the object still is of type LinkedDocument.
What would be a clean way to copy an object and let it become another one? I thought of calling update_column for type and every attribute I want to copy over, but before doing it that inelegant way, I wanted to ask here.
I am going to add my solution here, in case no one has a better one. Hopefully, it will help someone.
To let the object become another without having wrong queries because the where clause is checking for the wrong type, I manually updated the type column without invoking any callbacks before calling become.
# This is for rails3, where +update_column+ does not trigger
# validations or callbacks. For rails4, use
#
# self.update_columns {type: 'OriginalDocument'}
#
self.update_column :type, 'OriginalDocument'
document = self.becomes OriginalDocument
Now for the assignments, there were two problems: First, the attribute setters somehow may trigger an exception because of the delegations. Second, the attributes I wanted to mass-assign were not listed in e.g. attr_accessible intentionally because they were internal attributes. So I resorted to a loop with an ugly update_column statement producing way too much queries (since rails3 has no update_columns).
original.attributes.except('id', 'name', 'original_document_id').each do |k,v|
document.update_column k.to_sym, v
end
Considering the following
:company has_many :issues #ticket tracking example
I'd like to have routing such that any user in the company can go to /issues/:id (which is simple enough when using the default id column).
However, i'd like instead to have an issue id specific to the company (so each company would have their own issue 1, 2, 3 etc that isn't unique (and wouldn't use the internal id).
Is there any better way than calculating an id based on the last number in the db in the create and update actions in IssueController for that company id? (I can think of various issues here with race conditions when multiple records are being updated/created per company).
Thanks in advance!
I would take the issue_id handling down to the model level. You can use a before_validation callback to set the issue_id. This alone, even though the save call is wrapped in a transaction, won't prevent race conditions. You have to further ensure the uniqueness of the couple [ :company_id, :issue_id ] by adding a index/unique constraint to your issues table, as suggested by #PinnyM. So something like this
class Issue < ActiveRecord::Base
attr_accessible :company_id, :issue_id
belongs_to :company
before_validation :set_issue_id
private
def set_issue_id
self.issue_id = self.company.issues.size + 1
end
end
class Company < ActiveRecord::Base
has_many :issues
end
and inside a migration:
add_index :issues, [:issue_id, :company_id], :unique => true
And you could grab the correct issue in the controller like you said:
#issue = Issue.where(company_id: current_user.company.id, issue_id: params[:id])
Note that this doesn't provide a way to recover from a constraint violation exception in case that actually happened. See #PinnyM answer for a suggestion on how to handle this.
Hope this helps.
After misunderstanding the question the first time around, I'll take another shot at it.
You are looking for a way to have an issue 'counter' (to simulate an id) that is specific to a company_id. Although #deivid was on the right track, relying on issues.count will only work if you can guarantee that a company never makes more than one request at a time. This will generally be the case, but is not guaranteed and shouldn't be used alone as the core logic behind this counter.
You'll need to add a unique constraint/index to your issues table - this will ensure that counter can't be duplicated:
add_index :issues, [:issue_id, :company_id], :unique => true
Adding a :uniqueness constraint in the model will only mitigate the problem somewhat by making the window for the race condition smaller, but it can't guarantee uniqueness completely.
Note that in the event of a constraint violation in the DB, ActiveRecord can't recover from it within the transaction. You can try rescuing ActiveRecord::RecordNotUnique, recreating the issue (or just regenerating the issue_id using a fresh count) and saving again.