-
Notifications
You must be signed in to change notification settings - Fork 21.7k
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
Ensure #second (and others) acts like #first AR finder #13757
Conversation
Looking good to me! 👍 |
def find_first_with_limit(limit) | ||
def find_nth_with_limit(ordinal, limit) | ||
offset = case ordinal | ||
when :first |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function should not have knowledge of :first
, :second
and friends. We should probably just pass the offset_value
as argument.
@josevalim Ok, fixed that up for you! Any other concerns? |
The famous ordinal Array instance methods defined in ActiveSupport | ||
(`first`, `second`, `third`, `fourth`, and `fifth`) are now available as | ||
full-fledged finders in ActiveRecord. The biggest benefit of this is ordering | ||
of the records returned now defaults to the table's primary key in ascending order. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you add:
Fixes #13743.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added it after the examples! Thanks for the heads up!
@terracatta this looks good. Can you squash your commits? |
if loaded? | ||
@records.first | ||
else | ||
@first ||= find_first_with_limit(1).first | ||
@nth ||= find_nth_with_limit(offset, 1).first |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have a question: how problematic can this cached variable be now that we are using it for different "finds"?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Very problematic. It will cache only the first call.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ok so I removed the caching here, but obviously that isn't the right way to go as that will cause multiple queries to fire unnecessarily when calling .first
a bunch of times. I think the only option here is cache based on offset being passed into the private method.
Any other suggestions?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ok I just went with that
@rafaelfranca @carlosantoniodasilva @senny, commits are squashed, updated the changelog, fixed the caching issue by dynamically setting and manually memoizing the instance variable based on the offset passed in. Tested it again on several personal rails projects and the test suite looks good! Any other feedback? |
if loaded? | ||
@records.first | ||
else | ||
@first ||= find_first_with_limit(1).first | ||
unless instance_variable_defined?("@offset_#{offset||0}") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is better to initialize a empty hash in the initialize
of Relation and use the offset as the key to memoize it.
Since we are adding an internal variable to cache the value we have to update reset
method
def reset |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great suggestion, I'll implement it ASAP
@rafaelfranca, I made the requested changes, but couldn't reproduce your issue or create a failing test case (still trying), can you try again with the latest code and see if you still get a negative limit? |
This commit bring the famous ordinal Array instance methods defined in ActiveSupport into ActiveRecord as fully-fledged finders. These finders ensure a default ascending order of the table's primary key, and utilize the OFFSET SQL verb to locate the user's desired record. If an offset is defined in the query, calling #second adds to the offset to get the actual desired record. Fixes #13743.
@rafaelfranca Ok great, added better before examples for pg, sqlite3, and mysql2 gems. I also added some extra test cases for the offset functionality just in case! Anything else? Sorry for all the back and forth! |
Looks good. Thank you so much for working on this. |
Ensure #second (and others) acts like #first AR finder
Merged ❤️ |
@@ -364,19 +444,19 @@ def find_take | |||
end | |||
end | |||
|
|||
def find_first | |||
def find_nth(offset) | |||
if loaded? | |||
@records.first |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is apparently not going to work with already loaded associations. Just testing around I got two different issues:
# consider the models `Author` has many `Post`s
## 1st scenario - raises exception
>> carlos.posts.reload.first
Post Load (0.1ms) SELECT "posts".* FROM "posts" WHERE "posts"."author_id" = ? [["author_id", 1]]
=> #<Post id: 1, author_id: 1, title: "ZOMG", body: nil, created_at: "2014-01-21 11:44:22", updated_at: "2014-01-21 11:44:22">
>> carlos.posts.reload.second
Post Load (0.1ms) SELECT "posts".* FROM "posts" WHERE "posts"."author_id" = ? [["author_id", 1]]
NoMethodError: undefined method 'first' for nil:NilClass
from....rails/rails/activerecord/lib/active_record/relation/finder_methods.rb:449:in 'find_nth'
## 2nd scenario - returns the same record
>> carlos.posts.first
=> #<Post id: 1, author_id: 1, title: "ZOMG", body: nil, created_at: "2014-01-21 11:44:22", updated_at: "2014-01-21 11:44:22">
>> carlos.posts.second
=> #<Post id: 1, author_id: 1, title: "ZOMG", body: nil, created_at: "2014-01-21 11:44:22", updated_at: "2014-01-21 11:44:22">
>> carlos.posts.third
=> #<Post id: 1, author_id: 1, title: "ZOMG", body: nil, created_at: "2014-01-21 11:44:22", updated_at: "2014-01-21 11:44:22">
@terracatta can you take a look please? It'd be good to provide some tests for already loaded associations with some of these methods. Thanks.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Absolutely! Thanks for testing!
This PR addresses issue #13743 where the ordinal ActiveSupport Array extentions were not assuming a default order like
first
was.As suggested by @dhh this commit brings the famous ordinal Array instance methods defined
in ActiveSupport into ActiveRecord as fully-fledged finders.
These finders ensure a default ascending order of the table's primary key, and utilize the OFFSET SQL verb to locate the user's desired record. If an offset is defined in the query, calling #second adds
to the offset to get the actual desired record.
Example: