Napisałem kilka złożonych zapytań (przynajmniej do mnie) za pomocą interfejsu zapytań 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})
Oba te zapytania działają dobrze samodzielnie. Oba zwracają obiekty Post. Chciałbym połączyć te posty w jedną ActiveRelation. Ponieważ w pewnym momencie mogą pojawić się setki tysięcy postów, należy to zrobić na poziomie bazy danych. Gdyby to było zapytanie MySQL, mógłbym po prostu użyć UNION
operatora. Czy ktoś wie, czy mogę zrobić coś podobnego z interfejsem zapytań RoR?
ruby-on-rails
activerecord
union
active-relation
LandonSchropp
źródło
źródło
Post.watched_news_posts.watched_topic_posts
. Może być konieczne wysłanie parametrów do zakresów dla rzeczy takich jak:user_id
i:topic
.find_by_sql("#{watched_news_posts.to_sql} UNION #{watched_topic_posts.to_sql}")
. Nie testowałem tego, więc daj mi znać, jeśli spróbujesz. Prawdopodobnie jest też funkcjonalność ARel, która by działała.find_by_sql
nie można ich używać z innymi zapytaniami, które można łączyć w łańcuch, co oznacza, że muszę teraz przepisać moje filtry i zapytania will_paginate. Dlaczego ActiveRecord nie obsługujeunion
operacji?Odpowiedzi:
Oto krótki mały moduł, który napisałem, który umożliwia UNION wiele zakresów. Zwraca również wyniki jako instancję 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
Oto sedno: https://gist.github.com/tlowrimore/5162327
Edytować:
Zgodnie z prośbą, oto przykład działania 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
źródło
Również napotkałem ten problem, a teraz moją strategią jest generowanie kodu SQL (ręcznie lub przy użyciu
to_sql
istniejącego zakresu), a następnie umieszczanie go wfrom
klauzuli. Nie mogę zagwarantować, że jest bardziej wydajna niż przyjęta metoda, ale jest stosunkowo łagodna dla oczu i przywraca normalny obiekt ARel.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")
Możesz to zrobić również z dwoma różnymi modelami, ale musisz upewnić się, że oba "wyglądają tak samo" wewnątrz UNION - możesz użyć
select
w obu zapytaniach, aby upewnić się, że utworzą te same kolumny.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")
źródło
#or
w moim projekcie Rails 4. Przyjmując ten sam model:klass.from("(#{to_sql} union #{other_relation.to_sql}) as #{table_name}")
Na podstawie odpowiedzi Olives wymyśliłem inne rozwiązanie tego problemu. To trochę przypomina włamanie, ale zwraca przykład
ActiveRelation
, o co mi chodziło.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)
Nadal byłbym wdzięczny, gdyby ktoś miał jakieś sugestie, aby to zoptymalizować lub poprawić wydajność, ponieważ zasadniczo wykonuje trzy zapytania i wydaje się trochę zbędne.
źródło
Możesz także użyć klejnotu active_record_union Briana Hempela , który rozszerza się o
ActiveRecord
union
metodę dla lunet.Twoje zapytanie wyglądałoby tak:
Post.joins(:news => :watched). where(:watched => {:user_id => id}). union(Post.joins(:post_topic_relationships => {:topic => :watched} .where(:watched => {:user_id => id}))
Miejmy nadzieję, że
ActiveRecord
pewnego dnia zostanie to połączone .źródło
Co powiesz na...
def union(scope1, scope2) ids = scope1.pluck(:id) + scope2.pluck(:id) where(id: ids.uniq) end
źródło
pluck
wywołanie jest samo w sobie zapytaniem..order
albo.paginate
metody ... Utrzymuje klas ORMCzy możesz użyć OR zamiast UNION?
Wtedy możesz zrobić coś takiego:
Post.joins(:news => :watched, :post_topic_relationships => {:topic => :watched}) .where("watched.user_id = :id OR topic_watched.user_id = :id", :id => id)
(Ponieważ dwukrotnie dołączasz do obserwowanej tabeli, nie jestem pewien, jakie będą nazwy tabel dla zapytania)
Ponieważ jest wiele sprzężeń, może to być również dość ciężkie dla bazy danych, ale może być zoptymalizowane.
źródło
Prawdopodobnie poprawia to czytelność, ale niekoniecznie wydajność:
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
Ta metoda zwraca ActiveRecord :: Relation, więc możesz to nazwać w ten sposób:
my_posts.order("watched_item_type, post.id DESC")
źródło
Istnieje klejnot active_record_union. Może być pomocna
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
źródło
Po prostu uruchomiłbym dwa zapytania, których potrzebujesz i połączyłby tablice zwracanych rekordów:
@posts = watched_news_posts + watched_topics_posts
Albo przynajmniej przetestuj to. Czy uważasz, że kombinacja tablic w ruby będzie zbyt wolna? Patrząc na sugerowane zapytania w celu obejścia problemu, nie jestem przekonany, że wystąpi tak znacząca różnica w wydajności.
źródło
W podobnym przypadku zsumowałem dwie tablice i użyłem
Kaminari:paginate_array()
. Bardzo ładne i działające rozwiązanie. Nie mogłem użyćwhere()
, ponieważ muszę zsumować dwa wyniki z różnymiorder()
na tej samej tabeli.źródło
Mniej problemów i łatwiejsze do naśladowania:
def union_scope(*scopes) scopes[1..-1].inject(where(id: scopes.first)) { |all, scope| all.or(where(id: scope)) } end
Więc na koniec:
źródło
scopes.drop(1).reduce(where(id: scopes.first)) { |query, scope| query.or(where(id: scope)) }
!Elliot Nelson odpowiedział dobrze, z wyjątkiem przypadku, gdy niektóre relacje są puste. Zrobiłbym coś takiego:
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)
koniec
źródło
Oto jak dołączyłem do zapytań SQL używając UNION na mojej własnej aplikacji Ruby on Rails.
Możesz użyć poniższego jako inspiracji dla własnego kodu.
class Preference < ApplicationRecord scope :for, ->(object) { where(preferenceable: object) } end
Poniżej znajduje się UNIA, w której połączyłem razem zakresy.
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
źródło