ActiveRecord Query Union


90

Tôi đã viết một vài truy vấn phức tạp (ít nhất là với tôi) với giao diện truy vấn của 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})

Cả hai truy vấn này đều hoạt động tốt. Cả hai đều trả về đối tượng Đăng. Tôi muốn kết hợp các bài đăng này thành một ActiveRelation duy nhất. Vì có thể có hàng trăm nghìn bài đăng vào một thời điểm nào đó, điều này cần được thực hiện ở cấp cơ sở dữ liệu. Nếu đó là một truy vấn MySQL, tôi chỉ cần sử dụng UNIONtoán tử. Có ai biết liệu tôi có thể làm điều gì đó tương tự với giao diện truy vấn của RoR không?


Bạn sẽ có thể sử dụng phạm vi . Tạo 2 phạm vi và sau đó gọi cả hai thích Post.watched_news_posts.watched_topic_posts. Bạn có thể cần phải gửi nhân viên đến phạm vi cho những thứ như :user_id:topic.
Zabba

6
Cám ơn vì sự gợi ý. Theo tài liệu, "Phạm vi thể hiện sự thu hẹp của truy vấn cơ sở dữ liệu". Trong trường hợp của tôi, tôi không tìm kiếm các bài đăng ở cả watching_news_posts và watching_topic_posts. Thay vào đó, tôi đang tìm các bài đăng ở dạng watching_news_posts hoặc watching_topic_posts, không được phép trùng lặp. Điều này vẫn có thể thực hiện được với phạm vi?
LandonSchropp vào

1
Không thực sự khả thi. Có một plugin trên github được gọi là union nhưng nó sử dụng cú pháp cũ (phương thức lớp và tham số truy vấn kiểu băm), nếu điều đó thú vị với bạn, tôi sẽ nói hãy sử dụng nó ... nếu không, hãy viết nó ra trong một find_by_sql trong phạm vi của bạn.
jenjenut233

1
Tôi đồng ý với jenjenut233 và tôi nghĩ bạn có thể làm điều gì đó tương tự find_by_sql("#{watched_news_posts.to_sql} UNION #{watched_topic_posts.to_sql}"). Tôi chưa thử nghiệm điều đó, vì vậy hãy cho tôi biết nó diễn ra như thế nào nếu bạn thử nó. Ngoài ra, có thể có một số chức năng ARel sẽ hoạt động.
Wizard of Ogz,

2
Tôi đã viết lại các truy vấn dưới dạng truy vấn SQL. Chúng hoạt động ngay bây giờ, nhưng tiếc là không find_by_sqlthể được sử dụng với các truy vấn có thể thay đổi khác, có nghĩa là bây giờ tôi phải viết lại các bộ lọc và truy vấn will_paginate của mình. Tại sao ActiveRecord không hỗ trợ một unionhoạt động?
LandonSchropp,

Câu trả lời:


93

Đây là một mô-đun nhỏ nhanh chóng mà tôi đã viết cho phép bạn UNION nhiều phạm vi. Nó cũng trả về kết quả dưới dạng một thể hiện của 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

Đây là ý chính: https://gist.github.com/tlowrimore/5162327

Biên tập:

Theo yêu cầu, đây là một ví dụ về cách UnionScope hoạt động:

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
Đây thực sự là một cách trả lời đầy đủ hơn những cách khác được liệt kê ở trên. Hoạt động tuyệt vời!
ghayes

Ví dụ về cách sử dụng sẽ rất tốt.
ciembor

Theo yêu cầu, tôi đã thêm một ví dụ.
Tim Lowrimore

3
Giải pháp là "gần như" đúng và tôi đã cho nó một +1, nhưng tôi chạy vào một vấn đề mà tôi đã cố định ở đây: gist.github.com/lsiden/260167a4d3574a580d97
Lawrence I. Siden

7
Cảnh báo nhanh: phương pháp này rất có vấn đề từ góc độ hiệu suất với MySQL, vì truy vấn con sẽ được tính là phụ thuộc và được thực thi cho mỗi bản ghi trong bảng (xem percona.com/blog/2010/10/25/mysql-limitations-part -3-truy vấn con ).
shosti

