Union de requête ActiveRecord

J’ai écrit quelques requêtes complexes (du moins pour moi) avec l’interface de requête de Ruby on Rail:

watched_news_posts = Post.joins(:news => :watched).where(:watched => {:user_id => id}) watched_topic_posts = Post.joins(:post_topic_relationships => {:topic => :watched}).where(:watched => {:user_id => id}) 

Ces deux requêtes fonctionnent bien par elles-mêmes. Les deux renvoient des objects Post. Je voudrais combiner ces messages en un seul ActiveRelation. Comme il pourrait y avoir des centaines de milliers de publications à un moment donné, cela doit être fait au niveau de la firebase database. S’il s’agissait d’une requête MySQL, je pourrais simplement utiliser l’opérateur UNION . Est-ce que quelqu’un sait si je peux faire quelque chose de similaire avec l’interface de requête de RoR?

Voici un petit module rapide que j’ai écrit et qui vous permet d’UNION de multiples scopes. Il renvoie également les résultats sous la forme d’une instance d’ActiveRecord :: Relation.

 module ActiveRecord::UnionScope def self.included(base) base.send :extend, ClassMethods end module ClassMethods def union_scope(*scopes) id_column = "#{table_name}.id" sub_query = scopes.map { |s| s.select(id_column).to_sql }.join(" UNION ") where "#{id_column} IN (#{sub_query})" end end end 

Voici l’essentiel: https://gist.github.com/tlowrimore/5162327

Modifier:

Comme demandé, voici un exemple de la façon dont fonctionne UnionScope:

 class Property < ActiveRecord::Base include ActiveRecord::UnionScope # some silly, contrived scopes scope :active_nearby, -> { where(active: true).where('distance < = 25') } scope :inactive_distant, -> { where(active: false).where('distance >= 200') } # A union of the aforementioned scopes scope :active_near_and_inactive_distant, -> { union_scope(active_nearby, inactive_distant) } end 

J’ai également rencontré ce problème, et maintenant ma stratégie est de générer du SQL (à la main ou en utilisant to_sql sur une étendue existante), puis de le coller dans la clause from . Je ne peux pas garantir que ce soit plus efficace que votre méthode acceptée, mais c’est relativement facile pour les yeux et vous offre un object ARel normal.

 watched_news_posts = Post.joins(:news => :watched).where(:watched => {:user_id => id}) watched_topic_posts = Post.joins(:post_topic_relationships => {:topic => :watched}).where(:watched => {:user_id => id}) Post.from("(#{watched_news_posts.to_sql} UNION #{watched_topic_posts.to_sql}) AS posts") 

Vous pouvez également le faire avec deux modèles différents, mais vous devez vous assurer qu’ils sont tous les deux identiques dans l’UNION – vous pouvez utiliser select dans les deux requêtes pour vous assurer qu’ils produiront les mêmes colonnes.

 topics = Topic.select('user_id AS author_id, description AS body, created_at') comments = Comment.select('author_id, body, created_at') Comment.from("(#{comments.to_sql} UNION #{topics.to_sql}) AS comments") 

Sur la base de la réponse d’Olive, j’ai trouvé une autre solution à ce problème. Cela ressemble un peu à un hack, mais il retourne une instance d’ ActiveRelation , ce que je recherchais en premier lieu.

 Post.where('posts.id IN ( SELECT post_topic_relationships.post_id FROM post_topic_relationships INNER JOIN "watched" ON "watched"."watched_item_id" = "post_topic_relationships"."topic_id" AND "watched"."watched_item_type" = "Topic" WHERE "watched"."user_id" = ? ) OR posts.id IN ( SELECT "posts"."id" FROM "posts" INNER JOIN "news" ON "news"."id" = "posts"."news_id" INNER JOIN "watched" ON "watched"."watched_item_id" = "news"."id" AND "watched"."watched_item_type" = "News" WHERE "watched"."user_id" = ? )', id, id) 

