Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Add #create_or_find_by to lean on unique constraints#31989

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 ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Merged
dhh merged 7 commits intomasterfromcreate_or_find_by
Feb 15, 2018

Conversation

@dhh
Copy link
Member

@dhhdhh commentedFeb 13, 2018

Attempts to create a record with the given attributes in a table that has a unique constraint
on one or several of its columns. If a row already exists with one or several of these
unique constraints, the exception such an insertion would normally raise is caught,
and the existing record with those attributes is sought found using #find_by.

This is similar to #find_or_create_by, but avoids the problem of stale reads, as that methods needs
to first query the table, then attempt to insert a row if none is found. That leaves a timing gap
between the SELECT and the INSERT statements that can cause problems in high throughput applications.

There are several drawbacks to #create_or_find_by, though:

  • The underlying table must have the relevant columns defined with unique constraints.
  • A unique constraint violation may be triggered by only one, or at least less than all,
    of the given attributes. This means that the subsequent #find_by may fail to find a
    matching record, which will then return nil, rather than a record will the given attributes.
  • It relies on exception handling to handle control flow, which may be marginally slower. And

This method will always returns a record if all given attributes are covered by unique constraints,
but if creation was attempted and failed due to validation errors it won't be persisted, you get what
#create returns in such situation.

ecleel, guilleiguaran, amcaplan, satishkaushik, dillonwelch, josephpage, subintp, rainerborene, BenKanouse, liroyleshed, and 26 more reacted with thumbs up emojigleb-klimovich reacted with rocket emoji
@dhhdhh requested a review fromjeremyFebruary 13, 2018 23:47
@nynhex
Copy link

This is really good. I have multiple use cases for this. 👍

AnalyzePlatypus and grepsedawk reacted with thumbs up emoji

defcreate_or_find_by!(attributes, &block)
create!(attributes, &block)
rescueActiveRecord::RecordNotUnique
find_by(attributes)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Should raise in the fallbackfind case? It'd be surprising to ever get anil result (for whatever reason, including a missing unique index) from these methods.

Copy link
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Yeah, good point. Let's change that to find_by!.

Subscriber.create_or_find_by(nick:"bob",name:"the cat")
end
end

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