70

Tôi cũng đã gặp sự cố này và bây giờ chiến lược đi tới của tôi là tạo SQL (bằng tay hoặc sử dụng to_sqltrên phạm vi hiện có) và sau đó gắn nó vào frommệnh đề. Tôi không thể đảm bảo rằng nó hiệu quả hơn bất kỳ phương pháp nào được chấp nhận của bạn, nhưng nó tương đối dễ nhìn và trả lại cho bạn một đối tượng ARel bình thường.

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

Bạn cũng có thể làm điều này với hai mô hình khác nhau, nhưng bạn cần đảm bảo cả hai đều "trông giống nhau" bên trong UNION - bạn có thể sử dụng selecttrên cả hai truy vấn để đảm bảo chúng sẽ tạo ra các cột giống nhau.

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

giả sử nếu chúng tôi có hai mô hình khác nhau thì vui lòng cho tôi biết những gì sẽ là truy vấn cho unain.
Chitra

Câu trả lời rất hữu ích. Đối với người đọc trong tương lai, hãy nhớ phần "AS nhận xét" cuối cùng vì activerecord xây dựng truy vấn dưới dạng "CHỌN" nhận xét "." * "FROM" ... nếu bạn không chỉ định tên của nhóm hợp nhất HOẶC chỉ định một tên khác như "AS foo", quá trình thực thi sql cuối cùng sẽ không thành công.
HeyZiko

1
Đây chính xác là những gì tôi đang tìm kiếm. Tôi đã mở rộng ActiveRecord :: Relation để hỗ trợ #ortrong dự án Rails 4 của mình. Giả sử cùng một mô hình:klass.from("(#{to_sql} union #{other_relation.to_sql}) as #{table_name}")
M. Wyatt.

11

Dựa trên câu trả lời của Olives, tôi đã đưa ra một giải pháp khác cho vấn đề này. Nó có cảm giác hơi giống một vụ hack, nhưng nó trả về một ví dụ của ActiveRelation, đó là những gì tôi đã theo đuổi ngay từ đầu.

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)

Tôi vẫn đánh giá cao nếu ai đó có bất kỳ đề xuất nào để tối ưu hóa điều này hoặc cải thiện hiệu suất, vì về cơ bản nó đang thực hiện ba truy vấn và cảm thấy hơi thừa.


Làm cách nào tôi có thể làm điều tương tự với cái này: gist.github.com/2241307 Để nó tạo một lớp AR :: Relation thay vì một lớp Mảng?
Marc

10

Bạn cũng có thể sử dụng đá quý active_record_union của Brian Hempel mở rộng với một phương thức cho phạm vi.ActiveRecordunion

Truy vấn của bạn sẽ như thế này:

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

Hy vọng rằng điều này cuối cùng sẽ được hợp nhất vào ActiveRecordmột ngày nào đó.


8

Làm thế nào về...

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

15
Hãy cảnh báo rằng điều này sẽ thực hiện ba truy vấn thay vì một, vì bản thân mỗi pluckcuộc gọi là một truy vấn.
JacobEvelyn

3
Đây là một giải pháp thực sự tốt, becouse nó không trả lại một mảng, vì vậy sau đó bạn có thể sử dụng .orderhoặc .paginatecác phương pháp ... Nó giữ các lớp orm
mariowise

Hữu ích nếu các phạm vi có cùng một mô hình, nhưng điều này sẽ tạo ra hai truy vấn vì các điểm khó khăn.
jmjm

6

Bạn có thể sử dụng OR thay vì UNION không?

Sau đó, bạn có thể làm điều gì đó như:

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

(Vì bạn tham gia bảng đã xem hai lần nên tôi không chắc tên của các bảng sẽ là gì cho truy vấn)

Vì có rất nhiều liên kết, nó cũng có thể khá nặng trên cơ sở dữ liệu, nhưng nó có thể được tối ưu hóa.