J’apprécierais encore si quelqu’un a des suggestions pour optimiser cela ou améliorer la performance, car il exécute essentiellement trois requêtes et se sent un peu redondant.

Que diriez-vous…

 def union(scope1, scope2) ids = scope1.pluck(:id) + scope2.pluck(:id) where(id: ids.uniq) end 

Vous pouvez également utiliser la gem active_record_union de Brian Hempel qui étend ActiveRecord avec une méthode union pour les scopes.

Votre requête serait comme ceci:

 Post.joins(:news => :watched). where(:watched => {:user_id => id}). union(Post.joins(:post_topic_relationships => {:topic => :watched} .where(:watched => {:user_id => id})) 

J’espère que cela sera finalement fusionné dans ActiveRecord un jour.

Pourriez-vous utiliser un OU au lieu d’un UNION?

Ensuite, vous pourriez faire quelque chose comme:

 Post.joins(:news => :watched, :post_topic_relationships => {:topic => :watched}) .where("watched.user_id = :id OR topic_watched.user_id = :id", :id => id) 

(Puisque vous vous joignez à la table surveillée deux fois, je ne suis pas sûr de savoir quels seront les noms des tables pour la requête)

Comme il y a beaucoup de jointures, cela peut aussi être très lourd pour la firebase database, mais cela peut être optimisé.

Sans doute, cela améliore la lisibilité, mais pas nécessairement la performance:

 def my_posts Post.where < <-SQL, self.id, self.id posts.id IN (SELECT post_topic_relationships.post_id FROM post_topic_relationships INNER JOIN watched ON watched.watched_item_id = post_topic_relationships.topic_id AND watched.watched_item_type = "Topic" AND watched.user_id = ? UNION SELECT posts.id FROM posts INNER JOIN news ON news.id = posts.news_id INNER JOIN watched ON watched.watched_item_id = news.id AND watched.watched_item_type = "News" AND watched.user_id = ?) SQL end 

Cette méthode retourne un ActiveRecord :: Relation, vous pouvez donc l'appeler comme ceci:

 my_posts.order("watched_item_type, post.id DESC") 

Il y a un joyau active_record_union. Pourrait être utile

https://github.com/brianhempel/active_record_union

Avec ActiveRecordUnion, nous pouvons faire:

les messages (brouillon) de l’utilisateur actuel et tous les messages publiés de n’importe qui current_user.posts.union (Post.published) Qui équivaut au code SQL suivant:

SELECT "posts".* FROM ( SELECT "posts".* FROM "posts" WHERE "posts"."user_id" = 1 UNION SELECT "posts".* FROM "posts" WHERE (published_at < '2014-07-19 16:04:21.918366') ) posts

Je voudrais juste exécuter les deux requêtes dont vous avez besoin et combiner les tableaux des enregistrements renvoyés:

 @posts = watched_news_posts + watched_topics_posts 

Ou, au moins le tester. Pensez-vous que la combinaison de tableaux dans Ruby sera beaucoup trop lente? En regardant les questions suggérées pour contourner le problème, je ne suis pas convaincu qu’il y aura une différence de performance significative.

Dans un cas similaire, j’ai résumé deux tableaux et utilisé Kaminari:paginate_array() . Très belle solution de travail. Je n’ai pas pu utiliser where() , car je dois résumer deux résultats avec un order() différent order() sur la même table.

Elliot Nelson a bien répondu, sauf dans le cas où certaines des relations sont vides. Je ferais quelque chose comme ça:

 def union_2_relations(relation1,relation2) sql = "" if relation1.any? && relation2.any? sql = "(#{relation1.to_sql}) UNION (#{relation2.to_sql}) as #{relation1.klass.table_name}" elsif relation1.any? sql = relation1.to_sql elsif relation2.any? sql = relation2.to_sql end relation1.klass.from(sql) 

fin