ActiveRecord Sorgu Birliği


91

Ruby on Rail'in sorgu arayüzüyle birkaç karmaşık sorgu (en azından benim için) yazdım:

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})

Bu sorguların ikisi de kendi başlarına iyi çalışıyor. Her ikisi de Post nesnelerini döndürür. Bu gönderileri tek bir ActiveRelation'da birleştirmek istiyorum. Bir noktada yüz binlerce gönderi olabileceğinden, bunun veritabanı düzeyinde yapılması gerekir. Bir MySQL sorgusu olsaydı, UNIONoperatörü basitçe kullanabilirdim . RoR'un sorgu arayüzüne benzer bir şey yapıp yapamayacağımı bilen var mı?


Kapsam kullanabilmeniz gerekir . 2 kapsam oluşturun ve ardından ikisini de beğenin Post.watched_news_posts.watched_topic_posts. :user_idVe gibi şeyler için kapsamlara parametre göndermeniz gerekebilir :topic.
Zabba

6
Önerin için teşekkürler. Dokümanlara göre, "Kapsam, bir veritabanı sorgusunun daralmasını temsil eder". Benim durumumda, hem watched_news_posts hem de watched_topic_posts içindeki gönderileri aramıyorum. Bunun yerine, watched_news_posts veya watched_topic_posts içinde bulunan ve yinelemelere izin verilmeyen gönderileri arıyorum. Bunu kapsamlarla başarmak hala mümkün mü?
LandonSchropp

1
Kutudan çıkar çıkmaz pek mümkün değil. Github'da union adında bir eklenti var, ancak eski okul sözdizimini kullanıyor (sınıf yöntemi ve karma tarzı sorgu parametreleri), eğer sizin için uygunsa, onunla gidin derim ... aksi takdirde uzun yoldan yazın find_by_sql sizin kapsamınızda.
jenjenut233

1
Jenjenut233'e katılıyorum ve bunun gibi bir şey yapabileceğinizi düşünüyorum find_by_sql("#{watched_news_posts.to_sql} UNION #{watched_topic_posts.to_sql}"). Bunu test etmedim, o yüzden denerseniz nasıl olacağını bana bildirin. Ayrıca, muhtemelen işe yarayacak bazı ARel işlevleri vardır.
Ogz Sihirbazı

2
Sorguları SQL sorguları olarak yeniden yazdım. Şimdi çalışıyorlar, ancak maalesef find_by_sqldiğer zincirlenebilir sorgularla kullanılamıyor, bu da artık will_paginate filtrelerimi ve sorgularımı yeniden yazmam gerektiği anlamına geliyor. ActiveRecord bir unionişlemi neden desteklemiyor ?
LandonSchropp

Yanıtlar:


97

İşte yazdığım, çoklu kapsamları BİRLEŞTİRMENİZE izin veren küçük bir modül. Ayrıca sonuçları bir ActiveRecord :: Relation örneği olarak döndürür.

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

İşin özü: https://gist.github.com/tlowrimore/5162327

Düzenle:

İstendiği gibi, UnionScope'un nasıl çalıştığına dair bir örnek:

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

2
Bu gerçekten yukarıda listelenen diğerlerine çok daha eksiksiz bir cevap. Harika çalışıyor!
ghayes

Kullanım örneği güzel olurdu.
ciembor

İstendiği gibi bir örnek ekledim.
Tim Lowrimore

3
Çözüm "neredeyse" doğru ve buna +1 verdim, ancak burada düzelttiğim bir sorunla karşılaştım: gist.github.com/lsiden/260167a4d3574a580d97
Lawrence I. Siden

7
Hızlı uyarı: Bu yöntem MySQL ile performans açısından oldukça sorunludur, çünkü alt sorgu tablodaki her kayıt için bağımlı olarak sayılacak ve yürütülecektir (bkz. Percona.com/blog/2010/10/25/mysql-limitations-part -3-alt sorgular ).
shosti

72

