Combinez deux objets ActiveRecord :: Relation

174

Supposons que j'ai les deux objets suivants:

first_name_relation = User.where(:first_name => 'Tobias') # ActiveRecord::Relation
last_name_relation  = User.where(:last_name  => 'Fünke') # ActiveRecord::Relation

est-il possible de combiner les deux relations pour produire un ActiveRecord::Relationobjet contenant les deux conditions?

Remarque: je suis conscient que je peux enchaîner les endroits pour obtenir ce comportement, ce qui m'intéresse vraiment est le cas où j'ai deux ActiveRecord::Relationobjets séparés .

Patrick Klingemann
la source
Peut être utile: ActiveRecord OU requête Hash notation
potashin

Réponses:

202

Si vous souhaitez combiner en utilisant AND(intersection), utilisez merge:

first_name_relation.merge(last_name_relation)

Si vous souhaitez combiner en utilisant OR(union), utilisez :or

first_name_relation.or(last_name_relation)

Uniquement dans ActiveRecord 5+; pour 4.2 installez le where-or backport.

Andrew Marshall
la source
et si je veux que le résultat soit un ActiveRecord::Relationtype?
New Alexandria
1
@NewAlexandria Oui, dans tous les cas, Rails vous ment sur la classe de l'objet.
Andrew Marshall
77
Existe-t-il une version "OU" de la fusion?
Arcolye
8
@minohimself Probablement parce que c'est le résultat réel de la fusion de vos deux relations. Notez que mergec'est une intersection, pas une union.
Andrew Marshall
5
Pour référence future, la #orméthode a été ajoutée à ActiveRecord :: Relation en janvier 2015, et elle fera partie de Rails 5.0 , qui sera disponible fin 2015. Elle permet d'utiliser l'opérateur OR pour combiner les clauses WHERE ou HAVING. Vous pouvez commander HEAD si vous en avez besoin avant la sortie officielle. Voir Merge Pull Request # 16052 @Arcolye @AndrewMarshall @ Aldo-xoen-Giambelluca
Claudio Floreani
37

Les objets de relation peuvent être convertis en tableaux. Cela annule la possibilité d'utiliser des méthodes ActiveRecord par la suite, mais je n'en ai pas eu besoin. J'ai fait ça:

name_relation = first_name_relation + last_name_relation

Ruby 1.9, rails 3.2

