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 toutes seules. Les deux renvoient des objets Post. Je voudrais combiner ces articles en un seul ActiveRelation. Comme il pourrait y avoir des centaines de milliers de messages à un moment donné, cela doit être fait au niveau de la base de données. 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 qui vous permet de fédérer plusieurs portées. Il renvoie également les résultats en tant qu'instance de 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 le Gist: https://Gist.github.com/tlowrimore/5162327
Comme demandé, voici un exemple de la manière dont UnionScope fonctionne:
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 ma stratégie consiste maintenant à générer du code SQL (manuellement ou à l'aide de to_sql
sur une portée existante), puis à le coller dans la clause from
. Je ne peux pas garantir que ce soit plus efficace que votre méthode acceptée, mais il est relativement facile à regarder et vous rend un objet 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 "se ressemblent" dans UNION. Vous pouvez utiliser select
pour les deux requêtes afin de vous assurer qu'elles 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")
Basé sur la réponse d'Olives, j'ai trouvé une autre solution à ce problème. Cela ressemble un peu à un bidouillage, mais il retourne une instance de ActiveRelation
, ce que je recherchais au départ.
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 toujours si quelqu'un a des suggestions pour optimiser cela ou améliorer les performances, car il exécute 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 Brian Hempel / s active_record_union gem qui étend ActiveRecord
avec une méthode union
pour les portées.
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}))
Espérons que cela finira par être fusionné avec ActiveRecord
un jour.
Pourriez-vous utiliser un OR 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 rejoignez la table surveillée deux fois, je ne suis pas trop sûr du nom des tables pour la requête)
Comme il y a beaucoup de jointures, la base de données peut également être lourde, mais elle peut être optimisée.
On peut dire que cela améliore la lisibilité, mais pas nécessairement les performances:
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 une gem active_record_union . Peut-ê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 qui que ce soit current_user.posts.union (Post.published) Ce qui équivaut au 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 d'enregistrements qui sont retournés:
@posts = watched_news_posts + watched_topics_posts
Ou, au moins, testez-le. Pensez-vous que la combinaison de tableaux en Ruby sera beaucoup trop lente? En examinant les questions suggérées pour résoudre le problème, je ne suis pas convaincu qu'il y aura une différence de performance aussi significative.
Dans un cas similaire, j'ai résumé deux tableaux et utilisé Kaminari:paginate_array()
. Très belle et solution de travail. Je n'ai pas pu utiliser where()
, car je dois additionner deux résultats avec order()
différent sur la même table.
Elliot Nelson a bien répondu, sauf dans le cas où certaines 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
Voici comment j'ai joint des requêtes SQL en utilisant UNION sur ma propre application Ruby sur Rails.
Vous pouvez utiliser le texte ci-dessous pour vous inspirer de votre propre code.
class Preference < ApplicationRecord
scope :for, ->(object) { where(preferenceable: object) }
end
Ci-dessous l’UNION où j’ai joint les champs d’application.
def zone_preferences
zone = Zone.find params[:zone_id]
zone_sql = Preference.for(zone).to_sql
region_sql = Preference.for(zone.region).to_sql
operator_sql = Preference.for(Operator.current).to_sql
Preference.from("(#{zone_sql} UNION #{region_sql} UNION #{operator_sql}) AS preferences")
end