Uni Kueri ActiveRecord

90

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 UNIONoperatornya. Apakah ada yang tahu jika saya dapat melakukan sesuatu yang serupa dengan antarmuka kueri RoR?

LandonSchropp
sumber
Anda harus bisa menggunakan scope . Buat 2 cakupan lalu panggil keduanya suka Post.watched_news_posts.watched_topic_posts. Anda mungkin perlu mengirimkan params ke cakupan untuk hal-hal seperti :user_iddan :topic.
Zabba
6
Terima kasih untuk sarannya. Menurut dokumen tersebut, "Lingkup mewakili penyempitan kueri database". Dalam kasus saya, saya tidak mencari postingan yang ada di watching_news_posts dan watching_topic_posts. Sebaliknya, saya mencari postingan yang ada di watching_news_posts atau watching_topic_posts, tanpa ada duplikat yang diizinkan. Apakah ini masih mungkin dilakukan dengan cakupan?
LandonSchropp
1
Tidak mungkin di luar kotak. Ada sebuah plugin di github yang disebut union tetapi menggunakan sintaks sekolah lama (metode kelas dan parameter kueri gaya hash), jika itu keren bagi Anda, saya akan mengatakan pergi dengan itu ... jika tidak tuliskan jauh-jauh di a find_by_sql dalam cakupan Anda.
jenjenut233
1
Saya setuju dengan jenjenut233, dan menurut saya Anda bisa melakukan sesuatu seperti itu 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.
Wizard of Ogz
2
Saya menulis ulang kueri sebagai kueri SQL. Mereka berfungsi sekarang, tetapi sayangnya find_by_sqltidak 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 mendukung unionoperasi?
LandonSchropp

Jawaban:

93

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
Tim Lowrimore
sumber
2
Ini benar-benar jawaban yang jauh lebih lengkap dari yang lain yang tercantum di atas. Bekerja dengan baik!
ghayes
Contoh penggunaan akan menyenangkan.
ciembor
Seperti yang diminta, saya telah menambahkan contoh.
Tim Lowrimore
3
Solusinya "hampir" benar dan saya memberinya +1, tetapi saya mengalami masalah yang saya perbaiki di sini: gist.github.com/lsiden/260167a4d3574a580d97
Lawrence I. Siden
7
Peringatan cepat: metode ini sangat bermasalah dari perspektif kinerja dengan MySQL, karena subquery akan dihitung sebagai dependen dan dijalankan untuk setiap record dalam tabel (lihat percona.com/blog/2010/10/25/mysql-limitations-part -3-subkueri ).
shosti
71

Saya juga mengalami masalah ini, dan sekarang strategi masuk saya adalah menghasilkan SQL (dengan tangan atau menggunakan to_sqllingkup yang ada) dan kemudian memasukkannya ke dalam fromklausa. 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 selectkedua 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")
Elliot Nelson
sumber
misalkan jika kita memiliki dua model yang berbeda maka tolong beri tahu saya apa yang akan menjadi kueri untuk unoin.
Chitra
Jawaban yang sangat membantu. Untuk pembaca selanjutnya, ingat bagian akhir "AS comments" karena activerecord menyusun kueri sebagai 'PILIH "komentar". "*" FROM "... jika Anda tidak menentukan nama kumpulan gabungan ATAU tentukan nama yang berbeda seperti "AS foo", eksekusi sql terakhir akan gagal
HeyZiko
1
Inilah yang saya cari. Saya memperluas ActiveRecord :: Relation untuk mendukung #orproyek Rails 4 saya. Dengan asumsi model yang sama:klass.from("(#{to_sql} union #{other_relation.to_sql}) as #{table_name}")
M. Wyatt
11

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.

LandonSchropp
sumber
Bagaimana saya bisa melakukan hal yang sama dengan ini: gist.github.com/2241307 Jadi itu menciptakan kelas AR :: Relation daripada kelas Array?
Marc
10

