web-dev-qa-db-fra.com

ActiveRecord Arel OR condition

Comment pouvez-vous combiner 2 conditions différentes en utilisant logique OR au lieu de AND?

REMARQUE: 2 conditions sont générées comme des étendues Rails et ne peuvent pas être facilement changées en quelque chose comme where("x or y") directement.

Exemple simple:

admins = User.where(:kind => :admin)
authors = User.where(:kind => :author)

Il est facile d'appliquer la condition AND (qui dans ce cas particulier n'a aucun sens):

(admins.merge authors).to_sql
#=> select ... from ... where kind = 'admin' AND kind = 'author'

Mais comment pouvez-vous produire la requête suivante avec 2 relations Arel différentes déjà disponibles?

#=> select ... from ... where kind = 'admin' OR kind = 'author'

Il semble ( selon le readme d'Arel ):

L'opérateur OR n'est pas encore pris en charge

Mais j'espère que cela ne s'applique pas ici et je m'attends à écrire quelque chose comme:

(admins.or authors).to_sql
56

Je suis un peu en retard à la fête, mais voici la meilleure suggestion que j'ai pu faire:

admins = User.where(:kind => :admin)
authors = User.where(:kind => :author)

admins = admins.where_values.reduce(:and)
authors = authors.where_values.reduce(:and)

User.where(admins.or(authors)).to_sql
# => "SELECT \"users\".* FROM \"users\"  WHERE ((\"users\".\"kind\" = 'admin' OR \"users\".\"kind\" = 'author'))"
70
jswanner

Les requêtes ActiveRecord sont ActiveRecord::Relation objets (qui ne supportent pas exaspérément or), pas les objets Arel (qui le font).

[ [~ # ~] mise à jour [~ # ~] : à partir de Rails 5, "ou" est pris en charge dans ActiveRecord::Relation; voir https://stackoverflow.com/a/33248299/190135 ]

Mais heureusement, leur méthode where accepte les objets de requête ARel. Donc si User < ActiveRecord::Base...

users = User.arel_table
query = User.where(users[:kind].eq('admin').or(users[:kind].eq('author')))

query.to_sql montre maintenant le rassurant:

SELECT "users".* FROM "users"  WHERE (("users"."kind" = 'admin' OR "users"."kind" = 'author'))

Pour plus de clarté, vous pouvez extraire certaines variables temporaires de requête partielle:

users = User.arel_table
admin = users[:kind].eq('admin')
author = users[:kind].eq('author')
query = User.where(admin.or(author))

Et naturellement, une fois que vous avez la requête, vous pouvez utiliser query.all pour exécuter l'appel de base de données réel.

91
AlexChaffee

À partir de la page d'arel réelle :

L'opérateur OR fonctionne comme ceci:

users.where(users[:name].eq('bob').or(users[:age].lt(25)))
9
Dave Newton

Depuis Rails 5, nous avons ActiveRecord::Relation#or, vous permettant de faire ceci:

User.where(kind: :author).or(User.where(kind: :admin))

... qui se traduit dans le sql que vous attendez:

>> puts User.where(kind: :author).or(User.where(kind: :admin)).to_sql
SELECT "users".* FROM "users" WHERE ("users"."kind" = 'author' OR "users"."kind" = 'admin')
8
pje

J'ai rencontré le même problème en recherchant une alternative activerecord au #any_of De mongoid.

La réponse @jswanner est bonne, mais ne fonctionnera que si les paramètres where sont un Hash:

> User.where( email: 'foo', first_name: 'bar' ).where_values.reduce( :and ).method( :or )                                                
=> #<Method: Arel::Nodes::And(Arel::Nodes::Node)#or>

> User.where( "email = 'foo' and first_name = 'bar'" ).where_values.reduce( :and ).method( :or )                                         
NameError: undefined method `or' for class `String'

Pour pouvoir utiliser à la fois des chaînes et des hachages, vous pouvez utiliser ceci:

q1 = User.where( "email = 'foo'" )
q2 = User.where( email: 'bar' )
User.where( q1.arel.constraints.reduce( :and ).or( q2.arel.constraints.reduce( :and ) ) )

En effet, c'est moche, et vous ne voulez pas l'utiliser quotidiennement. Voici une implémentation de #any_of Que j'ai faite: https://Gist.github.com/oelmekki/5396826

Il a laissé faire ça:

> q1 = User.where( email: 'foo1' ); true                                                                                                 
=> true

> q2 = User.where( "email = 'bar1'" ); true                                                                                              
=> true

> User.any_of( q1, q2, { email: 'foo2' }, "email = 'bar2'" )
User Load (1.2ms)  SELECT "users".* FROM "users" WHERE (((("users"."email" = 'foo1' OR (email = 'bar1')) OR "users"."email" = 'foo2') OR (email = 'bar2')))

Edit: depuis lors, j'ai publié n joyau pour aider à construire OR requêtes .

3
kik

Créez simplement une étendue pour votre condition OR:

scope :author_or_admin, where(['kind = ? OR kind = ?', 'Author', 'Admin'])
1
Unixmonkey

En utilisant SmartTuple cela va ressembler à ceci:

tup = SmartTuple.new(" OR ")
tup << {:kind => "admin"}
tup << {:kind => "author"}
User.where(tup.compile)

OR

User.where((SmartTuple.new(" OR ") + {:kind => "admin"} + {:kind => "author"}).compile)

Vous pouvez penser que je suis biaisé, mais je considère toujours que les opérations de structure de données traditionnelles sont beaucoup plus claires et plus pratiques que le chaînage de méthodes dans ce cas particulier.

0
Alex Fortuna

Pour étendre réponse jswanner (qui est en fait une solution géniale et m'a aidé) pour googler les gens:

vous pouvez appliquer une portée comme celle-ci

scope :with_owner_ids_or_global, lambda{ |owner_class, *ids|
  with_ids = where(owner_id: ids.flatten).where_values.reduce(:and)
  with_glob = where(owner_id: nil).where_values.reduce(:and)
  where(owner_type: owner_class.model_name).where(with_ids.or( with_glob ))
}

User.with_owner_ids_or_global(Developer, 1, 2)
# =>  ...WHERE `users`.`owner_type` = 'Developer' AND ((`users`.`owner_id` IN (1, 2) OR `users`.`owner_id` IS NULL))
0
equivalent8