Comment enchaîner les requêtes de portée avec OU au lieu de AND?


137

J'utilise Rails3, ActiveRecord

Je me demande simplement comment puis-je chaîner les portées avec des instructions OR plutôt que AND.

par exemple

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

Cela renvoie normalement:

name = 'John' AND lastname = 'Smith'

mais j'aimerais:

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

Réponses:


107

Vous feriez

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

À l'heure actuelle, il n'y a pas d'autre support OR par la nouvelle syntaxe AR3 (c'est-à-dire sans utiliser une gemme tierce).


21
et juste pour des raisons de sécurité, cela vaut la peine de l'exprimer comme Person.where ("name =? OR lastname =?", 'John', 'Smith')
CambridgeMike

6
Ceci est toujours applicable dans Rails 4? Rien de mieux n'est venu dans Rails 4?
Donato

11
Aucun changement dans Rails 4, mais Rails 5 sera .orintégré .
lime le

3
ce ne sont pas des portées, il ne s'agit que de 2 clauses where ou un cas de portée très spécifique. Et si je voulais enchaîner des portées plus complexes, avec des jointures par exemple.
miguelfg

2
Avec Rails 5, vous pouvez également faire quelque chose comme, Person.where ("name = 'John'"). Or (Person.where ("lastname = 'Smith'"))
shyam le


48

Utiliser ARel

t = Person.arel_table

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

1
Quelqu'un peut-il dire si ce type de requête est interrompu dans Postgres?
Olivier Lacan

ARel est censé être indépendant de la base de données.
Simon Perepelitsa

Bien que cela semble être une bonne idée, ARel n'est pas une API publique et son utilisation peut endommager votre application de manière inattendue, je vous déconseille donc de faire très attention à l'évolution de l'API ARel.
Olivier Lacan

7
@OlivierLacan Arel est une API publique, ou plus précisément, elle en expose une. Active Record ne fournit que des méthodes pratiques qui l'utilisent sous le capot, il est tout à fait valable de l'utiliser seul. Il suit le contrôle de version sémantique, donc à moins que vous ne changiez de versions majeures (3.xx => 4.xx), il n'y a pas besoin de s'inquiéter de la rupture des changements.
Grey

41

Il suffit de publier la syntaxe Array pour la même colonne OU les requêtes pour vous aider.

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

2
La question vérifie John contre le nom et Smith contre le nom de famille
steven_noble

11
@steven_noble - c'est pourquoi j'ai mis en gras «même colonne» avec la requête ou. Je peux supprimer complètement le commentaire, mais j'ai pensé que ce serait utile pour les personnes qui liront des questions "ou" interrogées sur le SO.
daino3

38

Mise à jour pour Rails4

ne nécessite pas de gemmes tierces

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) }

Ancienne réponse

ne nécessite pas de gemmes tierces

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

5
c'est vraiment la seule réponse pure à la question du PO en termes de ressources et de méthode. les autres réponses soit (a) nécessitent des API tierces ou (b) nécessitent une interpolation orou d'autres opérateurs dans un bloc de texte, dont aucun n'est reflété dans le code d'OP.
allenwlee

6
voici un article décent expliquant coderwall.com/p/dgv7ag/…
allenwlee

4
Utilisé ...where_values.join(' OR ')dans mon cas
Sash

2
Vous pouvez omettre la .tap {|sc| sc.bind_values = [a, b].map(&:bind_values) }partie si vos étendues n'ont aucune variable nécessitant une liaison.
fatuhoku

22

Vous pouvez également utiliser la gemme MetaWhere pour ne pas mélanger votre code avec des éléments SQL:

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

3
+1. Le successeur de MetaWhere est squeel par le même auteur.
Thilo

22

Au cas où quelqu'un chercherait une réponse mise à jour à celle-ci, il semble qu'il existe une pull request existante pour l'introduire dans Rails: https://github.com/rails/rails/pull/9052 .

