-
Notifications
You must be signed in to change notification settings - Fork 21.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
create_or_find_by doesn't attempt to find the record when you also have validates_uniqueness_of :field #36027
Comments
To me, this reads as expected behaviour. If there was an email regex validator and it failed the record, then I'd expect it to throw and not create or find a record, so why wouldn't other validators also throw? That said, the specific case is a little more complicated given that there is a uniqueness validator with a unique constraint. At the time the create is run, there is no guarantee that the database has a constraint that matches with the uniqueness validator. Even if the presence and validity of the constraint could be verified, I would still expect your scenario to throw. Say that Comment.where(email: 'test@example.com').create_or_find_by!(message: 'Hi There')
=> #<Comment id: 1, message: "Hi There", email: "test@example.com">
Comment.where(email: 'test@example.com').create_or_find_by!(message: 'Bye')
(0.1ms) begin transaction
Comment Exists? (0.2ms) SELECT 1 AS one FROM "comments" WHERE "comments"."email" = ? LIMIT ? [["email", "test@example.com"], ["LIMIT", 1]]
(0.1ms) rollback transaction
Traceback (most recent call last):
1: from (irb):1
ActiveRecord::RecordInvalid (Validation failed: Email has already been taken) So in my mind, the following all follow the same pattern: Comment.where(email: 'test@example.com').create_or_find_by!(message: 'Hi There')
=> #<Comment id: 1, message: "Hi There", email: "test@example.com">
Comment.where(email: 'test@example.com').create_or_find_by!(message: 'Hi There')
(0.1ms) begin transaction
Comment Exists? (0.2ms) SELECT 1 AS one FROM "comments" WHERE "comments"."email" = ? LIMIT ? [["email", "test@example.com"], ["LIMIT", 1]]
(0.1ms) rollback transaction
Traceback (most recent call last):
1: from (irb):1
ActiveRecord::RecordInvalid (Validation failed: Email has already been taken)
# and
Comment.create_or_find_by!(email: 'test@example.com') I don't think there's a reliable way to know the full context of the query, the layout of the tables and their constraints in order to selectively ignore validators. My suggestion would be to override |
note: the rails/activerecord/lib/active_record/relation.rb Lines 209 to 213 in 89b8664
so an easy work around for this problem is to remove the uniqueness validation. I'm of the same mind as @daveallie that this seems like expected behaviour and not treating this validation in any special way. |
This issue has been automatically marked as stale because it has not been commented on for at least three months. |
In my opinion, the |
|
@swiknaba Totally! To me, the whole point of activerecord validations is as a backup to the database constraints and to get richer error feedback. Of course there is also a performance benefit to validating in memory. IMO they should always work in tandem, not one or the other. We should never have to rely 100% on the application for ensuring database validity. |
Can this be reopened? the issue was closed because stale, no opinion from a Rails team member has been provided yet, and the issue is still there. |
Just ran into this myself. It's beyond confusing that Rails' own uniqueness validation would break a Rails method intended to help enforce uniqueness. +1 to reopen. |
I think there's a reasonable argument to be made that this is surprising, so reopening the issue. I'd love to see a PR with a proposed fix, or an update to the docs that draws attention to this. |
Yeah, I certainly don't think that would hurt. I think it would be worth clarifying the statement "Attempts to create a record with the given attributes in a table that has a unique constraint on one or several of its columns." to mean explicitly database constraints on columns as opposed to validations as well as more context on the intent behind why the method was added. I believe the intent of the method
I think running the uniqueness validations would get rid of that benefit and put you back to square one of |
Yep, I agree with that. Let's clarify the docs. |
Anybody on it? Otherwise I'll make a PR to update the docs |
Please do @intrip, that would be great! 👍 |
- Mention explicitly in the docs that `create_of_find_by` requires a DB constraint. Closes rails#36027 Co-authored-by: Eileen M. Uchitelle <eileencodes@users.noreply.github.com>
Was running into this today. This is still super confusing.
Obviously you want to class Model < ApplicationRecord
validates :name, uniqueness: true
end
record = Model.create_or_find_by(name: 'test')
record.id # 1
record2 = Model.create_or_find_by(name: 'test')
record2.id # nil
record2.errors # #<ActiveModel::Errors [#<ActiveModel::Error attribute=name, type=taken, options={:value=>"test"}>]> What's even worse is that I can unknowingly interact with the faulty record which will create a duplicate record2.update!(name: 'something else')
record2.id # 2 Can this be reopened and addressed properly either in docs or behavior? |
It's helpful that the docs now read that this method requires uniqueness constraints in the database. Should we also mention that it also seems to require we explicitly not use the active uniqueness validation? |
I think that this should be addressed differently than removing the uniqueness constraint from the model as there are many part of the rails ecosystem that rely on ActiveRecord validation. I just came across this issue and patched locally by writing my own create_or_find_by as follows. def self.create_or_find_by_with_validation!(attributes, &block)
transaction(requires_new: true) { create!(attributes, &block) }
rescue ActiveRecord::RecordInvalid => exception
if attributes
.map do |key, _value|
if column_names.include?(key.to_s)
key
else
reflections.fetch(key.to_s).foreign_key.to_sym
end
end
.map { |key| exception.record.errors.of_kind?(key, :taken) }
.any?
find_by!(attributes)
else
raise exception
end
rescue ActiveRecord::RecordNotUnique
find_by!(attributes)
end Another solution would be for ActiveRecord::RecordInvalid to be an ancestor of ActiveRecord::RecordNotUnique and raising the latter when a uniqueness constraint fails in validation. This class hierarchy would give us backward compatibility |
Steps to reproduce
Expected behavior
create_or_find_by
should find the existing record when there is a uniqueness validation on the field.Actual behavior
An
ActiveRecord::RecordInvalid
exception is raised because the record is validated before it is created, which checks the database and finds an existing record.create_or_find_by
doesn't get theActiveRecord::RecordNotUnique
exception it is expecting so it doesn't attempt tofind
the record.System configuration
Rails version: master
Ruby version: ruby 2.5.3p105
The text was updated successfully, but these errors were encountered: