rails inner association active ruby-on-rails activerecord union activerelation

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

active_record_union

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.