He escrito un par de consultas complejas (al menos para mí) con la interfaz de consulta 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})
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 MySQL, simplemente podría usar el UNION
operador. ¿Alguien sabe si puedo hacer algo similar con la interfaz de consulta de RoR?
ruby-on-rails
activerecord
union
active-relation
LandonSchropp
fuente
fuente
Post.watched_news_posts.watched_topic_posts
. Es posible que deba enviar parámetros a los ámbitos para cosas como:user_id
y:topic
.find_by_sql("#{watched_news_posts.to_sql} UNION #{watched_topic_posts.to_sql}")
. No lo he probado, así que déjame saber cómo te va si lo intentas. Además, probablemente haya alguna funcionalidad ARel que funcione.find_by_sql
no se pueden usar con otras consultas encadenables, lo que significa que ahora tengo que reescribir mis filtros y consultas will_paginate también. ¿Por qué ActiveRecord no admite unaunion
operación?Respuestas:
Aquí hay un pequeño módulo rápido que escribí que le permite UNIONAR múltiples ámbitos. 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:
Según lo solicitado, 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
fuente
También me he encontrado con este problema, y ahora mi estrategia de referencia es generar SQL (a mano o con
to_sql
un alcance existente) y luego pegarlo en lafrom
cláusula. No puedo garantizar que sea más eficiente que su método aceptado, pero es relativamente agradable a la vista 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 "se vean iguales" dentro de UNION; puede usar
select
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")
fuente
#or
con mi proyecto Rails 4. Suponiendo el mismo modelo:klass.from("(#{to_sql} union #{other_relation.to_sql}) as #{table_name}")
Según la respuesta de Olives, se me ocurrió 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 que alguien tenga alguna sugerencia para optimizar esto o mejorar el rendimiento, porque básicamente se trata de ejecutar tres consultas y se siente un poco redundante.
fuente
También puede usar la gema active_record_union de Brian Hempel que se extiende con un método para ámbitos.
ActiveRecord
union
Tu 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}))
Con suerte, esto finalmente se fusionará en
ActiveRecord
algún día.fuente
Qué tal si...
def union(scope1, scope2) ids = scope1.pluck(:id) + scope2.pluck(:id) where(id: ids.uniq) end
fuente
pluck
llamada es una consulta en sí misma..order
o.paginate
... Mantiene las clases orm¿Podrías usar un quirófano en lugar de un UNION?
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 uniones, también puede ser bastante pesado en la base de datos, pero es posible que se pueda optimizar.
fuente
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 un ActiveRecord :: Relation, por lo que podría llamarlo así:
my_posts.order("watched_item_type, post.id DESC")
fuente
Hay una gema active_record_union. Podría ser útil
https://github.com/brianhempel/active_record_union
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
fuente
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ébalo. ¿Crees que la combinación de matrices en ruby será demasiado lenta? Al analizar las consultas sugeridas para solucionar el problema, no estoy convencido de que haya una diferencia de rendimiento tan significativa.
fuente
En un caso similar, sumé dos matrices y usé
Kaminari:paginate_array()
. Solución muy bonita y funcional. No pude usarwhere()
, porque necesito sumar dos resultados con diferentesorder()
en la misma tabla.fuente
Menos problemas y más fácil de seguir:
def union_scope(*scopes) scopes[1..-1].inject(where(id: scopes.first)) { |all, scope| all.or(where(id: scope)) } end
Así que al final:
fuente
scopes.drop(1).reduce(where(id: scopes.first)) { |query, scope| query.or(where(id: scope)) }
¡Gracias!Elliot Nelson respondió bien, excepto en el caso de que algunas de las relaciones estén vacías. Yo haría algo como eso:
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
fuente
Así es como me uní a consultas SQL usando UNION en mi propia aplicación ruby on rails.
Puede utilizar lo siguiente como inspiración en su propio código.
class Preference < ApplicationRecord scope :for, ->(object) { where(preferenceable: object) } end
A continuación se muestra la UNIÓN donde uní los ámbitos.
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
fuente