Skip to content
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

Fix SELECT COUNT queries when rendering ActiveRecord collections #40870

Merged
merged 4 commits into from
Dec 18, 2020
Merged

Fix SELECT COUNT queries when rendering ActiveRecord collections #40870

merged 4 commits into from
Dec 18, 2020

Conversation

aar0nr
Copy link
Contributor

@aar0nr aar0nr commented Dec 17, 2020

Fixes #40837

When rendering collections, calling size when the collection is an
ActiveRecord relation causes unwanted SELECT COUNT(*) queries. This
change ensures the collection is loaded before getting the size.

@rails-bot rails-bot bot added the actionview label Dec 17, 2020
@@ -144,7 +144,7 @@ def render_collection(collection, view, path, template, layout, block)
"render_collection.action_view",
identifier: identifier,
layout: layout && layout.virtual_path,
count: collection.size
count: collection.to_a.size
Copy link
Member

Choose a reason for hiding this comment

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

Would not this load the collection too early making the preload that happens inside PreloadCollectionIterator not happen anymore?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

No because the PreloadCollectionIterator has already been instantiated, and preloading has been disabled (the relation hasn't been loaded at that point). Then, to_a will load the relation without preloading. Finally, the PreloadCollectionIterator will handle the preloading of the associations.

Copy link
Member

Choose a reason for hiding this comment

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

Great! Thank you for investigating.

Copy link
Member

Choose a reason for hiding this comment

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

How about using collection.length instead?

Suggested change
count: collection.to_a.size
count: collection.length

to_a for relation will dup extra records.

def to_ary
records.dup
end
alias to_a to_ary

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@kamipo Good suggestion, thanks. The only catch was I had to put a check for the length method because there are tests that rely on passing collections that don't respond to length, so I delegate back to size if length isn't available.

@rafaelfranca
Copy link
Member

Is it possible to write test to make sure someone doesn't remove that to_a from there?

@aar0nr
Copy link
Contributor Author

aar0nr commented Dec 17, 2020

I added a test. I couldn't think of a better way to test this other than asserting on the queries during a render. Kind of hard to test since it's a complete side-effect.

I've never contributed to Rails before, so I'm not sure if I'm following the correct patterns or not, so let me know if I'm doing it wrong. There seems to be many different styles accumulated over the years. 🤣


@queries = []

ActiveSupport::Notifications.subscribe("sql.active_record") do |*, payload|
Copy link
Contributor

Choose a reason for hiding this comment

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

Looking at some of the other tests, you'll want to store the subscription in a variable, so you can unsubscribe at the end of the test. Right now, all subsequent tests are pushing their SQL into this array (a memory leak in the test suite).

For example:

ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@natematykiewicz Thanks, I've done as you suggested.

Fixes #40837

When rendering collections, calling `size` when the collection is an
ActiveRecord relation causes unwanted `SELECT COUNT(*)` queries. This
change ensures the collection is an array before getting the size, and
also loads the relation for any further array inspections.
Allows getting the size of a relation without duplicating records, but
still loads the relation. The length method existence needs to be
checked because you can pass in an `Enumerator`, which does not respond
to `length`.
@rafaelfranca rafaelfranca merged commit 432698e into rails:master Dec 18, 2020
rafaelfranca pushed a commit that referenced this pull request Dec 18, 2020
…40870)

* Fix `SELECT COUNT` queries when rendering ActiveRecord collections

Fixes #40837

When rendering collections, calling `size` when the collection is an
ActiveRecord relation causes unwanted `SELECT COUNT(*)` queries. This
change ensures the collection is an array before getting the size, and
also loads the relation for any further array inspections.

* Test queries when rendering relation collections

* Add `length` support to partial collection iterator

Allows getting the size of a relation without duplicating records, but
still loads the relation. The length method existence needs to be
checked because you can pass in an `Enumerator`, which does not respond
to `length`.

* Ensure unsubscribed from notifications after tests

[Rafael Mendonça França + aar0nr]
@aar0nr aar0nr deleted the fix-collection-rendering-size-query branch December 18, 2020 21:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Rendering ActiveRecord collections as non-derived partials results in SELECT COUNT query
4 participants