.merge: reusando um scope do model juntado
O que "JOINs no Rails" já cobre
A aula "JOINs no Rails: associações ActiveRecord", de SQL: fundamentos de JOIN, é completa. Ela trata de joins (um INNER JOIN que filtra e não carrega), de left_joins (um LEFT OUTER JOIN, inclusive anti-joins) e das três estratégias de carregamento includes, eager_load e preload. Ela percorre o problema N+1 de ponta a ponta, inclusive a forma aninhada includes(lessons: :challenge), a diferença essencial entre joins e includes, uma matriz de decisão e a gem bullet. Este curso não repete nada disso. Ele começa por .merge, que aquela aula nunca menciona, e entra no terreno que Associações ActiveRecord: through e Associações ActiveRecord: polimórficas prepararam mas nunca consultaram.
Filtrando por uma tabela juntada, de dois jeitos
Catalog::Course tem um scope active: scope :active, -> { where(active: true) }. Encontrar aulas que pertencem a um curso ativo pode ser escrito como um .where na tabela juntada:
Catalog::Lesson.joins(:course).where(courses: { active: true })
Ou pode reusar o scope que já existe em Catalog::Course, em vez de digitar a condição de novo:
Catalog::Lesson.joins(:course).merge(::Catalog::Course.active)
O .to_sql dos dois é idêntico: SELECT "lessons".* FROM "lessons" INNER JOIN "courses" ON "courses"."id" = "lessons"."course_id" WHERE "courses"."active" = TRUE. .merge pega as condições WHERE (e outras partes da consulta) da relation que você passa, Catalog::Course.active, e as junta na relation que chama, como se você tivesse escrito o .where à mão.
Esta linha já roda neste 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
Esta é a busca do catálogo, SearchQuery. Ela encontra aulas cujo título bate com um termo de busca, mas só em cursos active. .joins(:course) traz a tabela courses para filtrar, o que "JOINs no Rails" já cobre. .merge(::Catalog::Course.active) reusa o scope active de Course em vez de repetir where(active: true) na linha. Se o significado de "ativo" mudasse, digamos com uma segunda condição no scope, todo .merge(Course.active) do código pegaria a mudança. Um .where(courses: { active: true }) escrito à mão em vários query objects precisaria de cada cópia atualizada à mão.
Por que esta é uma mecânica diferente de joins/includes
joins, left_joins, includes, eager_load e preload respondem todos a "como esta associação é carregada, e ela filtra?" .merge responde a outra pergunta: "como reusar um scope já definido no model que juntei, em vez de reescrever a lógica na linha?" Ele funciona com qualquer uma das cinco estratégias de carregamento. .merge não liga se a associação entrou por joins ou por includes; ele pega condições de uma relation e as aplica a outra.
Pontos-chave
- "JOINs no Rails: associações ActiveRecord" trata de
joins,left_joins,includes,eager_load,preloade do problema N+1 por inteiro. Este curso começa depois disso, com.merge. Lesson.joins(:course).merge(Course.active)gera o mesmo SQL queLesson.joins(:course).where(courses: { active: true }):.mergejunta as condiçõesWHEREda outra relation na que chama.- O
Catalog::SearchQuerydeste app usajoins(:course).merge(Course.active), reusando o scopeactivedeCourseem vez de repetirwhere(active: true), e assim "ativo" fica definido num lugar só. .mergeresponde a "reusar um scope do model juntado", uma pergunta diferente da quejoins,includes,eager_loadepreloadrespondem sobre carregamento, e funciona com qualquer um deles.
Continue com a aula 2
A armadilha joins + .count: linhas duplicadas do JOIN
Por que joins(:lessons).count conta pares curso-aula, como .distinct.count corrige isso e por que includes(:lessons).count dispensa o JOIN.
Com uma conta grátis você também tem os quizzes, seu progresso e suas anotações, e a discussão.