Skip to content

MONGOID-5057 Add support for has_many :through#6151

Merged
jamis merged 22 commits into
mongodb:masterfrom
jamis:5057-has_many-through
Jun 9, 2026
Merged

MONGOID-5057 Add support for has_many :through#6151
jamis merged 22 commits into
mongodb:masterfrom
jamis:5057-has_many-through

Conversation

@jamis

@jamis jamis commented May 26, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements read-only :through associations for has_one and has_many, matching ActiveRecord's API:

class Customer
  has_one :franchise
  has_one :store, through: :franchise
end

class Physician
  has_many :appointments
  has_many :patients, through: :appointments
end

Both association types support :source to override the inferred source name, and :class_name for non-conventional class names. has_many :through additionally accepts :order. All through associations are read-only; write attempts raise Mongoid::Errors::ReadonlyAssociation. Eager loading via includes is supported for both types.

The two query patterns supported for has_many :through are the join-model pattern (FK on the intermediate, e.g. belongs_to :patient on Appointment) and the reverse-FK pattern (FK on the target, e.g. has_many :readers on Book).

Out of Scope

  • write operations
  • polymorphic through associations
  • custom :scope
  • embedded through associations
  • chaining through a has_many :through that is itself a through association
  • has_and_belongs_to_many as the source association.

jamis added 18 commits May 4, 2026 13:12
…ions

When eager_load is called with a HasOneThrough or HasManyThrough association,
log a warning naming the through associations and fall back to the includes-style
preload path, since $lookup does not support two-hop through queries.
…oped criteria

The metadata-level criteria(base) method must mirror the contract
established by HasMany and other associations: it takes the owner
document and returns a Criteria scoped to that owner's related records.
The previous split into an unscoped criteria/0 and a resolve(base)
method meant that any caller using the standard Relatable interface
(e.g. eager loaders) would receive an unscoped result.

Also add sum/avg/min/max to the proxy's delegator list so that
aggregation methods work consistently with other has_many proxies.
Copilot AI review requested due to automatic review settings May 26, 2026 16:32
@jamis jamis requested a review from a team as a code owner May 26, 2026 16:32
@jamis jamis requested a review from comandeo-mongo May 26, 2026 16:32

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR adds read-only :through support to Mongoid referenced associations, enabling has_one :through and has_many :through with includes preloading support and a dedicated Mongoid::Errors::ReadonlyAssociation error for mutation attempts.

Changes:

  • Introduces new association metadata/proxy/eager-loader implementations for Referenced::HasOneThrough and Referenced::HasManyThrough.
  • Extends association macro dispatch to select *_through association types when through: is provided.
  • Adds a localized ReadonlyAssociation error and updates eager loading behavior to fall back from $lookup when through associations are included.

Reviewed changes

Copilot reviewed 22 out of 22 changed files in this pull request and generated 10 comments.

