Armadilha sem escopo ActiveRecord

ActiveRecord permite que você defina escopos (e escopos padrão), como você deve saber. Mas, às vezes, você pode precisar acessar dados sem o escopo padrão.

Vamos ver um exemplo:

class User < ActiveRecord::Base
has_many
:posts
end

class Post < ActiveRecord::Base
default_scope
-> { where(draft: false) }

belongs_to
:user
end

Um exemplo muito simples, e esse escopo padrão permitirá que você não se importe com rascunhos de postagens.

Mas você logo descobrirá como isso pode facilmente enganá-lo.
Digamos que você precise acessar todos os rascunhos de um usuário, a abordagem óbvia será:

User.first.posts.where(draft: true)

E você encontrará um conjunto de resultados vazio. Isso ocorre porque o escopo padrão é aplicado, a consulta resultante é:

SELECT "posts".* FROM "posts"  WHERE "posts"."draft" = 'f' AND "posts"."user_id" = $1 AND "posts"."draft" = 't'  [["user_id", 1]]

Como você pode ver, você está procurando por postagens que sejam rascunho e não rascunho, o que leva a um conjunto vazio (a menos que esteja trabalhando em um sistema quântico ).

Bem, então deixe-me remover o escopo:

User.first.posts.unscoped.where(draft: true)

E em seus testes você pode pensar que resolveu seu problema, porque tentar isso em seu sistema de desenvolvimento parece apresentar a solução certa.

Mas vamos dar uma olhada na consulta resultante:

SELECT "posts".* FROM "posts"  WHERE "posts"."draft" = 't'

Espere, onde está nossa condição? O método é muito mais poderoso do que você pensava, certo? Está retirando tudo , até a relação de usuário que temos na mesma linha!"posts"."user_id"unscoped

Esse é o comportamento pretendido, mas pode levar a bugs muito difíceis de detectar.

Temos algumas soluções aqui.

Primeiro:

Post.unscoped{ User.first.posts.where(draft: true) }

Passar um bloco dentro removerá o escopo padrão, mas todas as condições dentro do bloco serão preservadas..unscoped

Segunda solução (feia):

Use rewhere

User.first.posts.rewhere(draft: true)

Mesmo que rewhereseja uma ferramenta poderosa, em uma primeira leitura você pode ficar confuso ao vê-la sem um whereantes explícito .

Terceiro (mais feio):

Post.unscoped.where(user_id: User.first.id, draft: true)

Estamos apenas ignorando o belongs_toaqui, parece mais um hack do que uma solução.

Quarta (melhor) solução:

Evite usar default_scopeem tudo

default_scope traz mais problemas do que benefícios a longo prazo:

Muda (silenciosamente) o comportamento das instruções mais utilizadas, assim , sem nenhuma dica.Post.firstPost.all

Veja também default_scopeé mau .

No final da história, tendo seu Postmodelo desta forma:

class Post < ActiveRecord::Base
belongs_to
:user

scope
:published, -> { where(draft: false) }
scope
:draft, -> { where(draft: false) }
end

E chame postsassim:

User.first.posts.draft

é muito mais claro e significativo.

Nota de rodapé : esteja ciente de que também scopesão influenciados por default_scope. Por exemplo, se você esperava que esta solução funcionasse:

class Post < ActiveRecord::Base
default_scope
-> { where(draft: false) }

belongs_to
:user

scope
:published, -> { where(draft: false) }
scope
:draft, -> { where(draft: true) }
end

Eu tenho más notícias para você, o resultado

User.first.posts.draft

levará a uma nossa consulta quântica:

SELECT "posts".* FROM "posts"  WHERE "posts"."draft" = 'f' AND "posts"."user_id" = $1 AND "posts"."draft" = 't'  [["user_id", 1]]