2
Xin lỗi vì liên lạc lại với bạn quá muộn, nhưng tôi đã đi nghỉ vài ngày qua. Vấn đề tôi gặp phải khi thử câu trả lời của bạn là phương thức nối đang khiến cả hai bảng được nối với nhau, thay vì hai truy vấn riêng biệt sau đó có thể được so sánh. Tuy nhiên, ý tưởng của bạn rất hay và đã cho tôi một ý tưởng khác. Cảm ơn đã giúp đỡ.
LandonSchropp vào

chọn sử dụng OR là chậm hơn so với so với UNION, tự hỏi bất kỳ giải pháp cho UNION thay
Nich

5

Có thể cho rằng, điều này cải thiện khả năng đọc, nhưng không nhất thiết là hiệu suất:

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

Phương thức này trả về một ActiveRecord :: Relation, vì vậy bạn có thể gọi nó như sau:

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

bạn nhận được posts.id từ đâu?
berto77

Có hai tham số self.id vì self.id được tham chiếu hai lần trong SQL - hãy xem hai dấu chấm hỏi.
richardsun

Đây là một ví dụ hữu ích về cách thực hiện truy vấn UNION và lấy lại ActiveRecord :: Relation. Cảm ơn.
Fitter Man

bạn có công cụ để tạo các loại truy vấn SDL này không - bạn đã làm như thế nào mà không mắc lỗi chính tả, v.v.?
BKSpurgeon

2

Có một viên ngọc active_record_union. Có thể hữu ích

https://github.com/brianhempel/active_record_union

Với ActiveRecordUnion, chúng ta có thể làm:

bài đăng (bản nháp) của người dùng hiện tại và tất cả các bài đăng đã xuất bản từ bất kỳ ai current_user.posts.union(Post.published) Tương đương với SQL sau:

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

Tôi sẽ chỉ chạy hai truy vấn bạn cần và kết hợp các mảng bản ghi được trả về:

@posts = watched_news_posts + watched_topics_posts

Hoặc, ít nhất là kiểm tra nó ra. Bạn có nghĩ rằng sự kết hợp mảng trong ruby ​​sẽ quá chậm? Nhìn vào các truy vấn được đề xuất để giải quyết vấn đề, tôi không tin rằng sẽ có sự khác biệt đáng kể về hiệu suất.


Trên thực tế, thực hiện @ posts = watching_news_posts & watching_topics_posts có thể tốt hơn vì đây là giao lộ và sẽ tránh được các hành vi gian lận.
Jeffrey Alan Lee

1
Tôi có ấn tượng là ActiveRelation tải các bản ghi của nó một cách lười biếng. Bạn sẽ không mất điều đó nếu bạn giao nhau giữa các mảng trong Ruby?
LandonSchropp

Rõ ràng là một union trả về một mối quan hệ đang bị dev in rails, nhưng tôi không biết nó sẽ ở phiên bản nào.
Jeffrey Alan Lee

1
mảng trả về này thay vào đó, hai kết quả truy vấn khác nhau của nó hợp nhất.
alexzg

1

Trong một trường hợp tương tự, tôi đã tổng hợp hai mảng và sử dụng Kaminari:paginate_array(). Giải pháp rất tốt và hiệu quả. Tôi không thể sử dụng where(), vì tôi cần tính tổng hai kết quả khác nhau order()trên cùng một bảng.


1

Ít vấn đề hơn và dễ theo dõi hơn:

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

Cuối cùng:

union_scope(watched_news_posts, watched_topic_posts)

1
Tôi đã thay đổi nó một chút thành: scopes.drop(1).reduce(where(id: scopes.first)) { |query, scope| query.or(where(id: scope)) }Thx!
eikes

0

Elliot Nelson trả lời tốt, ngoại trừ trường hợp một số quan hệ trống rỗng. Tôi sẽ làm một cái gì đó như thế:

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)

kết thúc


0

Đây là cách tôi tham gia các truy vấn SQL bằng UNION trên ứng dụng ruby ​​on rails của riêng tôi.

Bạn có thể sử dụng thông tin bên dưới làm nguồn cảm hứng cho mã của riêng bạn.

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

Dưới đây là UNION nơi tôi đã tham gia các phạm vi cùng nhau.

  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
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.