Anda juga bisa menggunakan Brian Hempel 's active_record_union permata yang memanjang ActiveRecorddengan unionmetode 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 ActiveRecordsuatu hari nanti.

dgilperez.dll
sumber
8

Bagaimana tentang...

def union(scope1, scope2)
  ids = scope1.pluck(:id) + scope2.pluck(:id)
  where(id: ids.uniq)
end
Richard Wan
sumber
15
Berhati-hatilah karena ini akan melakukan tiga kueri, bukan satu, karena setiap pluckpanggilan adalah kueri itu sendiri.
JacobEvelyn
3
Ini adalah solusi yang sangat bagus, karena tidak mengembalikan larik, jadi Anda dapat menggunakan .orderatau .paginatemetode ... Ini membuat kelas orm
mariowise
Berguna jika cakupannya memiliki model yang sama, tetapi ini akan menghasilkan dua kueri karena kesulitannya.
jmjm
6

Bisakah 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.

Zaitun
sumber
2
Maaf terlambat kembali kepada Anda, tetapi saya telah berlibur selama beberapa hari terakhir. Masalah yang saya hadapi ketika saya mencoba jawaban Anda adalah metode bergabung menyebabkan kedua tabel digabungkan, daripada dua kueri terpisah yang kemudian dapat dibandingkan. Namun, ide Anda masuk akal dan memberi saya ide lain. Terima kasih untuk bantuannya.
LandonSchropp
pilih menggunakan OR lebih lambat dari UNION, malah bertanya-tanya solusi apa pun untuk UNION
Nich
5

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")
richardsun
sumber
dari mana Anda mendapatkan posts.id?
berto77
Ada dua parameter self.id karena self.id direferensikan dua kali dalam SQL - lihat dua tanda tanya.
richardsun
Ini adalah contoh yang berguna tentang bagaimana melakukan kueri UNION dan mendapatkan kembali ActiveRecord :: Relation. Terima kasih.
Fitter Man
apakah Anda memiliki alat untuk menghasilkan jenis kueri SDL ini - bagaimana Anda melakukannya tanpa kesalahan ejaan, dll.?
BKSpurgeon
2

Ada permata active_record_union. Mungkin bisa membantu

https://github.com/brianhempel/active_record_union

Dengan ActiveRecordUnion, kita dapat melakukan:

posting pengguna saat ini (draf) dan semua posting yang diterbitkan dari siapa pun current_user.posts.union(Post.published) yang setara dengan SQL berikut:

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
Mike Lyubarskyy
sumber
1

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.

Jeffrey Alan Lee
sumber
Sebenarnya melakukan @ posts = watching_news_posts & watching_topics_posts mungkin lebih baik karena ini adalah persimpangan dan akan menghindari penipuan.
Jeffrey Alan Lee
1
Saya mendapat kesan ActiveRelation memuat recordnya dengan malas. Tidakkah Anda akan kehilangan itu jika Anda memotong array di Ruby?
LandonSchropp
Rupanya sebuah serikat pekerja yang mengembalikan suatu relasi berada di bawah dev dalam rel, tapi saya tidak tahu versi apa itu akan masuk.
Jeffrey Alan Lee
1
ini mengembalikan array, dua hasil kueri yang berbeda bergabung.
alexzg
1

Dalam kasus serupa saya menjumlahkan dua larik dan digunakan Kaminari:paginate_array(). Solusi yang sangat bagus dan berfungsi. Saya tidak dapat menggunakan where(), karena saya perlu menjumlahkan dua hasil dengan perbedaan order()pada tabel yang sama.

sekrett
sumber
1

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:

union_scope(watched_news_posts, watched_topic_posts)
Dmitry Polushkin
sumber
1
Saya mengubahnya sedikit menjadi: scopes.drop(1).reduce(where(id: scopes.first)) { |query, scope| query.or(where(id: scope)) }Thx!
eikes
0

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

Ehud
sumber
0

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
joeyk16
sumber