.merge: reusing a scope from the joined model
What "JOINs in Rails" already covers
The lesson "JOINs in Rails: ActiveRecord associations", in SQL: JOIN fundamentals, is thorough. It covers joins (an INNER JOIN that filters and doesn't load), left_joins (a LEFT OUTER JOIN, including anti-joins), and the three loading strategies includes, eager_load and preload. It walks through the N+1 problem end to end, including the nested includes(lessons: :challenge) form, the key difference between joins and includes, a decision matrix and the bullet gem. This course doesn't repeat any of it. It starts from .merge, which that lesson never mentions, and goes into ground that ActiveRecord associations: has_one/through and ActiveRecord associations: polymorphic set up but never queried.
Filtering on a joined table, two ways
Catalog::Course has an active scope: scope :active, -> { where(active: true) }. Finding lessons that belong to an active course can be written as a .where on the joined table:
Catalog::Lesson.joins(:course).where(courses: { active: true })
Or it can reuse the scope that already exists on Catalog::Course, instead of typing its condition again:
Catalog::Lesson.joins(:course).merge(::Catalog::Course.active)
The .to_sql of the two is identical: SELECT "lessons".* FROM "lessons" INNER JOIN "courses" ON "courses"."id" = "lessons"."course_id" WHERE "courses"."active" = TRUE. .merge takes the WHERE conditions (and other query parts) from the relation you pass it, Catalog::Course.active, and folds them into the calling relation, as if you had written the .where by hand.
This line already runs in this app
# app/queries/catalog/search_query.rb
lessons = ::Catalog::Lesson.joins(:course).merge(::Catalog::Course.active)
.includes(:course)
.where(title_match(:lessons, locale), pattern)
.order(:course_id, :position).limit(limit).to_a
This is the catalog search, SearchQuery. It finds lessons whose title matches a search term, but only in courses that are active. .joins(:course) brings in the courses table for filtering, which "JOINs in Rails" already covers. .merge(::Catalog::Course.active) reuses the active scope of Course instead of repeating where(active: true) inline. If the meaning of "active" changed, say a second condition added to the scope, every .merge(Course.active) in the codebase would pick it up. A hand-written .where(courses: { active: true }) spread across several query objects would need each copy updated by hand.
Why this is a different mechanic from joins/includes
joins, left_joins, includes, eager_load and preload all answer "how is this association loaded, and does it filter?" .merge answers a different question: "how do I reuse a scope already defined on the model I joined, instead of rewriting its logic inline?" It works with any of the five loading strategies. .merge doesn't care whether the association came in through joins or includes; it takes conditions from one relation and applies them to another.
Key takeaways
- "JOINs in Rails: ActiveRecord associations" covers
joins,left_joins,includes,eager_load,preloadand the N+1 problem in full. This course starts past that, with.merge. Lesson.joins(:course).merge(Course.active)generates the same SQL asLesson.joins(:course).where(courses: { active: true }):.mergefolds the other relation'sWHEREconditions into the calling one.- This app's
Catalog::SearchQueryusesjoins(:course).merge(Course.active), reusing theactivescope ofCourseinstead of repeatingwhere(active: true), so "active" is defined in one place. .mergeanswers "reuse a scope from the joined model", a different question from the onejoins,includes,eager_loadandpreloadanswer about loading, and it works with any of them.
Keep going with lesson 2
The joins + .count trap: duplicate rows from the JOIN
Why joins(:lessons).count counts course-lesson pairs, how .distinct.count fixes it, and why includes(:lessons).count skips the JOIN.
A free account also gives you the quizzes, your progress and notes, and the discussion.