ruby-on-rails - inner - join activerecord rails
ActiveRecord Query Union (11)
He escrito un par de consultas complejas (al menos para mí) con Ruby en la interfaz de consulta de 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})
Ambas consultas funcionan bien por sí mismas. Ambos devuelven objetos Post. Me gustaría combinar estas publicaciones en una sola ActiveRelation. Dado que podría haber cientos de miles de publicaciones en algún momento, esto debe hacerse a nivel de la base de datos. Si fuera una consulta de MySQL, simplemente podría usar el operador de UNION
. ¿Alguien sabe si puedo hacer algo similar con la interfaz de consulta de RoR?
¿Podría usar un OR en lugar de UNIÓN?
Entonces podrías hacer algo como:
Post.joins(:news => :watched, :post_topic_relationships => {:topic => :watched})
.where("watched.user_id = :id OR topic_watched.user_id = :id", :id => id)
(Dado que se une a la tabla observada dos veces, no estoy muy seguro de cuáles serán los nombres de las tablas para la consulta)
Dado que hay muchas combinaciones, también podría ser bastante pesado en la base de datos, pero podría optimizarse.
Aquí hay un pequeño y rápido módulo que escribí que le permite UNION múltiples alcances. También devuelve los resultados como una instancia 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
Aquí está la esencia: https://gist.github.com/tlowrimore/5162327
Editar:
Como se solicitó, aquí hay un ejemplo de cómo funciona 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
Elliot Nelson respondió bien, excepto en el caso en que algunas de las relaciones están vacías. Haría algo así:
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
En base a la respuesta de Olives, encontré otra solución a este problema. Se siente un poco como un truco, pero devuelve una instancia de ActiveRelation
, que es lo que buscaba en primer lugar.
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)
Todavía agradecería si alguien tiene alguna sugerencia para optimizar esto o mejorar el rendimiento, porque básicamente está ejecutando tres consultas y se siente un poco redundante.
En un caso similar, sumé dos matrices y utilicé Kaminari:paginate_array()
. Muy buena y funcional solución. No pude usar where()
, porque necesito sumar dos resultados con diferente order()
en la misma tabla.
Hay una gema active_record_union. Podría ser útil
Con ActiveRecordUnion, podemos hacer:
las publicaciones del usuario actual (borrador) y todas las publicaciones de cualquier usuario current_user.posts.union (Post.published) que es equivalente al siguiente SQL:
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
Podría decirse que esto mejora la legibilidad, pero no necesariamente el rendimiento:
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
Este método devuelve una relación ActiveRecord ::, por lo que podría llamarlo así:
my_posts.order("watched_item_type, post.id DESC")
Qué tal si...
def union(scope1, scope2)
ids = scope1.pluck(:id) + scope2.pluck(:id)
where(id: ids.uniq)
end
Simplemente ejecutaría las dos consultas que necesita y combinaría las matrices de registros que se devuelven:
@posts = watched_news_posts + watched_topics_posts
O, al menos, pruébelo. ¿Crees que la combinación de array en ruby será demasiado lenta? Mirando las consultas sugeridas para resolver el problema, no estoy convencido de que haya una diferencia significativa en el rendimiento.
También me he encontrado con este problema, y ahora mi estrategia ir es generar SQL (a mano o usando to_sql
en un alcance existente) y luego pegarlo en la cláusula from
. No puedo garantizar que sea más eficiente que su método aceptado, pero es relativamente fácil para los ojos y le devuelve un objeto 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")
También puede hacer esto con dos modelos diferentes, pero debe asegurarse de que ambos "tengan el mismo aspecto" dentro de UNION. Puede usar select
en ambas consultas para asegurarse de que producirán las mismas columnas.
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")
También puede usar la gema active_record_union Brian Hempel , que amplía ActiveRecord
con un método de union
para ámbitos.
Su consulta sería así:
Post.joins(:news => :watched).
where(:watched => {:user_id => id}).
union(Post.joins(:post_topic_relationships => {:topic => :watched}
.where(:watched => {:user_id => id}))
Esperemos que eventualmente este se fusione en ActiveRecord
algún día.