Como encadear consultas de escopo com OR em vez de AND?


137

Estou usando o Rails3, ActiveRecord

Imaginando como posso encadear os escopos com instruções OR em vez de AND.

por exemplo

Person.where(:name => "John").where(:lastname => "Smith")

Isso normalmente retorna:

name = 'John' AND lastname = 'Smith'

mas eu gostaria:

`name = 'John' OR lastname = 'Smith'

Respostas:


107

Você faria

Person.where('name=? OR lastname=?', 'John', 'Smith')

No momento, não há nenhum outro suporte OR pela nova sintaxe do AR3 (isto é, sem o uso de pedras preciosas de terceiros).


21
e apenas por uma questão de segurança, vale a pena expressar isso como Person.where ("name =? OR lastname =?", 'John', 'Smith')
CambridgeMike

6
Isso ainda é aplicável no Rails 4? Nada melhor surgiu no Rails 4?
Donato

11
Nenhuma alteração no Rails 4, mas o Rails 5 será .orincorporado .
cal

3
não são escopos, são apenas 2 cláusulas where ou um caso de escopo muito específico. E se eu gostaria de encadear escopos mais complexos, com junções, por exemplo.
miguelfg

2
Com o Rails 5, você também pode fazer algo como, Person.where ("name = 'John'"). Ou (Person.where ("lastname = 'Smith'")))
shyam

59

De acordo com essa solicitação , o Rails 5 agora suporta a seguinte sintaxe para encadear consultas:

Post.where(id: 1).or(Post.where(id: 2))

Há também um backport da funcionalidade no Rails 4.2 por meio desta gema.


48

Use o ARel

t = Person.arel_table

results = Person.where(
  t[:name].eq("John").
  or(t[:lastname].eq("Smith"))
)

1
Alguém pode comentar se esse tipo de consulta é interrompido no Postgres?
Olivier Lacan

O ARel deve ser agnóstico ao DB.
Simon Perepelitsa

Embora pareça uma boa ideia, o ARel não é uma API pública e o uso disso pode interromper seu aplicativo de maneiras inesperadas, por isso aconselho a menos que você preste muita atenção à evolução da API do ARel.
Olivier Lacan

7
@OlivierLacan Arel é uma API pública ou, mais precisamente, expõe uma. O Active Record apenas fornece métodos de conveniência que o usam sob o capô. É completamente válido usá-lo por conta própria. Ele segue o controle de versão semântico, portanto, a menos que você esteja alterando as versões principais (3.xx => 4.xx), não há necessidade de se preocupar em interromper as alterações.
Cinza

41

Basta postar a sintaxe da matriz para a mesma coluna OU consultas para ajudar a espreitar.

Person.where(name: ["John", "Steve"])

2
A verificação pergunta John contra o nome e Smith contra sobrenome
steven_noble

11
@steven_noble - é por isso que coloquei a mesma coluna em negrito com a consulta ou. Posso excluir o comentário completamente, mas achei que seria útil para as pessoas que lêem "ou" consultam as perguntas do SO.
daino3

38

Atualização para o Rails4

não requer gemas de terceiros

a = Person.where(name: "John") # or any scope 
b = Person.where(lastname: "Smith") # or any scope 
Person.where([a, b].map{|s| s.arel.constraints.reduce(:and) }.reduce(:or))\
  .tap {|sc| sc.bind_values = [a, b].map(&:bind_values) }

Resposta antiga

não requer gemas de terceiros

Person.where(
    Person.where(:name => "John").where(:lastname => "Smith")
      .where_values.reduce(:or)
)

5
essa é realmente a única resposta pura à pergunta do OP em termos de recursos e método. as outras respostas (a) exigem APIs de terceiros ou (b) exigem interpolação orou outros operadores dentro de um bloco de texto, nenhum dos quais é refletido no código do OP.
allenwlee

6
Aqui está um artigo decente explicando-o coderwall.com/p/dgv7ag/…
allenwlee

4
Usado ...where_values.join(' OR ')no meu caso
Sash

2
Você pode omitir a .tap {|sc| sc.bind_values = [a, b].map(&:bind_values) }peça se seus escopos não tiverem nenhuma variável que precise de ligação.
Fatuhoku

22

Você também pode usar a gema MetaWhere para não misturar seu código com o material SQL:

Person.where((:name => "John") | (:lastname => "Smith"))

3
+1. O sucessor do MetaWhere é squeel do mesmo autor.
Thilo

22

Caso alguém esteja procurando uma resposta atualizada para esta, parece que existe uma solicitação de recebimento existente para colocar isso no Rails: https://github.com/rails/rails/pull/9052 .

Graças ao patch de macacos de @ j-mcnally para ActiveRecord ( https://gist.github.com/j-mcnally/250eaaceef234dd8971b ), você pode fazer o seguinte:

Person.where(name: 'John').or.where(last_name: 'Smith').all

Ainda mais valiosa é a capacidade de encadear escopos com OR:

scope :first_or_last_name, ->(name) { where(name: name.split(' ').first).or.where(last_name: name.split(' ').last) }
scope :parent_last_name, ->(name) { includes(:parents).where(last_name: name) }

Em seguida, você pode encontrar todas as pessoas com nome ou sobrenome ou cujo pai com sobrenome

Person.first_or_last_name('John Smith').or.parent_last_name('Smith')

Não é o melhor exemplo para o uso disso, mas apenas tentando ajustá-lo à pergunta.


2
Qual versão ou Rails você precisa para isso?
Sash

3
A discussão mais recente e a solicitação de recebimento que fará isso no núcleo do Rails podem ser encontradas aqui: github.com/rails/rails/pull/16052 O Rails 5.0 é direcionado para o lançamento oficial da .orfuncionalidade da cadeia. Consegui usar o patch de macaco que mencionei no Rails> = 3.2; no entanto, você pode esperar o Rails 5 introduzir alterações de longa data em seu código.
Codenamev

1
Sim, ele foi fundido e lançado com Rails 5.
amoebe

10

Para mim (Rails 4.2.5), funciona apenas assim:

{ where("name = ? or name = ?", a, b) }

8

Este seria um bom candidato para o MetaWhere se você estiver usando o Rails 3.0+, mas não funciona no Rails 3.1. Você pode experimentar o rodo . É feito pelo mesmo autor. Veja como você executaria uma cadeia baseada em OR:

Person.where{(name == "John") | (lastname == "Smith")}

Você pode misturar e combinar AND / OR, entre muitas outras coisas incríveis .


8

Uma versão atualizada do Rails / ActiveRecord pode suportar essa sintaxe nativamente. Seria semelhante a:

Foo.where(foo: 'bar').or.where(bar: 'bar')

Conforme observado nesta solicitação de recebimento https://github.com/rails/rails/pull/9052

Por enquanto, basta seguir os seguintes procedimentos:

Foo.where('foo= ? OR bar= ?', 'bar', 'bar')

5
Não suportado no Rails 4.2.5
fatuhoku

1
Foo.where(foo: 'bar1').or.where(bar: 'bar2')não funciona. Deve estar em Foo.where (foo: 'bar1'). Or.where (Foo.where (bar: 'bar2')) #
Ana María Martínez Gómez

6

Trilhos 4 + Âmbito + Arel

class Creature < ActiveRecord::Base
    scope :is_good_pet, -> {
        where(
            arel_table[:is_cat].eq(true)
            .or(arel_table[:is_dog].eq(true))
            .or(arel_table[:eats_children].eq(false))
        )
    }
end

Tentei encadear escopos nomeados com .ou sem sorte, mas isso funcionou para encontrar qualquer coisa com esses conjuntos de booleanos. Gera SQL como

SELECT 'CREATURES'.* FROM 'CREATURES' WHERE ((('CREATURES'.'is_cat' = 1 OR 'CREATURES'.'is_dog' = 1) OR 'CREATURES'.'eats_children' = 0))

4

Trilhos 4

scope :combined_scope, -> { where("name = ? or name = ?", 'a', 'b') }

4

Se você deseja fornecer um escopo (em vez de trabalhar explicitamente em todo o conjunto de dados), veja o que você deve fazer com o Rails 5:

scope :john_or_smith, -> { where(name: "John").or(where(lastname: "Smith")) }

Ou:

def self.john_or_smith
  where(name: "John").or(where(lastname: "Smith"))
end

Isso está correto, mas uma resposta mais literal do que a mesma coisa que tecnicamente stackoverflow.com/a/42894753/1032882 faz.
RudyOnRails

3

Se você não pode escrever a cláusula where manualmente para incluir a instrução "ou" (ou seja, deseja combinar dois escopos), pode usar union:

Model.find_by_sql("#{Model.scope1.to_sql} UNION #{Model.scope2.to_sql}")

(fonte: União de consultas ActiveRecord )

Isso retornará todos os registros correspondentes a qualquer consulta. No entanto, isso retorna uma matriz, não um arel. Se você realmente deseja devolver um arel, verifique esta lista: https://gist.github.com/j-mcnally/250eaaceef234dd8971b .

Isso fará o trabalho, contanto que você não se importe com os trilhos de fixação de macacos.


2

Veja também estas questões relacionadas: aqui , aqui e aqui

Para trilhos 4, com base neste artigo e nesta resposta original

Person
  .unscoped # See the caution note below. Maybe you want default scope here, in which case just remove this line.
  .where( # Begin a where clause
    where(:name => "John").where(:lastname => "Smith")  # join the scopes to be OR'd
    .where_values  # get an array of arel where clause conditions based on the chain thus far
    .inject(:or)  # inject the OR operator into the arels 
    # ^^ Inject may not work in Rails3. But this should work instead:
    .joins(" OR ")
    # ^^ Remember to only use .inject or .joins, not both
  )  # Resurface the arels inside the overarching query

Observe a cautela do artigo no final:

Rails 4.1+

O Rails 4.1 trata o default_scope apenas como um escopo regular. O escopo padrão (se houver algum) está incluído no resultado where_values ​​e o injetar (: ou) adicionará ou declarará entre o escopo padrão e o seu where. Isso é ruim.

Para resolver isso, você só precisa desmarcar a consulta.


1

Essa é uma maneira muito conveniente e funciona bem no Rails 5:

Transaction
  .where(transaction_type: ["Create", "Correspond"])
  .or(
    Transaction.where(
      transaction_type: "Status",
      field: "Status",
      newvalue: ["resolved", "deleted"]
    )
  )
  .or(
    Transaction.where(transaction_type: "Set", field: "Queue")
  )

0

a squeelgema fornece uma maneira incrivelmente fácil de fazer isso (antes disso, usei algo como o método @ coloradoblue):

names = ["Kroger", "Walmart", "Target", "Aldi"]
matching_stores = Grocery.where{name.like_any(names)}

0

Portanto, a resposta para a pergunta original, você pode associar escopos com 'ou' em vez de 'e' parece ser "não, não pode". Mas você pode codificar manualmente um escopo ou consulta completamente diferente que faça o trabalho ou usar uma estrutura diferente do ActiveRecord, por exemplo, MetaWhere ou Squeel. Não é útil no meu caso

Estou 'ou' criando um escopo gerado pelo pg_search, que faz um pouco mais do que o select, ele inclui order by ASC, o que torna uma bagunça uma união limpa. Eu quero 'ou' com um escopo artesanal que faz coisas que não posso fazer no pg_search. Então eu tive que fazer assim.

Product.find_by_sql("(#{Product.code_starts_with('Tom').to_sql}) union (#{Product.name_starts_with('Tom').to_sql})")

Ou seja, transforme os escopos em sql, coloque colchetes em torno de cada um, junte-os e depois encontre_by_sql usando o sql gerado. É um pouco ruim, mas funciona.

Não, não me diga que posso usar "against: [: name,: code]" em pg_search, gostaria de fazer assim, mas o campo 'name' é um hstore, que o pg_search não pode manipular ainda. Portanto, o escopo pelo nome deve ser criado à mão e depois associado ao escopo pg_search.


-2
names = ["tim", "tom", "bob", "alex"]
sql_string = names.map { |t| "name = '#{t}'" }.join(" OR ")
@people = People.where(sql_string)
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.