Show a summary per file
File Description
spec/mongoid/errors/readonly_association_spec.rb Adds specs for the new ReadonlyAssociation error messaging.
spec/mongoid/association/referenced/has_one_through/proxy_spec.rb Tests has_one-through proxy behavior and eager loader selection.
spec/mongoid/association/referenced/has_one_through/eager_spec.rb Verifies includes-based preloading for has_one-through.
spec/mongoid/association/referenced/has_one_through_spec.rb Adds unit + integration coverage for has_one-through metadata and read-only setter.
spec/mongoid/association/referenced/has_many_through/proxy_spec.rb Tests has_many-through proxy read/mutation API surface.
spec/mongoid/association/referenced/has_many_through/eager_spec.rb Verifies includes-based preloading for has_many-through.
spec/mongoid/association/referenced/has_many_through_spec.rb Adds unit + integration coverage for has_many-through metadata, ordering, and patterns.
spec/mongoid/association/macros_spec.rb Ensures macros instantiate the correct *_through metadata when through: is present.
spec/mongoid/association/eager_loadable_spec.rb Confirms eager_load warns and falls back to preload for through associations.
lib/mongoid/errors/readonly_association.rb Implements new error class used for write attempts on through associations.
lib/mongoid/errors.rb Requires the new error class.
lib/mongoid/association/referenced/has_one_through/proxy.rb Adds proxy class for has_one-through association type.
lib/mongoid/association/referenced/has_one_through/eager.rb Adds two-query preloader for has_one-through.
lib/mongoid/association/referenced/has_one_through.rb Adds metadata/definition logic for has_one-through.
lib/mongoid/association/referenced/has_many_through/proxy.rb Adds read-only proxy wrapper for has_many-through criteria.
lib/mongoid/association/referenced/has_many_through/eager.rb Adds two-query preloader for has_many-through.
lib/mongoid/association/referenced/has_many_through.rb Adds metadata/definition logic for has_many-through criteria building and ids getter.
lib/mongoid/association/referenced.rb Requires the new through association implementations.
lib/mongoid/association/macros.rb Routes has_one/has_many to through-metadata when through: is provided.
lib/mongoid/association/eager_loadable.rb Detects through inclusions and falls back from $lookup eager loading with a warning.
lib/mongoid/association.rb Adds internal THROUGH_MACRO_MAPPING for has_one/has_many.
lib/config/locales/en.yml Adds i18n strings for the new readonly_association error.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread lib/mongoid/association/referenced/has_one_through.rb Outdated
Comment thread lib/mongoid/association/referenced/has_one_through/eager.rb Outdated
Comment thread lib/mongoid/association/referenced/has_one_through/eager.rb Outdated
Comment thread lib/mongoid/association/referenced/has_many_through.rb
Comment thread lib/mongoid/association/referenced/has_many_through/eager.rb Outdated
Comment thread lib/mongoid/association/referenced/has_many_through/eager.rb
Comment thread lib/mongoid/association/referenced/has_many_through/eager.rb
Comment thread lib/mongoid/association/referenced/has_one_through.rb
Comment thread lib/mongoid/association/referenced/has_one_through.rb
Comment thread lib/mongoid/association/referenced/has_many_through.rb
jamis added 3 commits May 26, 2026 11:23
- Remove :scope from ASSOCIATION_OPTIONS on both through types; it was
  listed as valid but never applied, causing silent no-ops
- Add HABTM guard to HasOneThrough#source_association to match the
  existing guard in HasManyThrough
- Fix reverse-FK eager loaders (has_one and has_many) to use
  source_assoc.primary_key when collecting intermediate PKs and joining
  back to targets; through_assoc.primary_key is the owner's key and
  gives wrong results when a custom :primary_key is configured
- Fix HasManyThrough#criteria reverse-FK branch with the same correction
- Eager loaders now store a Proxy rather than a raw document/array, so
  the cached type after includes() matches the non-eager getter
- HasManyThrough::Proxy accepts a preloaded: kwarg; enumerable methods
  (each, to_a, first, etc.) use the preloaded array when set, while
  query methods (where, pluck, etc.) always build a fresh Criteria
…hens

RSpec embeds context description strings in the JSON output. Em-dash
characters (U+2014, encoded as \xE2\x80\x94 in UTF-8) cause
Encoding::InvalidByteSequenceError when CI reads tmp/rspec.json under
LANG=C, where Encoding.default_external is US-ASCII.
@jamis jamis added the feature Adds a new feature, without breaking compatibility label May 28, 2026
@jamis jamis merged commit 7e5cff0 into mongodb:master Jun 9, 2026
76 checks passed
@jamis jamis deleted the 5057-has_many-through branch June 9, 2026 16:19
@sandstrom

sandstrom commented Jun 11, 2026

Copy link
Copy Markdown

I was wondering just now, does mongoid support this -- lo and behold! :)

Two small questions:

  • Are there any docs for this yet? Did find them in the diff, but maybe it's coming later or is in another repo?

  • Will the docs elaborate on query performance implications, because I'm guessing the underlying queries will either be more expensive than what you'd usually have in a SQL database, or need to use aggregation-queries?

@jamis

jamis commented Jun 11, 2026

Copy link
Copy Markdown
Contributor Author

There will be docs, but we have a separate documentation team and they use an internal repository. There is nothing available yet.

The docs will probably not going into great depth on performance implications for this feature; the current implementation is fairly naive, though, and optimized more for convenience than performance right now. This is minimal "v1". There is definitely more that could be done, both for performance optimization, and for extending what is supported.

@sandstrom

Copy link
Copy Markdown

Great! 🎉

Not trying to be annoying about performance. I think this is a great start.

As long as the docs just explains what queries will be made underr the hood, the reader can deduce what that means for them in terms of perf.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature Adds a new feature, without breaking compatibility

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants