Updated: ActiveRecord: count vs size vs length on associations

  • Added another caveat of #count

    count looks directly into the database, neglecting already loaded elements. This becomes a problem when you use that "db count" when processing the loaded elements.
    A common example is tests: Imagine a test that sets up a few records and finishes the test setup by asserting a number of associated records. If you use .count for that job, the assertion cannot see that the setup records have already loaded (i.e. cached) their associations. Tests relying on the setup will fail, because they will operate on the loaded records. You will hardly notice, because you rely on the assertion. Using .size avoids this problem.

  • Restructured & polished

Changes

  • -TLDR
  • -------
  • -- When counting records in an association, you should **use `#size` in most cases.**
  • -- It will not work if the parent record has never been saved. Also there are finer distinctions between `#size` and `#count`. See below.
  • +**TL;DR:** You should generally use `#size` to count associated records.
  • -## count
  • -- Always makes a `COUNT(*)` query if a counter cache is not set up.
  • -- If a counter cache is set up on the association, `#count` will return that cached value instead of executing a new query.
  • +## size
  • +- Counts already loaded elements
  • +- If the association is not loaded, falls back to a `COUNT` query
  • -## size, if the association has already been loaded
  • -- Counts elements in the already loaded array.
  • -- Does not make another query.
  • +## count
  • +- If a counter cache is set up, returns the cached value
  • +- Will issue a `COUNT` query else
  • -## size, if the association has not yet been loaded
  • -- Makes a `COUNT(*)` query, just like `#count`
  • +### Caveats
  • +- If you trigger a `COUNT` query for an unsaved record, Rails will try to load all children where the foreign key `IS NULL`. This is not what you want. To prevent this behavior, you can use `association.to_a.size`.
  • +- `count` looks directly into the database, **neglecting already loaded elements**. E.g. in tests, an intermediate `expect(record.association.count).to eq 1` to assert a precondition may be green. However, the association may still have already been loaded as empty `[]`, so your assertion is void. In this example, `record.association.size` would be correct.
  • ## length
  • -- Always loads the contents of the association into memory, then returns the number of elements loaded.
  • -
  • -
  • -## Caveat for unsaved records
  • -- If you trigger a `COUNT(*)` by whatever means, Rails will try to load all children where the foreign key `IS NULL`. This is not what you want, ever.
  • -- If you want to count children and not fall into this trap, use `association.to_a.size`.
  • +- Fetches the records from the database, then returns the number of elements
  • +None of these methods actually loads the association, i.e. `record.association(:association_name).loaded?` will not change.
Dominik Schöler 11 days ago