au milieu
la source
Mise à jour: dommage que cela ne fusionne pas par date
Daniel Morris
68
Il n'agit pas comme un tableau, il convertit votre relation en tableau. Ensuite, vous ne pouvez plus utiliser les méthodes ActiveRecord dessus comme .where () ...
Augustin Riedinger
1
Si vous ne vous souciez pas que le type de retour soit une relation, vous pouvez combiner plusieurs résultats avec first_name_relation | last_name_relation. Le "|" L'opérateur travaille également sur plusieurs relations. La valeur de résultat est un tableau.
MichaelZ
11
La question originale était: "est-il possible de combiner les deux relations pour produire un objet ActiveRecord :: Relation contenant les deux conditions?" Cette réponse renvoie un tableau ...
courtsimas
C'est une excellente solution pour de nombreux cas. Si vous n'avez besoin que d'une énumération d'enregistrements à parcourir (par exemple, vous ne vous souciez pas de savoir si c'est un tableau ou un ActiveRecord :: Relation) et que vous ne vous souciez pas de la surcharge de deux requêtes, cela résout le problème avec une syntaxe simple et évidente. J'aimerais pouvoir +2!
David Hempy
21

merge ne fonctionne pas comme OR . C'est simplement l'intersection (AND )

J'ai eu du mal avec ce problème pour combiner les objets ActiveRecord :: Relation en un seul et je n'ai trouvé aucune solution de travail pour moi.

Au lieu de chercher la bonne méthode pour créer une union à partir de ces deux ensembles, je me suis concentré sur l'algèbre des ensembles. Vous pouvez le faire de différentes manières en utilisant la loi de De Morgan

ActiveRecord fournit la méthode de fusion (AND) et vous pouvez également utiliser la méthode not ou none_of (NOT).

search.where.none_of(search.where.not(id: ids_to_exclude).merge(search.where.not("title ILIKE ?", "%#{query}%")))

Vous avez ici (A u B) '= A' ^ B '

MISE À JOUR: La solution ci-dessus est bonne pour les cas plus complexes. Dans votre cas, un peu comme ça suffira:

User.where('first_name LIKE ? OR last_name LIKE ?', 'Tobias', 'Fünke')
Tomasz Jaśkiewicz
la source
15

J'ai pu y parvenir, même dans de nombreuses situations étranges, en utilisant Arel intégré à Rails .

User.where(
  User.arel_table[:first_name].eq('Tobias').or(
    User.arel_table[:last_name].eq('Fünke')
  )
)

Cela fusionne les deux relations ActiveRecord en utilisant Arel ou .


Merge, comme cela a été suggéré ici, n'a pas fonctionné pour moi. Il a supprimé le 2ème ensemble d'objets de relation des résultats.

6 pieds Dan
la source
2
C'est la meilleure réponse, IMO. Cela fonctionne parfaitement pour les requêtes de type OR.
thekingoftruth
Merci d'avoir souligné cela, m'a beaucoup aidé. Les autres réponses ne fonctionnent que pour un petit sous-ensemble de champs, mais pour plus de dix mille identifiants dans l'instruction IN, les performances peuvent être très mauvaises.
onetwopunch
1
@KeesBriggs - ce n'est pas vrai. Je l'ai utilisé dans toutes les versions de Rails 4.
6ft Dan
14

Il existe une gemme appelée active_record_union qui pourrait être ce que vous recherchez.

Son exemple d'utilisation est le suivant:

current_user.posts.union(Post.published)
current_user.posts.union(Post.published).where(id: [6, 7])
current_user.posts.union("published_at < ?", Time.now)
user_1.posts.union(user_2.posts).union(Post.published)
user_1.posts.union_all(user_2.posts)
écho
la source
Merci d'avoir partagé. Même quatre ans après votre publication, cela a été la seule solution pour moi de créer une union à partir de ransacket acts-as-taggable-ontout en gardant mes ActiveRecordinstances intactes.
Benjamin Bojko
Lors de l'utilisation de cette gemme, vous ne pouvez pas obtenir un ActiveRecord :: Relation retourné par l'appel union () si vous avez des étendues définies sur le modèle. Consultez l'état de l'Union dans ActiveRecord dans le fichier Readme.
dechimp
9

C'est ainsi que je l'ai "géré" si vous utilisez pluck pour obtenir un identifiant pour chacun des enregistrements, joindre les tableaux ensemble et enfin faire une requête pour ces identifiants joints:

  transaction_ids = @club.type_a_trans.pluck(:id) + @club.type_b_transactions.pluck(:id) + @club.type_c_transactions.pluck(:id)
  @transactions = Transaction.where(id: transaction_ids).limit(100)
Daveomcd
la source
6

Si vous avez un tableau de relations activerecord et que vous souhaitez toutes les fusionner, vous pouvez faire

array.inject(:merge)
stevenspiel
la source
array.inject(:merge)n'a pas fonctionné pour le tableau des relations activerecord dans les rails 5.1.4. Mais array.flatten!fait.
zhisme
0

Brute force il:

first_name_relation = User.where(:first_name => 'Tobias') # ActiveRecord::Relation
last_name_relation  = User.where(:last_name  => 'Fünke') # ActiveRecord::Relation

all_name_relations = User.none
first_name_relation.each do |ar|
  all_name_relations.new(ar)
end
last_name_relation.each do |ar|
  all_name_relations.new(ar)
end
Richard Grossman
la source
"NoMethodError (méthode non définie` stringify_keys 'pour # <MyModel: 0x ...) dans Rails3, même après avoir changé MyModel.none en MyModel.where (1 = 2), car Rails3 n'a pas la méthode' none '.
JosephK