I think this is missing a test withcreate_with (to change the behavior of thecreate) and withwhere (to change the behavior offind!

I'm not sure if those method should be used withcreate_or_find_by but right now they can and I have no idea of the effect they would have, so better to test them to make sure it is what we expect.

Copy link
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

I don't see how that differs?create_or_find_by uses the same flow asfind_or_create_by, except instead of an|| we're using arescue. But the flow is the same.

Copy link
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

I mean, it's not that we can't test it, just that I don't think it teaches us anything interesting. The test would be identical to the one fortest_find_or_create_by_with_create_with.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

If it is the same behavior we want so I guess it is fine being tested in that test

# matching record, which will then return nil, rather than a record will the given attributes.
# * It relies on exception handling to handle control flow, which may be marginally slower. And
#
# This method will always returns a record if all given attributes are covered by unique constraints,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

This isn'ttotally true: another client could update or delete the row between the rejected INSERT and the subsequent SELECT. This race condition is complementary to the one in#find_or_create_by.

That caveat probably belongs in the "drawbacks" section, but even still I don't think we can make this claim here.

Copy link
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Good point. Will add that point. It's a much rarer race condition in many apps, I'd say.

# #create returns in such situation.
defcreate_or_find_by(attributes, &block)
create(attributes, &block)
rescueActiveRecord::RecordNotUnique
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

This needstransaction(requires_new: true) do around thecreate to work in an ongoing surrounding transaction (on at least PostgreSQL)

Copy link
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Why is this not necessary for find_or_create_by?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Because it doesn't cause an SQL error and then attempt to recover. PostgreSQL remembers when an error has occurred inside a transaction, and disallows all further operations until that transaction has been rolled back.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

If we are willing to require PG 9.5 or later for this (when using the PG adapter), we could just stickON CONFLICT DO NOTHING on the end instead. This would let us wrap the whole thing in a transaction instead of just the create, which eliminates any possibility of race conditions.

brendancarney and edgurgel reacted with thumbs up emoji
defcreate_or_find_by!(attributes, &block)
create!(attributes, &block)
rescueActiveRecord::RecordNotUnique
find_by!(attributes)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Do we want toretry here instead of raising if the find fails? Seems confusing forcreate_or_find_by! to raise RecordNotFound, just because of a racing delete.

Copy link
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

You can't retry this failure as it should only occur when you use attributes where not all of them are covered by unique constraints. It's more like a INVALID QUERY kind of failure.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Oh right, I was assuming the delete race, rather than operator error. I guess we could retry once, so we're safe from a poorly-timed simple delete. That way we'd only be tripped up by a racingdelete; insert; delete.

Copy link
MemberAuthor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

True. I'm not sure whether having just a single retry is a good warranty, though. Since it's hard for us to tell the difference between legit race condition and invalid query.

@dhh
Copy link
MemberAuthor

dhh commentedFeb 14, 2018 via email

TIL! Do you have an idea for a good test for that?
On Tue, Feb 13, 2018 at 4:49 PM, Matthew Draper ***@***.***> wrote: ***@***.**** commented on this pull request. ------------------------------ In activerecord/lib/active_record/relation.rb <#31989 (comment)>: > + # + # There are several drawbacks to #create_or_find_by, though: + # + # * The underlying table must have the relevant columns defined with unique constraints. + # * A unique constraint violation may be triggered by only one, or at least less than all, + # of the given attributes. This means that the subsequent #find_by may fail to find a + # matching record, which will then raise an `ActiveRecord::NotFound` exception, + # rather than a record will the given attributes. + # * It relies on exception handling to handle control flow, which may be marginally slower. And + # + # This method will always returns a record if all given attributes are covered by unique constraints, + # but if creation was attempted and failed due to validation errors it won't be persisted, you get what + # #create returns in such situation. + def create_or_find_by(attributes, &block) + create(attributes, &block) + rescue ActiveRecord::RecordNotUnique Because it doesn't cause an SQL error and then attempt to recover. PostgreSQL remembers when an error has occurred inside a transaction, and disallows all further operations until that transaction has been rolled back. — You are receiving this because you authored the thread. Reply to this email directly, view it on GitHub <#31989 (comment)>, or mute the thread <https://github.com/notifications/unsubscribe-auth/AAAKtbTMH-JrUyizELV10shNLxOgesnAks5tUi2lgaJpZM4SElV8> .

@matthewd
Copy link
Member

I think this should do it:

deftest_create_or_find_by_within_transactionassert_nilSubscriber.find_by(nick:"bob")subscriber=Subscriber.create!(nick:"bob")Subscriber.transactiondoassert_equalsubscriber,Subscriber.create_or_find_by(nick:"bob")assert_not_equalsubscriber,Subscriber.create_or_find_by(nick:"cat")endend

Copy link
Contributor

@tjschucktjschuck left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

Documentation edits.

# * The underlying table must have the relevant columns defined with unique constraints.
# * A unique constraint violation may be triggered by only one, or at least less than all,
# of the given attributes. This means that the subsequent #find_by may fail to find a
# matching record, which will then raise an `ActiveRecord::NotFound` exception,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

`ActiveRecord::NotFound` needs<tt> for Rdoc code formatting, not backticks.

# * A unique constraint violation may be triggered by only one, or at least less than all,
# of the given attributes. This means that the subsequent #find_by may fail to find a
# matching record, which will then raise an `ActiveRecord::NotFound` exception,
# rather than a record will the given attributes.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

will =>with

# Attempts to create a record with the given attributes in a table that has a unique constraint
# on one or several of its columns. If a row already exists with one or several of these
# unique constraints, the exception such an insertion would normally raise is caught,
# and the existing record with those attributes is sought found using #find_by.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

is sought found using #find_by — "sought" should be deleted.

# and the existing record with those attributes is sought found using #find_by.
#
# This is similar to #find_or_create_by, but avoids the problem of stale reads between the SELECT
# and the INSERT, as that methods needs to first query the table, then attempt to insert a row
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others.Learn more.

methods =>method

David Heinemeier Hansson added4 commitsFebruary 14, 2018 15:36
Otherwise PG will complain with "PG::InFailedSqlTransaction: ERROR:  current transaction is aborted, commands ignored until end of transaction block".Thanks@matthewd
@dhhdhh merged commitfe6adf4 intomasterFeb 15, 2018
@dhhdhh deleted the create_or_find_by branchFebruary 15, 2018 00:51
@AnalyzePlatypus
Copy link

Awesome!

@subhashsaran
Copy link

Good stuff.

@dhh
Copy link
MemberAuthor

dhh commentedFeb 21, 2018 via email

Happy to explore that as a PG 9.5 level-up somehow. But the whole feature shouldn’t depend on it imo.
On Feb 21, 2018, at 12:47, Sean Griffin ***@***.***> wrote:@sgrif commented on this pull request. In activerecord/lib/active_record/relation.rb: > + # + # There are several drawbacks to #create_or_find_by, though: + # + # * The underlying table must have the relevant columns defined with unique constraints. + # * A unique constraint violation may be triggered by only one, or at least less than all, + # of the given attributes. This means that the subsequent #find_by may fail to find a + # matching record, which will then raise an `ActiveRecord::NotFound` exception, + # rather than a record will the given attributes. + # * It relies on exception handling to handle control flow, which may be marginally slower. And + # + # This method will always returns a record if all given attributes are covered by unique constraints, + # but if creation was attempted and failed due to validation errors it won't be persisted, you get what + # #create returns in such situation. + def create_or_find_by(attributes, &block) + create(attributes, &block) + rescue ActiveRecord::RecordNotUnique If we are willing to require PG 9.5 or later for this (when using the PG adapter), we could just stick ON CONFLICT DO NOTHING on the end instead. This would let us wrap the whole thing in a transaction instead of just the create, which eliminates any possibility of race conditions. — You are receiving this because you modified the open/close state. Reply to this email directly, view it on GitHub, or mute the thread.

@sikachu
Copy link
Member

(Hmm,@sgrif's comment was gone?)

@sgrif I'm 👍 on expanding this feature for PG 9.5+ in PG adapter. Other RDBMS can fallback to unique-key-constaint-exception-then-find.

@yhirano55
Copy link
Contributor

I want to use these awesome class methods on Rails 5.2, so much.
Do you have any plans to backport to 5-2-stable?

@bogdanvlviv
Copy link
Contributor

I think we don't, "New features are only added to the master branch and will not be made available in point releases." byMaintenance Policy for Ruby on Rails.

@yhirano55
Copy link
Contributor

I see, thank you for your explanation.

@djezzzl
Copy link

djezzzl commentedDec 7, 2018
edited
Loading

Hi everybody,

As we start relying on the database constraints more, I'd suggest (and few people like the idea) to have the improved versions ofvalidates_uniqueness_of andbelongs_to fromdatabase_validations gem in theActiveRecord. I can prepare a PR if needed.

I just opened the issue#34650 where we can discuss the topic.

Any feedback is very appreciated!

jonatas, arathunku, NARKOZ, and swrobel reacted with heart emoji

MaximeLaurenty added a commit to MaximeLaurenty/rubocop-rails that referenced this pull requestAug 8, 2019
create_or_find_by will be implemented in Rails 6.0 (rails/rails#31989)However we've decided to use it before it comes out, and would love it to be in rubocop scope as well.I guess we're not the first ones to do it, so it could benefit to most.
Sign up for freeto join this conversation on GitHub. Already have an account?Sign in to comment

Reviewers

@jeremyjeremyjeremy left review comments

@matthewdmatthewdmatthewd left review comments

@rafaelfrancarafaelfrancarafaelfranca left review comments

@eugeneiuseugeneiuseugeneius left review comments

+2 more reviewers

@tjschucktjschucktjschuck left review comments

@sgrifsgrifsgrif left review comments

Reviewers whose approvals may not affect merge requirements

Assignees

No one assigned

Labels

None yet

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

15 participants

@dhh@nynhex@matthewd@AnalyzePlatypus@subhashsaran@sikachu@yhirano55@bogdanvlviv@djezzzl@jeremy@rafaelfranca@tjschuck@eugeneius@sgrif

[8]ページ先頭

©2009-2025 Movatter.jp