Ben de bu sorunla karşılaştım ve şimdi stratejim SQL oluşturmak (elle veya to_sqlmevcut bir kapsamda kullanarak ) ve sonra onu tümceye yapıştırmak from. Kabul ettiğiniz yöntemden daha verimli olduğunu garanti edemem, ancak gözler için nispeten kolaydır ve size normal bir ARel nesnesini geri verir.

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")

Bunu iki farklı modelle de yapabilirsiniz, ancak her ikisinin de UNION içinde "aynı göründüğünden" emin olmanız gerekir - selectaynı sütunları üreteceklerinden emin olmak için her iki sorguda da kullanabilirsiniz .

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")

varsayalım ki iki farklı modelimiz varsa o zaman lütfen unoin sorgusunun ne olacağını bana bildirin.
Chitra

Çok faydalı cevap. Gelecekteki okuyucular için, son "AS yorumları" bölümünü hatırlayın çünkü activerecord sorguyu "SEÇ" yorumlar "olarak oluşturur." * "FROM" ... birleşimli kümenin adını belirtmezseniz YA DA farklı bir ad belirtirseniz "AS foo", son sql yürütme başarısız olacak.
HeyZiko

1
Bu tam olarak aradığım şeydi. ActiveRecord :: Relation'ı #orRails 4 projemde destekleyecek şekilde genişlettim . Aynı modeli varsayarsak:klass.from("(#{to_sql} union #{other_relation.to_sql}) as #{table_name}")
M. Wyatt

11

Zeytin'in cevabına dayanarak, bu soruna başka bir çözüm buldum. Biraz hack gibi geliyor, ama bir örnek ActiveRelationveriyor, ilk başta peşinde olduğum şey buydu.

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)

Bunu optimize etmek veya performansı artırmak için herhangi bir önerisi olan olursa yine de memnun olurum, çünkü bu aslında üç sorgu yürütüyor ve biraz gereksiz hissediyor.


Aynı şeyi bununla nasıl yapabilirim: gist.github.com/2241307 Böylece bir Array sınıfı yerine bir AR :: Relation sınıfı oluşturur?
Marc

10

Ayrıca kullanabilirsiniz Brian Hempel 'ın active_record_union uzanan mücevher ActiveRecordbir ile unionkapsamları için yöntemiyle.

Sorgunuz şöyle olacaktır:

Post.joins(:news => :watched).
  where(:watched => {:user_id => id}).
  union(Post.joins(:post_topic_relationships => {:topic => :watched}
    .where(:watched => {:user_id => id}))

Umarım bu eninde sonunda ActiveRecordbir gün içinde birleştirilecektir .


7

Peki ya ...

def union(scope1, scope2)
  ids = scope1.pluck(:id) + scope2.pluck(:id)
  where(id: ids.uniq)
end

15
Her pluckçağrı kendi içinde bir sorgu olduğundan , bunun bir yerine üç sorgu gerçekleştireceği konusunda uyarılmalıdır .
JacobEvelyn

3
O, bir dizi dönmez böylece o zaman kullanabilirsiniz nedeniyle bu, gerçekten iyi bir çözümdür .orderveya .paginateyöntemleri Bu orm sınıfları tutar ...
mariowise

Kapsamlar aynı modelse kullanışlıdır, ancak bu, eklentiler nedeniyle iki sorgu oluşturacaktır.
jmjm

6

UNION yerine OR kullanabilir misiniz?

O zaman şöyle bir şey yapabilirsiniz:

Post.joins(:news => :watched, :post_topic_relationships => {:topic => :watched})
.where("watched.user_id = :id OR topic_watched.user_id = :id", :id => id)

(İzlenen tabloya iki kez katıldığınız için, sorgu için tabloların adlarının ne olacağından pek emin değilim)

Çok sayıda birleştirme olduğu için, veritabanında oldukça ağır olabilir, ancak optimize edilebilir.


2
Size bu kadar geç döndüğüm için üzgünüm ama son birkaç gündür tatildeyim. Cevabınızı denediğimde yaşadığım sorun, birleştirmeler yönteminin, daha sonra karşılaştırılabilecek iki ayrı sorgu yerine, her iki tablonun birleştirilmesine neden olmasıydı. Ancak fikriniz sağlamdı ve bana başka bir fikir verdi. Yardım için teşekkürler.
LandonSchropp

OR kullanmayı seçmek
UNION'dan

5

Muhtemelen, bu okunabilirliği artırır, ancak performansı mutlaka artırmaz:

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

Bu yöntem bir ActiveRecord :: Relation döndürür, böylece onu şöyle çağırabilirsiniz:

my_posts.order("watched_item_type, post.id DESC")

posts.id nereden alıyorsun?
berto77

SQL'de self.id iki kez referans verildiği için iki self.id parametresi vardır - iki soru işaretine bakın.
richardsun

Bu, bir UNION sorgusunun nasıl yapılacağına ve ActiveRecord :: Relation'ın nasıl geri alınacağına dair yararlı bir örnekti. Teşekkürler.
Tesisatçı Adam

bu tür SDL sorgularını oluşturmak için bir aracınız var mı - bunu yazım hataları olmadan nasıl yaptınız?
BKSpurgeon

2

Bir active_record_union mücevher var. Yardımcı olabilir

https://github.com/brianhempel/active_record_union

ActiveRecordUnion ile şunları yapabiliriz:

mevcut kullanıcının (taslak) gönderileri ve current_user.posts.union(Post.published) aşağıdaki SQL ile eşdeğer olan herkes tarafından yayınlanan tüm yayınlar :

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

1

İhtiyacınız olan iki sorguyu çalıştırır ve döndürülen kayıt dizilerini birleştiririm:

@posts = watched_news_posts + watched_topics_posts

Ya da en azından test edin. Ruby'deki dizi kombinasyonunun çok yavaş olacağını düşünüyor musunuz? Sorunun üstesinden gelmek için önerilen sorgulara baktığımda, önemli bir performans farkı olacağına ikna olmadım.


Aslında @ gönderiler = watched_news_posts & watched_topics_posts yapmak, bir kesişme olduğu ve kopyaları önleyeceği için daha iyi olabilir.
Jeffrey Alan Lee

1
ActiveRelation'ın kayıtlarını tembel bir şekilde yüklediği izlenimi altındaydım. Ruby'deki dizileri kesişirseniz, bunu kaybetmez miydiniz?
LandonSchropp

Görünüşe göre bir ilişki döndüren bir birleşim raylarda geliştiriliyor, ancak hangi sürümde olacağını bilmiyorum.
Jeffrey Alan Lee

1
bunun yerine bu dönüş dizisi, iki farklı sorgu sonucu birleşir.
alexzg

1

Benzer bir durumda iki diziyi topladım ve kullandım Kaminari:paginate_array(). Çok güzel ve çalışan bir çözüm. Kullanamadım where()çünkü order()aynı tabloda iki sonucu farklı olan iki sonucu toplamam gerekiyor .


1

Daha az sorun ve takip etmesi daha kolay:

    def union_scope(*scopes)
      scopes[1..-1].inject(where(id: scopes.first)) { |all, scope| all.or(where(id: scope)) }
    end

Yani sonunda:

union_scope(watched_news_posts, watched_topic_posts)

1
Ben biraz değiştirdim: scopes.drop(1).reduce(where(id: scopes.first)) { |query, scope| query.or(where(id: scope)) }Thx!
eikes

0

Elliot Nelson, bazı ilişkilerin boş olduğu durum dışında iyi cevap verdi. Böyle bir şey yapardım:

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)

son


0

Kendi ruby ​​on rails uygulamamda UNION kullanarak SQL sorgularına nasıl katıldım.

Aşağıdakileri kendi kodunuz için ilham kaynağı olarak kullanabilirsiniz.

class Preference < ApplicationRecord
  scope :for, ->(object) { where(preferenceable: object) }
end

Kapsamlara birlikte katıldığım UNION aşağıdadır.

  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
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.