I am trying to enforce uniqueness with unique index in my Rails project. And I found something confusing.
Like registering a user at my site, you need to provide an email address and a nickname, both need to be unique. I add unique index to both email and nickname. And when duplications come, I rescue exception ActiveRecord::RecordNotUnique, now here's the question, how could I know which field cause the exception?
Thanks a lot for the help.
IMHO you should also add uniqueness validators to your model. That allows you to use Rails' validations and error messages.
# add to model
validates :email, uniqueness: true
validates :nickname, uniqueness: true
Note that to ensure uniqueness on a database level a unique index is still needed.
In ActiveRecord, you can use the valid? method to run validations on your object. If it returns false, then you can view the error messages to determine which field(s) raised an error.
Example:
user = User.new(email: 'unique_email#google.com', nickname: 'non_unique_nickname')
user.valid? #This will run validations
# => false
user.errors.messages
# => {:nickname=>["has already been taken"]}
Related
I have a model, with some customised devise keys, like so:
:authentication_keys => [:username],
:reset_password_keys => [:username],
:strip_whitespace_keys => [:username]
I have also overriden the following method so that Devise doesn't expect a unique email address:
def will_save_change_to_email?
false
end
The problem is that I'm not receiving an ActiveRecord 'has already been taken' error until all other expectations are met, at which point I receive ActiveRecord::RecordNotUnique as expected.
Can anybody please help?
Thank you.
Edit: adding uniqueness: true ensures the error message is returned instantly, but I suspect I shouldn't have to add this.
So, I realised that when editing the authentication key for Devise, you do have to specify this on the model.
Seeing as the field already existed on my model, I skimmed over the below line from the Devise Guide and missed the :uniq at the very end.
rails generate migration add_username_to_users username:string:uniq
I have a User model with an email attribute. Various parts of my app conceive of an "email" differently; sometimes as a string, sometimes as a hash ({ token: 'foo', host: 'bar.com' }), sometimes as an object. This is bad; I want the concept of an email to be consistent wherever I use it.
So, I use an Email object that does what I want. I don't see any good reason to create an Email table; instead, I just want to create a new Email object corresponding to an email string whenever I need one. Therefore User looks like this:
class User < ActiveRecord::Base
def email
Email.new(read_attribute :email)
end
def email= email
write_attribute :email, email.to_s
end
end
However, this causes at least two issues:
I can't search for a user by email without an explicit call to to_s.
I can't run a uniqueness validation on the email column anymore. I get a TypeError: can't cast Email to string. (I can fix this with a custom validator.)
Questions:
Is there something wrong with this approach? The fact that it breaks my validation is a code smell to me.
Is there some way to get the existing validates :email, uniqueness: { case_sensitive: false } validation to work with these new accessor definitions?
I have a Company model and an Employer model. Employer belongs_to :company and Company has_many :employers. Within my Employer model I have the following validation:
validates :company_id, inclusion: {in: Company.pluck(:id).prepend(nil)}
I'm running into a problem where the above validation fails. Here is an example setup in a controller action that will cause the validation to fail:
company = Company.new(company_params)
# company_params contains nested attributes for employers
company.employers.each do |employer|
employer.password = SecureRandom.hex
end
company.employers.first.role = 'Admin' if client.employers.count == 1
company.save!
admin = company.employers.where(role: 'Admin').order(created_at: :asc).last
admin.update(some_attr: 'some_val')
On the last line in the example code snippet, admin.update will fail because the validation is checking to see if company_id is included in the list, which it is not, since the list was generated before company was saved.
Obviously there are ways around this such as grabbing the value of company.id and then using it to define admin later, but that seems like a roundabout solution. What I'd like to know is if there is a better way to solve this problem.
Update
Apparently the possible workaround I suggested doesn't even work.
new_company = Company.find(company.id)
admin = new_company.employers.where(role: 'Admin').order(created_at: :asc).last
admin.update
# Fails validation as before
I'm not sure I understand your question completely, but there is an issue in this part of the code:
validates :company_id, inclusion: {in: Company.pluck(:id).prepend(nil)}
The validation is configured on the class-level, so it won't work well with updates on that model (won't be re-evaluated on subsequent validations).
The docs state that you can use a block for inclusion in, so you could try to do that as well:
validates :company_id, inclusion: {in: ->() { Company.pluck(:id).prepend(nil) }}
Some people would recommend that you not even do this validation, but instead, have a database constraint on that column.
I believe you are misusing the inclusion validator here. If you want to validate that an associated model exists, instead of its id column having a value, you can do this in two ways. In ActivRecord, you can use a presence validator.
validates :company, presence: true
You should also use a foreign key constraint on the database level. This prevents a model from being saved if there is no corresponding record in the associated table.
add_foreign_key :employers, :companies
If it gets past ActiveRecord, the database will throw an error if there is no company record with the given company_id.
I am trying to require unique email addresses for a record in my RoR project, I have the following validation in place:
validates :email, presence: { :message => "You must provide an email address." }, uniqueness: { :message => "This email is already taken." }
validates_uniqueness_of :email, :message=>"This email is already taken"
Next to the form to add a record is a list of recently added entries. When I try to save a new record it renders the form over with an error however if you look at the list the item (with the duplicate email address) has been added. If you refresh the page or change the email in the form and resubmit the entry disappears from the list. I'm curious why it seems like the record is being saved even though the validation is firing properly.
I had thought it could be that I was creating the object with object.create(params) however when I changed that to object.build(params) it had no effect. All help is appreciated, thanks!
This is not a problem with your validation, but with the way how you render that list.
If you add your object to the list even if the validation was not successful than that element will be rendered exactly like all other elements in that list (since it has all nessessary values).
You can use the following methods to exclude such elements from a list or to handle them in a different way - grey out for example:
record.valid? # returns true if the record is valid
record.persisted? # returns true if the record exists in the database (was save)
whereas:
record.new_record? # returns true if the record wasn't saved into the database
By the way: You mix up the new and the old hash syntax in your validator definitions and the uniqueness validator is defined twice. You can change that to:
validates :email, presence: { message: 'You must provide an email address.' },
uniqueness: { message: 'This email is already taken.' }
create method creates an object and saves it to the database;
build method(Alias for: new) won't "create" a record in database, just create a new object in memory.
Validations are used to ensure that only valid data is saved into your database, validations are run before the record are sent to the database. Only Some methods will trigger validations.
create
create!
save
save!
update
update!
I've got a User model with three fields, :email, :display_name and :handle. Handle is created behind the scenes from the :display_name.
I'm using the following validations:
validates :display_name, :presence => :true, :uniqueness => { :message => "Sorry, another user has already chosen that name."}, :on => :update
validates :email, :presence => :true, :uniqueness => { :message => "An account with that email already exists." }
I use the handle as the to_param in the model. If the user fails the validation by submitting a :display_name that already exists, then tries to change it and resubmit the form, Rails seems to use the new handle as the validation for the email -- in other words, it assumes that the email doesn't belong to the current user and validation on the email then fails. At this point, Rails assumes that the changed display name/handle is the one to use for the look up and the update action can't complete at all, because it can't find the user based on the new handle.
Here's the update method:
def update
#user = User.find_by_handle(params[:id])
#handle = params[:user][:display_name]
#user.handle = #handle.parameterize
...
end
This problem doesn't happen when the validation first fails on a duplicate email, so I'm assuming it's something about the way I've written the update method -- maybe I should try setting the handle in the model?
maybe I should try setting the handle in the model?
^ This.
The controller isn't the place to do something like this. If it's model logic that's happening behind the scenes, beyond the user's control, why put it in controller code?
Do it instead in a before_save filter, which is guaranteed to run only after the chosen display name is determined to be available and the record is deemed valid. In this way the handle won't be changed on the cached record until it is actually committed to the db, eliminating the problem of the incorrectly generated URL.
before_save :generate_handle
...
def generate_handle
self.handle = display_name.parameterize
end