Como retornar uma relação ActiveRecord vazia?

246

Se eu tiver um escopo com um lambda e precisar de um argumento, dependendo do valor do argumento, talvez eu saiba que não haverá correspondências, mas ainda quero retornar uma relação, não uma matriz vazia:

scope :for_users, lambda { |users| users.any? ? where("user_id IN (?)", users.map(&:id).join(',')) : [] }

O que eu realmente quero é um método "none", o oposto de "all", que retorna uma relação que ainda pode ser encadeada, mas resulta em curto-circuito na consulta.

dzajic
fonte
Se você deixar a consulta executada, ela retornará uma relação: User.where ('id in (?)', []). Class => ActiveRecord :: Relation. Você está tentando evitar a consulta completamente?
Brian Deterling 02/02
1
Corrigir. Se eu sei que não pode haver correspondências, o ideal é que a consulta possa ser totalmente evitada. Eu simplesmente adicionei isso ao ActiveRecord :: Base: "def self.none; where (: id => 0); end" Parece funcionar muito bem para o que eu preciso.
Džajić
1
> Você está tentando evitar a consulta completamente? seria totalmente fazer sentido, uma espécie de coxo que precisa acertar DB para isso
dolzenko

Respostas:

478

Agora existe um mecanismo "correto" no Rails 4:

>> Model.none 
=> #<ActiveRecord::Relation []>
steveh7
fonte
14
Até agora, isso não foi portado novamente para 3.2 ou anterior. Só borda (4,0)
Chris Bloom
2
Tentei apenas com o Rails 4.0.5 e está funcionando. Esse recurso chegou à versão Rails 4.0.
Evolve
3
@AugustinRiedinger Model.scopedfaz o que você está procurando em trilhos 3.
Tim Diggins
9
A partir do Rails 4.0.5, Model.nonenão funciona. Você precisa usar um de seus nomes reais de modelo, por exemplo, User.noneou o que você tem.
Grant Birchmeier
77

Uma solução mais portátil que não requer uma coluna "id" e não assume que não haverá uma linha com um ID 0:

scope :none, where("1 = 0")

Ainda estou procurando uma maneira mais "correta".

steveh7
fonte
3
Sim, estou realmente surpreso que essas respostas sejam as melhores que temos. Eu acho que o ActiveRecord / Arel ainda deve ser bastante imaturo. Se eu tivesse que passar por perambulações para criar uma matriz vazia em Ruby, ficaria muito irritado. A mesma coisa aqui, basicamente.
Purplejacket 1/10/11
Embora seja um tanto hackiano, esta é a maneira correta para o Rails 3.2. Para o Rails 4, consulte a outra resposta de @ steveh7 aqui: stackoverflow.com/a/10001043/307308
scarver2
scope :none, -> { where("false") }
Nrose
43

Chegando no Rails 4

No Rails 4, um encadeamento ActiveRecord::NullRelationserá retornado de chamadas comoPost.none .

Nem ele nem métodos encadeados gerarão consultas ao banco de dados.

De acordo com os comentários:

O ActiveRecord :: NullRelation retornado herda de Relation e implementa o padrão Null Object. É um objeto com comportamento nulo definido e sempre retorna uma matriz vazia de registros sem consultar o banco de dados.

Veja o código fonte .

Nathan Long
fonte
42

Você pode adicionar um escopo chamado "none":

scope :none, where(:id => nil).where("id IS NOT ?", nil)

Isso fornecerá um ActiveRecord :: Relation vazio

Você também pode adicioná-lo ao ActiveRecord :: Base em um inicializador (se desejar):

class ActiveRecord::Base
 def self.none
   where(arel_table[:id].eq(nil).and(arel_table[:id].not_eq(nil)))
 end
end

Muitas maneiras de obter algo assim, mas certamente não é a melhor coisa a manter em uma base de código. Eu usei o escopo: none ao refatorar e descobrir que preciso garantir um ActiveRecord :: Relation vazio por um curto período de tempo.

Brandon
fonte
14
where('1=2')pode ser um pouco mais conciso
Marcin Raczkowski
10
Caso você não role para baixo para a nova resposta 'correta': Model.noneé assim que você faria isso.
quer
26
scope :none, limit(0)

É uma solução perigosa porque seu escopo pode estar acorrentado.

User.none.first

retornará o primeiro usuário. É mais seguro usar

scope :none, where('1 = 0')
bbrinck
fonte
2
Este é o correto 'scope: none, where (' 1 = 0 ')'. o outro irá falhar se você tiver a paginação
Federico
14

Eu acho que prefiro a aparência das outras opções:

scope :none, limit(0)

Levando a algo assim:

scope :users, lambda { |ids| ids.present? ? where("user_id IN (?)", ids) : limit(0) }
Alex
fonte
Eu prefiro este. Não sei por where(false)que não faria o trabalho - ele mantém o escopo inalterado.
Aceofspades
4
Cuidado que limit(0)será substituído se você ligar .firstou .lastmais tarde na cadeia, pois o Rails será anexado LIMIT 1a essa consulta.
zykadelic 4/12/12
2
@aceofspades onde (false) não funciona (Rails 3.0), mas onde ('false') funciona. Não que você provavelmente se importa agora é 2013 :)
Ritchie
Obrigado @Ritchie, desde então acho que também temos a nonerelação mencionada abaixo.
Aceofspades
3

Usar escopo:

escopo: for_users, lambda {| usuários | users.any? ? onde ("user_id IN (?)", users.map (&: id) .join (',')): com escopo}

Mas você também pode simplificar seu código com:

escopo: for_users, lambda {| usuários | onde (: user_id => users.map (&: id)) se users.any? }

Se você deseja um resultado vazio, use este (remova a condição if):

escopo: for_users, lambda {| usuários | onde (: user_id => users.map (&: id))}
Pan Thomakos
fonte
Retornar "escopo" ou zero não alcança o que eu quero, que é limitar os resultados a zero. O retorno de "escopo" ou nulo não afeta o escopo (o que é útil em alguns casos, mas não no meu). Eu vim com minha própria resposta (veja os comentários acima).
dzajic 4/02
Eu adicionei uma solução simples para você voltar um resultado vazio, bem :)
Pan Thomakos
1

Também existem variantes, mas todas elas estão solicitando ao banco de dados

where('false')
where('null')
fmnoise
fonte
2
BTW Observe que essas devem ser cadeias de caracteres. where(false)ou where(nil)é simplesmente ignorado.
mahemoff