Grâce au patch monkey de @ j-mcnally pour ActiveRecord ( https://gist.github.com/j-mcnally/250eaaceef234dd8971b ), vous pouvez effectuer les opérations suivantes:

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

La possibilité d'enchaîner les oscilloscopes avec 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) }

Ensuite, vous pouvez trouver toutes les personnes avec un prénom ou un nom de famille ou dont le parent avec le nom de famille

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

Ce n'est pas le meilleur exemple d'utilisation de ceci, mais simplement essayer de l'adapter à la question.


2
De quelle version ou de quels rails avez-vous besoin pour cela?
Sash

3
La dernière discussion et la demande d'extraction qui en fera le noyau de Rails peuvent être trouvées ici: github.com/rails/rails/pull/16052 Rails 5.0 est destiné à la sortie officielle de la .orfonctionnalité de chaîne. J'ai pu utiliser le patch monkey que j'ai mentionné sur Rails> = 3.2; Cependant, vous voudrez peut-être attendre que Rails 5 introduise des modifications de longue date dans votre code.
codenamev

1
Oui, il a été fusionné et publié avec Rails 5.
amoebe

10

Pour moi (Rails 4.2.5) cela ne fonctionne que comme ceci:

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

8

Ce serait un bon candidat pour MetaWhere si vous utilisez Rails 3.0+, mais cela ne fonctionne pas sur Rails 3.1. Vous voudrez peut-être essayer Squeel à la place. Il est fait par le même auteur. Voici comment vous exécuteriez une chaîne basée sur OR:

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

Vous pouvez mélanger et assortir AND / OR, parmi beaucoup d'autres choses géniales .


8

Une version mise à jour de Rails / ActiveRecord peut prendre en charge cette syntaxe de manière native. Cela ressemblerait à:

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

Comme indiqué dans cette demande d'extraction https://github.com/rails/rails/pull/9052

Pour l'instant, il suffit de s'en tenir aux éléments suivants:

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

5
Non pris en charge dans Rails 4.2.5
fatuhoku

1
Foo.where(foo: 'bar1').or.where(bar: 'bar2')ne marche pas. Cela devrait être Foo.where (foo: 'bar1'). Or.where (Foo.where (bar: 'bar2'))
Ana María Martínez Gómez

6

Rails 4 + Lunette + 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

J'ai essayé de chaîner des portées nommées avec .or et pas de chance, mais cela a fonctionné pour trouver quoi que ce soit avec ces booléens. Génère du SQL comme

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

4

Rails 4

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

4

Si vous cherchez à fournir une portée (au lieu de travailler explicitement sur l'ensemble de données), voici ce que vous devez faire avec 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

C'est correct, mais une réponse plus littérale que la même chose que stackoverflow.com/a/42894753/1032882 techniquement.
RudyOnRails

3

Si vous ne pouvez pas écrire la clause where manuellement pour inclure l'instruction "ou" (c'est-à-dire que vous souhaitez combiner deux portées), vous pouvez utiliser union:

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

(source: Union des requêtes ActiveRecord )

Ceci retournera tous les enregistrements correspondant à l'une ou l'autre des requêtes. Cependant, cela renvoie un tableau, pas un arel. Si vous voulez vraiment retourner un arel, vous vérifiez ce point essentiel: https://gist.github.com/j-mcnally/250eaaceef234dd8971b .

Cela fera le travail, tant que cela ne vous dérange pas de patcher les rails de monkey.


2

Voir également ces questions connexes: ici , ici et ici

Pour les rails 4, basé sur cet article et cette réponse originale

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

Notez la mise en garde de l'article à la fin:

Rails 4.1+

Rails 4.1 traite default_scope comme une portée normale. La portée par défaut (si vous en avez) est incluse dans le résultat where_values ​​et inject (: ou) ajoutera une instruction ou entre la portée par défaut et votre emplacement. C'est mauvais.

Pour résoudre ce problème, il vous suffit de supprimer la portée de la requête.


1

C'est un moyen très pratique et cela fonctionne bien dans 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

la squeelgemme fournit un moyen incroyablement simple d'accomplir cela (avant cela, j'utilisais quelque chose comme la méthode de @ coloradoblue):

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

0

Donc, la réponse à la question d'origine, pouvez-vous joindre des portées avec 'ou' au lieu de 'et' semble être "non, vous ne pouvez pas". Mais vous pouvez coder manuellement une portée ou une requête complètement différente qui fait le travail, ou utiliser un cadre différent d'ActiveRecord, par exemple MetaWhere ou Squeel. Pas utile dans mon cas

Je suis 'or' une portée générée par pg_search, qui fait un peu plus que sélectionner, elle inclut la commande par ASC, ce qui fait le désordre d'une union propre. Je veux le 'ou' avec une portée artisanale qui fait des choses que je ne peux pas faire dans pg_search. J'ai donc dû le faire comme ça.

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

C'est à dire transformer les portées en sql, mettre des crochets autour de chacun d'eux, les unir ensemble, puis find_by_sql en utilisant le sql généré. C'est un peu nul, mais ça marche.

Non, ne me dites pas que je peux utiliser "against: [: name,: code]" dans pg_search, j'aimerais le faire comme ça, mais le champ 'name' est un hstore, que pg_search ne peut pas gérer encore. Ainsi, la portée par nom doit être fabriquée à la main puis fusionnée avec la portée pg_search.


-2
names = ["tim", "tom", "bob", "alex"]
sql_string = names.map { |t| "name = '#{t}'" }.join(" OR ")
@people = People.where(sql_string)
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.