Saya telah menulis beberapa pertanyaan kompleks (setidaknya untuk saya) dengan antarmuka kueri 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})
Kedua kueri ini berfungsi dengan baik sendiri. Keduanya mengembalikan objek Post. Saya ingin menggabungkan posting ini menjadi satu ActiveRelation. Karena mungkin ada ratusan ribu postingan di beberapa titik, ini perlu dilakukan di tingkat database. Jika itu adalah kueri MySQL, saya cukup menggunakan UNION
operatornya. Apakah ada yang tahu jika saya dapat melakukan sesuatu yang serupa dengan antarmuka kueri RoR?
ruby-on-rails
activerecord
union
active-relation
LandonSchropp
sumber
sumber
Post.watched_news_posts.watched_topic_posts
. Anda mungkin perlu mengirimkan params ke cakupan untuk hal-hal seperti:user_id
dan:topic
.find_by_sql("#{watched_news_posts.to_sql} UNION #{watched_topic_posts.to_sql}")
. Saya belum mengujinya, jadi beri tahu saya bagaimana kelanjutannya jika Anda mencobanya. Juga, mungkin ada beberapa fungsi ARel yang akan berfungsi.find_by_sql
tidak dapat digunakan dengan kueri yang dapat dirantai lainnya, yang berarti saya sekarang harus menulis ulang filter dan kueri will_paginate saya juga. Mengapa ActiveRecord tidak mendukungunion
operasi?Jawaban:
Berikut adalah modul kecil singkat yang saya tulis yang memungkinkan Anda untuk UNION beberapa cakupan. Ia juga mengembalikan hasil sebagai turunan dari 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
Inilah intinya: https://gist.github.com/tlowrimore/5162327
Edit:
Seperti yang diminta, berikut adalah contoh cara kerja 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
sumber
Saya juga mengalami masalah ini, dan sekarang strategi masuk saya adalah menghasilkan SQL (dengan tangan atau menggunakan
to_sql
lingkup yang ada) dan kemudian memasukkannya ke dalamfrom
klausa. Saya tidak dapat menjamin itu lebih efisien daripada metode yang Anda terima, tetapi ini relatif mudah dilihat dan memberi Anda objek ARel normal kembali.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")
Anda dapat melakukan ini dengan dua model berbeda juga, tetapi Anda perlu memastikan keduanya "terlihat sama" di dalam UNION - Anda dapat menggunakan
select
kedua kueri untuk memastikan keduanya akan menghasilkan kolom yang sama.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")
sumber
#or
proyek Rails 4 saya. Dengan asumsi model yang sama:klass.from("(#{to_sql} union #{other_relation.to_sql}) as #{table_name}")
Berdasarkan jawaban Zaitun, saya menemukan solusi lain untuk masalah ini. Rasanya sedikit seperti retasan, tetapi mengembalikan contoh
ActiveRelation
, itulah yang saya kejar sejak awal.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)
Saya masih akan menghargai jika ada yang memiliki saran untuk mengoptimalkan ini atau meningkatkan kinerja, karena pada dasarnya ini menjalankan tiga kueri dan terasa sedikit berlebihan.
sumber
Anda juga bisa menggunakan Brian Hempel 's active_record_union permata yang memanjang
ActiveRecord
denganunion
metode untuk lingkup.Permintaan Anda akan seperti ini:
Post.joins(:news => :watched). where(:watched => {:user_id => id}). union(Post.joins(:post_topic_relationships => {:topic => :watched} .where(:watched => {:user_id => id}))
Mudah-mudahan ini pada akhirnya akan digabungkan menjadi
ActiveRecord
suatu hari nanti.sumber
Bagaimana tentang...
def union(scope1, scope2) ids = scope1.pluck(:id) + scope2.pluck(:id) where(id: ids.uniq) end
sumber
pluck
panggilan adalah kueri itu sendiri..order
atau.paginate
metode ... Ini membuat kelas ormBisakah Anda menggunakan OR sebagai ganti UNION?
Kemudian Anda dapat melakukan sesuatu seperti:
Post.joins(:news => :watched, :post_topic_relationships => {:topic => :watched}) .where("watched.user_id = :id OR topic_watched.user_id = :id", :id => id)
(Karena Anda bergabung dengan tabel yang diawasi dua kali, saya tidak terlalu yakin apa nama tabel untuk kueri)
Karena ada banyak gabungan, ini mungkin juga cukup berat di database, tetapi mungkin bisa dioptimalkan.
sumber
Bisa dibilang, ini meningkatkan keterbacaan, tetapi belum tentu kinerja:
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
Metode ini mengembalikan ActiveRecord :: Relation, jadi Anda bisa memanggilnya seperti ini:
my_posts.order("watched_item_type, post.id DESC")
sumber
Ada permata active_record_union. Mungkin bisa membantu
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
sumber
Saya hanya akan menjalankan dua kueri yang Anda butuhkan dan menggabungkan array catatan yang dikembalikan:
@posts = watched_news_posts + watched_topics_posts
Atau, setidaknya coba saja. Apakah menurut Anda kombinasi array dalam ruby akan terlalu lambat? Melihat permintaan yang disarankan untuk mengatasi masalah, saya tidak yakin akan ada perbedaan kinerja yang signifikan.
sumber
Dalam kasus serupa saya menjumlahkan dua larik dan digunakan
Kaminari:paginate_array()
. Solusi yang sangat bagus dan berfungsi. Saya tidak dapat menggunakanwhere()
, karena saya perlu menjumlahkan dua hasil dengan perbedaanorder()
pada tabel yang sama.sumber
Lebih sedikit masalah dan lebih mudah diikuti:
def union_scope(*scopes) scopes[1..-1].inject(where(id: scopes.first)) { |all, scope| all.or(where(id: scope)) } end
Jadi pada akhirnya:
sumber
scopes.drop(1).reduce(where(id: scopes.first)) { |query, scope| query.or(where(id: scope)) }
Thx!Elliot Nelson menjawab dengan baik, kecuali kasus di mana beberapa relasi kosong. Saya akan melakukan sesuatu seperti itu:
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)
akhir
sumber
Inilah cara saya bergabung dengan kueri SQL menggunakan UNION pada aplikasi ruby on rails saya sendiri.
Anda dapat menggunakan di bawah ini sebagai inspirasi pada kode Anda sendiri.
class Preference < ApplicationRecord scope :for, ->(object) { where(preferenceable: object) } end
Di bawah ini adalah UNION tempat saya menggabungkan cakupannya.
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
sumber